From f2e7eed58ccb642f711d51a4e297679995058350 Mon Sep 17 00:00:00 2001 From: Franco Charriol Date: Thu, 29 Apr 2021 15:05:46 -0300 Subject: [PATCH 001/493] bump: upgrade to 4.3.0 --- CHANGELOG.md | 2 ++ kibana.json | 4 ++-- package.json | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23ce96879..59c19ca596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to the Wazuh app project will be documented in this file. +## Wazuh v4.3.0 - Kibana 7.10.2 , 7.11.2 - Revision 4301 + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added diff --git a/kibana.json b/kibana.json index c45fa29dd1..1a7d503150 100644 --- a/kibana.json +++ b/kibana.json @@ -1,6 +1,6 @@ { "id": "wazuh", - "version": "4.2.0-4201-8", + "version": "4.3.0-4301", "kibanaVersion": "kibana", "configPath": [ "wazuh" @@ -27,4 +27,4 @@ ], "server": true, "ui": true -} +} \ No newline at end of file diff --git a/package.json b/package.json index 3c81b0aac5..197ab09459 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "wazuh", - "version": "4.2.0", - "revision": "4201-8", - "code": "4201-8", + "version": "4.3.0", + "revision": "4301-0", + "code": "4301-0", "kibana": { "version": "7.10.2" }, From c977b4c83b08031c0ca2a291768998eeac373542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= Date: Thu, 29 Apr 2021 20:08:18 +0200 Subject: [PATCH 002/493] Changed ossec to wazuh in sample data (#3121) --- CHANGELOG.md | 4 +++ .../lib/generate-alerts/sample-data/common.js | 2 +- .../sample-data/integrity-monitoring.js | 20 +++++++------- .../lib/generate-alerts/sample-data/mitre.js | 26 +++++++++---------- .../sample-data/policy-monitoring.js | 4 +-- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c19ca596..a4add642b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the Wazuh app project will be documented in this file. ## Wazuh v4.3.0 - Kibana 7.10.2 , 7.11.2 - Revision 4301 +### Changed + +- Channged ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added diff --git a/server/lib/generate-alerts/sample-data/common.js b/server/lib/generate-alerts/sample-data/common.js index dedf1a69be..9417d785bc 100644 --- a/server/lib/generate-alerts/sample-data/common.js +++ b/server/lib/generate-alerts/sample-data/common.js @@ -12,7 +12,7 @@ // Common data export const IPs = ['141.98.81.37', '54.10.24.5', '187.80.4.18', '134.87.21.47', '40.220.102.15', '45.124.37.241', '45.75.196.15', '16.4.20.20']; -export const Users = ["ossecm", "root", "ec2-user", "SYSTEM", "wazuh", "Administrators", "suricata", "ossec", "LOCAL\ Service", "NETWORK\ Service"]; +export const Users = ["root", "ec2-user", "SYSTEM", "wazuh", "Administrators", "suricata", "LOCAL\ Service", "NETWORK\ Service"]; export const Ports = ["22", "55047", "26874", "8905", "3014", "2222", "4547", "3475", "7558", "4277", "3527", "5784", "7854"]; export const Win_Hostnames = ['Win_Server_01', 'Win_Server_02', 'Win_Server_03', 'Win_Server_04']; export const Paths = ["/home/user/sample", "/tmp/sample", "/etc/sample"]; diff --git a/server/lib/generate-alerts/sample-data/integrity-monitoring.js b/server/lib/generate-alerts/sample-data/integrity-monitoring.js index ebc11f92a1..ea25c685ee 100644 --- a/server/lib/generate-alerts/sample-data/integrity-monitoring.js +++ b/server/lib/generate-alerts/sample-data/integrity-monitoring.js @@ -14,8 +14,8 @@ export const events = ["modified", "deleted", "added"]; export const attributes = ["mtime", "inode", "size", "tmp", "md5", "sha1", "sha256"]; export const pathsLinux = [ "/etc/resolv.conf", - "/var/ossec/queue/fim/db/fim.db-journal", - "/var/ossec/queue/fim/db/fim.db", + "/var/wazuh/queue/fim/db/fim.db-journal", + "/var/wazuh/queue/fim/db/fim.db", "/var/osquery/osquery.db/CURRENT", "/etc/sysconfig/network-scripts/ifcfg-eth1", "/etc/filebeat/fields.yml", @@ -27,7 +27,7 @@ export const pathsLinux = [ "/tmp/wazuh-config", "/run/utmp", "/etc/resolv.conf", - "/var/ossec/queue/fim/db/fim.db", + "/var/wazuh/queue/fim/db/fim.db", "/var/osquery/osquery.db/CURRENT", "/run/utmp" ]; @@ -42,10 +42,10 @@ export const pathsWindows = [ "[x32] HKEY_LOCAL_MACHINE\\Security\\SAM\\Domains\\Account\\Users\\000001F7", "[x32] HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\SharedAccess\\Epoch", "c:\\programdata\\microsoft\\windows defender\\scans\\mpenginedb.db-wal", - "c:\\program files (x86)\\ossec-agent\\wodles\\syscollector", - "c:\\program files (x86)\\ossec-agent\\rids\\sender_counter", - "c:\\program files (x86)\\ossec-agent\\queue\\fim\\db\\fim.db", - "c:\\program files (x86)\\ossec-agent\\ossec-agent.state", + "c:\\program files (x86)\\wazuh-agent\\wodles\\syscollector", + "c:\\program files (x86)\\wazuh-agent\\rids\\sender_counter", + "c:\\program files (x86)\\wazuh-agent\\queue\\fim\\db\\fim.db", + "c:\\program files (x86)\\wazuh-agent\\wazuh-agent.state", "[x32] HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\WinDefend", "[x32] HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\Services\\bam\\State\\UserSettings\\S-1-5-21-856620481-996501011-1859314257-500", ]; @@ -65,7 +65,7 @@ export const regulatory = [{ ], "description": "File added to the system.", "groups": [ - "ossec", + "wazuh", "syscheck" ], "id": "554", @@ -92,7 +92,7 @@ export const regulatory = [{ ], "description": "Integrity checksum changed.", "groups": [ - "ossec", + "wazuh", "syscheck" ], "id": "550", @@ -119,7 +119,7 @@ export const regulatory = [{ ], "description": "File deleted.", "groups": [ - "ossec", + "wazuh", "syscheck" ], "id": "553", diff --git a/server/lib/generate-alerts/sample-data/mitre.js b/server/lib/generate-alerts/sample-data/mitre.js index 55d6d16f87..3576b6b042 100644 --- a/server/lib/generate-alerts/sample-data/mitre.js +++ b/server/lib/generate-alerts/sample-data/mitre.js @@ -26,7 +26,7 @@ export const arrayMitreRules = [ nist_800_53: ['AU.6', 'AU.14', 'AU.5'], tsc: ['CC7.2', 'CC7.3', 'CC6.8'], mitre: { tactic: ['Defense Evasion'], id: ['T1089'], technique: ['Disabling Security Tools'] }, - groups: ['ossec'], + groups: ['wazuh'], description: 'Ossec agent disconnected.', }, { @@ -43,7 +43,7 @@ export const arrayMitreRules = [ nist_800_53: ['AU.6', 'AU.14', 'AU.5'], tsc: ['CC7.2', 'CC7.3', 'CC6.8'], mitre: { tactic: ['Defense Evasion'], id: ['T1089'], technique: ['Disabling Security Tools'] }, - groups: ['ossec'], + groups: ['wazuh'], description: 'Ossec agent removed.', }, { @@ -60,7 +60,7 @@ export const arrayMitreRules = [ id: ['T1017'], technique: ['Application Deployment Software'], }, - groups: ['rootcheck', 'ossec'], + groups: ['rootcheck', 'wazuh'], description: 'Windows Adware/Spyware application found.', }, { @@ -69,7 +69,7 @@ export const arrayMitreRules = [ id: 550, level: 7, status: 'enabled', - details: { category: 'ossec', decoded_as: 'syscheck_integrity_changed' }, + details: { category: 'wazuh', decoded_as: 'syscheck_integrity_changed' }, pci_dss: ['11.5'], gpg13: ['4.11'], gdpr: ['II_5.1.f'], @@ -77,7 +77,7 @@ export const arrayMitreRules = [ nist_800_53: ['SI.7'], tsc: ['PI1.4', 'PI1.5', 'CC6.1', 'CC6.8', 'CC7.2', 'CC7.3'], mitre: { tactic: ['Impact'], id: ['T1492'], technique: ['Stored Data Manipulation'] }, - groups: ['syscheck', 'ossec'], + groups: ['syscheck', 'wazuh'], description: 'Integrity checksum changed.', }, { @@ -86,7 +86,7 @@ export const arrayMitreRules = [ id: 553, level: 7, status: 'enabled', - details: { category: 'ossec', decoded_as: 'syscheck_deleted' }, + details: { category: 'wazuh', decoded_as: 'syscheck_deleted' }, pci_dss: ['11.5'], gpg13: ['4.11'], gdpr: ['II_5.1.f'], @@ -98,7 +98,7 @@ export const arrayMitreRules = [ id: ['T1107', 'T1485'], technique: ['File Deletion', 'Data Destruction'], }, - groups: ['syscheck', 'ossec'], + groups: ['syscheck', 'wazuh'], description: 'File deleted.', }, { @@ -115,7 +115,7 @@ export const arrayMitreRules = [ nist_800_53: ['AU.9', 'SI.4'], tsc: ['CC6.1', 'CC7.2', 'CC7.3', 'CC6.8'], mitre: { tactic: ['Impact'], id: ['T1492'], technique: ['Stored Data Manipulation'] }, - groups: ['attacks', 'ossec'], + groups: ['attacks', 'wazuh'], description: 'Log file size reduced.', }, { @@ -132,7 +132,7 @@ export const arrayMitreRules = [ nist_800_53: ['AU.9'], tsc: ['CC6.1', 'CC7.2', 'CC7.3'], mitre: { tactic: ['Defense Evasion'], id: ['T1070'], technique: ['Indicator Removal on Host'] }, - groups: ['logs_cleared', 'ossec'], + groups: ['logs_cleared', 'wazuh'], description: 'Microsoft Event log cleared.', }, { @@ -141,7 +141,7 @@ export const arrayMitreRules = [ id: 594, level: 5, status: 'enabled', - details: { category: 'ossec', if_sid: '550', hostname: 'syscheck-registry' }, + details: { category: 'wazuh', if_sid: '550', hostname: 'syscheck-registry' }, pci_dss: ['11.5'], gpg13: ['4.13'], gdpr: ['II_5.1.f'], @@ -149,7 +149,7 @@ export const arrayMitreRules = [ nist_800_53: ['SI.7'], tsc: ['PI1.4', 'PI1.5', 'CC6.1', 'CC6.8', 'CC7.2', 'CC7.3'], mitre: { tactic: ['Impact'], id: ['T1492'], technique: ['Stored Data Manipulation'] }, - groups: ['syscheck', 'ossec'], + groups: ['syscheck', 'wazuh'], description: 'Registry Integrity Checksum Changed', }, { @@ -158,7 +158,7 @@ export const arrayMitreRules = [ id: 597, level: 5, status: 'enabled', - details: { category: 'ossec', if_sid: '553', hostname: 'syscheck-registry' }, + details: { category: 'wazuh', if_sid: '553', hostname: 'syscheck-registry' }, pci_dss: ['11.5'], gpg13: ['4.13'], gdpr: ['II_5.1.f'], @@ -170,7 +170,7 @@ export const arrayMitreRules = [ id: ['T1107', 'T1485'], technique: ['File Deletion', 'Data Destruction'], }, - groups: ['syscheck', 'ossec'], + groups: ['syscheck', 'wazuh'], description: 'Registry Entry Deleted.', }, { diff --git a/server/lib/generate-alerts/sample-data/policy-monitoring.js b/server/lib/generate-alerts/sample-data/policy-monitoring.js index 53eb789ea4..d1ed30f4cf 100644 --- a/server/lib/generate-alerts/sample-data/policy-monitoring.js +++ b/server/lib/generate-alerts/sample-data/policy-monitoring.js @@ -47,7 +47,7 @@ export const rootkitsData = { "mail": false, "level": 7, "description": "Host-based anomaly detection event (rootcheck).", - "groups": ["ossec","rootcheck"], + "groups": ["wazuh","rootcheck"], "id": "510", "gdpr": ["IV_35.7.d"] }, @@ -74,7 +74,7 @@ export const trojansData = { "mail": false, "level": 7, "description": "Host-based anomaly detection event (rootcheck).", - "groups": ["ossec","rootcheck"], + "groups": ["wazuh","rootcheck"], "id": "510", "gdpr": ["IV_35.7.d"] }, From 028dc0ec1f314f50c3842cbd3d92889461f7285f Mon Sep 17 00:00:00 2001 From: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Date: Thu, 27 May 2021 18:04:21 +0200 Subject: [PATCH 003/493] Replace empty registry value name in FIM inventory (#3279) * Styled empty fields in the FIM section All the tables in the Integrity monitoring section that show an empty field now display a label with an icon noting them as empty, to help distinguish them from whitespace strings and to improve the look. * Fixed a typo in discovery table enhancement Cell in the table should be contained by a "td" tag not a "tr" * Changed formatting of modified files in order to comply with style guide * Updated Changelog * Replaced td of empty element with a span * Empty field text inside EuiCode tag truncates correctly at low widths * Removed !important tags from the new css class * Redid the call for mapped values so emptyFieldHandler handles the mapped values and not the root object. * Removed calls to emptyFieldHandler except registry value --- CHANGELOG.md | 3 +- .../fim/inventory/lib/empty-field-handler.js | 31 +++++++++++++++++++ .../agents/fim/inventory/lib/index.ts | 3 +- .../registryValues/registryValues.tsx | 3 +- .../modules/events-enhance-discover-fields.ts | 9 +++++- public/styles/common.scss | 7 +++++ 6 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 public/components/agents/fim/inventory/lib/empty-field-handler.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a4add642b4..ca9f0e2c74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to the Wazuh app project will be documented in this file. ### Changed -- Channged ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) +- Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) +- Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 diff --git a/public/components/agents/fim/inventory/lib/empty-field-handler.js b/public/components/agents/fim/inventory/lib/empty-field-handler.js new file mode 100644 index 0000000000..6ec68c21fb --- /dev/null +++ b/public/components/agents/fim/inventory/lib/empty-field-handler.js @@ -0,0 +1,31 @@ +/* + * Wazuh app - Integrity monitoring components + * Copyright (C) 2015-2021 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 { EuiCode, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +/* This function can be used to render possibly empty fields. +It takes a render function suitable for an EuiTable and returns another. */ +export const emptyFieldHandler = (renderFn = (value, record) => value) => { + return (value, record) => { + if (value === '' || value === undefined) { + return ( + + + Empty field + + ); + } else { + return renderFn(value, record); + } + }; +}; diff --git a/public/components/agents/fim/inventory/lib/index.ts b/public/components/agents/fim/inventory/lib/index.ts index 7427803104..a697bee2ce 100644 --- a/public/components/agents/fim/inventory/lib/index.ts +++ b/public/components/agents/fim/inventory/lib/index.ts @@ -1 +1,2 @@ -export { getFilterValues } from './getFilterValues'; \ No newline at end of file +export { getFilterValues } from './getFilterValues'; +export { emptyFieldHandler } from './empty-field-handler'; \ No newline at end of file diff --git a/public/components/agents/fim/inventory/registryValues/registryValues.tsx b/public/components/agents/fim/inventory/registryValues/registryValues.tsx index 3e4cf4d696..7ee31f5bc2 100644 --- a/public/components/agents/fim/inventory/registryValues/registryValues.tsx +++ b/public/components/agents/fim/inventory/registryValues/registryValues.tsx @@ -15,6 +15,7 @@ import { WzRequest } from '../../../../../react-services'; import React, { useEffect, useState } from 'react'; import valuesMock from './values.json'; import { DIRECTIONS } from '@elastic/eui/src/components/flex/flex_group'; +import { emptyFieldHandler } from '../lib'; export const RegistryValues = (props) => { const [values, setValues] = useState([]); @@ -51,7 +52,7 @@ export const RegistryValues = (props) => { field: 'value', name: 'Value name', sortable: true, - render: (item) => item.name, + render: (item) => (emptyFieldHandler()(item.name || "")), }, { field: 'value', diff --git a/public/components/common/modules/events-enhance-discover-fields.ts b/public/components/common/modules/events-enhance-discover-fields.ts index 0397f09be6..f68e6c8606 100644 --- a/public/components/common/modules/events-enhance-discover-fields.ts +++ b/public/components/common/modules/events-enhance-discover-fields.ts @@ -106,7 +106,14 @@ export const EventsEnhanceDiscoverCell = { }), { contentRegex: /(\w+)/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 diff --git a/public/styles/common.scss b/public/styles/common.scss index 958bc9ac58..45ca8cefca 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -808,6 +808,13 @@ md-switch.md-checked .md-thumb { display: block; } +.wz-ellipsis{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + /* * https://css-tricks.com/snippets/css/prevent-long-urls-from-breaking-out-of-container/ * Handling long URLs on error toasts. From 909a7d1acea62d331c525a4b432743b1a20f62e8 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Mon, 14 Jun 2021 09:32:51 -0300 Subject: [PATCH 004/493] Client side logging implementation (#3350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/3316 error handler orchestrator (#3327) * feat(errorBoundary): Added ErrorBoundary HOC and component and added loglevel dependency * feature(errorBoundary): Moved with the others HOCs. * feature(errorBoundary): Typo refactor. * First attempt LoggerService * Merged error boundary, integrated loggerService. * changed logger name, create logger-service test file * Updated CHANGELOG * Moved to react-services, changed name, traslates comments * feat(errorBoundary): Removed old integration * refactor(loggerService): Changed class for function methods. * test(logger-service): Added basic unit test to logger-service * refactor(logger-service): Applied new implementation of error-orchestrator service. * feature(logger-service): PR comments and some refactors. Co-authored-by: gabiwassan Co-authored-by: Ibarra Maximiliano * Added ErrorBoundary HOC and component. (#3321) * feat(errorBoundary): Added ErrorBoundary HOC and component and added loglevel dependency * feature(errorBoundary): Moved with the others HOCs. * feature(errorBoundary): Typo refactor. * feature(errorBoundary): Some refactors * feat(errorBoundary): PR comments and rollback agent-preview * doc(changelog): Update changelog * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Refactor props, pr comments. * feat(errorBoundary): Added unit test for error boundary. * feat(errorBoundary): Separated error boundary component of hoc * doc(error-boundary): Fixed and added licenses blocks. * feature(logger-service): PR comments * feature(logger-orchestrator): Refactors on management of severity. * feature(logger-orchestrator): Refactor on wz-blank-screen component. * feature(logger-orchestrator): Separated prompt component from error-boundary. * feature(logger-orchestrator): Typo. * test(error-boundary): Update snapshots. * test(error-boundary): Update snapshots * fix(logger-orchestrator): PR comments and refactors, fix unit tests. * test(error-orchestrator-base): Added simple unit test. Fixed licence block. * test(error-orchestrator-base): Added simple unit test to ErrorOrchestratorCritical * test(error-orchestrator-ui): Added simple unit test to ErrorOrchestratorUi * Create new backend service (#3324) * Add endpoint * Create new backend service * Add changelog * Renamed constants * Added interfaces, created new controller and renamed * Created ui-logged, to prevent logger superclass * Added types, fixed responses types * Added new route file to ui-logs, changed method put to post, added in index,ts * Added test files, we must create all unit tests to those new features * Fixed if condition * Rename tests files, created endpoints test * Changed controller name ui-logs, removed duplicated export * Fixed file comments * Applied prettier formater * Added new base class base-logger * Remove wrong constants and fix errors * test(ui-logger-controller): Added simple unit test. * test(ui-logs-controller): Fix params. * Added test to ui-logs controller * Renamed test files * test(logs-controller): Added mock to function checkFileExist + prettier. * Solve comments * Add copyright and remove unused import Co-authored-by: Ibarra Maximiliano Co-authored-by: gabiwassan * bugfix(error-orchestrator): Added some improvements and fixes. * test(ui-logs-controller): Updated unit test. * fix(error-orchestrator): PR comments * fix(error-orchestrator): PR comments + prettier. Co-authored-by: Maximiliano Ibarra Co-authored-by: Ibarra Maximiliano Co-authored-by: Pablo Martínez --- CHANGELOG.md | 8 + common/constants.ts | 145 +++++++++--- package.json | 3 +- .../error-boundary-prompt.scss | 3 + .../error-boundary-prompt.tsx | 49 ++++ .../error-boundary.test.tsx.snap | 153 +++++++++++++ .../error-boundary/error-boundary.test.tsx | 51 +++++ .../common/error-boundary/error-boundary.tsx | 85 +++++++ .../with-error-boundary.test.tsx.snap | 161 ++++++++++++++ .../with-error-boundary.test.tsx | 31 +++ .../error-boundary/with-error-boundary.tsx | 20 ++ public/components/common/hocs/index.ts | 2 + .../wz-blank-screen/wz-blank-screen.js | 47 ++-- .../agent/components/agents-preview.js | 16 +- public/kibana-services.ts | 10 +- .../error-orchestrator-base.test.ts | 44 ++++ .../error-orchestrator-base.ts | 38 ++++ .../error-orchestrator-business.test.ts | 101 +++++++++ .../error-orchestrator-business.ts | 43 ++++ .../error-orchestrator-critical.test.ts | 44 ++++ .../error-orchestrator-critical.ts | 23 ++ .../error-orchestrator-ui.test.ts | 87 ++++++++ .../error-orchestrator-ui.ts | 34 +++ .../error-orchestrator.factory.ts | 29 +++ .../error-orchestrator.service.ts | 23 ++ .../error-orchestrator/types.ts | 46 ++++ public/react-services/index.ts | 3 +- .../wz-user-permissions.test.ts | 2 +- server/controllers/index.ts | 4 +- server/controllers/wazuh-utils/index.ts | 2 + .../wazuh-utils/ui-logs.controller.test.ts | 64 ++++++ .../wazuh-utils/ui-logs.controller.ts | 92 ++++++++ .../{ => wazuh-utils}/wazuh-utils.ts | 16 +- server/lib/base-logger.ts | 210 ++++++++++++++++++ server/lib/logger.ts | 167 +------------- server/lib/ui-logger.ts | 18 ++ server/routes/index.ts | 3 +- server/routes/wazuh-utils/index.ts | 2 + server/routes/wazuh-utils/ui-logs.test.ts | 42 ++++ server/routes/wazuh-utils/ui-logs.ts | 39 ++++ .../{ => wazuh-utils}/wazuh-utils.test.ts | 0 .../routes/{ => wazuh-utils}/wazuh-utils.ts | 2 +- test/jest/config.js | 3 +- 43 files changed, 1719 insertions(+), 246 deletions(-) create mode 100644 public/components/common/error-boundary-prompt/error-boundary-prompt.scss create mode 100644 public/components/common/error-boundary-prompt/error-boundary-prompt.tsx create mode 100644 public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap create mode 100644 public/components/common/error-boundary/error-boundary.test.tsx create mode 100644 public/components/common/error-boundary/error-boundary.tsx create mode 100644 public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap create mode 100644 public/components/common/hocs/error-boundary/with-error-boundary.test.tsx create mode 100644 public/components/common/hocs/error-boundary/with-error-boundary.tsx create mode 100644 public/react-services/error-orchestrator/error-orchestrator-base.test.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-base.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-business.test.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-business.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-critical.test.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-critical.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-ui.test.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator-ui.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator.factory.ts create mode 100644 public/react-services/error-orchestrator/error-orchestrator.service.ts create mode 100644 public/react-services/error-orchestrator/types.ts create mode 100644 server/controllers/wazuh-utils/index.ts create mode 100644 server/controllers/wazuh-utils/ui-logs.controller.test.ts create mode 100644 server/controllers/wazuh-utils/ui-logs.controller.ts rename server/controllers/{ => wazuh-utils}/wazuh-utils.ts (89%) create mode 100644 server/lib/base-logger.ts create mode 100644 server/lib/ui-logger.ts create mode 100644 server/routes/wazuh-utils/index.ts create mode 100644 server/routes/wazuh-utils/ui-logs.test.ts create mode 100644 server/routes/wazuh-utils/ui-logs.ts rename server/routes/{ => wazuh-utils}/wazuh-utils.test.ts (100%) rename server/routes/{ => wazuh-utils}/wazuh-utils.ts (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9f0e2c74..37f4d4fd2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,19 @@ All notable changes to the Wazuh app project will be documented in this file. ## Wazuh v4.3.0 - Kibana 7.10.2 , 7.11.2 - Revision 4301 +### Added + +- Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) + ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) +### Added +- Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) +- Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added diff --git a/common/constants.ts b/common/constants.ts index bc7cdcdb62..af603b70cb 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -9,12 +9,12 @@ * * Find more information about this on the LICENSE file. */ -import path from 'path'; +import path from 'path'; // Index patterns - Wazuh alerts -export const WAZUH_INDEX_TYPE_ALERTS = "alerts"; -export const WAZUH_ALERTS_PREFIX = "wazuh-alerts-"; -export const WAZUH_ALERTS_PATTERN = "wazuh-alerts-*"; +export const WAZUH_INDEX_TYPE_ALERTS = 'alerts'; +export const WAZUH_ALERTS_PREFIX = 'wazuh-alerts-'; +export const WAZUH_ALERTS_PATTERN = 'wazuh-alerts-*'; // Default number of shards and replicas for indices export const WAZUH_INDEX_SHARDS = 2; @@ -22,10 +22,10 @@ export const WAZUH_INDEX_REPLICAS = 0; // Job - Wazuh monitoring -export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; -export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; -export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; -export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; +export const WAZUH_INDEX_TYPE_MONITORING = 'monitoring'; +export const WAZUH_MONITORING_PREFIX = 'wazuh-monitoring-'; +export const WAZUH_MONITORING_PATTERN = 'wazuh-monitoring-*'; +export const WAZUH_MONITORING_TEMPLATE_NAME = 'wazuh-agent'; export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = WAZUH_INDEX_SHARDS; export const WAZUH_MONITORING_DEFAULT_CREATION = 'd'; export const WAZUH_MONITORING_DEFAULT_ENABLED = true; @@ -34,9 +34,9 @@ export const WAZUH_MONITORING_DEFAULT_CRON_FREQ = '0 * * * * *'; // Job - Wazuh statistics -export const WAZUH_INDEX_TYPE_STATISTICS = "statistics"; -export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; -export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; +export const WAZUH_INDEX_TYPE_STATISTICS = 'statistics'; +export const WAZUH_STATISTICS_DEFAULT_PREFIX = 'wazuh'; +export const WAZUH_STATISTICS_DEFAULT_NAME = 'statistics'; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = WAZUH_INDEX_SHARDS; @@ -55,17 +55,37 @@ export const WAZUH_ROLE_ADMINISTRATOR_ID = 1; export const WAZUH_ROLE_ADMINISTRATOR_NAME = 'administrator'; // Sample data -export const WAZUH_SAMPLE_ALERT_PREFIX = "wazuh-alerts-4.x-"; +export const WAZUH_SAMPLE_ALERT_PREFIX = 'wazuh-alerts-4.x-'; export const WAZUH_SAMPLE_ALERTS_INDEX_SHARDS = 1; export const WAZUH_SAMPLE_ALERTS_INDEX_REPLICAS = 0; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY = "security"; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = "auditing-policy-monitoring"; -export const WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION = "threat-detection"; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY = 'security'; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING = 'auditing-policy-monitoring'; +export const WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION = 'threat-detection'; export const WAZUH_SAMPLE_ALERTS_DEFAULT_NUMBER_ALERTS = 3000; export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { - [WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY]: [{ syscheck: true }, { aws: true }, { gcp: true }, { authentication: true }, { ssh: true }, { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }], - [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [{ rootcheck: true }, { audit: true }, { openscap: true }, { ciscat: true }], - [WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION]: [{ vulnerabilities: true }, { virustotal: true }, { osquery: true }, { docker: true }, { mitre: true }] + [WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY]: [ + { syscheck: true }, + { aws: true }, + { gcp: true }, + { authentication: true }, + { ssh: true }, + { apache: true, alerts: 2000 }, + { web: true }, + { windows: { service_control_manager: true }, alerts: 1000 }, + ], + [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ + { rootcheck: true }, + { audit: true }, + { openscap: true }, + { ciscat: true }, + ], + [WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION]: [ + { vulnerabilities: true }, + { virustotal: true }, + { osquery: true }, + { docker: true }, + { mitre: true }, + ], }; // Security @@ -74,20 +94,20 @@ export const WAZUH_SECURITY_PLUGIN_OPEN_DISTRO_FOR_ELASTICSEARCH = 'Open Distro export const WAZUH_SECURITY_PLUGINS = [ WAZUH_SECURITY_PLUGIN_XPACK_SECURITY, - WAZUH_SECURITY_PLUGIN_OPEN_DISTRO_FOR_ELASTICSEARCH + WAZUH_SECURITY_PLUGIN_OPEN_DISTRO_FOR_ELASTICSEARCH, ]; // Default time filter set by the app export const WAZUH_TIME_FILTER_DEFAULT = { - from: "now-24h", - to: 'now' + from: 'now-24h', + to: 'now', }; //Default max buckets set by the app export const WAZUH_MAX_BUCKETS_DEFAULT = 200000; // App configuration -export const WAZUH_CONFIGURATION_CACHE_TIME = 10000 // time in ms; +export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; export const WAZUH_CONFIGURATION_SETTINGS_NEED_RESTART = [ 'wazuh.monitoring.enabled', 'wazuh.monitoring.frequency', @@ -104,33 +124,54 @@ export const WAZUH_CONFIGURATION_SETTINGS_NEED_HEALTH_CHECK = [ 'cron.statistics.index.creation', 'cron.statistics.index.shards', 'cron.statistics.index.replicas', - 'wazuh.monitoring.shards' -]; -export const WAZUH_CONFIGURATION_SETTINGS_NEED_RELOAD = [ - 'hideManagerAlerts', + 'wazuh.monitoring.shards', ]; +export const WAZUH_CONFIGURATION_SETTINGS_NEED_RELOAD = ['hideManagerAlerts']; // Reserved ids for Users/Role mapping export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; // Wazuh data path const WAZUH_DATA_KIBANA_BASE_PATH = 'data'; -export const WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH = path.join(__dirname, '../../../', WAZUH_DATA_KIBANA_BASE_PATH); +export const WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH = path.join( + __dirname, + '../../../', + WAZUH_DATA_KIBANA_BASE_PATH +); export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, 'wazuh'); // Wazuh data path - config export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); export const WAZUH_DATA_CONFIG_APP_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh.yml'); -export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh-registry.json'); +export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( + WAZUH_DATA_CONFIG_DIRECTORY_PATH, + 'wazuh-registry.json' +); // Wazuh data path - logs export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); -export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, 'wazuhapp-plain.log'); -export const WAZUH_DATA_LOGS_RAW_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, 'wazuhapp.log'); +export const WAZUH_DATA_LOGS_PLAIN_FILENAME = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + 'wazuhapp-plain.log' +); +export const WAZUH_DATA_LOGS_RAW_FILENAME = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + 'wazuhapp.log' +); + +// Wazuh data path - UI logs +export const WAZUH_UI_LOGS_PLAIN_FILENAME = path.join( + WAZUH_DATA_LOGS_DIRECTORY_PATH, + 'wazuh-ui-plain.log' +); +export const WAZUH_UI_LOGS_RAW_FILENAME = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, 'wazuh-ui.log'); // Wazuh data path - downloads export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'downloads'); -export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join(WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, 'reports'); +export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( + WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, + 'reports' +); // Queue export const WAZUH_QUEUE_CRON_FREQ = '*/15 * * * * *'; // Every 15 seconds @@ -181,22 +222,22 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'alerts.sample.prefix': WAZUH_SAMPLE_ALERT_PREFIX, hideManagerAlerts: false, 'logs.level': 'info', - 'enrollment.dns': '' + 'enrollment.dns': '', }; // Wazuh errors export const WAZUH_ERROR_DAEMONS_NOT_READY = 'ERROR3099'; // Agents -export enum WAZUH_AGENTS_OS_TYPE{ +export enum WAZUH_AGENTS_OS_TYPE { WINDOWS = 'windows', LINUX = 'linux', SUNOS = 'sunos', DARWIN = 'darwin', - OTHERS = '' + OTHERS = '', } -export enum WAZUH_MODULES_ID{ +export enum WAZUH_MODULES_ID { SECURITY_EVENTS = 'general', INTEGRITY_MONITORING = 'fim', AMAZON_WEB_SERVICES = 'aws', @@ -215,8 +256,40 @@ export enum WAZUH_MODULES_ID{ TSC = 'tsc', CIS_CAT = 'ciscat', VIRUSTOTAL = 'virustotal', - GDPR = 'gdpr' + GDPR = 'gdpr', } export const AUTHORIZED_AGENTS = 'authorized-agents'; export const HEALTH_CHECK = 'health-check'; + +// Health check +export const HEALTH_CHECK_REDIRECTION_TIME = 300; //ms + +// Kibana settings +// Default timeFilter set by the app +export const WAZUH_KIBANA_SETTING_TIME_FILTER = { + from: 'now-24h', + to: 'now', +}; +export const KIBANA_SETTING_NAME_TIME_FILTER = 'timepicker:timeDefaults'; + +// Default maxBuckets set by the app +export const WAZUH_KIBANA_SETTING_MAX_BUCKETS = 200000; +export const KIBANA_SETTING_NAME_MAX_BUCKETS = 'timelion:max_buckets'; + +// Default metaFields Kibana setting set by the app +export const WAZUH_KIBANA_SETTING_METAFIELDS = ['_source', '_index']; +export const KIBANA_SETTING_NAME_METAFIELDS = 'metaFields'; + +// Logger +export const UI_LOGGER_LEVELS = { + WARNING: 'WARNING', + INFO: 'INFO', + ERROR: 'ERROR', +}; + +export const UI_TOAST_COLOR = { + SUCCESS: 'success', + WARNING: 'warning', + DANGER: 'danger', +}; diff --git a/package.json b/package.json index 197ab09459..1e189b4f85 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "test:browser": "plugin-helpers test:browser", "test:jest": "node scripts/jest", "generate:api-4.0-info": "cd scripts/generate-api-4.0-info;./generate-api-4.0-info.sh;cd ../..", - "prebuild": "node scripts/generate-build-version" + "prebuild": "node scripts/generate-build-version" }, "dependencies": { "angular-animate": "1.7.8", @@ -48,6 +48,7 @@ "js2xmlparser": "^3.0.0", "json2csv": "^4.1.2", "jwt-decode": "^2.2.0", + "loglevel": "^1.7.1", "needle": "^2.0.1", "node-cron": "^1.1.2", "pdfmake": "0.1.65", diff --git a/public/components/common/error-boundary-prompt/error-boundary-prompt.scss b/public/components/common/error-boundary-prompt/error-boundary-prompt.scss new file mode 100644 index 0000000000..896b82f7cc --- /dev/null +++ b/public/components/common/error-boundary-prompt/error-boundary-prompt.scss @@ -0,0 +1,3 @@ +.wz-error-boundary__details { + white-space: pre-wrap; +} diff --git a/public/components/common/error-boundary-prompt/error-boundary-prompt.tsx b/public/components/common/error-boundary-prompt/error-boundary-prompt.tsx new file mode 100644 index 0000000000..b570b01baa --- /dev/null +++ b/public/components/common/error-boundary-prompt/error-boundary-prompt.tsx @@ -0,0 +1,49 @@ +/* + * Wazuh app - React Prompt handles rendering errors. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import './error-boundary-prompt.scss'; + +export const ErrorComponentPrompt = (props: { + errorTitle: any; + errorInfo?: any; + style?: any; + action?: React.ReactNode; +}) => { + const styles = { + error: { + borderTop: '1px solid #777', + borderBottom: '1px solid #777', + padding: '12px', + }, + }; + + return ( + Something went wrong.} + body={ +
+
+ {props.errorTitle && props.errorTitle.toString()} +
+ {props.errorInfo?.componentStack || ''} +
+
+ } + actions={props.action || null} + /> + ); +}; diff --git a/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap b/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap new file mode 100644 index 0000000000..0c0bd88dd0 --- /dev/null +++ b/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap @@ -0,0 +1,153 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ErrorBoundary component renders correctly to match the snapshot 1`] = ` + + + +
+ + Error: I crashed! I crash very hard + +
+ + + in ComponentWithError + in ErrorBoundary (created by WrapperComponent) + in WrapperComponent + +
+ + } + iconType="faceSad" + title={ +

+ Something went wrong. +

+ } + > +
+ + + + + + +
+ + + + +

+ Something went wrong. +

+
+ +
+ + +
+
+
+ + Error: I crashed! I crash very hard + +
+ + + in ComponentWithError + in ErrorBoundary (created by WrapperComponent) + in WrapperComponent + +
+
+
+
+ + +
+ + + +`; diff --git a/public/components/common/error-boundary/error-boundary.test.tsx b/public/components/common/error-boundary/error-boundary.test.tsx new file mode 100644 index 0000000000..cd5ca53a33 --- /dev/null +++ b/public/components/common/error-boundary/error-boundary.test.tsx @@ -0,0 +1,51 @@ +/* + * Wazuh app - React test for ErrorBoundary component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import ErrorBoundary from './error-boundary'; + +jest.mock('loglevel'); + +describe('ErrorBoundary component', () => { + const ComponentWithError = () => { + throw new Error('I crashed! I crash very hard'); + return <>; + }; + + it('renders correctly to match the snapshot', () => { + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should display an ErrorMessage if wrapped component throws', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiTitle').exists()).toBeTruthy(); + expect(wrapper.find('EuiText').exists('details')).toBeTruthy(); + expect(wrapper.find('EuiTitle').find('h2').text().trim()).toBe('Something went wrong.'); + expect(wrapper.find('EuiText').find('details').find('span').at(0).text()).toBe( + 'Error: I crashed! I crash very hard' + ); + }); +}); diff --git a/public/components/common/error-boundary/error-boundary.tsx b/public/components/common/error-boundary/error-boundary.tsx new file mode 100644 index 0000000000..395f9f3152 --- /dev/null +++ b/public/components/common/error-boundary/error-boundary.tsx @@ -0,0 +1,85 @@ +/* + * Wazuh app - React component for catch and handles rendering errors. + * + * Copyright (C) 2015-2021 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + * + */ + +import React, { Component } from 'react'; +import loglevel from 'loglevel'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { ErrorOrchestratorService } from '../../../react-services'; +import { ErrorComponentPrompt } from '../error-boundary-prompt/error-boundary-prompt'; + +export default class ErrorBoundary extends Component { + private logger: ErrorOrchestratorService; + constructor(props) { + super(props); + this.state = { errorTitle: null, errorInfo: null, style: null }; + this.context = this.constructor.displayName || this.constructor.name || undefined; + this.logger = new ErrorOrchestratorService(); + } + + componentDidCatch = (errorTitle, errorInfo) => catchFunc(errorTitle, errorInfo, this); + + render() { + const { errorTitle, style, errorInfo }: Readonly = this.state; + + if (errorInfo) { + return ; + } + return this.props.children; + } +} + +const catchFunc = (errorTitle, errorInfo, ctx) => { + try { + ctx.setState({ + errorTitle: errorTitle, + errorInfo: errorInfo, + }); + + const options: UIErrorLog = { + context: ctx.context, + level: UI_LOGGER_LEVELS.WARNING as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + display: false, + store: true, + error: { + error: errorTitle.name, + message: errorTitle.message, + title: errorTitle.toString(), + }, + }; + + ctx.logger.handleError(options); + } catch (error) { + const optionsCatch: UIErrorLog = { + context: ctx.context, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + display: false, + store: true, + error: { + error: error, + message: error?.message || '', + title: '', + }, + }; + + ctx.logger.handleError(optionsCatch); + } +}; diff --git a/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap b/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap new file mode 100644 index 0000000000..24212ec32e --- /dev/null +++ b/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withErrorBoundary hoc implementation renders correctly to match the snapshot 1`] = ` + + + + +
+ + Error: I crashed! I crash very hard + +
+ + + in ComponentWithError + in Unknown + in ErrorBoundary + in Unknown (created by WrapperComponent) + in WrapperComponent + +
+
+ } + iconType="faceSad" + title={ +

+ Something went wrong. +

+ } + > +
+ + + + + + +
+ + + + +

+ Something went wrong. +

+
+ +
+ + +
+
+
+ + Error: I crashed! I crash very hard + +
+ + + in ComponentWithError + in Unknown + in ErrorBoundary + in Unknown (created by WrapperComponent) + in WrapperComponent + +
+
+
+
+ + +
+ + + + +`; diff --git a/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx b/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx new file mode 100644 index 0000000000..6b6e7c37c6 --- /dev/null +++ b/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { withErrorBoundary } from './with-error-boundary'; +import { mount } from 'enzyme'; + +jest.mock('loglevel'); + +describe('withErrorBoundary hoc implementation', () => { + const ComponentWithError = () => { + throw new Error('I crashed! I crash very hard'); + return <>; + }; + + it('renders correctly to match the snapshot', () => { + const ErrorComponentWithHoc = withErrorBoundary(() => ); + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should display an ErrorMessage if wrapped HOC throws', () => { + const ErrorComponentWithHoc = withErrorBoundary(() => ); + const wrapper = mount(); + + expect(wrapper.find('EuiTitle').exists()).toBeTruthy(); + expect(wrapper.find('EuiText').exists('details')).toBeTruthy(); + expect(wrapper.find('EuiTitle').find('h2').text().trim()).toBe('Something went wrong.'); + expect(wrapper.find('EuiText').find('details').find('span').at(0).text()).toBe( + 'Error: I crashed! I crash very hard' + ); + }); +}); diff --git a/public/components/common/hocs/error-boundary/with-error-boundary.tsx b/public/components/common/hocs/error-boundary/with-error-boundary.tsx new file mode 100644 index 0000000000..5ddab9f13f --- /dev/null +++ b/public/components/common/hocs/error-boundary/with-error-boundary.tsx @@ -0,0 +1,20 @@ +/* + * Wazuh app - React HOCs handles rendering errors + * Copyright (C) 2015-2021 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 from 'react'; +import ErrorBoundary from '../../error-boundary/error-boundary'; + +export const withErrorBoundary = (WrappedComponent) => (props) => ( + + + +); diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index 55eae872df..81653072c2 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -30,3 +30,5 @@ export { withButtonOpenOnClick } from './withButtonOpenOnClick'; export { withAgentSupportModule } from './withAgentSupportModule'; export { withUserLogged } from './withUserLogged'; + +export { withErrorBoundary } from './error-boundary/with-error-boundary'; diff --git a/public/components/wz-blank-screen/wz-blank-screen.js b/public/components/wz-blank-screen/wz-blank-screen.js index b919c68af1..89e087c385 100644 --- a/public/components/wz-blank-screen/wz-blank-screen.js +++ b/public/components/wz-blank-screen/wz-blank-screen.js @@ -9,14 +9,9 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; -import { - EuiPage, - EuiPageContent, - EuiEmptyPrompt, - EuiButton, - EuiHorizontalRule -} from '@elastic/eui'; +import React, { Component } from 'react'; +import { EuiButton, EuiHorizontalRule, EuiPage, EuiPageContent } from '@elastic/eui'; +import { ErrorComponentPrompt } from '../common/error-boundary-prompt/error-boundary-prompt'; export class WzBlankScreen extends Component { constructor(props) { @@ -25,32 +20,32 @@ export class WzBlankScreen extends Component { } render() { + const elasticGuideUrl = + 'https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html'; + const wazuhTroubleshootingUrl = + 'https://documentation.wazuh.com/current/user-manual/kibana-app/troubleshooting.html'; + return ( - {this.props.errorToShow || 'Something went wrong'}} - body={ - - +

- - https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html - + Elastic guide +

- - https://documentation.wazuh.com/current/installation-guide/ - + Wazuh installation guide

-
- } - actions={ - - Refresh - + + + Refresh + + } />
diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index 9cb2c4ff5a..dbc8f9cdbf 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -120,7 +120,7 @@ export const AgentsPreview = compose( this.setState({ platforms: platformsModel, loading: false }); } catch (error) {} } - + removeFilters(){ this._isMount && this.setState({agentTableFilters: []}) } @@ -164,7 +164,7 @@ export const AgentsPreview = compose( {this.totalAgents > 0 && ( - + {this.summary && ( @@ -236,7 +236,7 @@ export const AgentsPreview = compose( - + this.props.tableProps.showAgent( this.lastAgent )}>{this.lastAgent.name} @@ -271,7 +271,7 @@ export const AgentsPreview = compose( style={{ whiteSpace: 'nowrap' }} titleSize="s" description="Most active agent" - titleColor="primary" + titleColor="primary" /> )} @@ -298,13 +298,13 @@ export const AgentsPreview = compose(
{this.props.resultState === 'loading' && ( -
+
) } - + - + - + )} diff --git a/public/kibana-services.ts b/public/kibana-services.ts index 33fa421690..c7167d09b4 100644 --- a/public/kibana-services.ts +++ b/public/kibana-services.ts @@ -16,6 +16,7 @@ import { DataPublicPluginStart } from '../../../src/plugins/data/public'; import { VisualizationsStart } from '../../../src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { AppPluginStartDependencies } from './types'; +import { ErrorOrchestrator } from '../common/constants'; export const [getCore, setCore] = createGetterSetter('Core'); export const [getPlugins, setPlugins] = createGetterSetter('Plugins'); @@ -23,7 +24,9 @@ export const [getToasts, setToasts] = createGetterSetter('Toasts'); export const [getHttp, setHttp] = createGetterSetter('Http'); export const [getUiSettings, setUiSettings] = createGetterSetter('UiSettings'); export const [getChrome, setChrome] = createGetterSetter('Chrome'); -export const [getScopedHistory, setScopedHistory] = createGetterSetter('ScopedHistory'); +export const [getScopedHistory, setScopedHistory] = createGetterSetter( + 'ScopedHistory' +); export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getSavedObjects, setSavedObjects] = createGetterSetter( 'SavedObjects' @@ -37,6 +40,9 @@ export const [getVisualizationsPlugin, setVisualizationsPlugin] = createGetterSe export const [getNavigationPlugin, setNavigationPlugin] = createGetterSetter< NavigationPublicPluginStart >('NavigationPlugin'); +export const [getErrorOrchestrator, setErrorOrchestrator] = createGetterSetter( + 'ErrorOrchestrator' +); /** * set bootstrapped inner angular module @@ -66,4 +72,4 @@ export function getDiscoverModule() { return discoverModule; } -export const [getCookies, setCookies] = createGetterSetter('Cookies'); \ No newline at end of file +export const [getCookies, setCookies] = createGetterSetter('Cookies'); diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.test.ts b/public/react-services/error-orchestrator/error-orchestrator-base.test.ts new file mode 100644 index 0000000000..b90361c916 --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-base.test.ts @@ -0,0 +1,44 @@ +/* + * Wazuh app - Unit test for ErrorOrchestratorBase. + * + * Copyright (C) 2015-2021 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 { ErrorOrchestratorBase } from './error-orchestrator-base'; +import { ErrorOrchestrator, UIErrorLog } from './types'; + +describe('Wazuh Error Orchestrator Base', () => { + describe('Given a valid options params ', () => { + it('Should be called displayError and storeError', () => { + const options: UIErrorLog = { + context: 'unitTest', + level: 'ERROR', + severity: 'UI', + display: true, + store: true, + error: { + error: 'error name test1', + message: 'message test1', + title: 'title jest testing1', + }, + }; + const errorOrchestratorBase: ErrorOrchestrator = new ErrorOrchestratorBase(); + const mockDisplayError = (ErrorOrchestratorBase.prototype.displayError = jest.fn()); + const mockStoreError = jest.spyOn(ErrorOrchestratorBase.prototype as any, 'storeError'); + mockStoreError.mockImplementation(() => {}) + + errorOrchestratorBase.loadErrorLog(options); + + expect(mockDisplayError).toBeCalledTimes(1); + expect(mockStoreError).toBeCalledTimes(1); + }); + }); +}); diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.ts b/public/react-services/error-orchestrator/error-orchestrator-base.ts new file mode 100644 index 0000000000..dc585ec3ec --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-base.ts @@ -0,0 +1,38 @@ +/* + * Wazuh app - Error Orchestrator base + * Copyright (C) 2015-2021 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 loglevel from 'loglevel'; +import { GenericRequest } from '../../react-services/generic-request'; +import { ErrorOrchestrator, UIErrorLog } from './types'; + +export class ErrorOrchestratorBase implements ErrorOrchestrator { + public loadErrorLog(errorLog: UIErrorLog) { + if (errorLog.display) this.displayError(errorLog); + if (errorLog.store) this.storeError(errorLog); + } + + public displayError(errorLog: UIErrorLog) { + throw new Error('Should be implemented!'); + } + + private async storeError(errorLog: UIErrorLog) { + try { + await GenericRequest.request('POST', `/utils/logs/ui`, { + message: errorLog.error.message, + level: errorLog.level, + location: errorLog.location, + }); + } catch (error) { + loglevel.error('Failed on request [POST /utils/logs/ui]', error); + } + } +} diff --git a/public/react-services/error-orchestrator/error-orchestrator-business.test.ts b/public/react-services/error-orchestrator/error-orchestrator-business.test.ts new file mode 100644 index 0000000000..c34a2133ab --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-business.test.ts @@ -0,0 +1,101 @@ +/* + * Wazuh app - Unit test for ErrorOrchestratorBusiness. + * + * Copyright (C) 2015-2021 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 { ErrorOrchestrator, UIErrorLog } from './types'; +import { getToasts } from '../../kibana-services'; +import { ErrorOrchestratorBusiness } from './error-orchestrator-business'; +import { ErrorToastOptions } from 'kibana/public'; + +const options: UIErrorLog = { + context: 'unitTest', + level: 'INFO', + severity: 'BUSINESS', + display: true, + store: false, + error: { + error: 'Testing toast INFO', + message: 'Message toast INFO', + title: 'title jest testing1', + }, +}; + +jest.mock('../../kibana-services', () => ({ + getToasts: () => ({ + addInfo: (mockError: string, toast: ErrorToastOptions) => {}, + }), +})); + +describe('Wazuh Error Orchestrator Business', () => { + describe('Given a valid options params for display toast INFO', () => { + it('Should be called toast and addInfo', () => { + const toast = { + title: 'title jest testing1', + toastMessage: 'Message loglevel INFO', + toastLifeTimeMs: 3000, + }; + + const mockError = 'Testing loglevel INFO'; + const mockMessage = 'Message loglevel INFO'; + const mockToastInfo = getToasts.prototype.addInfo = jest.fn(); + // const mockToastInfo = getToasts().addInfo(mockError, toast as ErrorToastOptions) = jest.fn(); + + const errorOrchestratorBusiness: ErrorOrchestrator = new ErrorOrchestratorBusiness(); + errorOrchestratorBusiness.loadErrorLog(options); + + expect(mockToastInfo.getToasts().addInfo).toBeCalled(); + expect(mockToastInfo).toBeCalledWith(mockMessage, mockError); + expect(mockToastInfo).toBeCalledTimes(1); + }); + }); + + // describe('Given a valid options params for display toast WARNING', () => { + // it('Should be called loglevelWarning', () => { + // options.level = 'WARNING'; + // options.error.error = 'Testing loglevel WARNING'; + // options.error.message = 'Message loglevel WARNING'; + // + // const mockError = 'Testing loglevel WARNING'; + // const mockMessage = 'Message loglevel WARNING'; + // + // const mockLoglevelWarning = loglevel.warn = jest.fn(); + // + // const errorOrchestratorUI: ErrorOrchestrator = new ErrorOrchestratorUI(); + // errorOrchestratorUI.loadErrorLog(options); + // + // expect(mockLoglevelWarning).toBeCalled(); + // expect(mockLoglevelWarning).toBeCalledWith(mockMessage, mockError); + // expect(mockLoglevelWarning).toBeCalledTimes(1); + // }); + // }); + // + // describe('Given a valid options params for display toast ERROR', () => { + // it('Should be called loglevelError', () => { + // options.level = 'ERROR'; + // options.error.error = 'Testing loglevel ERROR'; + // options.error.message = 'Message loglevel ERROR'; + // + // const mockError = 'Testing loglevel ERROR'; + // const mockMessage = 'Message loglevel ERROR'; + // + // const mockLoglevelError = loglevel.error = jest.fn(); + // + // const errorOrchestratorUI: ErrorOrchestrator = new ErrorOrchestratorUI(); + // errorOrchestratorUI.loadErrorLog(options); + // + // expect(mockLoglevelError).toBeCalled(); + // expect(mockLoglevelError).toBeCalledWith(mockMessage, mockError); + // expect(mockLoglevelError).toBeCalledTimes(1); + // }); + // }); +}); diff --git a/public/react-services/error-orchestrator/error-orchestrator-business.ts b/public/react-services/error-orchestrator/error-orchestrator-business.ts new file mode 100644 index 0000000000..13ecf0c326 --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-business.ts @@ -0,0 +1,43 @@ +/* + * Wazuh app - Error Orchestrator for business implementation + * Copyright (C) 2015-2021 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 { ErrorOrchestratorBase } from './error-orchestrator-base'; +import { UIErrorLog } from './types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { getToasts } from '../../kibana-services'; +import { ErrorToastOptions } from 'kibana/public'; + +export class ErrorOrchestratorBusiness extends ErrorOrchestratorBase { + public displayError(errorLog: UIErrorLog) { + const toast = { + error: errorLog.error, + title: errorLog.error.title, + toastMessage: errorLog.error.message, + message: errorLog.error.message, + toastLifeTimeMs: 3000, + }; + + switch (errorLog.level) { + case UI_LOGGER_LEVELS.INFO: + getToasts().addInfo(errorLog.error.error, toast as ErrorToastOptions); + break; + case UI_LOGGER_LEVELS.WARNING: + getToasts().addWarning(errorLog.error.error, toast as ErrorToastOptions); + break; + case UI_LOGGER_LEVELS.ERROR: + getToasts().addError(errorLog.error.error, toast as ErrorToastOptions); + break; + default: + console.log('No error level', errorLog.error.message, errorLog.error.error); + } + } +} diff --git a/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts b/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts new file mode 100644 index 0000000000..87531d7aae --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts @@ -0,0 +1,44 @@ +/* + * Wazuh app - Unit test for ErrorOrchestratorCritical. + * + * Copyright (C) 2015-2021 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 { ErrorOrchestrator, UIErrorLog } from './types'; +import { ErrorOrchestratorCritical } from './error-orchestrator-critical'; +import { WzMisc } from '../../factories/misc'; + +describe('Wazuh Error Orchestrator Critical', () => { + describe('Given a valid options params ', () => { + it('Should be called mockSetBlankScr and redirect to BlankScreen', () => { + const options: UIErrorLog = { + context: 'unitTest', + level: 'ERROR', + severity: 'UI', + display: true, + store: false, + error: { + error: 'error name test1', + message: 'message test1', + title: 'title jest testing1', + }, + }; + + const mockSetBlankScr = (WzMisc.prototype.setBlankScr = jest.fn()); + + const errorOrchestratorCritical: ErrorOrchestrator = new ErrorOrchestratorCritical(); + errorOrchestratorCritical.loadErrorLog(options); + + expect(mockSetBlankScr).toBeCalledTimes(1); + expect(window.location.href).toEqual('http://localhost/#/blank-screen'); + }); + }); +}); diff --git a/public/react-services/error-orchestrator/error-orchestrator-critical.ts b/public/react-services/error-orchestrator/error-orchestrator-critical.ts new file mode 100644 index 0000000000..7ca96fbe4a --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-critical.ts @@ -0,0 +1,23 @@ +/* + * Wazuh app - Error Orchestrator for critical implementation + * Copyright (C) 2015-2021 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 { ErrorOrchestratorBase } from './error-orchestrator-base'; +import { UIErrorLog } from './types'; +import { WzMisc } from '../../factories/misc'; + +export class ErrorOrchestratorCritical extends ErrorOrchestratorBase { + public displayError(errorLog: UIErrorLog) { + const wzMisc = new WzMisc(); + wzMisc.setBlankScr(errorLog.error.message); + window.location.href = '#/blank-screen'; + } +} diff --git a/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts b/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts new file mode 100644 index 0000000000..5a795a60f2 --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-ui.test.ts @@ -0,0 +1,87 @@ +/* + * Wazuh app - Unit test for ErrorOrchestratorUI. + * + * Copyright (C) 2015-2021 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 { ErrorOrchestrator, UIErrorLog } from './types'; +import { ErrorOrchestratorUI } from './error-orchestrator-ui'; +import loglevel from 'loglevel'; + +const options: UIErrorLog = { + context: 'unitTest', + level: 'INFO', + severity: 'UI', + display: true, + store: false, + error: { + error: 'Testing loglevel INFO', + message: 'Message loglevel INFO', + title: 'title jest testing1', + }, +}; + +describe('Wazuh Error Orchestrator UI', () => { + describe('Given a valid options params for log INFO', () => { + it('Should be called loglevelInfo', () => { + const mockLoglevelInfo = loglevel.info = jest.fn(); + const mockError = 'Testing loglevel INFO'; + const mockMessage = 'Message loglevel INFO'; + + const errorOrchestratorUI: ErrorOrchestrator = new ErrorOrchestratorUI(); + errorOrchestratorUI.loadErrorLog(options); + + expect(mockLoglevelInfo).toBeCalled(); + expect(mockLoglevelInfo).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelInfo).toBeCalledTimes(1); + }); + }); + + describe('Given a valid options params for log WARNING', () => { + it('Should be called loglevelWarning', () => { + options.level = 'WARNING'; + options.error.error = 'Testing loglevel WARNING'; + options.error.message = 'Message loglevel WARNING'; + + const mockError = 'Testing loglevel WARNING'; + const mockMessage = 'Message loglevel WARNING'; + + const mockLoglevelWarning = loglevel.warn = jest.fn(); + + const errorOrchestratorUI: ErrorOrchestrator = new ErrorOrchestratorUI(); + errorOrchestratorUI.loadErrorLog(options); + + expect(mockLoglevelWarning).toBeCalled(); + expect(mockLoglevelWarning).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelWarning).toBeCalledTimes(1); + }); + }); + + describe('Given a valid options params for log ERROR', () => { + it('Should be called loglevelError', () => { + options.level = 'ERROR'; + options.error.error = 'Testing loglevel ERROR'; + options.error.message = 'Message loglevel ERROR'; + + const mockError = 'Testing loglevel ERROR'; + const mockMessage = 'Message loglevel ERROR'; + + const mockLoglevelError = loglevel.error = jest.fn(); + + const errorOrchestratorUI: ErrorOrchestrator = new ErrorOrchestratorUI(); + errorOrchestratorUI.loadErrorLog(options); + + expect(mockLoglevelError).toBeCalled(); + expect(mockLoglevelError).toBeCalledWith(mockMessage, mockError); + expect(mockLoglevelError).toBeCalledTimes(1); + }); + }); +}); diff --git a/public/react-services/error-orchestrator/error-orchestrator-ui.ts b/public/react-services/error-orchestrator/error-orchestrator-ui.ts new file mode 100644 index 0000000000..03d992b110 --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator-ui.ts @@ -0,0 +1,34 @@ +/* + * Wazuh app - Error Orchestrator for UI implementation + * Copyright (C) 2015-2021 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 { ErrorOrchestratorBase } from './error-orchestrator-base'; +import { UIErrorLog } from './types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import loglevel from 'loglevel'; + +export class ErrorOrchestratorUI extends ErrorOrchestratorBase { + public displayError(errorLog: UIErrorLog) { + switch (errorLog.level) { + case UI_LOGGER_LEVELS.INFO: + loglevel.info(errorLog.error.message, errorLog.error.error); + break; + case UI_LOGGER_LEVELS.WARNING: + loglevel.warn(errorLog.error.message, errorLog.error.error); + break; + case UI_LOGGER_LEVELS.ERROR: + loglevel.error(errorLog.error.message, errorLog.error.error); + break; + default: + console.log('No error level', errorLog.error.message, errorLog.error.error); + } + } +} diff --git a/public/react-services/error-orchestrator/error-orchestrator.factory.ts b/public/react-services/error-orchestrator/error-orchestrator.factory.ts new file mode 100644 index 0000000000..1d71990a7b --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator.factory.ts @@ -0,0 +1,29 @@ +/* + * Wazuh app - Error Orchestrator factory + * Copyright (C) 2015-2021 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 { ErrorOrchestratorUI } from './error-orchestrator-ui'; +import { ErrorOrchestratorBusiness } from './error-orchestrator-business'; +import { ErrorOrchestratorCritical } from './error-orchestrator-critical'; +import { ErrorOrchestrator, UI_ERROR_SEVERITIES, UIErrorSeverity } from './types'; + +export const errorOrchestratorFactory = (severity: UIErrorSeverity): ErrorOrchestrator => { + switch (severity) { + case UI_ERROR_SEVERITIES.UI: + return new ErrorOrchestratorUI(); + case UI_ERROR_SEVERITIES.BUSINESS: + return new ErrorOrchestratorBusiness(); + case UI_ERROR_SEVERITIES.CRITICAL: + return new ErrorOrchestratorCritical(); + default: + break; + } +}; diff --git a/public/react-services/error-orchestrator/error-orchestrator.service.ts b/public/react-services/error-orchestrator/error-orchestrator.service.ts new file mode 100644 index 0000000000..5537693d32 --- /dev/null +++ b/public/react-services/error-orchestrator/error-orchestrator.service.ts @@ -0,0 +1,23 @@ +/* + * Wazuh app - Error Orchestrator service + * Copyright (C) 2015-2021 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 { errorOrchestratorFactory } from './error-orchestrator.factory'; +import { ErrorOrchestrator, UIErrorLog } from './types'; + +export class ErrorOrchestratorService { + public constructor() {} + + public handleError(uiErrorLog: UIErrorLog) { + const errorOrchestrator: ErrorOrchestrator = errorOrchestratorFactory(uiErrorLog.severity); + errorOrchestrator.loadErrorLog(uiErrorLog); + } +} diff --git a/public/react-services/error-orchestrator/types.ts b/public/react-services/error-orchestrator/types.ts new file mode 100644 index 0000000000..cbe593a7de --- /dev/null +++ b/public/react-services/error-orchestrator/types.ts @@ -0,0 +1,46 @@ +/* + * Wazuh app - Error logger types + * Copyright (C) 2015-2021 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. + */ + +type WARNING = 'WARNING'; +type INFO = 'INFO'; +type ERROR = 'ERROR'; +export type UILogLevel = WARNING | INFO | ERROR; + +type UI = 'UI'; +type BUSINESS = 'BUSINESS'; +type CRITICAL = 'CRITICAL'; +export type UIErrorSeverity = UI | BUSINESS | CRITICAL; +export const UI_ERROR_SEVERITIES = { + UI: 'UI', + BUSINESS: 'BUSINESS', + CRITICAL: 'CRITICAL', +}; + +export type UIError = { + message: string; + error: any; + title?: string; +}; + +export type UIErrorLog = { + context: string; + level: UILogLevel; + severity: UIErrorSeverity; + display?: boolean; + store?: boolean; + error: UIError; + location?: string; +}; + +export type ErrorOrchestrator = { + loadErrorLog: (uiErrorLog: UIErrorLog) => void; +}; diff --git a/public/react-services/index.ts b/public/react-services/index.ts index a10bf3b43d..0b3f7801c1 100644 --- a/public/react-services/index.ts +++ b/public/react-services/index.ts @@ -1,4 +1,5 @@ export { GenericRequest } from './generic-request'; export { WzRequest } from './wz-request'; export { ErrorHandler } from './error-handler'; -export { formatUIDate } from './time-service'; \ No newline at end of file +export { formatUIDate } from './time-service'; +export { ErrorOrchestratorService } from './error-orchestrator/error-orchestrator.service'; diff --git a/public/react-services/wz-user-permissions.test.ts b/public/react-services/wz-user-permissions.test.ts index fe2cb6f8a5..b204c83b39 100644 --- a/public/react-services/wz-user-permissions.test.ts +++ b/public/react-services/wz-user-permissions.test.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook for get query of Kibana searchBar + * Wazuh app - React test for wz-user-permissions * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 591a8f3fe4..6908244925 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -11,6 +11,6 @@ */ export { WazuhElasticCtrl } from './wazuh-elastic'; export { WazuhApiCtrl } from './wazuh-api'; -export { WazuhUtilsCtrl } from './wazuh-utils'; export { WazuhReportingCtrl } from './wazuh-reporting'; -export { WazuhHostsCtrl } from './wazuh-hosts' +export { WazuhHostsCtrl } from './wazuh-hosts'; +export * from './wazuh-utils'; diff --git a/server/controllers/wazuh-utils/index.ts b/server/controllers/wazuh-utils/index.ts new file mode 100644 index 0000000000..ecac9686fe --- /dev/null +++ b/server/controllers/wazuh-utils/index.ts @@ -0,0 +1,2 @@ +export * from './wazuh-utils'; +export * from './ui-logs.controller'; diff --git a/server/controllers/wazuh-utils/ui-logs.controller.test.ts b/server/controllers/wazuh-utils/ui-logs.controller.test.ts new file mode 100644 index 0000000000..5553f4bef1 --- /dev/null +++ b/server/controllers/wazuh-utils/ui-logs.controller.test.ts @@ -0,0 +1,64 @@ +import { UiLogsCtrl } from './ui-logs.controller'; +import { WAZUH_UI_LOGS_RAW_FILENAME } from '../../../common/constants'; +import uiLogger from '../../lib/ui-logger'; + +const readLastLines = require('read-last-lines'); + +const buildMockResponse = () => { + const res = {}; + res.ok = jest.fn().mockReturnValue(res); + return res; +}; + +const buildMockRequest = () => { + const req = {}; + req.body = jest.fn().mockReturnValue(req); + req.params = jest.fn().mockReturnValue(req); + return req; +}; + +describe('Spec UiLogsCtrl', function () { + describe('Check method getUiLogs ', () => { + it('Should 200 and return correct value', async () => { + const result = { body: { error: 0, rawLogs: ['my test mocked'] } }; + const mockResponse = buildMockResponse(); + jest.spyOn(readLastLines, 'read').mockReturnValue('my test mocked'); + jest.spyOn(uiLogger, 'checkFileExist').mockReturnValue(true); + + const controller = new UiLogsCtrl(); + await controller.getUiLogs(mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls.length).toBe(1); + expect(mockResponse.ok).toHaveBeenCalledWith(result); + }); + + it('Should 200 and return message Log has been added', async () => { + const result = { body: { error: 0, message: 'Log has been added', statusCode: 200 } }; + const mockResponse = buildMockResponse(); + jest.spyOn(readLastLines, 'read').mockReturnValue('Log has been added'); + jest.spyOn(uiLogger, 'checkFileExist').mockReturnValue(true); + + const mockRequest = buildMockRequest(); + mockRequest.body = { + level: 'error', + message: 'Message example', + location: 'Location example', + }; + + const controller = new UiLogsCtrl(); + await controller.createUiLogs(mockRequest, mockResponse); + + expect(mockResponse.ok).toHaveBeenCalledTimes(1); + expect(mockResponse.ok.mock.calls.length).toBe(1); + expect(mockResponse.ok).toHaveBeenCalledWith(result); + }); + + it('Should return a Array logs', async () => { + const controller = new UiLogsCtrl(); + let res = await controller.getUiFileLogs(WAZUH_UI_LOGS_RAW_FILENAME); + + expect(Array.isArray(res)).toBe(true); + }); + }); +}); diff --git a/server/controllers/wazuh-utils/ui-logs.controller.ts b/server/controllers/wazuh-utils/ui-logs.controller.ts new file mode 100644 index 0000000000..f2c7cc184d --- /dev/null +++ b/server/controllers/wazuh-utils/ui-logs.controller.ts @@ -0,0 +1,92 @@ +/* + * Wazuh app - Class for UI Logs functions + * Copyright (C) 2015-2021 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. + */ + +// Require some libraries +import { ErrorResponse } from '../../lib/error-response'; +import { read } from 'read-last-lines'; +import { WAZUH_UI_LOGS_RAW_FILENAME } from '../../../common/constants'; +import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; +import uiLogger from '../../lib/ui-logger'; + +export class UiLogsCtrl { + /** + * Constructor + * @param {*} server + */ + constructor() {} + + /** + * Returns Wazuh ui logs + * @param {Object} response + * @returns {Array} app logs or ErrorResponse + */ + async getUiLogs(response: KibanaResponseFactory) { + try { + return uiLogger.initDirectory().then(async () => { + if (!uiLogger.checkFileExist(WAZUH_UI_LOGS_RAW_FILENAME)) { + return response.ok({ + body: { + error: 0, + rawLogs: [], + }, + }); + } else { + let arrayLog = await this.getUiFileLogs(WAZUH_UI_LOGS_RAW_FILENAME); + return response.ok({ + body: { + error: 0, + rawLogs: arrayLog.filter((item) => typeof item === 'string' && item.length), + }, + }); + } + }); + } catch (error) { + return ErrorResponse(error.message || error, 3036, 500, response); + } + } + + /** + * Add new UI Log entry in ui logs file + * @param request + * @param response + * @returns success message or ErrorResponse + */ + async createUiLogs(request: KibanaRequest, response: KibanaResponseFactory) { + try { + const { location, message, level } = request.body; + await uiLogger.log(location, message, level); + return response.ok({ + body: { + statusCode: 200, + error: 0, + message: 'Log has been added', + }, + }); + } catch (error) { + return ErrorResponse(error.message || error, 3021, 500, response); + } + } + + /** + * Get UI logs from specific log file + * @param filepath + * @returns Array + */ + async getUiFileLogs(filepath) { + try { + const lastLogs = await read(filepath, 50); + return lastLogs.split('\n'); + } catch (err) { + throw err; + } + } +} diff --git a/server/controllers/wazuh-utils.ts b/server/controllers/wazuh-utils/wazuh-utils.ts similarity index 89% rename from server/controllers/wazuh-utils.ts rename to server/controllers/wazuh-utils/wazuh-utils.ts index 466923d44b..97567ec7ad 100644 --- a/server/controllers/wazuh-utils.ts +++ b/server/controllers/wazuh-utils/wazuh-utils.ts @@ -11,15 +11,15 @@ */ // Require some libraries -import { ErrorResponse } from '../lib/error-response'; -import { getConfiguration } from '../lib/get-configuration'; +import { ErrorResponse } from '../../lib/error-response'; +import { getConfiguration } from '../../lib/get-configuration'; import { read } from 'read-last-lines'; -import { UpdateConfigurationFile } from '../lib/update-configuration'; +import { UpdateConfigurationFile } from '../../lib/update-configuration'; import jwtDecode from 'jwt-decode'; -import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_DATA_LOGS_RAW_PATH } from '../../common/constants'; -import { ManageHosts } from '../lib/manage-hosts'; +import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_DATA_LOGS_RAW_FILENAME, WAZUH_UI_LOGS_RAW_PATH } from '../../../common/constants'; +import { ManageHosts } from '../../lib/manage-hosts'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; -import { getCookieValueByName } from '../lib/cookie'; +import { getCookieValueByName } from '../../lib/cookie'; const updateConfigurationFile = new UpdateConfigurationFile(); @@ -108,7 +108,7 @@ export class WazuhUtilsCtrl { async getAppLogs(context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) { try { const lastLogs = await read( - WAZUH_DATA_LOGS_RAW_PATH, + WAZUH_DATA_LOGS_RAW_FILENAME, 50 ); const spliterLog = lastLogs.split('\n'); @@ -126,4 +126,6 @@ export class WazuhUtilsCtrl { return ErrorResponse(error.message || error, 3036, 500, response); } } + + } diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts new file mode 100644 index 0000000000..ca7c8287d5 --- /dev/null +++ b/server/lib/base-logger.ts @@ -0,0 +1,210 @@ +/* + * Wazuh app - Settings controller + * Copyright (C) 2015-2021 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 winston from 'winston'; +import fs from 'fs'; +import path from 'path'; +import { getConfiguration } from './get-configuration'; +import { createDataDirectoryIfNotExists } from './filesystem'; + +import { WAZUH_DATA_LOGS_DIRECTORY_PATH } from '../../common/constants'; + +export interface IUIPlainLoggerSettings { + level: string; + message: string; +} + +export interface IUILoggerSettings extends IUIPlainLoggerSettings { + date: Date; + location: string; +} + +export class BaseLogger { + allowed: boolean = false; + wazuhLogger: winston.Logger | undefined = undefined; + wazuhPlainLogger: winston.Logger | undefined = undefined; + PLAIN_LOGS_PATH: string = ''; + RAW_LOGS_PATH: string = ''; + + constructor(plainLogsPath: string, rawLogsPath: string) { + this.PLAIN_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, plainLogsPath); + this.RAW_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, rawLogsPath); + } + + /** + * Initialize loggers, plain and raw logger + */ + private initLogger = () => { + const configurationFile = getConfiguration(); + const level = + typeof (configurationFile || {})['logs.level'] !== 'undefined' && + ['info', 'debug'].includes(configurationFile['logs.level']) + ? configurationFile['logs.level'] + : 'info'; + + // JSON logger + this.wazuhLogger = winston.createLogger({ + level, + format: winston.format.json(), + transports: [ + new winston.transports.File({ + filename: this.RAW_LOGS_PATH, + }), + ], + }); + + // Prevents from exit on error related to the logger. + this.wazuhLogger.exitOnError = false; + + // Plain text logger + this.wazuhPlainLogger = winston.createLogger({ + level, + format: winston.format.simple(), + transports: [ + new winston.transports.File({ + filename: this.PLAIN_LOGS_PATH, + }), + ], + }); + + // Prevents from exit on error related to the logger. + this.wazuhPlainLogger.exitOnError = false; + }; + + /** + * Checks if wazuh/logs exists. If it doesn't exist, it will be created. + */ + initDirectory = async () => { + try { + createDataDirectoryIfNotExists(); + createDataDirectoryIfNotExists('logs'); + if (typeof this.wazuhLogger === 'undefined' || typeof this.wazuhPlainLogger === 'undefined') { + this.initLogger(); + } + this.allowed = true; + return; + } catch (error) { + this.allowed = false; + return Promise.reject(error); + } + }; + + /** + * Returns given file size in MB, if the file doesn't exist returns 0 + * @param {*} filename Path to the file + */ + getFilesizeInMegaBytes = (filename) => { + if (this.allowed) { + if (fs.existsSync(filename)) { + const stats = fs.statSync(filename); + const fileSizeInMegaBytes = stats.size; + + return fileSizeInMegaBytes / 1000000.0; + } + } + return 0; + }; + + /** + * Check if file exist + * @param filename + * @returns boolean + */ + checkFileExist = (filename) => { + return fs.existsSync(filename); + }; + + /** + * Checks if the wazuhapp.log file size is greater than 100MB, if so it rotates the file. + */ + private checkFiles = () => { + if (this.allowed) { + // check raw log file + if (this.getFilesizeInMegaBytes(this.RAW_LOGS_PATH) >= 100) { + fs.renameSync( + this.RAW_LOGS_PATH, + `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp.${new Date().getTime()}.log` + ); + fs.writeFileSync( + this.RAW_LOGS_PATH, + JSON.stringify({ + date: new Date(), + level: 'info', + location: 'logger', + message: 'Rotated log file', + }) + '\n' + ); + } + + // check log file + if (this.getFilesizeInMegaBytes(this.PLAIN_LOGS_PATH) >= 100) { + fs.renameSync( + this.PLAIN_LOGS_PATH, + `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp-plain.${new Date().getTime()}.log` + ); + } + } + }; + + /** + * Get Current Date + * @returns string + */ + private yyyymmdd = () => { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth() + 1; + const d = now.getDate(); + const seconds = now.getSeconds(); + const minutes = now.getMinutes(); + const hour = now.getHours(); + return `${y}/${m < 10 ? '0' : ''}${m}/${d < 10 ? '0' : ''}${d} ${hour}:${minutes}:${seconds}`; + }; + + /** + * Main function to add a new log + * @param {*} location File where the log is being thrown + * @param {*} message Message to show + * @param {*} level Optional, default is 'error' + */ + + async log(location: string, message: string, level: string) { + return this.initDirectory() + .then(() => { + if (this.allowed) { + this.checkFiles(); + + const plainLogData: IUIPlainLoggerSettings = { + level: level || 'error', + message: `${this.yyyymmdd()}: ${location || 'Unknown origin'}: ${ + message || 'An error occurred' + }`, + }; + + this.wazuhPlainLogger.log(plainLogData); + + const logData: IUILoggerSettings = { + date: new Date(), + level: level || 'error', + location: location || 'Unknown origin', + message: message || 'An error occurred', + }; + + this.wazuhLogger.log(logData); + } + }) + .catch((error) => { + console.error(`Cannot create the logs directory due to:\n${error.message || error}`); + throw error; + }); + } +} diff --git a/server/lib/logger.ts b/server/lib/logger.ts index 7cb8115d79..ff1fad8fa9 100644 --- a/server/lib/logger.ts +++ b/server/lib/logger.ts @@ -9,165 +9,14 @@ * * Find more information about this on the LICENSE file. */ -import winston from 'winston'; -import fs from 'fs'; -import { getConfiguration } from './get-configuration'; -import { WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_DATA_LOGS_PLAIN_PATH, WAZUH_DATA_LOGS_RAW_PATH } from '../../common/constants'; -import { createDataDirectoryIfNotExists } from './filesystem'; +import { BaseLogger } from './base-logger'; +import { + WAZUH_DATA_LOGS_PLAIN_FILENAME, + WAZUH_DATA_LOGS_RAW_FILENAME, +} from '../../common/constants'; -let allowed = false; -let wazuhlogger = undefined; -let wazuhPlainLogger = undefined; +const logger = new BaseLogger(WAZUH_DATA_LOGS_PLAIN_FILENAME, WAZUH_DATA_LOGS_RAW_FILENAME); -/** - * Here we create the loggers - */ -const initLogger = () => { - const configurationFile = getConfiguration(); - const level = - typeof (configurationFile || {})['logs.level'] !== 'undefined' && - ['info', 'debug'].includes(configurationFile['logs.level']) - ? configurationFile['logs.level'] - : 'info'; - - // JSON logger - wazuhlogger = winston.createLogger({ - level, - format: winston.format.json(), - transports: [ - new winston.transports.File({ - filename: WAZUH_DATA_LOGS_RAW_PATH - }) - ] - }); - - // Prevents from exit on error related to the logger. - wazuhlogger.exitOnError = false; - - // Plain text logger - wazuhPlainLogger = winston.createLogger({ - level, - format: winston.format.simple(), - transports: [ - new winston.transports.File({ - filename: WAZUH_DATA_LOGS_PLAIN_PATH - }) - ] - }); - - // Prevents from exit on error related to the logger. - wazuhPlainLogger.exitOnError = false; +export const log = (location, message, level) => { + logger.log(location, message, level); }; - -/** - * Checks if wazuh/logs exists. If it doesn't exist, it will be created. - */ -const initDirectory = async () => { - try { - createDataDirectoryIfNotExists(); - createDataDirectoryIfNotExists('logs'); - if ( - typeof wazuhlogger === 'undefined' || - typeof wazuhPlainLogger === 'undefined' - ) { - initLogger(); - } - allowed = true; - return; - } catch (error) { - allowed = false; - return Promise.reject(error); - } -}; - -/** - * Returns given file size in MB, if the file doesn't exist returns 0 - * @param {*} filename Path to the file - */ -const getFilesizeInMegaBytes = filename => { - if (allowed) { - if (fs.existsSync(filename)) { - const stats = fs.statSync(filename); - const fileSizeInMegaBytes = stats.size; - - return fileSizeInMegaBytes / 1000000.0; - } - } - return 0; -}; - -/** - * Checks if the wazuhapp.log file size is greater than 100MB, if so it rotates the file. - */ -const checkFiles = () => { - if (allowed) { - if (getFilesizeInMegaBytes(WAZUH_DATA_LOGS_RAW_PATH) >= 100) { - fs.renameSync( - WAZUH_DATA_LOGS_RAW_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp.${new Date().getTime()}.log` - ); - fs.writeFileSync( - WAZUH_DATA_LOGS_RAW_PATH, - JSON.stringify({ - date: new Date(), - level: 'info', - location: 'logger', - message: 'Rotated log file' - }) + '\n' - ); - } - if (getFilesizeInMegaBytes(WAZUH_DATA_LOGS_PLAIN_PATH) >= 100) { - fs.renameSync( - WAZUH_DATA_LOGS_PLAIN_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp-plain.${new Date().getTime()}.log` - ); - } - } -}; - -const yyyymmdd = () => { - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth() + 1; - const d = now.getDate(); - const seconds = now.getSeconds(); - const minutes = now.getMinutes(); - const hour = now.getHours(); - return `${y}/${m < 10 ? '0' : ''}${m}/${ - d < 10 ? '0' : '' - }${d} ${hour}:${minutes}:${seconds}`; -}; - -/** - * Main function to add a new log - * @param {*} location File where the log is being thrown - * @param {*} message Message to show - * @param {*} level Optional, default is 'error' - */ -export function log(location, message, level) { - initDirectory() - .then(() => { - if (allowed) { - checkFiles(); - wazuhlogger.log({ - date: new Date(), - level: level || 'error', - location: location || 'Unknown origin', - message: message || 'An error occurred' - }); - try { - wazuhPlainLogger.log({ - level: level || 'error', - message: `${yyyymmdd()}: ${location || - 'Unknown origin'}: ${message || 'An error occurred'}` - }); - } catch (error) {} // eslint-disable-line - } - }) - .catch(error => - // eslint-disable-next-line - console.error( - `Cannot create the logs directory due to:\n${error.message || error}` - ) - ); -} diff --git a/server/lib/ui-logger.ts b/server/lib/ui-logger.ts new file mode 100644 index 0000000000..2ac71076e9 --- /dev/null +++ b/server/lib/ui-logger.ts @@ -0,0 +1,18 @@ +/* + * Wazuh app - Module for ui logging functions + * Copyright (C) 2015-2021 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 { BaseLogger } from './base-logger'; +import { + WAZUH_UI_LOGS_PLAIN_FILENAME, + WAZUH_UI_LOGS_RAW_FILENAME +} from '../../common/constants'; + +export default new BaseLogger(WAZUH_UI_LOGS_PLAIN_FILENAME,WAZUH_UI_LOGS_RAW_FILENAME); diff --git a/server/routes/index.ts b/server/routes/index.ts index 5f9b743be0..8af0e65dc0 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -2,7 +2,7 @@ import { IRouter } from 'kibana/server'; import { WazuhApiRoutes } from './wazuh-api'; import { WazuhElasticRoutes } from "./wazuh-elastic"; import { WazuhHostsRoutes } from "./wazuh-hosts"; -import { WazuhUtilsRoutes } from "./wazuh-utils"; +import { WazuhUtilsRoutes, UiLogsRoutes } from './wazuh-utils' import { WazuhReportingRoutes } from "./wazuh-reporting"; export const setupRoutes = (router: IRouter) => { @@ -11,4 +11,5 @@ export const setupRoutes = (router: IRouter) => { WazuhHostsRoutes(router); WazuhUtilsRoutes(router); WazuhReportingRoutes(router); + UiLogsRoutes(router); }; diff --git a/server/routes/wazuh-utils/index.ts b/server/routes/wazuh-utils/index.ts new file mode 100644 index 0000000000..f02845ce09 --- /dev/null +++ b/server/routes/wazuh-utils/index.ts @@ -0,0 +1,2 @@ +export { WazuhUtilsRoutes } from "./wazuh-utils"; +export { UiLogsRoutes } from './ui-logs'; \ No newline at end of file diff --git a/server/routes/wazuh-utils/ui-logs.test.ts b/server/routes/wazuh-utils/ui-logs.test.ts new file mode 100644 index 0000000000..292d4cd214 --- /dev/null +++ b/server/routes/wazuh-utils/ui-logs.test.ts @@ -0,0 +1,42 @@ +// To launch this file +// yarn test:jest --testEnvironment node --verbose server/routes/wazuh-utils/ui-logs +import axios from 'axios'; + +const buildAxiosOptions = (method: string, path: string, data: any = {}, headers: any = {}) => { + return { + method: method, + headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + url: `http://localhost:5601${path}`, + data: data, + }; +}; + +describe('Wazuh API - /utils/logs/ui', () => { + test('[200] Get UI Logs', () => { + const options = buildAxiosOptions('get', '/utils/logs/ui'); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); + }, 6000); +}); + +describe('Wazuh API - /utils/logs/ui', () => { + test('[200] Create UI Logs', () => { + const options = buildAxiosOptions('post', '/utils/logs/ui', { + message: 'Message test', + level: 'error', + location: 'Location', + }); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); + }, 6000); +}); diff --git a/server/routes/wazuh-utils/ui-logs.ts b/server/routes/wazuh-utils/ui-logs.ts new file mode 100644 index 0000000000..7c739fb333 --- /dev/null +++ b/server/routes/wazuh-utils/ui-logs.ts @@ -0,0 +1,39 @@ +/* + * Wazuh app - Module for UI Logs routes + * Copyright (C) 2015-2021 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 { UiLogsCtrl } from '../../controllers'; +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export const UiLogsRoutes = (router: IRouter) => { + const ctrl = new UiLogsCtrl(); + router.get( + { + path: '/utils/logs/ui', + validate: false, + }, + async (context, request, response) => await ctrl.getUiLogs(response) + ); + + router.post( + { + path: '/utils/logs/ui', + validate: { + body: schema.object({ + message: schema.string(), + level: schema.string(), + location: schema.string(), + }), + }, + }, + async (context, request, response) => await ctrl.createUiLogs(request, response) + ); +}; diff --git a/server/routes/wazuh-utils.test.ts b/server/routes/wazuh-utils/wazuh-utils.test.ts similarity index 100% rename from server/routes/wazuh-utils.test.ts rename to server/routes/wazuh-utils/wazuh-utils.test.ts diff --git a/server/routes/wazuh-utils.ts b/server/routes/wazuh-utils/wazuh-utils.ts similarity index 96% rename from server/routes/wazuh-utils.ts rename to server/routes/wazuh-utils/wazuh-utils.ts index f1d6608f51..52587dd0d3 100644 --- a/server/routes/wazuh-utils.ts +++ b/server/routes/wazuh-utils/wazuh-utils.ts @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -import { WazuhUtilsCtrl } from '../controllers'; +import { WazuhUtilsCtrl } from '../../controllers'; import { IRouter } from 'kibana/server'; import { schema } from '@kbn/config-schema'; diff --git a/test/jest/config.js b/test/jest/config.js index 3e6e911ff9..4a1fe3f72a 100644 --- a/test/jest/config.js +++ b/test/jest/config.js @@ -48,7 +48,8 @@ export default { 'target/', ], testMatch: [ - '**/*.test.{js,ts,tsx}' + '**/*.test.{js,ts,tsx}', + '**/*{js,ts,tsx}' ], transform: { '^.+\\.js$': `${kbnDir}/src/dev/jest/babel_transform.js`, From fa6ec972d8ceaa730707eb9556a61058a155dfe4 Mon Sep 17 00:00:00 2001 From: Franco Charriol Date: Mon, 14 Jun 2021 10:21:58 -0300 Subject: [PATCH 005/493] Update CHANGELOG.md --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f4d4fd2a..ab711cccf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,14 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) +- Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) +- Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) -### Added -- Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) -- Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 From 2a5265509a64e0fff02b012cc499d8c37d8e696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= Date: Tue, 15 Jun 2021 14:45:59 +0200 Subject: [PATCH 006/493] Feature/implement error boundary hoc (#3367) * Feature/3316 error handler orchestrator (#3327) * feat(errorBoundary): Added ErrorBoundary HOC and component and added loglevel dependency * feature(errorBoundary): Moved with the others HOCs. * feature(errorBoundary): Typo refactor. * First attempt LoggerService * Merged error boundary, integrated loggerService. * changed logger name, create logger-service test file * Updated CHANGELOG * Moved to react-services, changed name, traslates comments * feat(errorBoundary): Removed old integration * refactor(loggerService): Changed class for function methods. * test(logger-service): Added basic unit test to logger-service * refactor(logger-service): Applied new implementation of error-orchestrator service. * feature(logger-service): PR comments and some refactors. Co-authored-by: gabiwassan Co-authored-by: Ibarra Maximiliano * Added ErrorBoundary HOC and component. (#3321) * feat(errorBoundary): Added ErrorBoundary HOC and component and added loglevel dependency * feature(errorBoundary): Moved with the others HOCs. * feature(errorBoundary): Typo refactor. * feature(errorBoundary): Some refactors * feat(errorBoundary): PR comments and rollback agent-preview * doc(changelog): Update changelog * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Rollback * feat(errorBoundary): Refactor props, pr comments. * feat(errorBoundary): Added unit test for error boundary. * feat(errorBoundary): Separated error boundary component of hoc * doc(error-boundary): Fixed and added licenses blocks. * feature(logger-service): PR comments * feature(logger-orchestrator): Refactors on management of severity. * feature(logger-orchestrator): Refactor on wz-blank-screen component. * feature(logger-orchestrator): Separated prompt component from error-boundary. * feature(logger-orchestrator): Typo. * test(error-boundary): Update snapshots. * test(error-boundary): Update snapshots * fix(logger-orchestrator): PR comments and refactors, fix unit tests. * test(error-orchestrator-base): Added simple unit test. Fixed licence block. * test(error-orchestrator-base): Added simple unit test to ErrorOrchestratorCritical * test(error-orchestrator-ui): Added simple unit test to ErrorOrchestratorUi * Create new backend service (#3324) * Add endpoint * Create new backend service * Add changelog * Renamed constants * Added interfaces, created new controller and renamed * Created ui-logged, to prevent logger superclass * Added types, fixed responses types * Added new route file to ui-logs, changed method put to post, added in index,ts * Added test files, we must create all unit tests to those new features * Fixed if condition * Rename tests files, created endpoints test * Changed controller name ui-logs, removed duplicated export * Fixed file comments * Applied prettier formater * Added new base class base-logger * Remove wrong constants and fix errors * test(ui-logger-controller): Added simple unit test. * test(ui-logs-controller): Fix params. * Added test to ui-logs controller * Renamed test files * test(logs-controller): Added mock to function checkFileExist + prettier. * Solve comments * Add copyright and remove unused import Co-authored-by: Ibarra Maximiliano Co-authored-by: gabiwassan * bugfix(error-orchestrator): Added some improvements and fixes. * test(ui-logs-controller): Updated unit test. * Settings * Added hoc * Before rebase * rebase 4.3-7.10 * Feature/3316 error handler orchestrator (#3327) * feat(errorBoundary): Added ErrorBoundary HOC and component and added loglevel dependency * feature(errorBoundary): Moved with the others HOCs. * feature(errorBoundary): Typo refactor. * First attempt LoggerService * Merged error boundary, integrated loggerService. * changed logger name, create logger-service test file * Updated CHANGELOG * Moved to react-services, changed name, traslates comments * feat(errorBoundary): Removed old integration * refactor(loggerService): Changed class for function methods. * test(logger-service): Added basic unit test to logger-service * refactor(logger-service): Applied new implementation of error-orchestrator service. * feature(logger-service): PR comments and some refactors. Co-authored-by: gabiwassan Co-authored-by: Ibarra Maximiliano * Final implementations * Fix errors and apply prettier * Added changelog Co-authored-by: Maximiliano Ibarra Co-authored-by: gabiwassan Co-authored-by: Ibarra Maximiliano --- CHANGELOG.md | 2 +- .../add-modules-data/WzSampleDataWrapper.js | 20 +- .../components/agents/stats/agent-stats.tsx | 3 +- .../components/agents/syscollector/main.tsx | 16 +- .../agents-current-section-wrapper.tsx | 20 +- .../common/modules/discover/discover.tsx | 3 +- public/components/common/modules/main.tsx | 341 +++++++++--------- .../overview-current-section-wrapper.tsx | 20 +- .../components/common/permissions/prompt.tsx | 117 +++--- .../common/welcome/agents-welcome.js | 9 +- .../welcome/management-welcome-wrapper.js | 19 +- .../common/welcome/overview-welcome.js | 5 +- .../components/health-check/health-check.tsx | 5 +- .../management/cluster/cluster-disabled.js | 6 +- .../management/cluster/cluster-timelions.js | 161 ++++----- .../cluster/cluster-visualization.js | 34 +- .../management/cluster/node-list.tsx | 5 +- .../groups/multiple-agent-selector.tsx | 5 +- public/components/notifications/modal.tsx | 5 +- public/components/overview/mitre/mitre.tsx | 7 +- public/components/security/main.tsx | 3 +- public/components/settings/api/add-api.js | 5 +- public/components/settings/api/api-is-down.js | 5 +- public/components/settings/api/api-table.js | 10 +- .../settings/configuration/configuration.tsx | 17 +- public/components/settings/modules/modules.js | 16 +- .../components/settings/settings-logs/logs.js | 7 +- public/components/visualize/wz-visualize.js | 5 +- .../wz-agent-selector-wrapper.js | 23 +- .../components/wz-filter-bar/wz-filter-bar.js | 5 +- public/components/wz-menu/wz-menu-wrapper.js | 20 +- .../agent/components/agents-preview.js | 3 +- .../agent/components/agents-table.js | 5 +- .../agent/components/export-configuration.js | 5 +- .../agent/components/register-agent.js | 9 +- .../configuration/configuration-main.js | 52 +-- .../management/management-provider.js | 24 +- .../overview/components/requirement-card.js | 5 +- .../overview/components/select-agent.js | 5 +- .../controllers/overview/components/stats.js | 5 +- .../wz-logtest/components/logtest.tsx | 3 +- server/lib/base-logger.ts | 2 - 42 files changed, 481 insertions(+), 556 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab711cccf7..6dae2bc5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) +- Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added diff --git a/public/components/add-modules-data/WzSampleDataWrapper.js b/public/components/add-modules-data/WzSampleDataWrapper.js index 6fd6d81996..7f00da1c50 100644 --- a/public/components/add-modules-data/WzSampleDataWrapper.js +++ b/public/components/add-modules-data/WzSampleDataWrapper.js @@ -24,8 +24,9 @@ import { } from '@elastic/eui'; import WzSampleData from './sample-data' import WzReduxProvider from '../../redux/wz-redux-provider'; -import { withUserAuthorizationPrompt } from '../../components/common/hocs/withUserAuthorization'; +import { withUserAuthorizationPrompt, withErrorBoundary, withReduxProvider } from '../../components/common/hocs'; import store from '../../redux/store'; +import { compose } from 'redux'; import { updateSelectedSettingsSection } from '../../redux/actions/appStateActions'; import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; @@ -61,9 +62,7 @@ export class WzSampleDataProvider extends Component { - - - + @@ -73,11 +72,8 @@ export class WzSampleDataProvider extends Component { } } -const WzSampleDataWrapperWithAdministrator = withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME])(WzSampleDataProvider); -export function WzSampleDataWrapper() { - return ( - - - - ) -} +export const WzSampleDataWrapper = compose( + withErrorBoundary, + withReduxProvider, + withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]) +)(WzSampleDataProvider); diff --git a/public/components/agents/stats/agent-stats.tsx b/public/components/agents/stats/agent-stats.tsx index 0bb652062e..3f0f7059f8 100644 --- a/public/components/agents/stats/agent-stats.tsx +++ b/public/components/agents/stats/agent-stats.tsx @@ -21,7 +21,7 @@ import { EuiText } from '@elastic/eui'; -import { withGlobalBreadcrumb, withReduxProvider, withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withGlobalBreadcrumb, withReduxProvider, withGuard, withUserAuthorizationPrompt, withErrorBoundary } from '../../common/hocs'; import { compose } from 'redux'; import { WzRequest, formatUIDate } from '../../../react-services'; import { AgentStatTable } from './table'; @@ -80,6 +80,7 @@ const statsAgents: {title: string, field: string, render?: (value) => any}[] = [ ]; export const MainAgentStats = compose( + withErrorBoundary, withReduxProvider, withGlobalBreadcrumb(({agent}) => [ { diff --git a/public/components/agents/syscollector/main.tsx b/public/components/agents/syscollector/main.tsx index 0be7dff02e..958802f450 100644 --- a/public/components/agents/syscollector/main.tsx +++ b/public/components/agents/syscollector/main.tsx @@ -10,16 +10,8 @@ * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react'; -import { SyscollectorInventory } from './inventory' +import React from 'react'; +import { withErrorBoundary } from '../../common/hocs'; +import { SyscollectorInventory } from './inventory'; -export class MainSyscollector extends Component { - - constructor(props) { - super(props); - } - - render() { - return ( ) - } -} +export const MainSyscollector = withErrorBoundary(SyscollectorInventory); diff --git a/public/components/common/modules/agents-current-section-wrapper.tsx b/public/components/common/modules/agents-current-section-wrapper.tsx index cb5f5921c1..1c9b46700d 100644 --- a/public/components/common/modules/agents-current-section-wrapper.tsx +++ b/public/components/common/modules/agents-current-section-wrapper.tsx @@ -10,20 +10,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import WzCurrentAgentsSection from './agents-current-section' +import WzCurrentAgentsSection from './agents-current-section'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../hocs'; -export class WzCurrentAgentsSectionWrapper extends Component { - constructor(props) { - super(props); - } - - render() { - return ( - - - - ); - } -} \ No newline at end of file +export const WzCurrentAgentsSectionWrapper = compose (withErrorBoundary, withReduxProvider) (WzCurrentAgentsSection); diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 0d99e5d902..868ccfb3d3 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -21,7 +21,7 @@ import { WazuhConfig } from '../../../../react-services/wazuh-config'; import { formatUIDate } from '../../../../react-services/time-service'; import { KbnSearchBar } from '../../../kbn-search-bar'; import { FlyoutTechnique } from '../../../../components/overview/mitre/components/techniques/components/flyout-technique'; -import { withReduxProvider } from '../../../common/hocs'; +import { withErrorBoundary, withReduxProvider } from '../../../common/hocs'; import { connect } from 'react-redux'; import { compose } from 'redux'; import _ from 'lodash'; @@ -57,6 +57,7 @@ const mapStateToProps = state => ({ }); export const Discover = compose( + withErrorBoundary, withReduxProvider, connect(mapStateToProps) )(class Discover extends Component { diff --git a/public/components/common/modules/main.tsx b/public/components/common/modules/main.tsx index 22d8e5ff34..02e7fa9e0d 100644 --- a/public/components/common/modules/main.tsx +++ b/public/components/common/modules/main.tsx @@ -27,197 +27,216 @@ import { getAngularModule, getDataPlugin, getUiSettings } from '../../../kibana- import { MainModuleAgent } from './main-agent' import { MainModuleOverview } from './main-overview'; import store from '../../../redux/store'; -import WzReduxProvider from '../../../redux/wz-redux-provider.js'; - -export class MainModule extends Component { - constructor(props) { - super(props); - this.reportingService = new ReportingService(); - this.state = { - selectView: false, - loadingReport: false, - switchModule: false, - showAgentInfo: false - }; - const app = getAngularModule(); - this.$rootScope = app.$injector.get('$rootScope'); - } - - async componentDidMount() { - if (!(ModulesDefaults[this.props.section] || {}).notModule) { - this.tabs = (ModulesDefaults[this.props.section] || {}).tabs || [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }]; - this.buttons = (ModulesDefaults[this.props.section] || {}).buttons || ['reporting', 'settings']; +import { compose } from 'redux'; +import { withReduxProvider,withErrorBoundary } from '../hocs'; + +export const MainModule = compose( + withErrorBoundary, + withReduxProvider +)( + class MainModule extends Component { + constructor(props) { + super(props); + this.reportingService = new ReportingService(); + this.state = { + selectView: false, + loadingReport: false, + switchModule: false, + showAgentInfo: false, + }; + const app = getAngularModule(); + this.$rootScope = app.$injector.get('$rootScope'); } - } - componentWillUnmount() { - const { filterManager } = getDataPlugin().query; - if (filterManager.getFilters() && filterManager.getFilters().length) { - filterManager.removeAll(); + async componentDidMount() { + if (!(ModulesDefaults[this.props.section] || {}).notModule) { + this.tabs = (ModulesDefaults[this.props.section] || {}).tabs || [ + { id: 'dashboard', name: 'Dashboard' }, + { id: 'events', name: 'Events' }, + ]; + this.buttons = (ModulesDefaults[this.props.section] || {}).buttons || [ + 'reporting', + 'settings', + ]; + } } - } - canBeInit(tab) { //checks if the init table can be set - let canInit = false; - this.tabs.forEach(element => { - if (element.id === tab && (!element.onlyAgent || (element.onlyAgent && this.props.agent))) { - canInit = true; + componentWillUnmount() { + const { filterManager } = getDataPlugin().query; + if (filterManager.getFilters() && filterManager.getFilters().length) { + filterManager.removeAll(); } - }); - return canInit; - } - - renderTabs(agent = false) { - const { selectView } = this.state; - if (!agent) { + } + canBeInit(tab) { + //checks if the init table can be set + let canInit = false; + this.tabs.forEach((element) => { + if (element.id === tab && (!element.onlyAgent || (element.onlyAgent && this.props.agent))) { + canInit = true; + } + }); + return canInit; } - return ( - - - {this.tabs.map((tab, index) => { - if (!tab.onlyAgent || (tab.onlyAgent && this.props.agent)) { - return this.onSelectedTabChanged(tab.id)} - isSelected={selectView === tab.id} - key={index} - > - {tab.name} - - } - } - )} - - - ); - } + renderTabs(agent = false) { + const { selectView } = this.state; + if (!agent) { + } + return ( + + + {this.tabs.map((tab, index) => { + if (!tab.onlyAgent || (tab.onlyAgent && this.props.agent)) { + return ( + this.onSelectedTabChanged(tab.id)} + isSelected={selectView === tab.id} + key={index} + > + {tab.name} + + ); + } + })} + + + ); + } - startVis2PngByAgent = async () => { - const agent = - (this.props.agent || store.getState().appStateReducers.currentAgentData || {}).id || false; - await this.reportingService.startVis2Png(this.props.section, agent); - }; + startVis2PngByAgent = async () => { + const agent = + (this.props.agent || store.getState().appStateReducers.currentAgentData || {}).id || false; + await this.reportingService.startVis2Png(this.props.section, agent); + }; - async startReport() { - try { - this.setState({ loadingReport: true }); - const isDarkModeTheme = getUiSettings().get('theme:darkMode'); - if (isDarkModeTheme) { - const defaultTextColor = '#DFE5EF'; - try { - $('.euiButtonEmpty__text').css('color', 'black'); + async startReport() { + try { + this.setState({ loadingReport: true }); + const isDarkModeTheme = getUiSettings().get('theme:darkMode'); + if (isDarkModeTheme) { + const defaultTextColor = '#DFE5EF'; + try { + $('.euiButtonEmpty__text').css('color', 'black'); + await this.startVis2PngByAgent(); + $('.euiButtonEmpty__text').css('color', defaultTextColor); + } catch (e) { + $('.euiButtonEmpty__text').css('color', defaultTextColor); + this.setState({ loadingReport: false }); + } + } else { await this.startVis2PngByAgent(); - $('.euiButtonEmpty__text').css('color', defaultTextColor); - } catch (e) { - $('.euiButtonEmpty__text').css('color', defaultTextColor); - this.setState({ loadingReport: false }); } - } else { - await this.startVis2PngByAgent(); + } finally { + this.setState({ loadingReport: false }); } - } finally { - this.setState({ loadingReport: false }); } - } - renderReportButton() { - return ( - (this.props.disabledReport && - - - this.startReport()}> - Generate report + renderReportButton() { + return ( + (this.props.disabledReport && ( + + + this.startReport()} + > + Generate report - - - - || ( + + + )) || ( this.startReport()}> + onClick={async () => this.startReport()} + > Generate report - )) - ); - } + + ) + ); + } - renderDashboardButton() { - const href = `#/overview?tab=${this.props.section}&agentId=${this.props.agent.id}` - return ( - - this.onSelectedTabChanged('dashboard')}> - Dashboard + renderDashboardButton() { + const href = `#/overview?tab=${this.props.section}&agentId=${this.props.agent.id}`; + return ( + + this.onSelectedTabChanged('dashboard')} + > + Dashboard - - ); - } + + ); + } - renderSettingsButton() { - return ( - - this.onSelectedTabChanged('settings')}> - Configuration + renderSettingsButton() { + return ( + + this.onSelectedTabChanged('settings')} + > + Configuration - - ); - } + + ); + } - loadSection(id) { - this.setState({ selectView: id }); - } + loadSection(id) { + this.setState({ selectView: id }); + } - onSelectedTabChanged(id) { - if (id !== this.state.selectView) { - if (id === 'events' || id === 'dashboard' || id === 'inventory') { - this.$rootScope.moduleDiscoverReady = false; - if (this.props.switchSubTab) this.props.switchSubTab(id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels') - window.location.href = window.location.href.replace( - new RegExp("tabView=" + "[^\&]*"), - `tabView=${id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels'}`); - this.afterLoad = id; - this.loadSection('loader'); - } else { - this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); + onSelectedTabChanged(id) { + if (id !== this.state.selectView) { + if (id === 'events' || id === 'dashboard' || id === 'inventory') { + this.$rootScope.moduleDiscoverReady = false; + if (this.props.switchSubTab) + this.props.switchSubTab( + id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels' + ); + window.location.href = window.location.href.replace( + new RegExp('tabView=' + '[^&]*'), + `tabView=${id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels'}` + ); + this.afterLoad = id; + this.loadSection('loader'); + } else { + this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); + } } } - } - render() { - const { agent } = this.props; - const { selectView } = this.state; - const mainProps = { - selectView, - afterLoad: this.afterLoad, - buttons: this.buttons, - tabs: this.tabs, - renderTabs: () => this.renderTabs(), - renderReportButton: () => this.renderReportButton(), - renderDashboardButton: () => this.renderDashboardButton(), - renderSettingsButton: () => this.renderSettingsButton(), - loadSection: (id) => this.loadSection(id), - onSelectedTabChanged: (id) => this.onSelectedTabChanged(id) + render() { + const { agent } = this.props; + const { selectView } = this.state; + const mainProps = { + selectView, + afterLoad: this.afterLoad, + buttons: this.buttons, + tabs: this.tabs, + renderTabs: () => this.renderTabs(), + renderReportButton: () => this.renderReportButton(), + renderDashboardButton: () => this.renderDashboardButton(), + renderSettingsButton: () => this.renderSettingsButton(), + loadSection: (id) => this.loadSection(id), + onSelectedTabChanged: (id) => this.onSelectedTabChanged(id), + }; + return ( + <> + {(agent && ) || + (this.props.section && this.props.section !== 'welcome' && ( + + ))} + + ); } - return ( - - {agent && - - || ((this.props.section && this.props.section !== 'welcome') && - ) - } - - ); } -} +); diff --git a/public/components/common/modules/overview-current-section-wrapper.tsx b/public/components/common/modules/overview-current-section-wrapper.tsx index e3f09aa96a..103493d686 100644 --- a/public/components/common/modules/overview-current-section-wrapper.tsx +++ b/public/components/common/modules/overview-current-section-wrapper.tsx @@ -10,20 +10,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import WzCurrentOverviewSection from './overview-current-section' +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../hocs'; +import WzCurrentOverviewSection from './overview-current-section'; -export class WzCurrentOverviewSectionWrapper extends Component { - constructor(props) { - super(props); - } - - render() { - return ( - - - - ); - } -} \ No newline at end of file +export const WzCurrentOverviewSectionWrapper = compose (withErrorBoundary, withReduxProvider) (WzCurrentOverviewSection); diff --git a/public/components/common/permissions/prompt.tsx b/public/components/common/permissions/prompt.tsx index 4203c90e4c..37e5747059 100644 --- a/public/components/common/permissions/prompt.tsx +++ b/public/components/common/permissions/prompt.tsx @@ -11,56 +11,79 @@ */ import React, { Fragment } from 'react'; -import { useUserPermissionsRequirements } from '../hooks/useUserPermissions'; -import { useUserRolesRequirements } from '../hooks/useUserRoles'; -import { EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; -import { TUserPermissions, TUserPermissionsFunction, TUserRoles, TUserRolesFunction } from '../permissions/button'; +import { useUserPermissionsRequirements, useUserRolesRequirements } from '../hooks'; +import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; +import { + TUserPermissions, + TUserPermissionsFunction, + TUserRoles, + TUserRolesFunction, +} from '../permissions/button'; import { WzPermissionsFormatted } from './format'; +import { withErrorBoundary } from '../hocs/error-boundary/with-error-boundary'; -interface IEmptyPromptNoPermissions{ - permissions?: TUserPermissions - roles?: TUserRoles - actions?: React.ReactNode +interface IEmptyPromptNoPermissions { + permissions?: TUserPermissions; + roles?: TUserRoles; + actions?: React.ReactNode; } - -export const WzEmptyPromptNoPermissions = ({permissions, roles, actions}: IEmptyPromptNoPermissions) => { - const prompt = (You have no permissions} - body={ - - {permissions && ( -
- This section requires the {permissions.length > 1 ? 'permissions' : 'permission'}: - {WzPermissionsFormatted(permissions)} -
- )} - {permissions && roles && ()} - {roles && ( -
- This section requires {roles.map(role => ({role})).reduce((accum, cur) => [accum, ', ', cur])} {roles.length > 1 ? 'roles' : 'role'} -
- )} -
- } - actions={actions} - />) - return ( - // {prompt} - prompt - ) -} - -interface IPromptNoPermissions{ - permissions?: TUserPermissions | TUserPermissionsFunction - roles?: TUserRoles | TUserRolesFunction - children?: React.ReactNode - rest?: any +export const WzEmptyPromptNoPermissions = withErrorBoundary( + ({ permissions, roles, actions }: IEmptyPromptNoPermissions) => { + return ( + You have no permissions} + body={ + + {permissions && ( +
+ This section requires the {permissions.length > 1 ? 'permissions' : 'permission'}: + {WzPermissionsFormatted(permissions)} +
+ )} + {permissions && roles && } + {roles && ( +
+ This section requires{' '} + {roles + .map((role) => {role}) + .reduce((accum, cur) => [accum, ', ', cur])}{' '} + {roles.length > 1 ? 'roles' : 'role'} +
+ )} +
+ } + actions={actions} + /> + ); + } +); +interface IPromptNoPermissions { + permissions?: TUserPermissions | TUserPermissionsFunction; + roles?: TUserRoles | TUserRolesFunction; + children?: React.ReactNode; + rest?: any; } -export const WzPromptPermissions = ({permissions = null, roles = null, children, ...rest} : IPromptNoPermissions) => { - const [userPermissionRequirements, userPermissions] = useUserPermissionsRequirements(typeof permissions === 'function' ? permissions(rest) : permissions); - const [userRolesRequirements, userRoles] = useUserRolesRequirements(typeof roles === 'function' ? roles(rest) : roles); +export const WzPromptPermissions = ({ + permissions = null, + roles = null, + children, + ...rest +}: IPromptNoPermissions) => { + const [userPermissionRequirements, userPermissions] = useUserPermissionsRequirements( + typeof permissions === 'function' ? permissions(rest) : permissions + ); + const [userRolesRequirements, userRoles] = useUserRolesRequirements( + typeof roles === 'function' ? roles(rest) : roles + ); - return (userPermissionRequirements || userRolesRequirements) ? : children; -} \ No newline at end of file + return userPermissionRequirements || userRolesRequirements ? ( + + ) : ( + children + ); +}; diff --git a/public/components/common/welcome/agents-welcome.js b/public/components/common/welcome/agents-welcome.js index 01c4637c27..044d83aa45 100644 --- a/public/components/common/welcome/agents-welcome.js +++ b/public/components/common/welcome/agents-welcome.js @@ -23,12 +23,9 @@ import { EuiFlexGrid, EuiButtonEmpty, EuiTitle, - EuiHealth, - EuiHorizontalRule, EuiPage, EuiButton, EuiPopover, - EuiSelect, EuiLoadingChart, EuiToolTip, EuiButtonIcon, @@ -54,8 +51,9 @@ import { updateCurrentAgentData } from '../../../redux/actions/appStateActions'; import WzTextWithTooltipIfTruncated from '../wz-text-with-tooltip-if-truncated'; import { getAngularModule } from '../../../kibana-services'; import { hasAgentSupportModule } from '../../../react-services/wz-agents'; +import { withErrorBoundary } from '../hocs'; -export class AgentsWelcome extends Component { +export const AgentsWelcome = withErrorBoundary (class AgentsWelcome extends Component { _isMount = false; constructor(props) { super(props); @@ -330,7 +328,6 @@ export class AgentsWelcome extends Component { ); - } buildTabCard(tab, icon) { @@ -614,4 +611,4 @@ export class AgentsWelcome extends Component {
); } -} +}) diff --git a/public/components/common/welcome/management-welcome-wrapper.js b/public/components/common/welcome/management-welcome-wrapper.js index bb2073a578..0652ada40b 100644 --- a/public/components/common/welcome/management-welcome-wrapper.js +++ b/public/components/common/welcome/management-welcome-wrapper.js @@ -12,22 +12,9 @@ * * DELETE THIS WRAPPER WHEN WELCOME SCREEN WAS NOT BE CALLED FROM ANGULARJS */ -import React, { Component } from 'react'; import ManagementWelcome from './management-welcome'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; import './welcome.scss'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../hocs'; -export class ManagementWelcomeWrapper extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - render() { - return ( - - - - ); - } -} +export const ManagementWelcomeWrapper = compose (withErrorBoundary,withReduxProvider)(ManagementWelcome) diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 520f888ebe..0f94fd6c4c 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -31,8 +31,9 @@ import { updateCurrentTab } from '../../../redux/actions/appStateActions'; import store from '../../../redux/store'; import './welcome.scss'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; +import { withErrorBoundary } from '../hocs'; -export class OverviewWelcome extends Component { +export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends Component { constructor(props) { super(props); this.strtools = new StringsTools(); @@ -186,4 +187,4 @@ export class OverviewWelcome extends Component { ); } -} \ No newline at end of file +}) \ No newline at end of file diff --git a/public/components/health-check/health-check.tsx b/public/components/health-check/health-check.tsx index 53aa528e16..5eae460b61 100644 --- a/public/components/health-check/health-check.tsx +++ b/public/components/health-check/health-check.tsx @@ -13,8 +13,9 @@ import { WAZUH_ERROR_DAEMONS_NOT_READY, WAZUH_INDEX_TYPE_STATISTICS, WAZUH_INDEX import { checkKibanaSettings, checkKibanaSettingsTimeFilter, checkKibanaSettingsMaxBuckets } from './lib'; import store from '../../redux/store'; import { updateWazuhNotReadyYet } from '../../redux/actions/appStateActions.js'; +import { withErrorBoundary } from '../common/hocs'; -export class HealthCheck extends Component { +export const HealthCheck = withErrorBoundary (class HealthCheck extends Component { checkPatternCount = 0; constructor(props) { super(props); @@ -502,4 +503,4 @@ export class HealthCheck extends Component {
); } -}; +}); diff --git a/public/components/management/cluster/cluster-disabled.js b/public/components/management/cluster/cluster-disabled.js index 979bdc0b94..8222d54a99 100644 --- a/public/components/management/cluster/cluster-disabled.js +++ b/public/components/management/cluster/cluster-disabled.js @@ -1,7 +1,7 @@ import React, { Component, Fragment } from 'react'; import { EuiPage, EuiPageContent, EuiEmptyPrompt } from '@elastic/eui'; - -export class ClusterDisabled extends Component { +import { withErrorBoundary } from '../../common/hocs' +export const ClusterDisabled = withErrorBoundary (class ClusterDisabled extends Component { constructor(props) { super(props); this.state = {}; @@ -43,4 +43,4 @@ export class ClusterDisabled extends Component { ); } -} +}) diff --git a/public/components/management/cluster/cluster-timelions.js b/public/components/management/cluster/cluster-timelions.js index 8d0aaeecbf..63f4f33772 100644 --- a/public/components/management/cluster/cluster-timelions.js +++ b/public/components/management/cluster/cluster-timelions.js @@ -1,107 +1,86 @@ import React, { Component } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiButtonIcon -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiButtonIcon } from '@elastic/eui'; import WzReduxProvider from '../../../redux/wz-redux-provider'; import KibanaVis from '../../../kibana-integrations/kibana-vis'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../../common/hocs'; -export class ClusterTimelions extends Component { +export const ClusterTimelions = compose (withErrorBoundary,withReduxProvider) (class ClusterTimelions extends Component { constructor(props) { super(props); this.state = {}; } - expand = id => { + expand = (id) => { this.setState({ expandedVis: this.state.expandedVis === id ? false : id }); }; render() { return ( - - - - - - -

- {'Cluster alerts summary'} -

- - this.expand( - 'Wazuh-App-Cluster-monitoring-Overview-Manager' - ) - } - iconType="expand" - aria-label="Expand" - /> -
-
- - - -
-
-
-
- - - - -

- {'Alerts by node summary'} -

- - this.expand('Wazuh-App-Cluster-monitoring-Overview') - } - iconType="expand" - aria-label="Expand" - /> -
-
- - - -
-
-
-
-
-
+ + + + + +

{'Cluster alerts summary'}

+ this.expand('Wazuh-App-Cluster-monitoring-Overview-Manager')} + iconType="expand" + aria-label="Expand" + /> +
+
+ + + +
+
+
+
+ + + + +

{'Alerts by node summary'}

+ this.expand('Wazuh-App-Cluster-monitoring-Overview')} + iconType="expand" + aria-label="Expand" + /> +
+
+ + + +
+
+
+
+
); } -} +}) diff --git a/public/components/management/cluster/cluster-visualization.js b/public/components/management/cluster/cluster-visualization.js index e80377a1dd..7a961cf826 100644 --- a/public/components/management/cluster/cluster-visualization.js +++ b/public/components/management/cluster/cluster-visualization.js @@ -1,23 +1,15 @@ -import React, { Component } from 'react'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; +import React from 'react'; import KibanaVis from '../../../kibana-integrations/kibana-vis'; +import { withErrorBoundary, withReduxProvider } from '../../../components/common/hocs'; +import { compose } from 'redux'; -export class KibanaVisWrapper extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - - render() { - return ( -
- - - -
- ); - } -} +export const KibanaVisWrapper = compose( + withErrorBoundary, + withReduxProvider +)((props) => { + return ( +
+ +
+ ); +}); \ No newline at end of file diff --git a/public/components/management/cluster/node-list.tsx b/public/components/management/cluster/node-list.tsx index e57129e691..d383bdf948 100644 --- a/public/components/management/cluster/node-list.tsx +++ b/public/components/management/cluster/node-list.tsx @@ -1,8 +1,9 @@ import React, { Component } from 'react'; import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiButtonIcon, EuiTitle, EuiInMemoryTable, EuiFieldSearch } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; +import { withErrorBoundary } from '../../common/hocs'; -export class NodeList extends Component { +export const NodeList = withErrorBoundary (class NodeList extends Component { constructor(props) { super(props); this.state = { @@ -109,4 +110,4 @@ export class NodeList extends Component { ); } -}; +}); diff --git a/public/components/management/groups/multiple-agent-selector.tsx b/public/components/management/groups/multiple-agent-selector.tsx index 0c73835a04..aab5fde185 100644 --- a/public/components/management/groups/multiple-agent-selector.tsx +++ b/public/components/management/groups/multiple-agent-selector.tsx @@ -19,8 +19,9 @@ import { WzRequest } from '../../../react-services/wz-request'; import './multiple-agent-selector.scss' import $ from 'jquery'; import { WzFieldSearchDelay } from '../../common/search'; +import { withErrorBoundary } from '../../common/hocs'; -export class MultipleAgentSelector extends Component { +export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelector extends Component { constructor(props) { super(props); this.state = { @@ -672,4 +673,4 @@ export class MultipleAgentSelector extends Component { ); } -}; +}); diff --git a/public/components/notifications/modal.tsx b/public/components/notifications/modal.tsx index 2bcaccf172..1680613acb 100644 --- a/public/components/notifications/modal.tsx +++ b/public/components/notifications/modal.tsx @@ -27,9 +27,10 @@ import { import { useSelector, useDispatch } from 'react-redux'; import { updateToastNotificationsModal } from '../../redux/actions/appStateActions'; -import { withReduxProvider } from '../common/hocs' +import { withReduxProvider, withErrorBoundary } from '../common/hocs'; +import { compose } from 'redux'; -export const ToastNotificationsModal = withReduxProvider(() => { +export const ToastNotificationsModal = compose (withErrorBoundary, withReduxProvider)(() => { const [isOpen, setIsOpen] = useState(false); const toastNotification = useSelector(state => state.appStateReducers.toastNotification); const dispatch = useDispatch(); diff --git a/public/components/overview/mitre/mitre.tsx b/public/components/overview/mitre/mitre.tsx index 71fdbbf884..257955f35f 100644 --- a/public/components/overview/mitre/mitre.tsx +++ b/public/components/overview/mitre/mitre.tsx @@ -25,14 +25,13 @@ import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { ModulesHelper } from '../../common/modules/modules-helper'; import { getDataPlugin, getToasts } from '../../../kibana-services'; -import { WzEmptyPromptNoPermissions } from "../../common/permissions/prompt"; +import { withErrorBoundary } from "../../common/hocs" export interface ITactic { [key:string]: string[] } - -export class Mitre extends Component { +export const Mitre = withErrorBoundary (class Mitre extends Component { _isMount = false; timefilter: { getTime(): TimeRange @@ -190,5 +189,5 @@ export class Mitre extends Component {
); } -} +}) diff --git a/public/components/security/main.tsx b/public/components/security/main.tsx index 511ac59578..22f6844bb3 100644 --- a/public/components/security/main.tsx +++ b/public/components/security/main.tsx @@ -19,7 +19,7 @@ import { API_USER_STATUS_RUN_AS } from '../../../server/lib/cache-api-user-has-r import { AppState } from '../../react-services/app-state'; import { ErrorHandler } from '../../react-services/error-handler'; import { RolesMapping } from './roles-mapping/roles-mapping'; -import { withReduxProvider, withGlobalBreadcrumb, withUserAuthorizationPrompt } from '../common/hocs'; +import { withReduxProvider, withGlobalBreadcrumb, withUserAuthorizationPrompt, withErrorBoundary } from '../common/hocs'; import { compose } from 'redux'; import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; import { updateSecuritySection } from '../../redux/actions/securityActions'; @@ -48,6 +48,7 @@ const tabs = [ ]; export const WzSecurity = compose( + withErrorBoundary, withReduxProvider, withGlobalBreadcrumb([{ text: '' }, { text: 'Security' }]), withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]) diff --git a/public/components/settings/api/add-api.js b/public/components/settings/api/add-api.js index e7ffe9b70d..e10e8bf5d0 100644 --- a/public/components/settings/api/add-api.js +++ b/public/components/settings/api/add-api.js @@ -25,8 +25,9 @@ import { EuiCallOut, EuiPanel } from '@elastic/eui'; +import { withErrorBoundary } from '../../common/hocs'; -export class AddApi extends Component { +export const AddApi = withErrorBoundary (class AddApi extends Component { constructor(props) { super(props); this.state = { @@ -203,7 +204,7 @@ export class AddApi extends Component { return view; } -} +}) AddApi.propTypes = { checkForNewApis: PropTypes.func, diff --git a/public/components/settings/api/api-is-down.js b/public/components/settings/api/api-is-down.js index 16eb39ba3e..c81f9f87e5 100644 --- a/public/components/settings/api/api-is-down.js +++ b/public/components/settings/api/api-is-down.js @@ -30,8 +30,9 @@ import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; +import { withErrorBoundary } from '../../common/hocs'; -export class ApiIsDown extends Component { +export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { constructor(props) { super(props); this.state = { @@ -257,7 +258,7 @@ hosts: ); } -} +}); ApiIsDown.propTypes = { apiEntries: PropTypes.array, diff --git a/public/components/settings/api/api-table.js b/public/components/settings/api/api-table.js index fe3cec4133..3bdab8d65e 100644 --- a/public/components/settings/api/api-table.js +++ b/public/components/settings/api/api-table.js @@ -28,13 +28,15 @@ import { EuiIcon } from '@elastic/eui'; import { WzButtonPermissions } from '../../common/permissions/button'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; import store from '../../../redux/store'; import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; import { AppState } from '../../../react-services/app-state'; import { API_USER_STATUS_RUN_AS } from '../../../../server/lib/cache-api-user-has-run-as'; +import { withErrorBoundary, withReduxProvider } from '../../common/hocs'; +import { compose } from 'redux' -export class ApiTable extends Component { + +export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiTable extends Component { constructor(props) { super(props); @@ -295,7 +297,6 @@ export class ApiTable extends Component { return ( - @@ -344,11 +345,10 @@ export class ApiTable extends Component { loading={this.state.refreshingEntries} /> - ); } -} +}); ApiTable.propTypes = { apiEntries: PropTypes.array, diff --git a/public/components/settings/configuration/configuration.tsx b/public/components/settings/configuration/configuration.tsx index 62e7dc75bb..bc8a712fda 100644 --- a/public/components/settings/configuration/configuration.tsx +++ b/public/components/settings/configuration/configuration.tsx @@ -27,11 +27,11 @@ import { categoriesEquivalence, formEquivalence } from '../../../utils/config-equivalences'; -import WzReduxProvider from '../../../redux/wz-redux-provider' import store from '../../../redux/store' import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; -import { withUserAuthorizationPrompt } from '../../common/hocs/withUserAuthorization' +import { withUserAuthorizationPrompt, withErrorBoundary, withReduxProvider } from '../../common/hocs' import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../../common/constants'; +import { compose } from 'redux'; export type ISetting = { setting: string @@ -81,11 +81,8 @@ const WzConfigurationSettingsProvider = (props) => { ); } -const WzConfigurationSettingsWrapper = withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME])(WzConfigurationSettingsProvider); -export function WzConfigurationSettings(props) { - return( - - - - ); -} +export const WzConfigurationSettings = compose ( + withErrorBoundary, + withReduxProvider, + withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]) +)(WzConfigurationSettingsProvider); diff --git a/public/components/settings/modules/modules.js b/public/components/settings/modules/modules.js index 22abc8f78e..c0c07613c1 100644 --- a/public/components/settings/modules/modules.js +++ b/public/components/settings/modules/modules.js @@ -16,8 +16,9 @@ import { AppState } from '../../../react-services/app-state'; import WzReduxProvider from '../../../redux/wz-redux-provider'; import store from '../../../redux/store'; import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; -import { withUserAuthorizationPrompt } from '../../common/hocs/withUserAuthorization'; +import { withUserAuthorizationPrompt, withErrorBoundary, withReduxProvider } from '../../common/hocs'; import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../../common/constants'; +import { compose } from 'redux' export class EnableModulesWrapper extends Component { constructor(props) { @@ -165,11 +166,8 @@ export class EnableModulesWrapper extends Component { } } -const WzEnableModulesWithAdministrator = withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME])(EnableModulesWrapper); -export function EnableModules() { - return( - - - - ); -} \ No newline at end of file +export const EnableModules = compose ( + withErrorBoundary, + withReduxProvider, + withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]) +)(EnableModulesWrapper); \ No newline at end of file diff --git a/public/components/settings/settings-logs/logs.js b/public/components/settings/settings-logs/logs.js index b69b4e3206..bdb642d32c 100644 --- a/public/components/settings/settings-logs/logs.js +++ b/public/components/settings/settings-logs/logs.js @@ -27,8 +27,9 @@ import { import { formatUIDate } from '../../../react-services/time-service'; import store from '../../../redux/store'; import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; +import { withErrorBoundary } from '../../common/hocs'; -export default class SettingsLogs extends Component { +class SettingsLogs extends Component { constructor(props) { super(props); this.offset = 275; @@ -137,3 +138,7 @@ export default class SettingsLogs extends Component { ); } } + +export default withErrorBoundary( + SettingsLogs +) \ No newline at end of file diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 5cf22181d4..21ffca0f15 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -36,12 +36,13 @@ import { PatternHandler } from '../../react-services/pattern-handler'; import { getToasts } from '../../kibana-services'; import { SecurityAlerts } from './components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { withReduxProvider } from '../common/hocs'; +import { withReduxProvider,withErrorBoundary } from '../common/hocs'; +import { compose } from 'redux'; const visHandler = new VisHandlers(); -export const WzVisualize = withReduxProvider(class WzVisualize extends Component { +export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class WzVisualize extends Component { _isMount = false; constructor(props) { super(props); diff --git a/public/components/wz-agent-selector/wz-agent-selector-wrapper.js b/public/components/wz-agent-selector/wz-agent-selector-wrapper.js index f469f8bd8d..290e54efdf 100644 --- a/public/components/wz-agent-selector/wz-agent-selector-wrapper.js +++ b/public/components/wz-agent-selector/wz-agent-selector-wrapper.js @@ -11,22 +11,11 @@ * Find more information about this on the LICENSE file. * */ -import React, { Component } from 'react'; import WzAgentSelector from './wz-agent-selector'; -import WzReduxProvider from '../../redux/wz-redux-provider'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../common/hocs'; - -export class WzAgentSelectorWrapper extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - render() { - return ( - - - - ); - } -} +export const WzAgentSelectorWrapper = compose( + withErrorBoundary, + withReduxProvider +)(WzAgentSelector); \ No newline at end of file diff --git a/public/components/wz-filter-bar/wz-filter-bar.js b/public/components/wz-filter-bar/wz-filter-bar.js index 2b0d06332c..ada604ae3c 100644 --- a/public/components/wz-filter-bar/wz-filter-bar.js +++ b/public/components/wz-filter-bar/wz-filter-bar.js @@ -13,8 +13,9 @@ import React, { Component } from 'react'; import { EuiComboBox } from '@elastic/eui'; import PropTypes from 'prop-types'; import './wz-filter-bar.scss'; +import { withErrorBoundary } from '../common/hocs'; -export class WzFilterBar extends Component { +export const WzFilterBar = withErrorBoundary (class WzFilterBar extends Component { constructor(props) { super(props); const { model, selectedOptions } = this.props; @@ -295,7 +296,7 @@ export class WzFilterBar extends Component { /> ); } -} +}); WzFilterBar.propTypes = { clickAction: PropTypes.func, diff --git a/public/components/wz-menu/wz-menu-wrapper.js b/public/components/wz-menu/wz-menu-wrapper.js index 92f5ef409d..5d16e61145 100644 --- a/public/components/wz-menu/wz-menu-wrapper.js +++ b/public/components/wz-menu/wz-menu-wrapper.js @@ -11,22 +11,10 @@ * Find more information about this on the LICENSE file. * */ -import React, { Component } from 'react'; +import React from 'react'; import WzMenu from './wz-menu'; -import WzReduxProvider from '../../redux/wz-redux-provider'; import './wz-menu.scss'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../common/hocs'; -export class WzMenuWrapper extends Component { - constructor(props) { - super(props); - this.state = {}; - } - - render() { - return ( - - - - ); - } -} +export const WzMenuWrapper = compose (withErrorBoundary, withReduxProvider)(WzMenu); diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index dbc8f9cdbf..4f8ad4f9ad 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -21,7 +21,6 @@ import { EuiStat, EuiLoadingChart, EuiSpacer, - EuiText, EuiEmptyPrompt, EuiToolTip } from '@elastic/eui'; @@ -40,8 +39,10 @@ import { WzDatePicker } from '../../../components/wz-date-picker/wz-date-picker' import { withReduxProvider, withGlobalBreadcrumb, withUserAuthorizationPrompt } from '../../../components/common/hocs'; import { formatUIDate } from '../../../../public/react-services/time-service'; import { compose } from 'redux'; +import { withErrorBoundary } from '../../../components/common/hocs' export const AgentsPreview = compose( + withErrorBoundary, withReduxProvider, withGlobalBreadcrumb([{ text: '' }, { text: 'Agents' }]), withUserAuthorizationPrompt([[{action: 'agent:read', resource: 'agent:id:*'},{action: 'agent:read', resource: 'agent:group:*'}]]) diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 077f4f78e5..1e1432b246 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -40,8 +40,9 @@ import { WzSearchBar, filtersToObject } from '../../../components/wz-search-bar' import { getAgentFilterValues } from '../../../controllers/management/components/management/groups/get-agents-filters-values'; import { WzButtonPermissions } from '../../../components/common/permissions/button'; import { formatUIDate } from '../../../react-services/time-service'; +import { withErrorBoundary } from '../../../components/common/hocs' -export class AgentsTable extends Component { +export const AgentsTable = withErrorBoundary (class AgentsTable extends Component { _isMount = false; constructor(props) { super(props); @@ -1024,7 +1025,7 @@ export class AgentsTable extends Component { ); } -} +}); AgentsTable.propTypes = { wzReq: PropTypes.func, diff --git a/public/controllers/agent/components/export-configuration.js b/public/controllers/agent/components/export-configuration.js index 004b428b6a..40dda2331b 100644 --- a/public/controllers/agent/components/export-configuration.js +++ b/public/controllers/agent/components/export-configuration.js @@ -22,8 +22,9 @@ import { import PropTypes from 'prop-types'; import { UnsupportedComponents } from '../../../utils/components-os-support'; import { WAZUH_AGENTS_OS_TYPE } from '../../../../common/constants'; +import { withErrorBoundary } from '../../../components/common/hocs'; -export class ExportConfiguration extends Component { +export const ExportConfiguration = withErrorBoundary (class ExportConfiguration extends Component { constructor(props) { super(props); @@ -163,7 +164,7 @@ export class ExportConfiguration extends Component { ); } -} +}); ExportConfiguration.propTypes = { exportConfiguration: PropTypes.func, diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index ca150d594b..085689fb37 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -14,14 +14,11 @@ import { version } from '../../../../package.json'; import { WazuhConfig } from '../../../react-services/wazuh-config'; import { EuiSteps, - EuiTabs, EuiTabbedContent, EuiFlexGroup, EuiFlexItem, EuiPanel, - EuiButtonToggle, EuiButtonGroup, - EuiFormRow, EuiComboBox, EuiFieldText, EuiText, @@ -38,7 +35,7 @@ import { EuiCode } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; - +import { withErrorBoundary } from '../../../components/common/hocs' const architectureButtons = [ { @@ -114,7 +111,7 @@ const pTextCheckConnectionStyle = { marginTop: '3em', }; -export class RegisterAgent extends Component { +export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Component { constructor(props) { super(props); this.wazuhConfig = new WazuhConfig(); @@ -703,4 +700,4 @@ export class RegisterAgent extends Component { ); } -} +}) diff --git a/public/controllers/management/components/management/configuration/configuration-main.js b/public/controllers/management/components/management/configuration/configuration-main.js index 6a4980a58c..b5993a92a8 100644 --- a/public/controllers/management/components/management/configuration/configuration-main.js +++ b/public/controllers/management/components/management/configuration/configuration-main.js @@ -10,51 +10,37 @@ * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react'; -import WzReduxProvider from '../../../../../redux/wz-redux-provider'; import WzConfigurationSwitch from './configuration-switch'; -import { updateGlobalBreadcrumb } from '../../../../../redux/actions/globalBreadcrumbActions'; -import store from '../../../../../redux/store'; +import { + withErrorBoundary, + withGlobalBreadcrumb, + withReduxProvider, +} from '../../../../../components/common/hocs'; +import { compose } from 'redux' -class WzConfigurationMain extends Component { - constructor(props) { - super(props); - } - - setGlobalBreadcrumb() { +export default compose( + withErrorBoundary, + withReduxProvider, + withGlobalBreadcrumb((props) => { let breadcrumb = false; - if (this.props.agent.id === '000') { + if (props.agent.id === '000') { breadcrumb = [ { text: '' }, { text: 'Management', href: '/app/wazuh#/manager' }, - { text: 'Configuration' } + { text: 'Configuration' }, ]; } else { breadcrumb = [ { text: '' }, { text: 'Agents', - href: '#/agents-preview' + href: '#/agents-preview', }, - { agent: this.props.agent }, - { text: 'Configuration' } + { agent: props.agent }, + { text: 'Configuration' }, ]; } - store.dispatch(updateGlobalBreadcrumb(breadcrumb)); - $('#breadcrumbNoTitle').attr("title",""); - } - - async componentDidMount() { - this.setGlobalBreadcrumb(); - } - - render() { - return ( - - - - ); - } -} - -export default WzConfigurationMain; + $('#breadcrumbNoTitle').attr('title', ''); + return breadcrumb; + }) +)(WzConfigurationSwitch); diff --git a/public/controllers/management/components/management/management-provider.js b/public/controllers/management/components/management/management-provider.js index e370f80015..6d2dbcf586 100644 --- a/public/controllers/management/components/management/management-provider.js +++ b/public/controllers/management/components/management/management-provider.js @@ -1,21 +1,9 @@ -import React, { Component } from 'react'; +import { compose } from 'redux'; +import { withErrorBoundary, withReduxProvider } from '../../../../components/common/hocs'; // Redux -import store from '../../../../redux/store'; -import WzReduxProvider from '../../../../redux/wz-redux-provider'; import WzManagementMain from '../management/management-main'; -export default class WzManagement extends Component { - constructor(props) { - super(props); - this.state = {}; - this.store = store; - } - - render() { - return ( - - - - ); - } -} +export default compose( + withErrorBoundary, + withReduxProvider +)(WzManagementMain); \ No newline at end of file diff --git a/public/controllers/overview/components/requirement-card.js b/public/controllers/overview/components/requirement-card.js index 0c2818872a..883e405c45 100644 --- a/public/controllers/overview/components/requirement-card.js +++ b/public/controllers/overview/components/requirement-card.js @@ -19,8 +19,9 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { withErrorBoundary } from '../../../components/common/hocs'; -export class RequirementCard extends Component { +export const RequirementCard = withErrorBoundary (class RequirementCard extends Component { constructor(props) { super(props); this.state = { @@ -171,7 +172,7 @@ export class RequirementCard extends Component { ); } -} +}); RequirementCard.propTypes = { items: PropTypes.array, diff --git a/public/controllers/overview/components/select-agent.js b/public/controllers/overview/components/select-agent.js index c5f5b54f6c..3bda19e17d 100644 --- a/public/controllers/overview/components/select-agent.js +++ b/public/controllers/overview/components/select-agent.js @@ -26,8 +26,9 @@ import { // import { WzRequest } from '../../../../react-services/wz-request'; import { WzRequest } from '../../../react-services/wz-request'; +import { withErrorBoundary } from '../../../components/common/hocs'; -export class SelectAgent extends Component { +export const SelectAgent = withErrorBoundary (class SelectAgent extends Component { constructor(props) { super(props); @@ -274,7 +275,7 @@ export class SelectAgent extends Component { ); } -} +}); SelectAgent.propTypes = { items: PropTypes.array diff --git a/public/controllers/overview/components/stats.js b/public/controllers/overview/components/stats.js index e866c01331..58ce8323d5 100644 --- a/public/controllers/overview/components/stats.js +++ b/public/controllers/overview/components/stats.js @@ -13,8 +13,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiPage, EuiToolTip } from '@elastic/eui'; +import { withErrorBoundary } from '../../../components/common/hocs'; -export class Stats extends Component { +export const Stats = withErrorBoundary (class Stats extends Component { constructor(props) { super(props); @@ -123,7 +124,7 @@ export class Stats extends Component { ); } -} +}); Stats.propTypes = { total: PropTypes.any, diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index c1a7df3676..adf4f91a3a 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -27,7 +27,7 @@ import { EuiTitle, } from '@elastic/eui'; import { WzRequest } from '../../../react-services'; -import { withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; +import { withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; import { compose } from 'redux'; type LogstestProps = { @@ -38,6 +38,7 @@ type LogstestProps = { }; export const Logtest = compose( + withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt([{ action: 'logtest:run', resource: `*:*:*` }]) )((props: LogstestProps) => { diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index ca7c8287d5..ab73e1ede6 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -182,7 +182,6 @@ export class BaseLogger { .then(() => { if (this.allowed) { this.checkFiles(); - const plainLogData: IUIPlainLoggerSettings = { level: level || 'error', message: `${this.yyyymmdd()}: ${location || 'Unknown origin'}: ${ @@ -198,7 +197,6 @@ export class BaseLogger { location: location || 'Unknown origin', message: message || 'An error occurred', }; - this.wazuhLogger.log(logData); } }) From 2f4588c954c269063d8bb20ff9001ccb07d0d5a2 Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Tue, 15 Jun 2021 14:51:05 +0200 Subject: [PATCH 007/493] Feature/add status and type in vulnerabilities table (#3196) * refactor in vulnerabilities table component * refactor code in vuls inventory and add new table component with export csv * adapat table * finished refactor table component * delete console logs and fix wrong version * add new fields in suggestions * add changelog * changes in component table and remove status and type columns * fix columns position --- CHANGELOG.md | 3 +- public/components/agents/vuls/inventory.tsx | 131 +----------------- .../agents/vuls/inventory/filterBar.tsx | 53 ------- .../agents/vuls/inventory/flyout.tsx | 2 +- .../components/agents/vuls/inventory/index.ts | 2 - .../agents/vuls/inventory/table.tsx | 124 +++++------------ .../tables/components/export-table-csv.tsx | 62 +++++++++ public/components/common/tables/index.ts | 3 +- .../common/tables/table-default.tsx | 91 ++++++++++++ .../tables/table-with-search-bar-wz-api.tsx | 44 ------ .../common/tables/table-with-search-bar.tsx | 2 + .../components/common/tables/table-wz-api.tsx | 96 +++++++++++++ .../management/groups/group-agents-table.js | 5 +- 13 files changed, 296 insertions(+), 322 deletions(-) delete mode 100644 public/components/agents/vuls/inventory/filterBar.tsx create mode 100644 public/components/common/tables/components/export-table-csv.tsx create mode 100644 public/components/common/tables/table-default.tsx delete mode 100644 public/components/common/tables/table-with-search-bar-wz-api.tsx create mode 100644 public/components/common/tables/table-wz-api.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dae2bc5b8..a793755bd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) +- Added fields status and type in vulnerabilities table [#3196] (https://github.com/wazuh/wazuh-kibana-app/pull/3196) ### Changed @@ -28,7 +29,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added vulnerabilities inventory that affect to an agent [#3069](https://github.com/wazuh/wazuh-kibana-app/pull/3069) - Added retry button to check api again in health check [#3109](https://github.com/wazuh/wazuh-kibana-app/pull/3109) - Added `wazuh-statistics` template and a new mapping for these indices [#3111](https://github.com/wazuh/wazuh-kibana-app/pull/3111) -- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126) +- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126 ### Changed diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 0044d09979..3cd5d0701b 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -14,31 +14,20 @@ import React, { Component } from 'react'; import { EuiPanel, EuiPage, - EuiTitle, - EuiLoadingSpinner, EuiSpacer, EuiProgress, EuiFlexGroup, EuiFlexItem, - EuiButtonEmpty } from '@elastic/eui'; import { InventoryTable, - FilterBar } from './inventory/'; -import { WzRequest } from '../../../react-services/wz-request'; -import exportCsv from '../../../react-services/wz-csv'; -import { getToasts } from '../../../kibana-services'; import { ICustomBadges } from '../../wz-search-bar/components'; -import { filtersToObject } from '../../wz-search-bar'; export class Inventory extends Component { _isMount = false; state: { - filters: []; - totalItems: number; isLoading: Boolean; - items: []; customBadges: ICustomBadges[]; }; @@ -47,13 +36,9 @@ export class Inventory extends Component { constructor(props) { super(props); this.state = { - filters: [], - items: [], - totalItems: 0, isLoading: true, customBadges: [] } - this.onFiltersChange.bind(this); } async componentDidMount() { @@ -61,133 +46,25 @@ export class Inventory extends Component { await this.loadAgent(); } - componentDidUpdate(prevProps) { - if (JSON.stringify(this.props.agent) !== JSON.stringify(prevProps.agent)){ - this.setState({isLoading: true}, this.loadAgent) - } - } - componentWillUnmount() { this._isMount = false; } async loadAgent() { - const {totalItems, items} = await this.getItemNumber(); if (this._isMount){ - this.setState({ totalItems, items, isLoading: false }); - } - } - - getStoreFilters(props) { - const { section, selectView, agent } = props; - const filters = JSON.parse(window.localStorage.getItem(`wazuh-${section}-${selectView}-vulnerability-${agent['id']}`) || '{}'); - return filters; - } - - setStoreFilters(filters) { - const { section, selectView, agent } = this.props; - window.localStorage.setItem(`wazuh-${section}-${selectView}-vulnerability-${agent['id']}`, JSON.stringify(filters)) - } - - onFiltersChange = (filters) => { - this.setStoreFilters(filters); - this.setState({ filters }); - } - - onTotalItemsChange = (totalItems: number) => { - this.setState({ totalItemsFile: totalItems }); - } - - buildFilter() { - const filters = filtersToObject(this.state.filters); - const filter = { - ...filters, - limit: '15' - }; - return filter; - } - - async getItemNumber() { - try { - const agentID = this.props.agent.id; - let response = await WzRequest.apiReq('GET', `/vulnerability/${agentID}`, { - params: this.buildFilter(), - }); - return { - totalItems: ((response.data || {}).data || {}).total_affected_items || 0, - items: ((response.data || {}).data || {}).affected_items || [], - }; - } catch (error) { - this.setState({ isLoading: false }); - this.showToast('danger', error, 3000); - } - } - - renderTitle() { - const { isLoading, totalItems } = this.state; - - return ( - - - -

Vulnerabilities {isLoading === true ? : ({ totalItems })}

-
-
- - this.downloadCsv()}> - Export formatted - - -
- ) - } - - showToast = (color, title, time) => { - getToasts().add({ - color: color, - title: title, - toastLifeTimeMs: time, - }); - }; - - async downloadCsv() { - const { filters } = this.state; - try { - const filtersObject = filtersToObject(filters); - const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); - this.showToast('success', 'Your download should begin automatically...', 3000); - await exportCsv( - '/vulnerability/' + this.props.agent.id, - [ - ...formatedFilters - ], - `vuls-vulnerabilities` - ); - } catch (error) { - this.showToast('danger', error, 3000); + this.setState({ isLoading: false }); } } renderTable() { - const { filters, items, totalItems } = this.state; return (
- + {...this.props}/>
) } - - + loadingInventory() { return @@ -205,11 +82,9 @@ export class Inventory extends Component { return this.loadingInventory() } const table = this.renderTable(); - const title = this.renderTitle(); return - {title} {table} diff --git a/public/components/agents/vuls/inventory/filterBar.tsx b/public/components/agents/vuls/inventory/filterBar.tsx deleted file mode 100644 index e8abfcb7bf..0000000000 --- a/public/components/agents/vuls/inventory/filterBar.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Wazuh app - Agent vulnerabilities components - * Copyright (C) 2015-2021 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import { getFilterValues } from './lib'; -import { WzSearchBar, IFilter, IWzSuggestItem } from '../../../../components/wz-search-bar' -import { ICustomBadges } from '../../../wz-search-bar/components'; -import { - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; - -export class FilterBar extends Component { - - suggestions: IWzSuggestItem[] = [ - {type: 'q', label: 'name', description:"Filter by package ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('name', value, this.props.agent.id)}, - {type: 'q', label: 'cve', description:"Filter by CVE ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('cve', value, this.props.agent.id)}, - {type: 'q', label: 'version', description:"Filter by CVE version", operators:['=','!=', '~'], values: async (value) => getFilterValues('version', value, this.props.agent.id)}, - {type: 'q', label: 'architecture', description:"Filter by architecture", operators:['=','!=', '~'], values: async (value) => getFilterValues('architecture', value, this.props.agent.id)}, - ] - - props!:{ - onFiltersChange(filters:IFilter[]): void - agent: {id: string, agentPlatform: string} - onChangeCustomBadges?(customBadges: ICustomBadges[]): void - customBadges?: ICustomBadges[] - filters: IFilter[] - } - - render() { - const { onFiltersChange, filters} = this.props; - return ( - - - - - - ) - } -} \ No newline at end of file diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 32bb343111..c9d6eea64e 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -71,7 +71,6 @@ export class FlyoutDetail extends Component { const { currentItem } = this.state; const title = `${currentItem.name} ${currentItem.cve}`; const id = title.replace(/ /g, '_'); - return ( this.props.closeFlyout()} @@ -101,6 +100,7 @@ export class FlyoutDetail extends Component { { 'rule.groups': 'vulnerability-detector' }, { 'data.vulnerability.package.name': currentItem.name }, { 'data.vulnerability.cve': currentItem.cve }, + { 'data.vulnerability.type': currentItem.type }, { 'data.vulnerability.package.architecture': currentItem.architecture }, { 'data.vulnerability.package.version': currentItem.version }, { 'agent.id': this.props.agentId }, diff --git a/public/components/agents/vuls/inventory/index.ts b/public/components/agents/vuls/inventory/index.ts index 1238860c71..8c4dab0b8a 100644 --- a/public/components/agents/vuls/inventory/index.ts +++ b/public/components/agents/vuls/inventory/index.ts @@ -1,3 +1 @@ -export { FilterBar } from './filterBar'; - export { InventoryTable } from './table'; \ No newline at end of file diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 31f18e00e1..f9194e93c4 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -12,23 +12,19 @@ import React, { Component } from 'react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiBasicTable, Direction, EuiOverlayMask, } from '@elastic/eui'; -import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; -import { filtersToObject, IFilter } from '../../../wz-search-bar'; +import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; +import { TableWzAPI } from '../../../../components/common/tables'; +import { getFilterValues } from './lib'; export class InventoryTable extends Component { state: { - items: [] error?: string pageIndex: number pageSize: number - totalItems: number sortField: string isFlyoutVisible: Boolean sortDirection: Direction @@ -36,11 +32,16 @@ export class InventoryTable extends Component { currentItem: {} }; + suggestions: IWzSuggestItem[] = [ + {type: 'q', label: 'name', description:"Filter by package ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('name', value, this.props.agent.id)}, + {type: 'q', label: 'cve', description:"Filter by CVE ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('cve', value, this.props.agent.id)}, + {type: 'q', label: 'version', description:"Filter by CVE version", operators:['=','!=', '~'], values: async (value) => getFilterValues('version', value, this.props.agent.id)}, + {type: 'q', label: 'architecture', description:"Filter by architecture", operators:['=','!=', '~'], values: async (value) => getFilterValues('architecture', value, this.props.agent.id)}, + ] + props!: { filters: IFilter[] agent: any - items: [] - totalItems: number, onTotalItemsChange: Function } @@ -48,10 +49,8 @@ export class InventoryTable extends Component { super(props); this.state = { - items: props.items, pageIndex: 0, pageSize: 15, - totalItems: 0, sortField: 'name', sortDirection: 'asc', isLoading: false, @@ -60,10 +59,6 @@ export class InventoryTable extends Component { } } - async componentDidMount() { - this.setState({totalItems: this.props.totalItems}); - } - closeFlyout() { this.setState({ isFlyoutVisible: false, currentItem: {} }); } @@ -77,32 +72,10 @@ export class InventoryTable extends Component { async componentDidUpdate(prevProps) { const { filters } = this.props; if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getItems); + this.setState({ pageIndex: 0, isLoading: true }); } } - async getItems() { - const agentID = this.props.agent.id; - try { - const items = await WzRequest.apiReq( - 'GET', - `/vulnerability/${agentID}`, - { params: this.buildFilter()}, - ); - - this.props.onTotalItemsChange((((items || {}).data || {}).data || {}).total_affected_items); - - this.setState({ - items: (((items || {}).data || {}).data || {}).affected_items || {}, - totalItems: (((items || {}).data || {}).data || {}).total_affected_items - 1, - isLoading: false, - error: undefined - }); - } catch (error) { - this.setState({error, isLoading: false}) - } -} - buildSortFilter() { const { sortField, sortDirection } = this.state; const direction = (sortDirection === 'asc') ? '+' : '-'; @@ -122,47 +95,33 @@ export class InventoryTable extends Component { return filter; } - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, - () => this.getItems() - ); - }; - columns() { let width; (((this.props.agent || {}).os || {}).platform || false) === 'windows' ? width = '60px' : width = '80px'; return [ { - field: 'cve', - name: 'CVE', + field: 'version', + name: 'Version', sortable: true, truncateText: true, width: `${width}` }, { - field: 'name', - name: 'Name', + field: 'architecture', + name: 'Architecture', sortable: true, width: '100px' }, { - field: 'version', - name: 'Version', + field: 'cve', + name: 'CVE', sortable: true, truncateText: true, width: `${width}` }, { - field: 'architecture', - name: 'Architecture', + field: 'name', + name: 'Name', sortable: true, width: '100px' } @@ -178,43 +137,28 @@ export class InventoryTable extends Component { }; }; - const { items, pageIndex, pageSize, totalItems, sortField, sortDirection, isLoading, error } = this.state; + const { error } = this.state; const columns = this.columns(); - const pagination = { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100], - } - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - + const selectFields = 'select=cve,architecture,version,name' return ( - - - - - + ); } render() { const table = this.renderTable(); + return (
{table} diff --git a/public/components/common/tables/components/export-table-csv.tsx b/public/components/common/tables/components/export-table-csv.tsx new file mode 100644 index 0000000000..b0f968321b --- /dev/null +++ b/public/components/common/tables/components/export-table-csv.tsx @@ -0,0 +1,62 @@ +/* + * Wazuh app - Table with search bar + * Copyright (C) 2015-2021 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 from 'react'; +import { + EuiFlexItem, + EuiButtonEmpty +} from '@elastic/eui'; +import { filtersToObject } from '../../../wz-search-bar/'; +import exportCsv from '../../../../react-services/wz-csv'; +import { getToasts } from '../../../../kibana-services'; + +export function ExportTableCsv({endpoint,totalItems,filters,title}){ + + const showToast = (color, title, time) => { + getToasts().add({ + color: color, + title: title, + toastLifeTimeMs: time, + }); + }; + + const downloadCsv = async () => { + try { + const filtersObject = filtersToObject(filters); + const formatedFilters = Object.keys(filtersObject).map(key => ({name: key, value: filtersObject[key]})); + showToast('success', 'Your download should begin automatically...', 3000); + await exportCsv( + endpoint, + [ + ...formatedFilters + ], + `vuls-${(title).toLowerCase()}` + ); + } catch (error) { + showToast('danger', error, 3000); + } + } + + return + downloadCsv()}> + Export formatted + + +} + +// Set default props +ExportTableCsv.defaultProps = { + endpoint:'/', + totalItems:0, + filters: [], + title:"" + }; \ No newline at end of file diff --git a/public/components/common/tables/index.ts b/public/components/common/tables/index.ts index 842f0b4cee..27431d14af 100644 --- a/public/components/common/tables/index.ts +++ b/public/components/common/tables/index.ts @@ -12,4 +12,5 @@ export { TableWithSearchBar } from './table-with-search-bar'; -export { TableWithSearchBarWzAPI } from './table-with-search-bar-wz-api'; \ No newline at end of file +export { TableWzAPI } from './table-wz-api'; +export { TableDeafult } from './table-default'; \ No newline at end of file diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx new file mode 100644 index 0000000000..b4eabce870 --- /dev/null +++ b/public/components/common/tables/table-default.tsx @@ -0,0 +1,91 @@ +/* + * Wazuh app - Table with search bar + * Copyright (C) 2015-2021 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, { useState, useEffect } from 'react'; +import { EuiBasicTable } from '@elastic/eui'; + +export function TableDeafult({ + onSearch, + tableColumns, + rowProps, + tablePageSizeOptions = [15, 25, 50, 100], + tableInitialSortingDirection = 'asc', + tableInitialSortingField = '', + tableProps = {}, + reload +}) + { + + const [loading, setLoading] = useState(false); + const [items, setItems] = useState([]); + const [totalItems, setTotalItems] = useState(0); + const [filters, setFilters] = useState([]); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: tablePageSizeOptions[0] + }); + + const [sorting, setSorting] = useState({ + sort: { + field: tableInitialSortingField, + direction: tableInitialSortingDirection, + } + }); + + function tableOnChange({ page = {}, sort = {} }){ + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort; + setPagination({ + pageIndex, + pageSize + }); + setSorting({ + sort: { + field, + direction, + }, + }); + }; + + useEffect(() => { + (async function(){ + try{ + setLoading(true); + const { items, totalItems } = await onSearch(filters, pagination, sorting); + setItems(items); + setTotalItems(totalItems); + }catch(error){ + setItems([]); + setTotalItems(0); + } + setLoading(false); + })() + }, [filters, pagination, sorting, reload]); + + const tablePagination = { + ...pagination, + totalItemCount: totalItems, + pageSizeOptions: tablePageSizeOptions + } + return <> + + +} diff --git a/public/components/common/tables/table-with-search-bar-wz-api.tsx b/public/components/common/tables/table-with-search-bar-wz-api.tsx deleted file mode 100644 index 91deb30381..0000000000 --- a/public/components/common/tables/table-with-search-bar-wz-api.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Wazuh app - Table with search bar - * Copyright (C) 2015-2021 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, { useCallback } from 'react'; -import { filtersToObject } from '../../wz-search-bar/'; -import { TableWithSearchBar } from './table-with-search-bar'; -import { WzRequest } from '../../../react-services/wz-request'; - -export function TableWithSearchBarWzAPI({endpoint, ...rest}){ - const onSearch = useCallback(async function(filters, pagination, sorting){ - try { - const { pageIndex, pageSize } = pagination; - const { field, direction } = sorting.sort; - - const params = { - ...filtersToObject(filters), - offset: pageIndex * pageSize, - limit: pageSize, - sort: `${direction === 'asc' ? '+' : '-'}${field}` - }; - - const response = await WzRequest.apiReq('GET', endpoint, { params }); - - const { affected_items, total_affected_items } = ((response || {}).data || {}).data; - - return {items: affected_items, totalItems: total_affected_items}; - } catch (error) { - return Promise.reject(error); - } - },[]); - return -} \ No newline at end of file diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 9218d53843..74732c4f94 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -20,6 +20,7 @@ export function TableWithSearchBar({ searchBarPlaceholder = 'Filter or search', searchBarProps = {}, tableColumns, + rowProps, tablePageSizeOptions = [15, 25, 50, 100], tableInitialSortingDirection = 'asc', tableInitialSortingField = '', @@ -96,6 +97,7 @@ export function TableWithSearchBar({ pagination={tablePagination} sorting={sorting} onChange={tableOnChange} + rowProps={rowProps} {...tableProps} /> diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx new file mode 100644 index 0000000000..6fb9ea1658 --- /dev/null +++ b/public/components/common/tables/table-wz-api.tsx @@ -0,0 +1,96 @@ +/* + * Wazuh app - Table with search bar + * Copyright (C) 2015-2021 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, { useCallback, useState } from 'react'; +import { + EuiTitle, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { filtersToObject } from '../../wz-search-bar'; +import { TableWithSearchBar } from './table-with-search-bar'; +import { TableDeafult } from './table-default' +import { WzRequest } from '../../../react-services/wz-request'; +import { ExportTableCsv } from './components/export-table-csv'; + +export function TableWzAPI({endpoint, ...rest}){ + + const [results, setResults] = useState({items: {}, totalItems: 0}); + const [filters, setFilters] = useState([]); + + const onSearch = useCallback(async function(filters, pagination, sorting){ + try { + const { pageIndex, pageSize } = pagination; + const { field, direction } = sorting.sort; + setFilters(filters) + const params = { + ...filtersToObject(filters), + offset: pageIndex * pageSize, + limit: pageSize, + sort: `${direction === 'asc' ? '+' : '-'}${field}` + }; + + const response = await WzRequest.apiReq('GET', endpoint, { params }); + + const { affected_items, total_affected_items } = ((response || {}).data || {}).data; + const results = {items: affected_items, totalItems: total_affected_items} + setResults(results) + return results; + } catch (error) { + return Promise.reject(error); + } + },[]); + + const renderTableHeader = () => { + return ( + + + {rest.title ? +

{rest.title}  {rest.reload === true ? : ({ results.totalItems })}

+
: ''} +
+ {rest.downloadCsv ? : ''} +
+ ) + } + + const renderTable = () => { + return ( + rest.searchTable ? + : + + ) + } + + const header = renderTableHeader(); + const table = renderTable(); + + return <> + {header} + {table} + +} + +// Set default props +TableWzAPI.defaultProps = { + title: null, + downloadCsv: false, + searchBar: false, + rowProps: false, +}; \ No newline at end of file diff --git a/public/controllers/management/components/management/groups/group-agents-table.js b/public/controllers/management/components/management/groups/group-agents-table.js index 3cedb5e83b..4a1a24f3b2 100644 --- a/public/controllers/management/components/management/groups/group-agents-table.js +++ b/public/controllers/management/components/management/groups/group-agents-table.js @@ -31,7 +31,7 @@ import { } from '../../../../../redux/actions/groupsActions'; import { getAgentFilterValues } from './get-agents-filters-values'; -import { TableWithSearchBarWzAPI } from '../../../../../components/common/tables'; +import { TableWzAPI } from '../../../../../components/common/tables'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../components/common/buttons'; @@ -146,12 +146,13 @@ class WzGroupAgentsTable extends Component { const { error } = this.props.state; if (!error) { return ( - ); } else { From 41c471d8fc0b4e81ca7f73cf8280127f453d1308 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Thu, 17 Jun 2021 13:00:23 -0300 Subject: [PATCH 008/493] Added lowercase levels in storeError (#3377) * Added lowercase levels in storeError * Updated CHANGELOG Co-authored-by: Ibarra Maximiliano --- CHANGELOG.md | 1 + .../error-orchestrator/error-orchestrator-base.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a793755bd5..1a41c1c212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added retry button to check api again in health check [#3109](https://github.com/wazuh/wazuh-kibana-app/pull/3109) - Added `wazuh-statistics` template and a new mapping for these indices [#3111](https://github.com/wazuh/wazuh-kibana-app/pull/3111) - Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126 +- Added lowercase levels in storeError [#3377](https://github.com/wazuh/wazuh-kibana-app/pull/3377) ### Changed diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.ts b/public/react-services/error-orchestrator/error-orchestrator-base.ts index dc585ec3ec..bda80e239a 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-base.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-base.ts @@ -26,9 +26,14 @@ export class ErrorOrchestratorBase implements ErrorOrchestrator { private async storeError(errorLog: UIErrorLog) { try { + let winstonLevel = errorLog.level.toLowerCase(); + if(errorLog.level === 'WARNING'){ + winstonLevel = 'warn'; + } + await GenericRequest.request('POST', `/utils/logs/ui`, { message: errorLog.error.message, - level: errorLog.level, + level: winstonLevel, location: errorLog.location, }); } catch (error) { From 7503895dbfbdd42b9bcccc3946aeeb7c12fbbf17 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Thu, 17 Jun 2021 22:24:27 +0200 Subject: [PATCH 009/493] [Feature] Mitre Att&ck Intelligence + adapt Framework (#3368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor in vulnerabilities table component * refactor code in vuls inventory and add new table component with export csv * adapat table * finished refactor table component * delete console logs and fix wrong version * add new fields in suggestions * add changelog * changes in component table and remove status and type columns * fix columns position * feat(mitre): Add Mitre Att&ck intelligence section - Created Intelligence tab in Mitre Att&ck Module - Created left and right panel - Created resource button for the left panel - Created search bar for searhin in all resources - Created list of each resource * feat(mitre_intelligence): Modify the search results view and another improvements - Modify the Search results view - Improve useAsyncAction hook - Add Mitre Att&ck Intelligence to Agent modules component - Improve TableWithSearchBar component to accept filters as props - Refactor Mitre Atta&ck resources - Refator PanelSplit component - Fix filtersToObject helper - Update test * feat(mitre_att&ck_intelligence): Render description as markdown - Create Markdown component - Apply the Markdown component to the resource description in the resouce table * feat(mitre_atta&ck_intelligence): Add like operator to search resource by description * change endpoint and adapt component in mitre * fix flyout * feat(mirte_att&ck_integillence): Add the References resource - Added to left panel - Added resource view * add redirect to intelligence * fix merge * fix merge flyout * fix package version * fix(mitre_att&ck_intelligence): Organize resource suggestions and remove Reference resource * fix PR comments and add intelligence section redirect * Created new Mitre flyout * Changelog * fix comments PR * add redirect values in query params * apply prettier * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout * clear comments and imports * fix error handler techniques * delete session storage * delete files and fix get techniques data * Created new Mitre flyout (#3344) * Created new Mitre flyout * Changelog * Erasing comments * Erasing console.log * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout Co-authored-by: Antonio David Gutiérrez * fix redirect flyout to rules * feat(frontend/mitre_att&ck_intelligence): Removed welcome intelligence - Removed welcome intelligence view and adjustments when doing a general search - Set a resource type as selected - Update test * fix comments PR * fix(mitre_att&ck_intelligence): Change how to open the resource details flyout - Change how to open the resource details flyout - Refactor some componentes properties - Removed not used code * changelog: Added PR to chengelog * Update CHANGELOG.md * fix(mitre_att&ck_intelligence): Fix error in table-default.tsx * fix(mitre_att&ck_intelligence): PR request changes: - Add tests for: - Components: Markdown, PanelSplit - React Hooks: useAsyncAction - Renamed files - Add justification for using dangerouslySetInnerHTML property - Refactor requests to get mitre techniques - Fix tooltips to open tactic/technique details in Framework - Added some missing semicolon - Fix CSS class wz-markdown-wrapper name * fix get mitre Techniques from api Co-authored-by: eze9252 Co-authored-by: CPAlejandro Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: Franco Charriol --- CHANGELOG.md | 2 + common/api-info/endpoints.json | 597 +++++++++++++++++- common/api-info/security-actions.json | 34 +- public/components/common/hooks/index.ts | 2 + .../common/hooks/use_async_action.test.tsx | 76 +++ .../common/hooks/use_async_action.ts | 32 + .../common/modules/discover/discover.tsx | 3 +- .../components/common/modules/main-agent.tsx | 2 + .../common/modules/main-overview.tsx | 7 +- .../common/modules/modules-defaults.js | 2 +- .../__snapshots__/panel_split.test.tsx.snap | 36 ++ public/components/common/panels/index.ts | 1 + .../common/panels/panel_split.test.tsx | 26 + .../components/common/panels/panel_split.tsx | 30 + public/components/common/tables/index.ts | 7 +- .../common/tables/table-default.tsx | 6 +- .../common/tables/table-with-search-bar.tsx | 9 +- .../components/common/tables/table-wz-api.tsx | 63 +- public/components/common/util/index.ts | 1 + .../__snapshots__/markdown.test.tsx.snap | 13 + .../common/util/markdown/markdown.test.tsx | 30 + .../common/util/markdown/markdown.tsx | 38 ++ .../flyout-technique/flyout-technique.tsx | 139 ++-- .../search-bar-techniques.tsx | 11 - .../techniques/components/technique/index.ts | 11 - .../components/technique/teqnique.tsx | 11 - .../components/techniques/techniques.tsx | 99 ++- public/components/overview/mitre/lib/index.ts | 2 - .../overview/mitre/lib/mitre_techniques.ts | 268 -------- public/components/overview/mitre/mitre.tsx | 16 +- .../__snapshots__/intelligence.test.tsx.snap | 50 ++ .../all_resources.tsx | 48 ++ .../all_resources_search_results.tsx | 63 ++ .../index.ts | 5 +- .../intelligence.test.tsx | 40 ++ .../intelligence.tsx | 98 +++ .../intelligence_left_panel.tsx | 44 ++ .../intelligence_right_panel.tsx | 41 ++ .../mitre_attack_intelligence/resource.tsx | 87 +++ .../resource_button.scss | 18 + .../resource_button.tsx | 26 + .../resource_detail_flyout.tsx | 102 +++ .../resource_detail_references_table.tsx | 80 +++ .../mitre_attack_intelligence/resources.tsx | 128 ++++ .../wz-search-bar/lib/filters-to-object.ts | 2 +- .../management/ruleset/rule-info.js | 27 +- public/styles/common.scss | 2 +- 47 files changed, 1940 insertions(+), 495 deletions(-) create mode 100644 public/components/common/hooks/use_async_action.test.tsx create mode 100644 public/components/common/hooks/use_async_action.ts create mode 100644 public/components/common/panels/__snapshots__/panel_split.test.tsx.snap create mode 100644 public/components/common/panels/index.ts create mode 100644 public/components/common/panels/panel_split.test.tsx create mode 100644 public/components/common/panels/panel_split.tsx create mode 100644 public/components/common/util/markdown/__snapshots__/markdown.test.tsx.snap create mode 100644 public/components/common/util/markdown/markdown.test.tsx create mode 100644 public/components/common/util/markdown/markdown.tsx delete mode 100644 public/components/overview/mitre/components/techniques/components/search-bar-tecniques/search-bar-techniques.tsx delete mode 100644 public/components/overview/mitre/components/techniques/components/technique/index.ts delete mode 100644 public/components/overview/mitre/components/techniques/components/technique/teqnique.tsx delete mode 100644 public/components/overview/mitre/lib/mitre_techniques.ts create mode 100644 public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap create mode 100644 public/components/overview/mitre_attack_intelligence/all_resources.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx rename public/components/overview/{mitre/components/techniques/components/search-bar-tecniques => mitre_attack_intelligence}/index.ts (81%) create mode 100644 public/components/overview/mitre_attack_intelligence/intelligence.test.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/intelligence.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/resource.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/resource_button.scss create mode 100644 public/components/overview/mitre_attack_intelligence/resource_button.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx create mode 100644 public/components/overview/mitre_attack_intelligence/resources.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a41c1c212..81edad6db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - Added fields status and type in vulnerabilities table [#3196] (https://github.com/wazuh/wazuh-kibana-app/pull/3196) +- Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) +- Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index dfae980a92..089b5ce3a8 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -4964,22 +4964,413 @@ ] }, { - "name": "/mitre", - "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_attack", - "description": "Return the requested attacks from MITRE database", - "summary": "Get MITRE attacks", + "name": "/mitre/groups", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_groups", + "description": "Return the groups from MITRE database", + "summary": "Get MITRE groups", "tags": [ - "Mitre" + "MITRE" ], "query": [ { - "name": "id", - "description": "MITRE attack ID", + "name": "group_ids", + "description": "List of MITRE's group IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE group ID" + } + } + }, + { + "name": "limit", + "description": "Maximum number of elements to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 500 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/metadata", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_metadata", + "description": "Return the metadata from MITRE database", + "summary": "Get MITRE metadata", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/mitigations", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_mitigations", + "description": "Return the mitigations from MITRE database", + "summary": "Get MITRE mitigations", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 500 + } + }, + { + "name": "mitigation_ids", + "description": "List of MITRE's mitigations IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE mitigation ID" + } + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/references", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_references", + "description": "Return the references from MITRE database", + "summary": "Get MITRE references", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 500 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "reference_ids", + "description": "List of MITRE's references IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE Reference ID" + } + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/software", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_software", + "description": "Return the software from MITRE database", + "summary": "Get MITRE software", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 500 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", "schema": { "type": "string", - "format": "alphanumeric" + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "software_ids", + "description": "List of MITRE's software IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE software ID" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" } }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/tactics", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_tactics", + "description": "Return the tactics from MITRE database", + "summary": "Get MITRE tactics", + "tags": [ + "MITRE" + ], + "query": [ { "name": "limit", "description": "Maximum number of elements to return", @@ -5002,19 +5393,96 @@ } }, { - "name": "phase_name", - "description": "Show results filtered by phase", + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", "schema": { "type": "string", - "format": "alphanumeric" + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } } }, { - "name": "platform_name", - "description": "Show results filtered by platform", + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", "schema": { "type": "string", - "format": "alphanumeric" + "format": "sort" + } + }, + { + "name": "tactic_ids", + "description": "List of MITRE's tactics IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE tactic ID" + } + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/techniques", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_techniques", + "description": "Return the techniques from MITRE database", + "summary": "Get MITRE techniques", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 500 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 } }, { @@ -5059,6 +5527,17 @@ "format": "sort" } }, + { + "name": "technique_ids", + "description": "List of MITRE's techniques IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE technique ID" + } + } + }, { "name": "wait_for_complete", "description": "Disable timeout response", @@ -5336,7 +5815,7 @@ }, { "name": "mitre", - "description": "Filters by MITRE attack ID", + "description": "Filters by MITRE technique ID", "schema": { "type": "string", "format": "alphanumeric" @@ -5891,7 +6370,7 @@ "description": "Filter by command", "schema": { "type": "string", - "format": "alphanumeric" + "format": "symbols_alphanumeric_param" } }, { @@ -6168,6 +6647,17 @@ "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6283,6 +6773,17 @@ "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6359,6 +6860,17 @@ "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6442,6 +6954,17 @@ "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -7953,7 +8476,7 @@ "description": "Filter by command", "schema": { "type": "string", - "format": "alphanumeric" + "format": "symbols_alphanumeric_param" } }, { @@ -8511,6 +9034,46 @@ } ] }, + { + "name": "/agents/reconnect", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.reconnect_agents", + "description": "Force reconnect all agents or a list of them", + "summary": "Force reconnect agents", + "tags": [ + "Agents" + ], + "query": [ + { + "name": "agents_list", + "description": "List of agent IDs (separated by comma), all agents selected by default if not specified", + "schema": { + "type": "array", + "items": { + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" + } + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, { "name": "/agents/restart", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents", diff --git a/common/api-info/security-actions.json b/common/api-info/security-actions.json index ed25084d5d..87f3a4ef57 100644 --- a/common/api-info/security-actions.json +++ b/common/api-info/security-actions.json @@ -203,14 +203,14 @@ "group:read": { "description": "Access agent groups information (id, name, agents, etc)", "resources": [ - "*:*","group:id" + "group:id" ], "example": { "actions": [ "group:create" ], "resources": [ - "*:*:*" + "group:id:*" ], "effect": "allow" }, @@ -296,6 +296,26 @@ "GET /cluster/{node_id}/configuration/{component}/{configuration}" ] }, + "agent:reconnect": { + "description": "Force reconnect agents", + "resources": [ + "agent:id", + "agent:group" + ], + "example": { + "actions": [ + "agent:reconnect" + ], + "resources": [ + "agent:id:050", + "agent:id:049" + ], + "effect": "deny" + }, + "related_endpoints": [ + "PUT /agents/reconnect" + ] + }, "ciscat:read": { "description": "Access CIS-CAT results for agents", "resources": [ @@ -552,7 +572,7 @@ ] }, "mitre:read": { - "description": "Access attacks information from MITRE database", + "description": "Access information from MITRE database", "resources": [ "*:*" ], @@ -566,7 +586,13 @@ "effect": "allow" }, "related_endpoints": [ - "GET /mitre" + "GET /mitre/groups", + "GET /mitre/metadata", + "GET /mitre/mitigations", + "GET /mitre/references", + "GET /mitre/software", + "GET /mitre/tactics", + "GET /mitre/techniques" ] }, "rootcheck:clear": { diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 63537e4229..62a22c2b3d 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -31,3 +31,5 @@ export { useRefreshAngularDiscover } from './useResfreshAngularDiscover'; export { useAllowedAgents } from './useAllowedAgents'; export { useApiRequest } from './useApiRequest'; + +export * from './use_async_action'; diff --git a/public/components/common/hooks/use_async_action.test.tsx b/public/components/common/hooks/use_async_action.test.tsx new file mode 100644 index 0000000000..ba901663c9 --- /dev/null +++ b/public/components/common/hooks/use_async_action.test.tsx @@ -0,0 +1,76 @@ +/* + * Wazuh app - Test for React hook for build async action runners + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { useAsyncAction } from './use_async_action'; + +const sleep = (miliseconds: number) => new Promise(res => setTimeout(res,miliseconds)); + +const NO_DATA = 'no data'; +const RESPONSE_SUCCESS = 'Example data'; +const RESPOSE_ERROR = 'Example error'; + +const TestComponent = ({action}) => { + const { data, error, running, run } = useAsyncAction(action,[]); + + return (
+ +
{String(running)}
+
{data || NO_DATA}
+
{String(error)}
+
); +}; + +describe('useAsyncAction hook', () => { + test('should run the asynchronous action and display the data', async () => { + const component = mount( { + await sleep(500); + return RESPONSE_SUCCESS; + }}/>); + + expect(component.find('#running').text()).toBe('false'); + expect(component.find('#data').text()).toBe(NO_DATA); + expect(component.find('#error').text()).toBe('null'); + + await sleep(200); + component.find('button').simulate('click'); + + await sleep(100); + expect(component.find('#running').text()).toBe('true'); + + await sleep(550); + expect(component.find('#running').text()).toBe('false'); + expect(component.find('#data').text()).toBe(RESPONSE_SUCCESS); + }); + + test('should run the asynchronous action and display an error', async () => { + const component = mount( { + await sleep(500); + throw RESPOSE_ERROR; + return RESPONSE_SUCCESS; + }} />); + expect(component.find('#running').text()).toBe('false'); + expect(component.find('#data').text()).toBe(NO_DATA); + expect(component.find('#error').text()).toBe('null'); + + await sleep(200); + component.find('button').simulate('click'); + + await sleep(100); + expect(component.find('#running').text()).toBe('true'); + + await sleep(550); + expect(component.find('#running').text()).toBe('false'); + expect(component.find('#data').text()).toBe('no data'); + expect(component.find('#error').text()).toBe(RESPOSE_ERROR); + }); +}); diff --git a/public/components/common/hooks/use_async_action.ts b/public/components/common/hooks/use_async_action.ts new file mode 100644 index 0000000000..d71167255e --- /dev/null +++ b/public/components/common/hooks/use_async_action.ts @@ -0,0 +1,32 @@ +/* + * Wazuh app - React hook for build async action runners + * Copyright (C) 2015-2021 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 { useCallback, useState } from 'react'; + +export function useAsyncAction(action, dependencies = []){ + const [running, setRunning] = useState(false); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + const run = useCallback(async (...params) => { + try{ + setRunning(true); + const data = await action(...params); + setData(data); + }catch(error){ + setError(error); + }finally{ + setRunning(false); + }; + }, dependencies); + + return { data, error, running, run }; +} \ No newline at end of file diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 868ccfb3d3..5fd915c6e5 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -97,6 +97,7 @@ export const Discover = compose( query?: { language: "kuery" | "lucene", query: string } type?: any, updateTotalHits: Function, + openIntelligence: Function, includeFilters?: string, initialColumns: string[], shareFilterManager: FilterManager, @@ -431,7 +432,7 @@ export const Discover = compose( width = '15%'; } if (item === 'rule.mitre.id') { - link = (ev, x) => { this.setState({ showMitreFlyout: true, selectedTechnique: x }) }; + link = (ev, x, e) => this.props.openIntelligence(e,'techniques',x); } if(arrayCompilance.indexOf(item) !== -1) { width = '30%'; diff --git a/public/components/common/modules/main-agent.tsx b/public/components/common/modules/main-agent.tsx index cdde9852a1..8b5eb683ba 100644 --- a/public/components/common/modules/main-agent.tsx +++ b/public/components/common/modules/main-agent.tsx @@ -42,6 +42,7 @@ import { getAngularModule } from '../../../kibana-services'; import { withAgentSupportModule } from '../../../components/common/hocs'; import { connect } from 'react-redux'; import { compose } from 'redux'; +import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; export class MainModuleAgent extends Component { props!: { @@ -350,6 +351,7 @@ const ModuleTabViewer = compose( {section === 'fim' && selectView==='inventory' && } {section === 'sca' && selectView==='inventory' && } {section === 'mitre' && selectView === 'inventory' && } + {section === 'mitre' && selectView === 'intelligence' && } {/* {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && props.onSelectedTabChanged(id)} />} */} {/* -------------------------------------------------------------------------- */} diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index d8db49306b..ea0f44496b 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -41,6 +41,8 @@ import { withAgentSupportModule } from '../../../components/common/hocs'; import { connect } from 'react-redux'; import { compose } from 'redux'; +import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; + export class MainModuleOverview extends Component { constructor(props) { super(props); @@ -214,7 +216,10 @@ const ModuleTabViewer = compose( {section === 'vuls' && selectView==='inventory' && } {section === 'mitre' && selectView === 'inventory' && } - {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && props.onSelectedTabChanged(id)} />} + {section === 'mitre' && selectView === 'intelligence' && } + {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && ( + props.onSelectedTabChanged(id)} /> + )} {/* -------------------------------------------------------------------------- */} }) diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index df5e3ff01a..0a7232f2c2 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -32,7 +32,7 @@ export const ModulesDefaults = { }, mitre: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Framework' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], + tabs: [{id: 'intelligence', name: 'Intelligence'}, { id: 'inventory', name: 'Framework' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], buttons: ['reporting'] }, vuls: { diff --git a/public/components/common/panels/__snapshots__/panel_split.test.tsx.snap b/public/components/common/panels/__snapshots__/panel_split.test.tsx.snap new file mode 100644 index 0000000000..8e7cd61add --- /dev/null +++ b/public/components/common/panels/__snapshots__/panel_split.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PanelSplit container should render the component 1`] = ` + + + +
+ Side panel +
+
+ +
+ Content panel +
+
+
+
+`; diff --git a/public/components/common/panels/index.ts b/public/components/common/panels/index.ts new file mode 100644 index 0000000000..fe73636ad6 --- /dev/null +++ b/public/components/common/panels/index.ts @@ -0,0 +1 @@ +export * from './panel_split'; \ No newline at end of file diff --git a/public/components/common/panels/panel_split.test.tsx b/public/components/common/panels/panel_split.test.tsx new file mode 100644 index 0000000000..a77bb27515 --- /dev/null +++ b/public/components/common/panels/panel_split.test.tsx @@ -0,0 +1,26 @@ + +/* + * Wazuh app - PanelSplit Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { PanelSplit } from './panel_split'; + +describe('PanelSplit container', () => { + test('should render the component', () => { + const component = shallow(Side panel
} content={
Content panel
}/>); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/panels/panel_split.tsx b/public/components/common/panels/panel_split.tsx new file mode 100644 index 0000000000..9538458475 --- /dev/null +++ b/public/components/common/panels/panel_split.tsx @@ -0,0 +1,30 @@ +/* + * Wazuh app - Panel split component + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +export const PanelSplit = ({side, sideColor='#80808014', content, panelProps = {}, sideProps = {}, contentProps = {}}) => { + const {style: sidePropsSytle, ...restSideProps} = sideProps; + const {style: contentPropsSytle, ...restContentProps} = contentProps; + return ( + + + + {side} + + + {content} + + + + ) +} diff --git a/public/components/common/tables/index.ts b/public/components/common/tables/index.ts index 27431d14af..2574dc47b5 100644 --- a/public/components/common/tables/index.ts +++ b/public/components/common/tables/index.ts @@ -10,7 +10,6 @@ * Find more information about this on the LICENSE file. */ - -export { TableWithSearchBar } from './table-with-search-bar'; -export { TableWzAPI } from './table-wz-api'; -export { TableDeafult } from './table-default'; \ No newline at end of file +export * from './table-with-search-bar'; +export * from './table-wz-api'; +export * from './table-default'; diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx index b4eabce870..ec1e7c27a6 100644 --- a/public/components/common/tables/table-default.tsx +++ b/public/components/common/tables/table-default.tsx @@ -16,7 +16,6 @@ import { EuiBasicTable } from '@elastic/eui'; export function TableDeafult({ onSearch, tableColumns, - rowProps, tablePageSizeOptions = [15, 25, 50, 100], tableInitialSortingDirection = 'asc', tableInitialSortingField = '', @@ -76,16 +75,13 @@ export function TableDeafult({ totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions } - return <> - - } diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 74732c4f94..363728f425 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -25,14 +25,15 @@ export function TableWithSearchBar({ tableInitialSortingDirection = 'asc', tableInitialSortingField = '', tableProps = {}, - reload + reload, + ...rest }) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); - const [filters, setFilters] = useState([]); + const [filters, setFilters] = useState(rest.filters || []); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: tablePageSizeOptions[0] @@ -75,6 +76,10 @@ export function TableWithSearchBar({ })() }, [filters, pagination, sorting, reload]); + useEffect(() => { + setFilters(rest.filters || []) + }, [rest.filters]); + const tablePagination = { ...pagination, totalItemCount: totalItems, diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx index 6fb9ea1658..2ce607f3d7 100644 --- a/public/components/common/tables/table-wz-api.tsx +++ b/public/components/common/tables/table-wz-api.tsx @@ -25,14 +25,16 @@ import { ExportTableCsv } from './components/export-table-csv'; export function TableWzAPI({endpoint, ...rest}){ - const [results, setResults] = useState({items: {}, totalItems: 0}); + const [totalItems, setTotalItems] = useState(0); const [filters, setFilters] = useState([]); + const [isLoading, setIsLoading] = useState(false); const onSearch = useCallback(async function(filters, pagination, sorting){ try { const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; - setFilters(filters) + setIsLoading(true); + setFilters(filters); const params = { ...filtersToObject(filters), offset: pageIndex * pageSize, @@ -42,31 +44,31 @@ export function TableWzAPI({endpoint, ...rest}){ const response = await WzRequest.apiReq('GET', endpoint, { params }); - const { affected_items, total_affected_items } = ((response || {}).data || {}).data; - const results = {items: affected_items, totalItems: total_affected_items} - setResults(results) - return results; + const { affected_items: items, total_affected_items: totalItems } = ((response || {}).data || {}).data; + setIsLoading(false); + setTotalItems(totalItems); + return { items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, totalItems }; } catch (error) { + setIsLoading(false); + setTotalItems(0); return Promise.reject(error); - } + }; },[]); - const renderTableHeader = () => { - return ( - - - {rest.title ? -

{rest.title}  {rest.reload === true ? : ({ results.totalItems })}

-
: ''} -
- {rest.downloadCsv ? : ''} -
- ) - } + const header = ( + + + {rest.title && ( + +

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

+
+ )} +
+ {rest.downloadCsv && } +
+ ) - const renderTable = () => { - return ( - rest.searchTable ? + const table = rest.searchTable ? - ) - } - - const header = renderTableHeader(); - const table = renderTable(); - return <> - {header} - {table} - + return ( + <> + {header} + {table} + ) } // Set default props TableWzAPI.defaultProps = { title: null, downloadCsv: false, - searchBar: false, - rowProps: false, + searchBar: false }; \ No newline at end of file diff --git a/public/components/common/util/index.ts b/public/components/common/util/index.ts index a4ccbf1403..669665dff4 100644 --- a/public/components/common/util/index.ts +++ b/public/components/common/util/index.ts @@ -14,3 +14,4 @@ export { AgentGroupTruncate, GroupTruncate} from './agent-group-truncate'; export { TruncateHorizontalComponents } from './truncate-horizontal-components/truncate-horizontal-components'; export { GroupingComponents } from './grouping-components'; + export * from './markdown/markdown'; diff --git a/public/components/common/util/markdown/__snapshots__/markdown.test.tsx.snap b/public/components/common/util/markdown/__snapshots__/markdown.test.tsx.snap new file mode 100644 index 0000000000..d97200a1bd --- /dev/null +++ b/public/components/common/util/markdown/__snapshots__/markdown.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Markdown container should render the component 1`] = ` +
Example text

+", + } + } +/> +`; diff --git a/public/components/common/util/markdown/markdown.test.tsx b/public/components/common/util/markdown/markdown.test.tsx new file mode 100644 index 0000000000..7b935d1723 --- /dev/null +++ b/public/components/common/util/markdown/markdown.test.tsx @@ -0,0 +1,30 @@ + +/* + * Wazuh app - Markdown Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { Markdown } from './markdown'; + +describe('Markdown container', () => { + test('should render the component', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('should render a link', () => { + const component = shallow(); + expect(component.html().includes('label')).toBe(true); + }); +}); diff --git a/public/components/common/util/markdown/markdown.tsx b/public/components/common/util/markdown/markdown.tsx new file mode 100644 index 0000000000..9dc3d5d100 --- /dev/null +++ b/public/components/common/util/markdown/markdown.tsx @@ -0,0 +1,38 @@ +/* + * Wazuh app - React component that renders markdown + * Copyright (C) 2015-2021 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 from 'react'; +import MarkdownIt from 'markdown-it'; +import classnames from 'classnames'; + +const md = new MarkdownIt({ + html: true, + linkify: true, + breaks: true, + typographer: true +}); + +interface MarkdownProps{ + markdown: string + className?: string +}; + +export const Markdown = ({markdown, className = ''}: MarkdownProps) => ( +
+
+); diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index dfda83fe2e..99dda5b392 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -56,6 +56,7 @@ export class FlyoutTechnique extends Component { props!: { currentTechniqueData: any currentTechnique: string + tacticsObject: any } filterManager: FilterManager; @@ -118,9 +119,9 @@ export class FlyoutTechnique extends Component { try{ this.setState({loading: true, techniqueData: {}}); const { currentTechnique } = this.props; - const result = await WzRequest.apiReq('GET', '/mitre', { + const result = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `id=${currentTechnique}` + q: `references.external_id=${currentTechnique}` } }); const rawData = (((result || {}).data || {}).data || {}).affected_items @@ -130,36 +131,18 @@ export class FlyoutTechnique extends Component { } } - formatTechniqueData (rawData) { - const { platform_name, phase_name} = rawData; - const { name, description, x_mitre_version: version, x_mitre_data_sources, external_references } = rawData.json; - - const replaced_external_references = []; - let index_replaced_external_references = 0; - let last_citation_string = ''; - const descriptionWithCitations = external_references.reduce((accum, reference) => { - return accum - .replace(new RegExp(`\\(Citation: ${reference.source_name}\\)`,'g'), (token) => { - if(last_citation_string !== token){ - index_replaced_external_references++; - replaced_external_references.push({...reference, index: index_replaced_external_references}); - last_citation_string = token; - } - return `[${String(index_replaced_external_references)}]`; - }) - }, description); - this.setState({techniqueData: { name, description: descriptionWithCitations, phase_name, platform_name, version, x_mitre_data_sources, external_references, replaced_external_references }, loading: false }) + findTacticName(tactics){ + const { tacticsObject } = this.props; + return tactics.map((element) => { + const tactic = Object.values(tacticsObject).find(obj => obj.id === element); + return { id:tactic.references[0].external_id, name: tactic.name}; + }); } - getArrayFormatted(arrayText) { - try { - const stringText = arrayText.toString(); - const splitString = stringText.split(','); - const resultString = splitString.join(', '); - return resultString; - } catch (err) { - return arrayText; - } + formatTechniqueData (rawData) { + const { tactics, name, mitre_version } = rawData; + const tacticsObj = this.findTacticName(tactics) + this.setState({techniqueData: { name, mitre_version, tacticsObj }, loading: false }); } renderHeader() { @@ -180,7 +163,7 @@ export class FlyoutTechnique extends Component { ) } - + renderBody() { const { currentTechnique } = this.props; const { techniqueData } = this.state; @@ -194,7 +177,7 @@ export class FlyoutTechnique extends Component { const formattedDescription = techniqueData.description ? (
) @@ -204,60 +187,43 @@ export class FlyoutTechnique extends Component { title: 'ID', description: ( - + content={`Open ${currentTechnique} details in the Intelligence section`}> + {this.props.openIntelligence(e,'techniques',currentTechnique);e.stopPropagation()}}> {currentTechnique} ) }, { - title: 'Tactic', - description: this.getArrayFormatted( - techniqueData.phase_name - ) - }, - { - title: 'Platform', - description: this.getArrayFormatted( - techniqueData.platform_name - ) - }, - { - title: 'Data sources', - description: this.getArrayFormatted( - techniqueData.x_mitre_data_sources - ) + title: 'Tactics', + description: techniqueData.tacticsObj + ? techniqueData.tacticsObj.map((tactic) => { + return ( + <> + + { + this.props.openIntelligence(e, "tactics", tactic.id); + e.stopPropagation(); + }} + > + {tactic.name} + + +
+ + ); + }) + : "" }, { title: 'Version', - description: techniqueData.version - }, - { - title: 'Description', - description: formattedDescription - }, + description: techniqueData.mitre_version + } ]; - if(techniqueData && techniqueData.replaced_external_references && techniqueData.replaced_external_references.length > 0){ - data.push({ - title: 'References', - description: ( - - - {techniqueData.replaced_external_references.map((external_reference, external_reference_index) => ( -
- {external_reference.index}. - - {external_reference.source_name} - -
- ) - )} -
-
- ) - }) - } return ( - -

- More info:{' '} - - {`MITRE ATT&CK - ${currentTechnique}`} - -

)} @@ -320,7 +279,15 @@ export class FlyoutTechnique extends Component { initialIsOpen={true}> - this.updateTotalHits(total)}/> + this.updateTotalHits(total)} + openIntelligence={(e,redirectTo,itemId) => this.props.openIntelligence(e,redirectTo,itemId)} + /> @@ -336,8 +303,8 @@ export class FlyoutTechnique extends Component { renderLoading(){ return ( - - + + ) } diff --git a/public/components/overview/mitre/components/techniques/components/search-bar-tecniques/search-bar-techniques.tsx b/public/components/overview/mitre/components/techniques/components/search-bar-tecniques/search-bar-techniques.tsx deleted file mode 100644 index 0be09f42eb..0000000000 --- a/public/components/overview/mitre/components/techniques/components/search-bar-tecniques/search-bar-techniques.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2021 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. - */ diff --git a/public/components/overview/mitre/components/techniques/components/technique/index.ts b/public/components/overview/mitre/components/techniques/components/technique/index.ts deleted file mode 100644 index 0be09f42eb..0000000000 --- a/public/components/overview/mitre/components/techniques/components/technique/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2021 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. - */ diff --git a/public/components/overview/mitre/components/techniques/components/technique/teqnique.tsx b/public/components/overview/mitre/components/techniques/components/technique/teqnique.tsx deleted file mode 100644 index 0be09f42eb..0000000000 --- a/public/components/overview/mitre/components/techniques/components/technique/teqnique.tsx +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Wazuh app - Mitre alerts components - * Copyright (C) 2015-2021 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. - */ diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 5373e76776..3a4f1a3cc3 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -29,14 +29,14 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FlyoutTechnique } from './components/flyout-technique/'; -import { mitreTechniques, getElasticAlerts, IFilterParams } from '../../lib' +import { getElasticAlerts, IFilterParams } from '../../lib'; import { ITactic } from '../../'; import { withWindowSize } from '../../../../../components/common/hocs/withWindowSize'; import { WzRequest } from '../../../../../react-services/wz-request'; import {WAZUH_ALERTS_PATTERN} from '../../../../../../common/constants'; import { AppState } from '../../../../../react-services/app-state'; -import { WzFieldSearchDelay } from '../../../../common/search' -import { getDataPlugin } from '../../../../../kibana-services'; +import { WzFieldSearchDelay } from '../../../../common/search'; +import { getDataPlugin, getToasts } from '../../../../../kibana-services'; export const Techniques = withWindowSize(class Techniques extends Component { _isMount = false; @@ -56,6 +56,7 @@ export const Techniques = withWindowSize(class Techniques extends Component { hideAlerts: boolean, actionsOpen: string, filteredTechniques: boolean | [string] + mitreTechniques: [], isSearching: boolean } @@ -70,6 +71,7 @@ export const Techniques = withWindowSize(class Techniques extends Component { hideAlerts: false, actionsOpen: "", filteredTechniques: false, + mitreTechniques: [], isSearching: false } this.onChangeFlyout.bind(this); @@ -77,6 +79,7 @@ export const Techniques = withWindowSize(class Techniques extends Component { async componentDidMount(){ this._isMount = true; + await this.buildMitreTechniquesFromApi(); } shouldComponentUpdate(nextProps, nextState) { @@ -102,6 +105,15 @@ export const Techniques = withWindowSize(class Techniques extends Component { this._isMount = false; } + showToast(color: string, title: string = '', text: string = '', time: number = 3000) { + getToasts().add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); + }; + async getTechniquesCount() { try{ const {indexPattern, filters} = this.props; @@ -177,29 +189,76 @@ export const Techniques = withWindowSize(class Techniques extends Component { } } + async getMitreTechniques (params) { + try{ + return await WzRequest.apiReq("GET", "/mitre/techniques", { params }); + }catch(error){ + this.showToast( + 'danger', + 'Error', + `Mitre techniques could not be fetched: ${error}`, + 3000 + ); + return []; + } + } + + async buildMitreTechniquesFromApi () { + const limitResults = 500; + const params = { limit: limitResults }; + this.setState({ isSearching: true }); + const output = await this.getMitreTechniques(params); + const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; + let mitreTechniques = []; + mitreTechniques.push(...output.data.data.affected_items); + if (totalItems && output.data && output.data.data && totalItems > limitResults) { + const extraResults = await Promise.all( + Array(Math.ceil((totalItems-params.limit)/params.limit)).fill() + .map(async (_,index) => { + const response = await this.getMitreTechniques({...params, offset: limitResults * (1+index)}); + return response.data.data.affected_items; + }) + ); + mitreTechniques.push(...extraResults.flat()); + } + this.setState({ mitreTechniques: mitreTechniques, isSearching: false }); + } + + buildObjTechniques(techniques){ + const techniquesObj = []; + techniques.forEach(element => { + const mitreObj = this.state.mitreTechniques.find(item => item.id === element); + if(mitreObj){ + const mitreTechniqueName = mitreObj.name; + const mitreTechniqueID = mitreObj.references.find(item => item.source === "mitre-attack").external_id; + mitreTechniqueID ? techniquesObj.push({ id : mitreTechniqueID, name: mitreTechniqueName}) : ''; + } + }); + return techniquesObj; + } + renderFacet() { const { tacticsObject } = this.props; const { techniquesCount } = this.state; + let hash = {}; let tacticsToRender: Array = []; - const currentTechniques = Object.keys(tacticsObject).map(tacticsKey => ({tactic: tacticsKey, techniques: tacticsObject[tacticsKey]})) + const currentTechniques = Object.keys(tacticsObject).map(tacticsKey => ({tactic: tacticsKey, techniques: this.buildObjTechniques(tacticsObject[tacticsKey].techniques)})) .filter(tactic => this.props.selectedTactics[tactic.tactic]) .map(tactic => tactic.techniques) .flat() .filter((techniqueID, index, array) => array.indexOf(techniqueID) === index); - tacticsToRender = currentTechniques - .filter(techniqueID => this.state.filteredTechniques ? this.state.filteredTechniques.includes(techniqueID) : techniqueID) - .map(techniqueID => { + .filter(technique => this.state.filteredTechniques ? this.state.filteredTechniques.includes(technique.id) : technique.id && hash[technique.id] ? false : hash[technique.id] = true) + .map(technique => { return { - id: techniqueID, - label: `${techniqueID} - ${mitreTechniques[techniqueID].name}`, - quantity: (techniquesCount.find(item => item.key === techniqueID) || {}).doc_count || 0 + id: technique.id, + label: `${technique.id} - ${technique.name}`, + quantity: (techniquesCount.find(item => item.key === technique.id) || {}).doc_count || 0 } }) .filter(technique => this.state.hideAlerts ? technique.quantity !== 0 : true); - const tacticsToRenderOrdered = tacticsToRender.sort((a, b) => b.quantity - a.quantity).map( (item,idx) => { - const tooltipContent = `View details of ${mitreTechniques[item.id].name} (${item.id})`; + const tooltipContent = `View details of ${item.label} (${item.id})`; const toolTipAnchorClass = "wz-display-inline-grid" + (this.state.hover=== item.id ? " wz-mitre-width" : " "); return( - {item.id} - {mitreTechniques[item.id].name} + {item.label} @@ -281,6 +340,11 @@ export const Techniques = withWindowSize(class Techniques extends Component { this.props.onSelectedTabChanged('dashboard'); } + openIntelligence(e,redirectTo,itemId){ + this.props.onSelectedTabChanged('intelligence'); + window.location.href = window.location+`&tabRedirect=${redirectTo}&idToRedirect=${itemId}` + } + /** * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} * @param filter @@ -314,14 +378,13 @@ export const Techniques = withWindowSize(class Techniques extends Component { try{ if(searchValue){ this._isMount && this.setState({isSearching: true}); - const response = await WzRequest.apiReq('GET', '/mitre', { + const response = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - select: "id", search: searchValue, limit: 500 } }); - const filteredTechniques = ((((response || {}).data || {}).data).affected_items || []).map(item => item.id); + const filteredTechniques = ((((response || {}).data || {}).data).affected_items || []).map(item => item.references.filter(reference => reference.source === "mitre-attack")[0].external_id); this._isMount && this.setState({ filteredTechniques, isSearching: false }); }else{ this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); @@ -403,9 +466,11 @@ export const Techniques = withWindowSize(class Techniques extends Component { this.openDashboard(e,itemId)} openDiscover={(e,itemId) => this.openDiscover(e,itemId)} + openIntelligence={(e,redirectTo,itemId) => this.openIntelligence(e,redirectTo,itemId)} onChangeFlyout={this.onChangeFlyout} currentTechniqueData={this.state.currentTechniqueData} - currentTechnique={currentTechnique} /> + currentTechnique={currentTechnique} + tacticsObject={this.props.tacticsObject} /> } diff --git a/public/components/overview/mitre/lib/index.ts b/public/components/overview/mitre/lib/index.ts index 64e0901655..b14fdde744 100644 --- a/public/components/overview/mitre/lib/index.ts +++ b/public/components/overview/mitre/lib/index.ts @@ -11,5 +11,3 @@ */ export { IFilterParams, getElasticAlerts, getIndexPattern } from './elastic-helpers'; - -export { mitreTechniques } from './mitre_techniques'; \ No newline at end of file diff --git a/public/components/overview/mitre/lib/mitre_techniques.ts b/public/components/overview/mitre/lib/mitre_techniques.ts deleted file mode 100644 index 8b64d60aad..0000000000 --- a/public/components/overview/mitre/lib/mitre_techniques.ts +++ /dev/null @@ -1,268 +0,0 @@ -export const mitreTechniques = { - T1001: { name: "Data Obfuscation" }, - T1002: { name: "Data Compressed" }, - T1003: { name: "Credential Dumping" }, - T1004: { name: "Winlogon Helper DLL" }, - T1005: { name: "Data from Local System" }, - T1006: { name: "File System Logical Offsets" }, - T1007: { name: "System Service Discovery" }, - T1008: { name: "Fallback Channels" }, - T1009: { name: "Binary Padding" }, - T1010: { name: "Application Window Discovery" }, - T1011: { name: "Exfiltration Over Other Network Medium" }, - T1012: { name: "Query Registry" }, - T1013: { name: "Port Monitors" }, - T1014: { name: "Rootkit" }, - T1015: { name: "Accessibility Features" }, - T1016: { name: "System Network Configuration Discovery" }, - T1017: { name: "Application Deployment Software" }, - T1018: { name: "Remote System Discovery" }, - T1019: { name: "System Firmware" }, - T1020: { name: "Automated Exfiltration" }, - T1021: { name: "Remote Services" }, - T1022: { name: "Data Encrypted" }, - T1023: { name: "Shortcut Modification" }, - T1024: { name: "Custom Cryptographic Protocol" }, - T1025: { name: "Data from Removable Media" }, - T1026: { name: "Multiband Communication" }, - T1027: { name: "Obfuscated Files or Information" }, - T1028: { name: "Windows Remote Management" }, - T1029: { name: "Scheduled Transfer" }, - T1030: { name: "Data Transfer Size Limits" }, - T1031: { name: "Modify Existing Service" }, - T1032: { name: "Standard Cryptographic Protocol" }, - T1033: { name: "System Owner/User Discovery" }, - T1034: { name: "Path Interception" }, - T1035: { name: "Service Execution" }, - T1036: { name: "Masquerading" }, - T1037: { name: "Logon Scripts" }, - T1038: { name: "DLL Search Order Hijacking" }, - T1039: { name: "Data from Network Shared Drive" }, - T1040: { name: "Network Sniffing" }, - T1041: { name: "Exfiltration Over Command and Control Channel" }, - T1042: { name: "Change Default File Association" }, - T1043: { name: "Commonly Used Port" }, - T1044: { name: "File System Permissions Weakness" }, - T1045: { name: "Software Packing" }, - T1046: { name: "Network Service Scanning" }, - T1047: { name: "Windows Management Instrumentation" }, - T1048: { name: "Exfiltration Over Alternative Protocol" }, - T1049: { name: "System Network Connections Discovery" }, - T1050: { name: "New Service" }, - T1051: { name: "Shared Webroot" }, - T1052: { name: "Exfiltration Over Physical Medium" }, - T1053: { name: "Scheduled Task" }, - T1054: { name: "Indicator Blocking" }, - T1055: { name: "Process Injection" }, - T1056: { name: "Input Capture" }, - T1057: { name: "Process Discovery" }, - T1058: { name: "Service Registry Permissions Weakness" }, - T1059: { name: "Command-Line Interface" }, - T1060: { name: "Registry Run Keys / Startup Folder" }, - T1061: { name: "Graphical User Interface" }, - T1062: { name: "Hypervisor" }, - T1063: { name: "Security Software Discovery" }, - T1064: { name: "Scripting" }, - T1065: { name: "Uncommonly Used Port" }, - T1066: { name: "Indicator Removal from Tools" }, - T1067: { name: "Bootkit" }, - T1068: { name: "Exploitation for Privilege Escalation" }, - T1069: { name: "Permission Groups Discovery" }, - T1070: { name: "Indicator Removal on Host" }, - T1071: { name: "Standard Application Layer Protocol" }, - T1072: { name: "Third-party Software" }, - T1073: { name: "DLL Side-Loading" }, - T1074: { name: "Data Staged" }, - T1075: { name: "Pass the Hash" }, - T1076: { name: "Remote Desktop Protocol" }, - T1077: { name: "Windows Admin Shares" }, - T1078: { name: "Valid Accounts" }, - T1079: { name: "Multilayer Encryption" }, - T1080: { name: "Taint Shared Content" }, - T1081: { name: "Credentials in Files" }, - T1082: { name: "System Information Discovery" }, - T1083: { name: "File and Directory Discovery" }, - T1084: { name: "Windows Management Instrumentation Event Subscription" }, - T1085: { name: "Rundll32" }, - T1086: { name: "PowerShell" }, - T1087: { name: "Account Discovery" }, - T1088: { name: "Bypass User Account Control" }, - T1089: { name: "Disabling Security Tools" }, - T1090: { name: "Connection Proxy" }, - T1091: { name: "Replication Through Removable Media" }, - T1092: { name: "Communication Through Removable Media" }, - T1093: { name: "Process Hollowing" }, - T1094: { name: "Custom Command and Control Protocol" }, - T1095: { name: "Standard Non-Application Layer Protocol" }, - T1096: { name: "NTFS File Attributes" }, - T1097: { name: "Pass the Ticket" }, - T1098: { name: "Account Manipulation" }, - T1099: { name: "Timestomp" }, - T1100: { name: "Web Shell" }, - T1101: { name: "Security Support Provider" }, - T1102: { name: "Web Service" }, - T1103: { name: "AppInit DLLs" }, - T1104: { name: "Multi-Stage Channels" }, - T1105: { name: "Remote File Copy" }, - T1106: { name: "Execution through API" }, - T1107: { name: "File Deletion" }, - T1108: { name: "Redundant Access" }, - T1109: { name: "Component Firmware" }, - T1110: { name: "Brute Force" }, - T1111: { name: "Two-Factor Authentication Interception" }, - T1112: { name: "Modify Registry" }, - T1113: { name: "Screen Capture" }, - T1114: { name: "Email Collection" }, - T1115: { name: "Clipboard Data" }, - T1116: { name: "Code Signing" }, - T1117: { name: "Regsvr32" }, - T1118: { name: "InstallUtil" }, - T1119: { name: "Automated Collection" }, - T1120: { name: "Peripheral Device Discovery" }, - T1121: { name: "Regsvcs/Regasm" }, - T1122: { name: "Component Object Model Hijacking" }, - T1123: { name: "Audio Capture" }, - T1124: { name: "System Time Discovery" }, - T1125: { name: "Video Capture" }, - T1126: { name: "Network Share Connection Removal" }, - T1127: { name: "Trusted Developer Utilities" }, - T1128: { name: "Netsh Helper DLL" }, - T1129: { name: "Execution through Module Load" }, - T1130: { name: "Install Root Certificate" }, - T1131: { name: "Authentication Package" }, - T1132: { name: "Data Encoding" }, - T1133: { name: "External Remote Services" }, - T1134: { name: "Access Token Manipulation" }, - T1135: { name: "Network Share Discovery" }, - T1136: { name: "Create Account" }, - T1137: { name: "Office Application Startup" }, - T1138: { name: "Application Shimming" }, - T1139: { name: "Bash History" }, - T1140: { name: "Deobfuscate/Decode Files or Information" }, - T1141: { name: "Input Prompt" }, - T1142: { name: "Keychain" }, - T1143: { name: "Hidden Window" }, - T1144: { name: "Gatekeeper Bypass" }, - T1145: { name: "Private Keys" }, - T1146: { name: "Clear Command History" }, - T1147: { name: "Hidden Users" }, - T1148: { name: "HISTCONTROL" }, - T1149: { name: "LC_MAIN Hijacking" }, - T1150: { name: "Plist Modification" }, - T1151: { name: "Space after Filename" }, - T1152: { name: "Launchctl" }, - T1153: { name: "Source" }, - T1154: { name: "Trap" }, - T1155: { name: "AppleScript" }, - T1156: { name: ".bash_profile and .bashrc" }, - T1157: { name: "Dylib Hijacking" }, - T1158: { name: "Hidden Files and Directories" }, - T1159: { name: "Launch Agent" }, - T1160: { name: "Launch Daemon" }, - T1161: { name: "LC_LOAD_DYLIB Addition" }, - T1162: { name: "Login Item" }, - T1163: { name: "Rc.common" }, - T1164: { name: "Re-opened Applications" }, - T1165: { name: "Startup Items" }, - T1166: { name: "Setuid and Setgid" }, - T1167: { name: "Securityd Memory" }, - T1168: { name: "Local Job Scheduling" }, - T1169: { name: "Sudo" }, - T1170: { name: "Mshta" }, - T1171: { name: "LLMNR/NBT-NS Poisoning and Relay" }, - T1172: { name: "Domain Fronting" }, - T1173: { name: "Dynamic Data Exchange" }, - T1174: { name: "Password Filter DLL" }, - T1175: { name: "Component Object Model and Distributed COM" }, - T1176: { name: "Browser Extensions" }, - T1177: { name: "LSASS Driver" }, - T1178: { name: "SID-History Injection" }, - T1179: { name: "Hooking" }, - T1180: { name: "Screensaver" }, - T1181: { name: "Extra Window Memory Injection" }, - T1182: { name: "AppCert DLLs" }, - T1183: { name: "Image File Execution Options Injection" }, - T1184: { name: "SSH Hijacking" }, - T1185: { name: "Man in the Browser" }, - T1186: { name: "Process Doppelgänging" }, - T1187: { name: "Forced Authentication" }, - T1188: { name: "Multi-hop Proxy" }, - T1189: { name: "Drive-by Compromise" }, - T1190: { name: "Exploit Public-Facing Application" }, - T1191: { name: "CMSTP" }, - T1192: { name: "Spearphishing Link" }, - T1193: { name: "Spearphishing Attachment" }, - T1194: { name: "Spearphishing via Service" }, - T1195: { name: "Supply Chain Compromise" }, - T1196: { name: "Control Panel Items" }, - T1197: { name: "BITS Jobs" }, - T1198: { name: "SIP and Trust Provider Hijacking" }, - T1199: { name: "Trusted Relationship" }, - T1200: { name: "Hardware Additions" }, - T1201: { name: "Password Policy Discovery" }, - T1202: { name: "Indirect Command Execution" }, - T1203: { name: "Exploitation for Client Execution" }, - T1204: { name: "User Execution" }, - T1205: { name: "Port Knocking" }, - T1206: { name: "Sudo Caching" }, - T1207: { name: "DCShadow" }, - T1208: { name: "Kerberoasting" }, - T1209: { name: "Time Providers" }, - T1210: { name: "Exploitation of Remote Services" }, - T1211: { name: "Exploitation for Defense Evasion" }, - T1212: { name: "Exploitation for Credential Access" }, - T1213: { name: "Data from Information Repositories" }, - T1214: { name: "Credentials in Registry" }, - T1215: { name: "Kernel Modules and Extensions" }, - T1216: { name: "Signed Script Proxy Execution" }, - T1217: { name: "Browser Bookmark Discovery" }, - T1218: { name: "Signed Binary Proxy Execution" }, - T1219: { name: "Remote Access Tools" }, - T1220: { name: "XSL Script Processing" }, - T1221: { name: "Template Injection" }, - T1222: { name: "File and Directory Permissions Modification" }, - T1223: { name: "Compiled HTML File" }, - T1480: { name: "Execution Guardrails" }, - T1482: { name: "Domain Trust Discovery" }, - T1483: { name: "Domain Generation Algorithms" }, - T1484: { name: "Group Policy Modification" }, - T1485: { name: "Data Destruction" }, - T1486: { name: "Data Encrypted for Impact" }, - T1487: { name: "Disk Structure Wipe" }, - T1488: { name: "Disk Content Wipe" }, - T1489: { name: "Service Stop" }, - T1490: { name: "Inhibit System Recovery" }, - T1491: { name: "Defacement" }, - T1492: { name: "Stored Data Manipulation" }, - T1493: { name: "Transmitted Data Manipulation" }, - T1494: { name: "Runtime Data Manipulation" }, - T1495: { name: "Firmware Corruption" }, - T1496: { name: "Resource Hijacking" }, - T1497: { name: "Virtualization/Sandbox Evasion" }, - T1498: { name: "Network Denial of Service" }, - T1499: { name: "Endpoint Denial of Service" }, - T1500: { name: "Compile After Delivery" }, - T1501: { name: "Systemd Service" }, - T1502: { name: "Parent PID Spoofing" }, - T1503: { name: "Credentials from Web Browsers" }, - T1504: { name: "PowerShell Profile" }, - T1505: { name: "Server Software Component" }, - T1506: { name: "Web Session Cookie" }, - T1514: { name: "Elevated Execution with Prompt" }, - T1518: { name: "Software Discovery" }, - T1519: { name: "Emond" }, - T1522: { name: "Cloud Instance Metadata API" }, - T1525: { name: "Implant Container Image" }, - T1526: { name: "Cloud Service Discovery" }, - T1527: { name: "Application Access Token" }, - T1528: { name: "Steal Application Access Token" }, - T1529: { name: "System Shutdown/Reboot" }, - T1530: { name: "Data from Cloud Storage Object" }, - T1531: { name: "Account Access Removal" }, - T1534: { name: "Internal Spearphishing" }, - T1535: { name: "Unused/Unsupported Cloud Regions" }, - T1536: { name: "Revert Cloud Instance" }, - T1537: { name: "Transfer Data to Cloud Account" }, - T1538: { name: "Cloud Service Dashboard" }, - T1539: { name: "Steal Web Session Cookie" } -} \ No newline at end of file diff --git a/public/components/overview/mitre/mitre.tsx b/public/components/overview/mitre/mitre.tsx index 257955f35f..3264a55e4d 100644 --- a/public/components/overview/mitre/mitre.tsx +++ b/public/components/overview/mitre/mitre.tsx @@ -115,22 +115,12 @@ export const Mitre = withErrorBoundary (class Mitre extends Component { async buildTacticsObject() { try { - const data = await WzRequest.apiReq('GET', '/mitre', { - params: { - select: "phase_name" - } - }); + const data = await WzRequest.apiReq('GET', '/mitre/tactics', {}); const result = (((data || {}).data || {}).data || {}).affected_items; const tacticsObject = {}; result && result.forEach(item => { - const {id, phase_name} = item; - phase_name.forEach( (tactic) => { - if(!tacticsObject[tactic]){ - tacticsObject[tactic] = []; - } - tacticsObject[tactic].push(id); - }) - }); + tacticsObject[item.name] = item; + }); this._isMount && this.setState({tacticsObject, isLoading: false}); } catch(err) { this.setState({ isLoading: false }); diff --git a/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap b/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap new file mode 100644 index 0000000000..76dd8c47cb --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/__snapshots__/intelligence.test.tsx.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Module Mitre Att&ck intelligence container should render the component 1`] = ` + + + + } + contentProps={ + Object { + "style": Object { + "maxHeight": "calc(100vh - 255px)", + "overflowX": "hidden", + "overflowY": "auto", + }, + } + } + side={ + + } + sideProps={ + Object { + "style": Object { + "minWidth": 145, + "overflowX": "hidden", + "width": "15%", + }, + } + } + /> + + +`; \ No newline at end of file diff --git a/public/components/overview/mitre_attack_intelligence/all_resources.tsx b/public/components/overview/mitre_attack_intelligence/all_resources.tsx new file mode 100644 index 0000000000..d6985913f9 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/all_resources.tsx @@ -0,0 +1,48 @@ +/* + * Wazuh app - React component for show all resources. + * + * Copyright (C) 2015-2021 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, {useCallback, useState} from 'react'; +import { + EuiTitle, + EuiSpacer +} from '@elastic/eui'; +import { ModuleMitreAttackIntelligenceAllResourcesSearchResults } from './all_resources_search_results'; +import { ModuleMitreAttackIntelligenceFlyout } from './resource_detail_flyout'; + +export const ModuleMitreAttackIntelligenceAllResources = ({ results, loading }) => { + const [details, setDetails] = useState(null); + + const selectResource = useCallback((item) => { + setDetails(({...item, ['references.external_id']: item.references.find(reference => reference.source === 'mitre-attack')?.external_id})); + },[]); + + const closeFlyout = useCallback(() => { + setDetails(null); + },[]); + + return ( + <> +

Search results

+ + + + {details && ( + closeFlyout()} + onSelectResource={setDetails} + /> + )} + + ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx b/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx new file mode 100644 index 0000000000..1e2511f830 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/all_resources_search_results.tsx @@ -0,0 +1,63 @@ +/* + * Wazuh app - React component that shows the searching resutls of Mitre Att&ck resources + * + * Copyright (C) 2015-2021 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 from 'react'; + +import { + EuiAccordion, + EuiButtonEmpty, + EuiCallOut, + EuiProgress, + EuiSpacer, + EuiButton +} from '@elastic/eui'; + +import { withGuard } from '../../../components/common/hocs'; + +const LoadingProgress = () => ( + +); + +export const ModuleMitreAttackIntelligenceAllResourcesSearchResults = withGuard(({loading}) => loading, LoadingProgress)(({ results, onSelectResource }) => { + const thereAreResults = results && results.length > 0; + return thereAreResults + ? results.map(item => ( + + See more results + : undefined} + buttonContent={{item.name} ({item.totalResults})} + paddingSize='none' + initialIsOpen={true} + > + {item.results.map((result, resultIndex) => ( + onSelectResource(result)} + > + {result[item.fieldName]} + + ))} + + ), []).reduce((accum, cur) => [accum, , cur]) + : +}); diff --git a/public/components/overview/mitre/components/techniques/components/search-bar-tecniques/index.ts b/public/components/overview/mitre_attack_intelligence/index.ts similarity index 81% rename from public/components/overview/mitre/components/techniques/components/search-bar-tecniques/index.ts rename to public/components/overview/mitre_attack_intelligence/index.ts index 0be09f42eb..46b2909e30 100644 --- a/public/components/overview/mitre/components/techniques/components/search-bar-tecniques/index.ts +++ b/public/components/overview/mitre_attack_intelligence/index.ts @@ -1,5 +1,6 @@ /* - * Wazuh app - Mitre alerts components + * Wazuh app - Mitre Att&ck intelligence index. + * * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -9,3 +10,5 @@ * * Find more information about this on the LICENSE file. */ + +export * from './intelligence'; \ No newline at end of file diff --git a/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx b/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx new file mode 100644 index 0000000000..faf5783d3f --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx @@ -0,0 +1,40 @@ + +/* + * Wazuh app - ModuleMitreAttackIntelligence Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { ModuleMitreAttackIntelligence } from './intelligence'; + +jest.mock('../../../react-services', () => ({ + WzRequest: () => ({ + apiReq: (method: string, path: string, params: any) => { + return { + data: { + data: { + affected_items: [] + } + } + } + } + }) +})); + +describe('Module Mitre Att&ck intelligence container', () => { + test('should render the component', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/public/components/overview/mitre_attack_intelligence/intelligence.tsx b/public/components/overview/mitre_attack_intelligence/intelligence.tsx new file mode 100644 index 0000000000..86d5daabc0 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/intelligence.tsx @@ -0,0 +1,98 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck intelligence. + * + * Copyright (C) 2015-2021 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, { useCallback, useState, useRef, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { MitreAttackResources } from './resources'; +import { ModuleMitreAttackIntelligenceLeftPanel } from './intelligence_left_panel'; +import { ModuleMitreAttackIntelligenceRightPanel } from './intelligence_right_panel'; +import { useAsyncAction } from '../../common/hooks'; +import { WzRequest } from '../../../react-services'; +import { PanelSplit } from '../../common/panels'; + +export const ModuleMitreAttackIntelligence = () => { + const [selectedResource, setSelectedResource] = useState(MitreAttackResources[0].id); + const [searchTermAllResources, setSearchTermAllResources] = useState(''); + const searchTermAllResourcesLastSearch = useRef(''); + const [resourceFilters, setResourceFilters] = useState([]); + const searchTermAllResourcesUsed = useRef(false); + const searchTermAllResourcesAction = useAsyncAction(async (searchTerm) => { + selectedResource !== null && setSelectedResource(null); + searchTermAllResourcesUsed.current = true; + searchTermAllResourcesLastSearch.current = searchTerm; + const limitResults = 5; + return (await Promise.all( + MitreAttackResources.map( + async resource => { + const response = await WzRequest.apiReq('GET', resource.apiEndpoint, {params: { search: searchTerm, limit: limitResults }}); + return { + id: resource.id, + name: resource.label, + fieldName: resource.fieldName, + results: response?.data?.data?.affected_items, + totalResults: response?.data?.data?.total_affected_items, + loadMoreResults: response?.data?.data?.total_affected_items && response?.data?.data?.total_affected_items > limitResults && (() => { + setResourceFilters([{field: 'search', value: searchTermAllResourcesLastSearch.current}]); + setSelectedResource(resource.id); + }) + } + } + ) + )).filter(searchTermAllResourcesResponse => searchTermAllResourcesResponse.results.length) + } + , [searchTermAllResources]); + + useEffect(() => { + const urlParams = new URLSearchParams(location.href); + const redirectTab = urlParams.get('tabRedirect'); + if (redirectTab) { + setSelectedResource(redirectTab); + } + },[]); + + const onSelectResource = useCallback((resourceID) => { + setResourceFilters([]); + setSelectedResource(prevSelectedResource => prevSelectedResource === resourceID && searchTermAllResourcesUsed.current ? null : resourceID); + }, [searchTermAllResourcesUsed.current]); + + const onSearchTermAllResourcesChange = useCallback((searchTerm) => { + setSearchTermAllResources(searchTerm); + }, []); + + return ( + + + } + sideProps={{style: {width: '15%' , minWidth: 145, overflowX: "hidden"}}} + content={} + contentProps={{style: { maxHeight: 'calc(100vh - 255px)', overflowY: 'auto', overflowX: 'hidden'}}} + /> + + + ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx b/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx new file mode 100644 index 0000000000..96529cc84c --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/intelligence_left_panel.tsx @@ -0,0 +1,44 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck intelligence left panel. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { WzFieldSearchDelay } from '../../../components/common/search'; +import { MitreAttackResources } from './resources'; +import { ModuleMitreAttackIntelligenceResourceButton } from './resource_button' + +export const ModuleMitreAttackIntelligenceLeftPanel = ({onSelectResource, selectedResource, onSearchTermAllResourcesChange, onSearchTermAllResourcesSearch}) => { + return ( + <> +
+ +
+ {MitreAttackResources.map(resource => ( + onSelectResource(resource.id)} + > + {resource.label} + + ))} + + ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx b/public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx new file mode 100644 index 0000000000..ab32f85958 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/intelligence_right_panel.tsx @@ -0,0 +1,41 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck intelligence right panel. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { MitreAttackResources } from './resources'; +import { ModuleMitreAttackIntelligenceAllResources } from './all_resources'; +import { ModuleMitreAttackIntelligenceResource } from './resource'; + +export const ModuleMitreAttackIntelligenceRightPanel = ({ + loading, + results, + resourceFilters, + selectedResource, +}) => ( + <> + {!selectedResource && ( + + )} + {MitreAttackResources.map(resource => resource.id === selectedResource + ? + : null + )} + +); diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx new file mode 100644 index 0000000000..8782b4be54 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -0,0 +1,87 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck resource items. + * + * Copyright (C) 2015-2021 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, { useCallback, useEffect, useMemo, useState } from 'react'; +import { TableWzAPI } from '../../../components/common/tables'; +import { WzRequest } from '../../../react-services'; +import { ModuleMitreAttackIntelligenceFlyout } from './resource_detail_flyout'; + +export const ModuleMitreAttackIntelligenceResource = ({ + label, + searchBarSuggestions, + apiEndpoint, + tableColumnsCreator, + initialSortingField, + resourceFilters +}) => { + const [details, setDetails] = useState(null); + + useEffect(() => { + const urlParams = new URLSearchParams(location.href); + const redirectTab = urlParams.get('tabRedirect'); + const idToRedirect = urlParams.get("idToRedirect"); + if(redirectTab && idToRedirect){ + const endpoint = `/mitre/${redirectTab}?q=references.external_id=${idToRedirect}`; + getMitreItemToRedirect(endpoint); + urlParams.delete('tabRedirect'); + urlParams.delete('idToRedirect'); + window.history.pushState({},document.title,'#/overview/?tab=mitre') + } + },[]); + + + const getMitreItemToRedirect = async (endpoint) => { + try { + const res = await WzRequest.apiReq("GET", endpoint, {}); + const data = res?.data?.data.affected_items.map((item) => ({ + ...item, + ["references.external_id"]: item?.references?.find( + (reference) => reference.source === "mitre-attack" + )?.external_id, + })); + setDetails(data[0]); + } catch { + return {}; + } + }; + + const tableColumns = useMemo(() => tableColumnsCreator(setDetails), []); + + const closeFlyout = useCallback(() => { + setDetails(null); + },[]); + + return ( + <> + ({...item, ['references.external_id']: item?.references?.find(reference => reference.source === 'mitre-attack')?.external_id})} + filters={resourceFilters} + /> + {details && ( + closeFlyout()} + onSelectResource={setDetails} + /> + )} + + ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/resource_button.scss b/public/components/overview/mitre_attack_intelligence/resource_button.scss new file mode 100644 index 0000000000..e2645284d8 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resource_button.scss @@ -0,0 +1,18 @@ +.moduleMitreAttackIntelligenceResourceButton{ + width: 100%; + text-align: left; + padding: 0 8px; + height: 40px; + &:hover{ + background-color: rgba(128, 128, 128, 0.08); + text-decoration: underline; + } +} + +.moduleMitreAttackIntelligenceResourceButton--selected{ + background-color: rgba(128, 128, 128, 0.15); + font-weight: 700; + &:hover{ + background-color: rgba(128, 128, 128, 0.15); + } +} \ No newline at end of file diff --git a/public/components/overview/mitre_attack_intelligence/resource_button.tsx b/public/components/overview/mitre_attack_intelligence/resource_button.tsx new file mode 100644 index 0000000000..4499823574 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resource_button.tsx @@ -0,0 +1,26 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck resource button. + * + * Copyright (C) 2015-2021 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 from 'react'; +import './resource_button.scss'; + +export const ModuleMitreAttackIntelligenceResourceButton = ({children, isSelected = false, className = '', ...rest }) => { + return ( + + ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx new file mode 100644 index 0000000000..28047e37c1 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx @@ -0,0 +1,102 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck intelligence flyout. + * + * Copyright (C) 2015-2021 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, { useRef, Fragment } from 'react'; +import { MitreAttackResources } from './resources'; +import { ReferencesTable } from './resource_detail_references_table'; + +import { + EuiFlyout, + EuiFlyoutHeader, + EuiOverlayMask, + EuiTitle, + EuiText, + EuiFlexGroup, + EuiFlyoutBody, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { Markdown } from '../../common/util'; + +interface DetailFlyoutType { + details: any, + closeFlyout: () => {onClick: () => void}, + onSelectResource: (resource: any) => void, +} + +export const ModuleMitreAttackIntelligenceFlyout = ({details, closeFlyout, onSelectResource}: DetailFlyoutType) => { + const startReference = useRef(null); + + return ( + + + + +

Details

+
+
+ +
+ + {MitreAttackResources[0].mitreFlyoutHeaderProperties.map(detailProperty => ( + +
+ + {detailProperty.label} + +
+ + {detailProperty.render ? detailProperty.render(details[detailProperty.id]) : details[detailProperty.id]} + +
+ ))} +
+
+ + +
+ + Description + +
+ + { details.description ? : ''} + +
+
+ + + {MitreAttackResources.filter((item) => details[item.id]).map((item) => + + { startReference.current?.scrollIntoView()}} + /> + + + )} + + +
+
+
+ ) +}; diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx new file mode 100644 index 0000000000..6861e48a89 --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx @@ -0,0 +1,80 @@ +/* + * Wazuh app - React component for showing the Mitre Att&ck intelligence flyout tables. + * + * Copyright (C) 2015-2021 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 , {useState, useEffect} from 'react'; +import { WzRequest } from '../../../react-services'; +import { + EuiAccordion, + SortDirection, + EuiInMemoryTable, +} from '@elastic/eui'; + +type backToTopType = () => void; + +interface referencesTableType { + referencesName: string, + referencesArray: Array, + columns: any, + backToTop: backToTopType +}; + +export const ReferencesTable = ({referencesName, referencesArray, columns, backToTop} : referencesTableType) => { + const [isLoading, setIsLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + getValues(); + backToTop(); + }, [referencesArray]); + + const getValues = async () => { + setIsLoading(true); + // We extract the ids from the references tables and count them in a string for the call that will extract the info + const maxLength = 8100; + const namesConcatenated = referencesArray.reduce((namesArray = [''], element) => { + namesArray[namesArray.length -1].length >= maxLength && namesArray.push(''); + namesArray[namesArray.length -1] += `${namesArray[namesArray.length -1].length > 0 ? ',' :''}${element}`; + return namesArray; + }, ['']); + + // We make the call to extract the necessary information from the references tables + try{ + const data = await Promise.all(namesConcatenated.map(async (nameConcatenated) => { + const queryResult = await WzRequest.apiReq('GET', `/mitre/${referencesName}?${referencesName.replace(/s\s*$/, '')}_ids=${nameConcatenated}`, {}); + return ((((queryResult || {}).data || {}).data || {}).affected_items || []).map((item) => ({...item, ['references.external_id']: item.references.find(reference => reference.source === 'mitre-attack')?.external_id})); + })); + setData(data.flat()); + } + catch (error){}; + setIsLoading(false); + }; + + return ( + + + + ); +}; diff --git a/public/components/overview/mitre_attack_intelligence/resources.tsx b/public/components/overview/mitre_attack_intelligence/resources.tsx new file mode 100644 index 0000000000..320e4d3e9b --- /dev/null +++ b/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -0,0 +1,128 @@ +/* + * Wazuh app - Mitre Att&ck resouces. + * + * Copyright (C) 2015-2021 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 { WzRequest } from '../../../react-services'; +import { Markdown } from '../../common/util'; +import { formatUIDate } from '../../../react-services'; +import React from 'react'; +import { EuiLink } from '@elastic/eui'; + +const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) => async (input: string) => { + try{ + const response = await WzRequest.apiReq('GET', endpoint, {}); + return response?.data?.data.affected_items + .map(item => ({ + ...item, + ['references.external_id']: item?.references?.find(reference => reference.source === 'mitre-attack')?.external_id + })) + .map(item => item[field]) + .filter(item => item && item.toLowerCase().includes(input.toLowerCase())) + .sort() + .slice(0,9) + }catch(error){ + return []; + }; +}; + +function buildResource(label: string, labelResource: string){ + const id = label.toLowerCase(); + const endpoint: string = `/mitre/${id}`; + return { + label: label, + id, + searchBarSuggestions: [ + { + type: 'q', + label: 'description', + description: `${labelResource} description`, + operators: ['~'], + values: (input) => input ? [input] : [] + }, + { + type: 'q', + label: 'name', + description: `${labelResource} name`, + operators: ['=', '!='], + values: getMitreAttackIntelligenceSuggestions(endpoint, 'name') + }, + { + type: 'q', + label: 'references.external_id', + description: `${labelResource} ID`, + operators: ['=', '!='], + values: getMitreAttackIntelligenceSuggestions(endpoint, 'references.external_id') + } + ], + apiEndpoint: endpoint, + fieldName: 'name', + initialSortingField: 'name', + tableColumnsCreator: (openResourceDetails) => [ + { + field: 'references.external_id', + name: 'ID', + width: '12%', + render: (value, item) => openResourceDetails(item)}>{value} + }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '30%', + render: (value, item) => openResourceDetails(item)}>{value} + }, + { + field: 'description', + name: 'Description', + sortable: true, + render: (value) => value ? : '', + truncateText: true + } + ], + mitreFlyoutHeaderProperties: [ + { + label: 'ID', + id: 'references.external_id', + }, + { + label: 'Name', + id: 'name' + }, + { + label: 'Created Time', + id: 'created_time', + render: (value) => value ? ( + formatUIDate(value) + ) : '' + }, + { + label: 'Modified Time', + id: 'modified_time', + render: (value) => value ? ( + formatUIDate(value) + ) : '' + }, + { + label: 'Version', + id: 'mitre_version' + }, + ], + } +}; + +export const MitreAttackResources = [ + buildResource('Groups', 'Group'), + buildResource('Mitigations', 'Mitigation'), + buildResource('Software', 'Software'), + buildResource('Tactics', 'Tactic'), + buildResource('Techniques', 'Technique') +]; diff --git a/public/components/wz-search-bar/lib/filters-to-object.ts b/public/components/wz-search-bar/lib/filters-to-object.ts index 0015e6c513..941f23e1d6 100644 --- a/public/components/wz-search-bar/lib/filters-to-object.ts +++ b/public/components/wz-search-bar/lib/filters-to-object.ts @@ -16,7 +16,7 @@ export interface IFilter { } function buildQFilter(oldQ, newQ) { - const parsedQ = `(${newQ})`.replace(/ and | or /gi, parseConjuntions); + const parsedQ = (oldQ ? `(${newQ})` : newQ).replace(/ and | or /gi, parseConjuntions); return `${!!oldQ ? `${oldQ};` : ''}${parsedQ}`; } diff --git a/public/controllers/management/components/management/ruleset/rule-info.js b/public/controllers/management/components/management/ruleset/rule-info.js index ae1088c2e9..29725ed52f 100644 --- a/public/controllers/management/components/management/ruleset/rule-info.js +++ b/public/controllers/management/components/management/ruleset/rule-info.js @@ -371,6 +371,23 @@ class WzRuleInfo extends Component { ); } + async getTacticsNames(tactics){ + try{ + let tacticsObj = [] + const data = await WzRequest.apiReq('GET', '/mitre/tactics', { + params: { + tactic_ids: tactics.toString() + } + }) + const formattedData = (((((data || {}).data).data || {}).affected_items || []) || {}); + formattedData && formattedData.forEach(item => { + tacticsObj.push(item.name); + }); + return tacticsObj; + }catch(error){ + return []; + } + } async addMitreInformation(compliance, currentRuleId){ try{ @@ -378,16 +395,16 @@ class WzRuleInfo extends Component { const mitreName = []; const mitreIds = []; const mitreTactics = await Promise.all(compliance.map(async (i) => { - const data = await WzRequest.apiReq('GET', '/mitre', { + const data = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `id=${i}` + q: `references.external_id=${i}` } }); const formattedData = (((((data || {}).data).data || {}).affected_items || [])[0] || {}); - const techniques = formattedData.phase_name || []; - mitreName.push(formattedData.json.name); + const tactics = this.getTacticsNames(formattedData.tactics) || []; + mitreName.push(formattedData.name); mitreIds.push(i); - return techniques; + return tactics; })); if(mitreTactics.length){ let removeDuplicates = (arr) => arr.filter((v,i) => arr.indexOf(v) === i) diff --git a/public/styles/common.scss b/public/styles/common.scss index 45ca8cefca..17316a71b9 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1477,7 +1477,7 @@ div.euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom.wz-menu max-width: 60vw; } -.wz-markdown-wapper code { +.wz-markdown-wrapper code { color: #1c2226; background-color: #e6e6e6; border-radius: 3px; From 722441a0b68ed259b158237831255b8221a3db55 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Fri, 18 Jun 2021 06:01:06 -0300 Subject: [PATCH 010/493] feat(error-orchestrator): Improved on createGetterSetter (#3376) * feat(error-orchestrator): Improved on createGetterSetter * bugfix(error-orchestrator): Added default value of disaply and store, remove location of types and fixed toastMessage of addError * feat(error-orchestrator): Added creatorGetterSetter on wazuh-app to avoid dependence on Kibana. --- .../common/error-boundary/error-boundary.tsx | 9 ++---- public/kibana-services.ts | 16 ++++------ public/plugin.ts | 5 +++- .../error-orchestrator-business.ts | 4 +-- .../error-orchestrator.service.ts | 10 ++++++- .../error-orchestrator/types.ts | 3 +- public/react-services/index.ts | 9 +++++- public/utils/create-getter-setter.ts | 29 +++++++++++++++++++ 8 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 public/utils/create-getter-setter.ts diff --git a/public/components/common/error-boundary/error-boundary.tsx b/public/components/common/error-boundary/error-boundary.tsx index 395f9f3152..3824766ef0 100644 --- a/public/components/common/error-boundary/error-boundary.tsx +++ b/public/components/common/error-boundary/error-boundary.tsx @@ -13,7 +13,6 @@ */ import React, { Component } from 'react'; -import loglevel from 'loglevel'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES, @@ -21,16 +20,14 @@ import { UIErrorSeverity, UILogLevel, } from '../../../react-services/error-orchestrator/types'; -import { ErrorOrchestratorService } from '../../../react-services'; import { ErrorComponentPrompt } from '../error-boundary-prompt/error-boundary-prompt'; +import { getErrorOrchestrator } from '../../../react-services'; export default class ErrorBoundary extends Component { - private logger: ErrorOrchestratorService; constructor(props) { super(props); this.state = { errorTitle: null, errorInfo: null, style: null }; this.context = this.constructor.displayName || this.constructor.name || undefined; - this.logger = new ErrorOrchestratorService(); } componentDidCatch = (errorTitle, errorInfo) => catchFunc(errorTitle, errorInfo, this); @@ -65,7 +62,7 @@ const catchFunc = (errorTitle, errorInfo, ctx) => { }, }; - ctx.logger.handleError(options); + getErrorOrchestrator().handleError(options); } catch (error) { const optionsCatch: UIErrorLog = { context: ctx.context, @@ -80,6 +77,6 @@ const catchFunc = (errorTitle, errorInfo, ctx) => { }, }; - ctx.logger.handleError(optionsCatch); + getErrorOrchestrator().handleError(optionsCatch); } }; diff --git a/public/kibana-services.ts b/public/kibana-services.ts index c7167d09b4..8a2cdd2e08 100644 --- a/public/kibana-services.ts +++ b/public/kibana-services.ts @@ -1,22 +1,21 @@ -let angularModule: any = null; -let discoverModule: any = null; - import { ChromeStart, + CoreStart, HttpStart, IUiSettingsClient, - ToastsStart, - SavedObjectsStart, OverlayStart, + SavedObjectsStart, ScopedHistory, - CoreStart, + ToastsStart, } from 'kibana/public'; import { createGetterSetter } from '../../../src/plugins/kibana_utils/common'; import { DataPublicPluginStart } from '../../../src/plugins/data/public'; import { VisualizationsStart } from '../../../src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { AppPluginStartDependencies } from './types'; -import { ErrorOrchestrator } from '../common/constants'; + +let angularModule: any = null; +let discoverModule: any = null; export const [getCore, setCore] = createGetterSetter('Core'); export const [getPlugins, setPlugins] = createGetterSetter('Plugins'); @@ -40,9 +39,6 @@ export const [getVisualizationsPlugin, setVisualizationsPlugin] = createGetterSe export const [getNavigationPlugin, setNavigationPlugin] = createGetterSetter< NavigationPublicPluginStart >('NavigationPlugin'); -export const [getErrorOrchestrator, setErrorOrchestrator] = createGetterSetter( - 'ErrorOrchestrator' -); /** * set bootstrapped inner angular module diff --git a/public/plugin.ts b/public/plugin.ts index 03ab4344f5..167d9743b6 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -24,6 +24,8 @@ import { } from './types'; import { Cookies } from 'react-cookie'; import { AppState } from './react-services/app-state'; +import { setErrorOrchestrator } from './react-services'; +import { ErrorOrchestratorService } from './react-services/error-orchestrator/error-orchestrator.service'; const innerAngularName = 'app/wazuh'; @@ -64,7 +66,7 @@ export class WazuhPlugin implements Plugin('ErrorOrchestratorService'); + diff --git a/public/utils/create-getter-setter.ts b/public/utils/create-getter-setter.ts new file mode 100644 index 0000000000..0dab24c6c5 --- /dev/null +++ b/public/utils/create-getter-setter.ts @@ -0,0 +1,29 @@ +/* + * Wazuh app - Create Getter Setter + * Copyright (C) 2015-2021 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 type Get = () => T; +export type Set = (value: T) => void; + +export const createGetterSetter = (name: string): [Get, Set] => { + let value: T; + + const get: Get = () => { + if (!value) throw new Error(`${name} was not set.`); + return value; + }; + + const set: Set = (newValue) => { + value = newValue; + }; + + return [get, set]; +}; From d2c08fe2ee721a86f10bb5b850a47a341c6f7167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= Date: Fri, 18 Jun 2021 13:45:58 +0200 Subject: [PATCH 011/493] Fix creation of json file after a ui log (#3378) --- .../error-orchestrator/error-orchestrator-base.ts | 2 +- server/lib/base-logger.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.ts b/public/react-services/error-orchestrator/error-orchestrator-base.ts index bda80e239a..7b3bace617 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-base.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-base.ts @@ -34,7 +34,7 @@ export class ErrorOrchestratorBase implements ErrorOrchestrator { await GenericRequest.request('POST', `/utils/logs/ui`, { message: errorLog.error.message, level: winstonLevel, - location: errorLog.location, + location: errorLog.context || errorLog.error.error.stack, }); } catch (error) { loglevel.error('Failed on request [POST /utils/logs/ui]', error); diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index ab73e1ede6..987cac5253 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -36,8 +36,8 @@ export class BaseLogger { RAW_LOGS_PATH: string = ''; constructor(plainLogsPath: string, rawLogsPath: string) { - this.PLAIN_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, plainLogsPath); - this.RAW_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, rawLogsPath); + this.PLAIN_LOGS_PATH = plainLogsPath; + this.RAW_LOGS_PATH = rawLogsPath; } /** From 2da70b250c23b1daf87a228e7a26e613a49524c0 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Tue, 22 Jun 2021 13:55:43 +0200 Subject: [PATCH 012/493] Fix creation of ui log file and app log file --- common/constants.ts | 18 +++++++++++------- .../wazuh-utils/ui-logs.controller.test.ts | 4 ++-- .../wazuh-utils/ui-logs.controller.ts | 6 +++--- server/controllers/wazuh-utils/wazuh-utils.ts | 4 ++-- server/lib/base-logger.ts | 18 ++++++++++++------ server/lib/filesystem.ts | 14 ++++++++++++++ 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index af603b70cb..73fec7669f 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -150,21 +150,25 @@ export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( // Wazuh data path - logs export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); -export const WAZUH_DATA_LOGS_PLAIN_FILENAME = path.join( +export const WAZUH_DATA_LOGS_PLAIN_FILENAME = 'wazuhapp-plain.log'; +export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - 'wazuhapp-plain.log' + WAZUH_DATA_LOGS_PLAIN_FILENAME ); -export const WAZUH_DATA_LOGS_RAW_FILENAME = path.join( +export const WAZUH_DATA_LOGS_RAW_FILENAME = 'wazuhapp.log'; +export const WAZUH_DATA_LOGS_RAW_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - 'wazuhapp.log' + WAZUH_DATA_LOGS_RAW_FILENAME ); // Wazuh data path - UI logs -export const WAZUH_UI_LOGS_PLAIN_FILENAME = path.join( +export const WAZUH_UI_LOGS_PLAIN_FILENAME = 'wazuh-ui-plain.log'; +export const WAZUH_UI_LOGS_RAW_FILENAME = 'wazuh-ui.log'; +export const WAZUH_UI_LOGS_PLAIN_PATH = path.join( WAZUH_DATA_LOGS_DIRECTORY_PATH, - 'wazuh-ui-plain.log' + WAZUH_UI_LOGS_PLAIN_FILENAME ); -export const WAZUH_UI_LOGS_RAW_FILENAME = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, 'wazuh-ui.log'); +export const WAZUH_UI_LOGS_RAW_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, WAZUH_UI_LOGS_RAW_FILENAME); // Wazuh data path - downloads export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'downloads'); diff --git a/server/controllers/wazuh-utils/ui-logs.controller.test.ts b/server/controllers/wazuh-utils/ui-logs.controller.test.ts index 5553f4bef1..8828560c61 100644 --- a/server/controllers/wazuh-utils/ui-logs.controller.test.ts +++ b/server/controllers/wazuh-utils/ui-logs.controller.test.ts @@ -1,5 +1,5 @@ import { UiLogsCtrl } from './ui-logs.controller'; -import { WAZUH_UI_LOGS_RAW_FILENAME } from '../../../common/constants'; +import { WAZUH_UI_LOGS_RAW_PATH } from '../../../common/constants'; import uiLogger from '../../lib/ui-logger'; const readLastLines = require('read-last-lines'); @@ -56,7 +56,7 @@ describe('Spec UiLogsCtrl', function () { it('Should return a Array logs', async () => { const controller = new UiLogsCtrl(); - let res = await controller.getUiFileLogs(WAZUH_UI_LOGS_RAW_FILENAME); + let res = await controller.getUiFileLogs(WAZUH_UI_LOGS_RAW_PATH); expect(Array.isArray(res)).toBe(true); }); diff --git a/server/controllers/wazuh-utils/ui-logs.controller.ts b/server/controllers/wazuh-utils/ui-logs.controller.ts index f2c7cc184d..74e1980e10 100644 --- a/server/controllers/wazuh-utils/ui-logs.controller.ts +++ b/server/controllers/wazuh-utils/ui-logs.controller.ts @@ -13,7 +13,7 @@ // Require some libraries import { ErrorResponse } from '../../lib/error-response'; import { read } from 'read-last-lines'; -import { WAZUH_UI_LOGS_RAW_FILENAME } from '../../../common/constants'; +import { WAZUH_UI_LOGS_RAW_PATH } from '../../../common/constants'; import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; import uiLogger from '../../lib/ui-logger'; @@ -32,7 +32,7 @@ export class UiLogsCtrl { async getUiLogs(response: KibanaResponseFactory) { try { return uiLogger.initDirectory().then(async () => { - if (!uiLogger.checkFileExist(WAZUH_UI_LOGS_RAW_FILENAME)) { + if (!uiLogger.checkFileExist(WAZUH_UI_LOGS_RAW_PATH)) { return response.ok({ body: { error: 0, @@ -40,7 +40,7 @@ export class UiLogsCtrl { }, }); } else { - let arrayLog = await this.getUiFileLogs(WAZUH_UI_LOGS_RAW_FILENAME); + let arrayLog = await this.getUiFileLogs(WAZUH_UI_LOGS_RAW_PATH); return response.ok({ body: { error: 0, diff --git a/server/controllers/wazuh-utils/wazuh-utils.ts b/server/controllers/wazuh-utils/wazuh-utils.ts index 97567ec7ad..d1fecda4c3 100644 --- a/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/server/controllers/wazuh-utils/wazuh-utils.ts @@ -16,7 +16,7 @@ import { getConfiguration } from '../../lib/get-configuration'; import { read } from 'read-last-lines'; import { UpdateConfigurationFile } from '../../lib/update-configuration'; import jwtDecode from 'jwt-decode'; -import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_DATA_LOGS_RAW_FILENAME, WAZUH_UI_LOGS_RAW_PATH } from '../../../common/constants'; +import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_DATA_LOGS_RAW_PATH, WAZUH_UI_LOGS_RAW_PATH } from '../../../common/constants'; import { ManageHosts } from '../../lib/manage-hosts'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; import { getCookieValueByName } from '../../lib/cookie'; @@ -108,7 +108,7 @@ export class WazuhUtilsCtrl { async getAppLogs(context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) { try { const lastLogs = await read( - WAZUH_DATA_LOGS_RAW_FILENAME, + WAZUH_DATA_LOGS_RAW_PATH, 50 ); const spliterLog = lastLogs.split('\n'); diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index 987cac5253..ae3afb5933 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -14,7 +14,7 @@ import winston from 'winston'; import fs from 'fs'; import path from 'path'; import { getConfiguration } from './get-configuration'; -import { createDataDirectoryIfNotExists } from './filesystem'; +import { createDataDirectoryIfNotExists, createLogFileIfNotExists } from './filesystem'; import { WAZUH_DATA_LOGS_DIRECTORY_PATH } from '../../common/constants'; @@ -33,11 +33,15 @@ export class BaseLogger { wazuhLogger: winston.Logger | undefined = undefined; wazuhPlainLogger: winston.Logger | undefined = undefined; PLAIN_LOGS_PATH: string = ''; + PLAIN_LOGS_FILE_NAME: string = ''; RAW_LOGS_PATH: string = ''; + RAW_LOGS_FILE_NAME: string = ''; - constructor(plainLogsPath: string, rawLogsPath: string) { - this.PLAIN_LOGS_PATH = plainLogsPath; - this.RAW_LOGS_PATH = rawLogsPath; + constructor(plainLogsFile: string, rawLogsFile: string) { + this.PLAIN_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, plainLogsFile); + this.RAW_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, rawLogsFile); + this.PLAIN_LOGS_FILE_NAME= plainLogsFile; + this.RAW_LOGS_FILE_NAME = rawLogsFile; } /** @@ -127,12 +131,14 @@ export class BaseLogger { * Checks if the wazuhapp.log file size is greater than 100MB, if so it rotates the file. */ private checkFiles = () => { + createLogFileIfNotExists(this.RAW_LOGS_PATH); + createLogFileIfNotExists(this.PLAIN_LOGS_PATH); if (this.allowed) { // check raw log file if (this.getFilesizeInMegaBytes(this.RAW_LOGS_PATH) >= 100) { fs.renameSync( this.RAW_LOGS_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp.${new Date().getTime()}.log` + `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.RAW_LOGS_FILE_NAME}` ); fs.writeFileSync( this.RAW_LOGS_PATH, @@ -149,7 +155,7 @@ export class BaseLogger { if (this.getFilesizeInMegaBytes(this.PLAIN_LOGS_PATH) >= 100) { fs.renameSync( this.PLAIN_LOGS_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/wazuhapp-plain.${new Date().getTime()}.log` + `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.PLAIN_LOGS_FILE_NAME}` ); } } diff --git a/server/lib/filesystem.ts b/server/lib/filesystem.ts index 194448db9e..3dde0e1f74 100644 --- a/server/lib/filesystem.ts +++ b/server/lib/filesystem.ts @@ -8,6 +8,20 @@ export const createDirectoryIfNotExists = (directory: string): void => { }; }; +export const createLogFileIfNotExists = (filePath : string): void => { + if (!fs.existsSync(filePath)) { + fs.writeFileSync( + filePath, + JSON.stringify({ + date: new Date(), + level: 'info', + location: 'logger', + message: 'Log file creation', + }) + '\n' + ); + }; +}; + export const createDataDirectoryIfNotExists = (directory?: string) => { const absoluteRoute = directory ? path.join(WAZUH_DATA_ABSOLUTE_PATH, directory) : WAZUH_DATA_ABSOLUTE_PATH; if (!fs.existsSync(absoluteRoute)) { From 57741c23d0744d934ff689295c65e0b0feb1e47b Mon Sep 17 00:00:00 2001 From: gabiwassan Date: Tue, 22 Jun 2021 10:45:45 -0300 Subject: [PATCH 013/493] bugfix(error-boundary): Fix context value. --- public/components/common/error-boundary/error-boundary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/error-boundary/error-boundary.tsx b/public/components/common/error-boundary/error-boundary.tsx index 3824766ef0..f1df247350 100644 --- a/public/components/common/error-boundary/error-boundary.tsx +++ b/public/components/common/error-boundary/error-boundary.tsx @@ -50,7 +50,7 @@ const catchFunc = (errorTitle, errorInfo, ctx) => { }); const options: UIErrorLog = { - context: ctx.context, + context: errorInfo.componentStack, level: UI_LOGGER_LEVELS.WARNING as UILogLevel, severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, display: false, From 7513a801a9262ba00458179d4f6091ff4cd19976 Mon Sep 17 00:00:00 2001 From: Franco Charriol Date: Tue, 22 Jun 2021 12:47:08 -0300 Subject: [PATCH 014/493] Merge latest 4.2-7.10 into 4.3-7.10 (#3383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feature/remove module titles (#3160) * Remove module titles * Add popover to breadcrumb * Update changelog * Fix errors * Fix the broken links when using server.basePath Kibana setting (#3161) * fix(frontend): Fix broken link when use 'server.basePath` Kibana setting - Fix links in Wazuh breadcrumb - Fix some redirection links * feat(prettier): Added prettierrc config file. (#3168) * [Feature]: Add Wazuh help links as extension in Kibana help menu (#3170) * feat(frontend): Add Wazuh help links as an extension in Kibana help menu * fix(changelog): Add PR to changelog * fix(changelog): Fix PR number and link * fix(help-links): Replace component and removed color styles from Wazuh lunk in the Kibana help button * Fixing filter in reports (#3173) * Removed sha1 data and correct date format (#3189) * [FEATURE] Add redirect to group details using the URL (#3184) * feat(groups): Add redirect to group details using the URL - Redirect to group details when the URL contains group query param * doc(changelog): add missing changelog for #3189 * Fixed screen flickers in Cluster Visualization (#3159) * Modify the default settings related to monitoring indices creation (#3174) * fix(configuration): Changed WAZUH_MONITORING_DEFAULT_CREATION app setting from 'd' to 'w' (weekly) * fix(configuration): Replace wazuh.monitorin.creation from d to w (weekly) - Fixed cron.prefix setting * fix(monitoring): Set monitoring default indices shards to 1. Another fixes in configurations * fix(monitoring): fix wazuh.monitoring.shards value in the initail app configuration * doc(changelog): add missing changelogs * doc(changelog): remove duplicated changelogs * Bugfix default index patterns [Health-check] - 4.2 (#3232) * bugfix(health-check): Fixed creation of default index and added all section create and refresh index in heath-check. * fix: some bad behaviors * doc(changelog): add fix to changelog * fix: reject promise on error and refactor * fix: refactor required fields * feat: refactor const * Update CHANGELOG.md Co-authored-by: Franco Charriol Co-authored-by: Franco Charriol * Fix validation of list of valid index patterns for alerts (#3236) * Last changes from Tag 4.2.0 4201 (#3246) * Fixed fields overlap in the agent summary screen (#3217) * [Refactor] Health check (#3197) * refactor(healthcheck): Adapt the refactorized health check component to last changes - Added monitoring and statistics index patterns checks - Added logic to retry the checks with a refresh button - Apply the retry to API connection check - Export react services in index file - Create appConfig in the Redux store * feat(healtcheck): Replace health check initial state to waiting * fet(healthcheck): Add can retry to healthcheck checks * fix(menu): Fix error in toast from WzMenu and revome unnecessary return in PatternHandler * fix(health-check): Fix create index pattern when change the setting in Settings > Configuration and loop in health check * fix(health-check): renamed files from appConfig Redux actions and reducer * fix(frontend): Replace config singleton saving to Redux * fix(health-check): Fix infinite loop rendering component when a check is disabled in the configuration * fix(health-check): Rename health checks titles * fix(health-check): Fix the tests for Health check * refactor(health-check): Request changes, add max buckets check and some improvements - Request changes - Added the max buckets check when the component is mounted - Created the `useRootScope` hook - Improved the export in the HOCs and hooks index files - Removed the `lib` folder - Removed the `health-check` old component * Add modal to different sections (#3221) * Roles Mapping * Investigating problem with lifecylce * Created new mask and fix role mapping * Create and edit user * Create and edit roles * Edit policy * Polices edit and create * Management * Update Changelog * Change copyright * Fix errors * Feature Disable Wazuh by roles (#3222) * fix package name in add new agent (#3233) * fix index typo settings (#3234) * [Fix] Visualize button on Events (Discover component) (#3237) * fix(discover) Ser UiActions of the Discover plugin - This fixes the Visualize button was missing in the Discover sidebar when expanding a index field * add error when add sample data fails (#3241) * fix(navbar): Hide navbar wazuh label. (#3240) * Vulnerabilities inventory cleanup 3242 (#3243) * Removed check for active agent Now a list of vulnerabilities will be shown regardless of agent activity * Removed name from the top of the flyout Now only the CVE remains * added missing modules in agent menu (#3244) * Removed tooltip in last breadcrumb in header breadcrumb (#3250) * Improve api selector (#3175) * error adding selectors * added select api and select index pattern on menu * remove unused variables * error adding selectors * added select api and select index pattern on menu * remove unused variables * bugfix(apiSelector): Fixed index pattern selection, bad behavior when switching from navbar. * Added Popover when screen width is small * Changed popover icon * Resolved requested changes * Removed selectors in wazuh menu, removed margin in breadcrumb * Removed margins style in breadcrumb * Added tooltip in popover selectors button * fixed marginLeft syntax in breadcrumb * Fixed conditions to show selectors in popover * Fixed change API selector * Fixed selectors sizes * Update CHANGELOG.md Co-authored-by: gabiwassan Co-authored-by: Ibarra Maximiliano Co-authored-by: Franco Charriol Co-authored-by: Franco Charriol * Bugfix/3273 error check wazuh no security (#3292) * Fixed error in check-wazuh * updated CHANGELOG * fix(changelog): Moved PR number to a existent change Co-authored-by: Ibarra Maximiliano Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Add validation form creating new rule/decoder (#3274) * Add modal when creating new rule * Update changelog * Change if/else * Change rest of if/else * Change flyout title * Fix error when saving a rule * Fix errors in PDF reports (#3272) * Remove dupicate subsection * Fix duplicate Who data * fix date format pdf * Fix width columns * Add changelog * Remove unused variable * Translate comments * Use camelCase * Removed unnecessary tabs * Transalate comments * fix(reporting): Replace array constructor * fix(changelog): Removed spaces in markdown links Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix typerror deploy new agent Safari 12 (#3289) * Fix unsupported function in Safari 12 * Update changelog Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix 3209/improving and removing WUI error logs (#3260) * Improving and removing WUI error logs * Fix * Added Changelog * Comment upgrades * Adding contant to tryCatchForIndexPermissionError * Fixes * More fixes and upgrades * fix(server): Moved a server log in /api/check-stored endpoint - Moved a server log in endpoint - Renamed variable in monitoring job * fix(monitoring): Replaced multiple try/catch block by one. Co-authored-by: Desvelao Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> * Fix/graph single api 3239 (#3256) * Added filter for current api in kibana-vis The Evolution graph in agents/ should display only the status of agents that belong to the selected API * Fixed partial matches Fixed issue where a manager whose name is a substring of another's would show data from both * Added a getManagers method to agents preview This method requests the managers from the current API and extracts the unique occurence of each of the managers, returning a list of the names of all the managers the agents in the current API have. Also modified the filter in kibana-vis so it makes use of an array of managers instead of a single one. * Updated CHANGELOG * Changed the filter parameters from manager to cluster name Seeing how the only way to find the complete list of managers is getting the complete list of agents, and this being possibly taxing on the API, the filter dependency has been switched to `cluster.name` Now the graph will conflict if 2 clusters share the same name instead of 2 managers. * Applied formating and adjusted filter metadata * Added logic to pick a filter for manager or cluster when appropriate * fix(monitoring): Moved the filter of agents evolution visualization to KibanaVis Co-authored-by: Desvelao * changelog: Fix changes in revision 4202 * [SCA] Fix the render of the checks in a SCA check (#3297) * fix(sca): Fix the render of the checks in a SCA check - Now display each check.rule * changelog: Add PR to changelog * add session token to rulest (#3257) * add session token to rulest * add changelog * fix code * fix packge json * bufix(logtest): Replaced request to get token. We have a token for this session on the return of the PUT logtest. * docs(changelog): Fixed format and messages * bugfix(logtest): Replaced promise.all, we need take token for the next request. * fix(logtest): Use the same token generated in the first request to another logs * fix(logtest): Add control when the token was gotten * fix(logtest): Fix variable when got the token * Added buttom to clear session in logtest and add control of logtest token to Redux * Update logtest.tsx * [Health check] Add logs with the details about the check (#3258) * feat(health-check): Add logs to the check of Health check - Add logs to checks - Add button to open/close the check logs - Add new section to Settings: Miscellaneous - Add control when the health chekc is run in `debug` mode (no redirection after cheks are ready) - Refactor some functions - Some ajustments to Settins secions related to spaces * fix(health-check): Fix health check redirection and update tests - Fix health check redirection when all checks are ready - Update health check tests * feat(health-check): Remove notification when opening the check logs and there is some check action * fix(health-check): Fix unknown error and added improvements to notifications - Fix unknown error after restart the cluster and try to pass the health check - Remove the notification dot of check details when open for first time - Display the check details button when there are logs - Updated the tests (snapshot and tests) * Remove status text. Improve styles. Small log text fix * Added animated codeblock transition * Fixed react warnings * Fixed animation style * Fixed unit test * Only show log button when error or debug mode * updated unit test snapshots * Fix typo * Update CHANGELOG.md * Adapt kibana integrations to public interfaces of saved objects for 7.11/7.12 (#3309) * fix: update kibana-discover and kibana-vis to 7.11 public interfaces * update[changelog]: add to changelog * fix: remove comment code * Fix alerts summary reports (#3303) * Fix some alerts summary reports * Fix date CIS CAT module * Disabled index pattern checks in Healthcheck (#3311) * feat(health-check): Add logs to the check of Health check - Add logs to checks - Add button to open/close the check logs - Add new section to Settings: Miscellaneous - Add control when the health chekc is run in `debug` mode (no redirection after cheks are ready) - Refactor some functions - Some ajustments to Settins secions related to spaces * fix(health-check): Fix health check redirection and update tests - Fix health check redirection when all checks are ready - Update health check tests * feat(health-check): Remove notification when opening the check logs and there is some check action * fix(health-check): Fix unknown error and added improvements to notifications - Fix unknown error after restart the cluster and try to pass the health check - Remove the notification dot of check details when open for first time - Display the check details button when there are logs - Updated the tests (snapshot and tests) * Remove status text. Improve styles. Small log text fix * Added animated codeblock transition * Fixed react warnings * Fixed animation style * Fixed unit test * Only show log button when error or debug mode * updated unit test snapshots * Fix typo * Disabled index pattern checks * Updated CHANGELOG * Removed awaitFor, fixed title, and label * Changed some services names * fix: refactor architecture and remove unnecessary checks * fix error agent view does not render correctly (#3306) * fix error agent view does not render correctly * update CHANGELOG.md * add new file .scss and functions declarations * prettier * Fix visualizations with dark mode background in PDF report (#3315) * Normalize visData table property for 7.12 retro-compatibility (#3323) * Use the longest value in column to get column width in PDF report tables (#3326) * Use the longest value in column to get column width * Added changelog * Improved code readability * New attributes added for integration testing (#3331) * New attributes added for integration testing * New constants added for integration testing * Added new constants files for cypress * updated selectors and atributes * Hide Wazuh breadcrumb label and Wazuh api selector css fix (#3347) * Change css breadcrumb selector + Wazuh api selector css fix * Fix/elastic UI upgrade breaking changes in 7.11 and 7.12 3318 (#3345) * Added required legend to EuiButtonGroup component * Removed deprecated withTitle on EuiPopover component * Added changelog * Improved code readability * Fix error due short graphs (#3349) * Changed the way of hiding unloaded charts by setting them to display: none instead of height * Updated changelog * Applied prettier and moved logical operations outside render return to improve legibility * Fixed a typo in changelog * Fix/3320 visualization label overlap (#3355) * Refactor of visualization definitions For each visualization, for each attribute that was a string of a JSON object, they were converted to an object inside a `JSON.stringify()` and applied the formatter in order to make them easier to read and maintain. * Added a filter true to all the line, area and histogram visualizations This should prevent any labels from ever overlapping at low widths * Fix export to csv table button 7.11 & 7.12 (#3358) * Refactor of visualization definitions For each visualization, for each attribute that was a string of a JSON object, they were converted to an object inside a `JSON.stringify()` and applied the formatter in order to make them easier to read and maintain. * Added a filter true to all the line, area and histogram visualizations This should prevent any labels from ever overlapping at low widths * Updated changelog * Added toolbar buttons to tables * Added changelog * Added changelog * Fixed changelog Co-authored-by: Manuel Gómez Castro * doc: add missing changelog for revision 4108 * doc: change 4.2.1 for revision 4202 * fix: move createGetter to common-services.ts * fix: update import for getErrorOrchestrator * fix: add Copyright comment in the new file Co-authored-by: Pablo Martínez Co-authored-by: Toni <34042064+Desvelao@users.noreply.github.com> Co-authored-by: Gabriel Wassan Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Co-authored-by: Federico Rodriguez Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Co-authored-by: Maximiliano Ibarra Co-authored-by: Ibarra Maximiliano Co-authored-by: Pablo Martínez Co-authored-by: Desvelao Co-authored-by: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> Co-authored-by: Manuel Gómez Castro --- .prettierrc | 5 + CHANGELOG.md | 75 +- common/constants.ts | 69 +- common/wazu-menu/wz-menu-management.cy.ts | 28 + common/wazu-menu/wz-menu-overview.cy.ts | 33 + common/wazu-menu/wz-menu-security.cy.ts | 18 + common/wazu-menu/wz-menu-settings.cy.ts | 22 + common/wazu-menu/wz-menu-tools.cy.ts | 16 + public/app.js | 14 +- public/assets/icon_google_groups.svg | 16 + .../add-modules-data/WzSampleDataWrapper.js | 10 +- .../add-modules-data-main.tsx | 2 +- .../add-modules-data/module-guide.js | 1 + .../add-modules-data/sample-data.tsx | 4 +- .../agents/fim/inventory/fileDetail.tsx | 9 +- .../agents/fim/inventory/filterBar.tsx | 12 +- .../agents/fim/inventory/registry-table.tsx | 2 + .../registryValues/registryValues.tsx | 2 + .../agents/sca/components/rule-text.tsx | 9 +- public/components/agents/sca/inventory.tsx | 2 +- .../agents/vuls/inventory/flyout.tsx | 2 +- public/components/agents/vuls/main.tsx | 8 - .../common/error-boundary/error-boundary.tsx | 2 +- .../globalBreadcrumb/globalBreadcrumb.scss | 7 +- .../globalBreadcrumb/globalBreadcrumb.tsx | 7 +- public/components/common/hocs/index.ts | 35 +- public/components/common/hooks/index.ts | 37 +- .../components/common/hooks/use-app-config.ts | 19 + .../components/common/hooks/useRootScope.ts | 22 + .../common/modules/main-overview.tsx | 201 ++- public/components/common/modules/main.tsx | 17 +- .../modules/overview-current-section.tsx | 6 +- public/components/common/util/index.ts | 1 + .../common/util/wz-overlay-mask-interface.tsx | 121 ++ .../components/common/welcome/agents-info.js | 6 +- .../common/welcome/components/menu-agent.js | 6 +- public/components/eui-loader.js | 2 +- .../__snapshots__/check-result.test.tsx.snap | 97 ++ .../components/check-result.test.tsx | 89 ++ .../health-check/components/check-result.tsx | 142 ++ .../components/inspect-logs-button.tsx | 48 + .../health-check/components/result-icons.tsx | 57 + .../health-check.container.test.tsx.snap | 183 +++ .../container/health-check.container.test.tsx | 93 ++ .../container/health-check.container.tsx | 258 ++++ .../components/health-check/health-check.tsx | 506 ------- public/components/health-check/index.ts | 1 + .../health-check/lib/check-kibana-settings.ts | 36 - .../health-check/lib/check-max-buckets.ts | 43 - .../health-check/lib/check-time-filter.ts | 43 - public/components/health-check/lib/index.ts | 3 - .../services/check-api.service.ts | 66 +- .../check-fields.service.ts | 30 + .../check-index-pattern-object.service.ts | 103 ++ .../check-index-pattern.service.ts | 31 + .../check-template.service.ts | 36 + .../services/check-kibana-settings.service.ts | 54 + .../services/check-pattern-support.service.ts | 49 + .../services/check-setup.service.ts | 50 + .../components/health-check/services/index.ts | 19 + .../health-check/types/check_logger.ts | 7 + .../types/result-icons-presets.ts | 49 + .../components/requirements/requirements.tsx | 1 - .../subrequirements/subrequirements.tsx | 1 - .../components/overview/metrics/metrics.tsx | 5 +- .../mitre/components/tactics/tactics.tsx | 1 - .../components/techniques/techniques.tsx | 1 - .../security/policies/create-policy.tsx | 431 ++++++ .../security/policies/edit-policy.tsx | 736 +++++----- .../security/policies/policies-table.tsx | 18 +- .../components/security/policies/policies.tsx | 302 +--- .../components/roles-mapping-create.tsx | 177 ++- .../components/roles-mapping-edit.tsx | 196 ++- .../roles-mapping/components/rule-editor.tsx | 28 +- .../security/roles-mapping/roles-mapping.tsx | 32 +- .../components/security/roles/create-role.tsx | 293 ++-- .../components/security/roles/edit-role.tsx | 325 +++-- public/components/security/roles/roles.tsx | 33 +- .../security/users/components/create-user.tsx | 269 ++-- .../security/users/components/edit-user.tsx | 276 ++-- .../settings/miscellaneous/miscellaneous.tsx | 101 ++ .../components/settings/settings-logs/logs.js | 12 +- .../components/wz-menu/wz-menu-management.js | 87 +- public/components/wz-menu/wz-menu-overview.js | 122 +- public/components/wz-menu/wz-menu-security.js | 29 +- public/components/wz-menu/wz-menu-settings.js | 50 +- public/components/wz-menu/wz-menu-tools.js | 17 +- public/components/wz-menu/wz-menu.js | 278 +++- public/components/wz-menu/wz-menu.scss | 17 +- .../wz-search-badges/context-menu.tsx | 26 +- .../agent/components/agents-preview.js | 77 +- .../agent/components/agents-preview.scss | 42 + .../agent/components/register-agent.js | 27 +- .../configuration/configuration-main.js | 2 +- .../edit-configuration/edit-configuration.js | 14 +- .../util-components/configuration-path.js | 41 +- .../management/groups/groups-editor.js | 204 +-- .../management/groups/groups-main.js | 19 +- .../components/management/mg-logs/logs.js | 2 +- .../management/reporting/reporting-main.js | 2 +- .../management/ruleset/ruleset-editor.js | 272 ++-- .../management/ruleset/ruleset-overview.js | 2 +- .../statistics/statistics-overview.js | 2 +- .../management/status/status-overview.js | 2 +- public/controllers/management/management.js | 17 +- public/controllers/management/monitoring.js | 2 +- public/controllers/settings/index.js | 2 + public/controllers/settings/settings.js | 1 + .../wz-logtest/components/logtest.tsx | 76 +- public/factories/vis-handlers.js | 15 +- public/kibana-integrations/kibana-discover.js | 2 + public/kibana-integrations/kibana-vis.js | 222 +-- .../visualizations/_saved_vis.ts | 12 +- public/plugin.ts | 20 +- public/react-services/common-services.ts | 19 + public/react-services/generic-request.js | 5 +- public/react-services/index.ts | 36 +- .../react-services/load-app-config.service.ts | 41 + public/react-services/pattern-handler.js | 50 +- public/react-services/saved-objects.js | 106 +- public/react-services/wazuh-config.js | 10 +- public/react-services/wz-request.ts | 5 +- public/redux/actions/appConfigActions.ts | 34 + public/redux/actions/appStateActions.js | 11 + public/redux/reducers/appConfigReducers.ts | 56 + public/redux/reducers/appStateReducers.js | 12 +- public/redux/reducers/rootReducers.js | 2 + public/redux/types.ts | 17 + public/services/resolves/get-config.js | 6 +- public/services/resolves/get-saved-search.js | 101 +- public/services/routes.js | 40 +- public/styles/common.scss | 39 +- public/styles/component.scss | 9 + public/styles/dark_theme/wz_theme_dark.scss | 3 - public/templates/management/management.html | 12 +- public/templates/settings/settings.html | 5 + public/types.ts | 4 +- public/utils/add_help_menu_to_app.tsx | 54 + public/utils/config-equivalences.js | 2 +- public/utils/index.ts | 4 +- public/utils/wz-logo-menu.js | 8 +- server/controllers/wazuh-api.ts | 35 +- server/controllers/wazuh-reporting.ts | 127 +- .../visualizations/agents/agents-audit.ts | 730 +++++++--- .../visualizations/agents/agents-aws.ts | 610 +++++++- .../visualizations/agents/agents-ciscat.ts | 664 +++++++-- .../visualizations/agents/agents-docker.ts | 380 ++++- .../visualizations/agents/agents-fim.ts | 568 ++++++-- .../visualizations/agents/agents-gcp.ts | 822 ++++++++++- .../visualizations/agents/agents-gdpr.ts | 303 +++- .../visualizations/agents/agents-general.ts | 873 +++++++++--- .../visualizations/agents/agents-hipaa.ts | 618 +++++++- .../visualizations/agents/agents-mitre.ts | 737 +++++++++- .../visualizations/agents/agents-nist.ts | 534 ++++++- .../visualizations/agents/agents-oscap.ts | 1250 +++++++++++------ .../visualizations/agents/agents-osquery.ts | 666 ++++++++- .../visualizations/agents/agents-pci.ts | 309 +++- .../visualizations/agents/agents-pm.ts | 306 +++- .../visualizations/agents/agents-tsc.ts | 303 +++- .../agents/agents-virustotal.ts | 679 ++++++--- .../visualizations/agents/agents-vuls.ts | 1157 ++++++++++++--- .../visualizations/agents/agents-welcome.ts | 610 +++++++- .../visualizations/cluster/monitoring.ts | 170 ++- .../visualizations/cluster/statistics.ts | 346 +++-- .../visualizations/overview/overview-audit.ts | 317 ++++- .../visualizations/overview/overview-aws.ts | 612 +++++++- .../overview/overview-ciscat.ts | 668 +++++++-- .../overview/overview-docker.ts | 378 ++++- .../visualizations/overview/overview-fim.ts | 501 ++++++- .../visualizations/overview/overview-gcp.ts | 575 +++++++- .../visualizations/overview/overview-gdpr.ts | 605 +++++++- .../overview/overview-general.ts | 962 ++++++++++--- .../visualizations/overview/overview-hipaa.ts | 731 +++++++++- .../visualizations/overview/overview-mitre.ts | 633 ++++++++- .../visualizations/overview/overview-nist.ts | 690 ++++++++- .../visualizations/overview/overview-oscap.ts | 1240 ++++++++++------ .../overview/overview-osquery.ts | 628 ++++++++- .../visualizations/overview/overview-pci.ts | 609 +++++++- .../visualizations/overview/overview-pm.ts | 350 ++++- .../visualizations/overview/overview-tsc.ts | 609 +++++++- .../overview/overview-virustotal.ts | 1134 ++++++++++----- .../visualizations/overview/overview-vuls.ts | 1154 ++++++++++++--- server/lib/api-interceptor.ts | 11 +- server/lib/initial-wazuh-config.ts | 14 +- server/lib/reporting/agent-configuration.ts | 2 +- server/lib/reporting/printer.ts | 52 +- server/lib/update-registry.ts | 2 +- server/routes/wazuh-api.ts | 8 + server/start/cron-scheduler/error-handler.ts | 2 +- server/start/cron-scheduler/save-document.ts | 8 +- server/start/index.ts | 3 +- server/start/initialize/index.ts | 66 +- server/start/monitoring/index.ts | 68 +- .../start/tryCatchForIndexPermissionError.ts | 35 + 194 files changed, 26973 insertions(+), 7064 deletions(-) create mode 100644 .prettierrc create mode 100644 common/wazu-menu/wz-menu-management.cy.ts create mode 100644 common/wazu-menu/wz-menu-overview.cy.ts create mode 100644 common/wazu-menu/wz-menu-security.cy.ts create mode 100644 common/wazu-menu/wz-menu-settings.cy.ts create mode 100644 common/wazu-menu/wz-menu-tools.cy.ts create mode 100644 public/assets/icon_google_groups.svg create mode 100644 public/components/common/hooks/use-app-config.ts create mode 100644 public/components/common/hooks/useRootScope.ts create mode 100644 public/components/common/util/wz-overlay-mask-interface.tsx create mode 100644 public/components/health-check/components/__snapshots__/check-result.test.tsx.snap create mode 100644 public/components/health-check/components/check-result.test.tsx create mode 100644 public/components/health-check/components/check-result.tsx create mode 100644 public/components/health-check/components/inspect-logs-button.tsx create mode 100644 public/components/health-check/components/result-icons.tsx create mode 100644 public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap create mode 100644 public/components/health-check/container/health-check.container.test.tsx create mode 100644 public/components/health-check/container/health-check.container.tsx delete mode 100644 public/components/health-check/health-check.tsx create mode 100644 public/components/health-check/index.ts delete mode 100644 public/components/health-check/lib/check-kibana-settings.ts delete mode 100644 public/components/health-check/lib/check-max-buckets.ts delete mode 100644 public/components/health-check/lib/check-time-filter.ts delete mode 100644 public/components/health-check/lib/index.ts create mode 100644 public/components/health-check/services/check-index-pattern/check-fields.service.ts create mode 100644 public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts create mode 100644 public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts create mode 100644 public/components/health-check/services/check-index-pattern/check-template.service.ts create mode 100644 public/components/health-check/services/check-kibana-settings.service.ts create mode 100644 public/components/health-check/services/check-pattern-support.service.ts create mode 100644 public/components/health-check/services/check-setup.service.ts create mode 100644 public/components/health-check/services/index.ts create mode 100644 public/components/health-check/types/check_logger.ts create mode 100644 public/components/health-check/types/result-icons-presets.ts create mode 100644 public/components/security/policies/create-policy.tsx create mode 100644 public/components/settings/miscellaneous/miscellaneous.tsx create mode 100644 public/controllers/agent/components/agents-preview.scss create mode 100644 public/react-services/common-services.ts create mode 100644 public/react-services/load-app-config.service.ts create mode 100644 public/redux/actions/appConfigActions.ts create mode 100644 public/redux/reducers/appConfigReducers.ts create mode 100644 public/redux/types.ts create mode 100644 public/utils/add_help_menu_to_app.tsx create mode 100644 server/start/tryCatchForIndexPermissionError.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..d440171380 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 81edad6db8..841ec2c7d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,57 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 + +### Added + +- Wazuh help links in the Kibana help menu [#3170](https://github.com/wazuh/wazuh-kibana-app/pull/3170) +- Redirect to group details using the `group` query param in the URL [#3184](https://github.com/wazuh/wazuh-kibana-app/pull/3184) +- Configuration to disable Wazuh App access from X-Pack/ODFE role [#3222](https://github.com/wazuh/wazuh-kibana-app/pull/3222) [#3292](https://github.com/wazuh/wazuh-kibana-app/pull/3292) +- Added confirmation message when closing a form [#3221](https://github.com/wazuh/wazuh-kibana-app/pull/3221) +- Improvement to hide navbar Wazuh label. [#3240](https://github.com/wazuh/wazuh-kibana-app/pull/3240) +- Add modal creating new rule/decoder [#3274](https://github.com/wazuh/wazuh-kibana-app/pull/3274) + +### Changed + +- Removed module titles [#3160](https://github.com/wazuh/wazuh-kibana-app/pull/3160) +- Changed default `wazuh.monitoring.creation` app setting from `d` to `w` [#3174](https://github.com/wazuh/wazuh-kibana-app/pull/3174) +- Changed default `wazuh.monitoring.shards` app setting from `2` to `1` [#3174](https://github.com/wazuh/wazuh-kibana-app/pull/3174) +- Removed Sha1 field from registry key detail [#3189](https://github.com/wazuh/wazuh-kibana-app/pull/3189) +- Removed tooltip in last breadcrumb in header breadcrumb [3250](https://github.com/wazuh/wazuh-kibana-app/pull/3250) +- Refactored the Health check component [#3197](https://github.com/wazuh/wazuh-kibana-app/pull/3197) +- Added version in package downloaded name in agent deploy command [#3210](https://github.com/wazuh/wazuh-kibana-app/issues/3210) +- Removed restriction to allow only current active agents from vulnerability inventory [#3243](https://github.com/wazuh/wazuh-kibana-app/pull/3243) +- Health check actions notifications refactored and added debug mode [#3258](https://github.com/wazuh/wazuh-kibana-app/pull/3258) +- Improved visualizations object configuration readability [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) +- Changed the way kibana-vis hides the visualization while loading, this should prevent errors caused by having a 0 height visualization [#3349](https://github.com/wazuh/wazuh-kibana-app/pull/3349) + +### Fixed + +- Fixed screen flickers in Cluster visualization [#3159](https://github.com/wazuh/wazuh-kibana-app/pull/3159) +- Fixed the broken links when using `server.basePath` Kibana setting [#3161](https://github.com/wazuh/wazuh-kibana-app/pull/3161) +- Fixed filter in reports [#3173](https://github.com/wazuh/wazuh-kibana-app/pull/3173) +- Fixed typo error in Settings/Configuration [#3234](https://github.com/wazuh/wazuh-kibana-app/pull/3234) +- Fixed fields overlap in the agent summary screen [#3217](https://github.com/wazuh/wazuh-kibana-app/pull/3217) +- Fixed Ruleset Test, each request is made in a different session instead of all in the same session [#3257](https://github.com/wazuh/wazuh-kibana-app/pull/3257) +- Fixed the `Visualize` button is not displaying when expanding a field in the Events sidebar [#3237](https://github.com/wazuh/wazuh-kibana-app/pull/3237) +- Fix modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) +- Fix improving and removing WUI error logs [#3260](https://github.com/wazuh/wazuh-kibana-app/pull/3260) +- Fix some errors of PDF reports [#3272](https://github.com/wazuh/wazuh-kibana-app/pull/3272) +- Fix TypeError when selecting macOS agent deployment in a Safari Browser [#3289](https://github.com/wazuh/wazuh-kibana-app/pull/3289) +- Fix error in how the SCA check's checks are displayed [#](https://github.com/wazuh/wazuh-kibana-app/pull/3297) +- Fixed message of error when add sample data fails [#3241](https://github.com/wazuh/wazuh-kibana-app/pull/3241) +- Fixed modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) +- Fixed dark mode visualization background in pdf reports [#3315](https://github.com/wazuh/wazuh-kibana-app/pull/3315) +- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) +- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) +- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) +- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) +- Fixed error that caused the labels in certain visualizations to overlap [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) +- Fixed export to csv button in dashboards tables [#3358](https://github.com/wazuh/wazuh-kibana-app/pull/3358) +- Fixed Elastic UI breaking changes in 7.12 [#3345](https://github.com/wazuh/wazuh-kibana-app/pull/3345) +- Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added @@ -31,8 +82,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Added vulnerabilities inventory that affect to an agent [#3069](https://github.com/wazuh/wazuh-kibana-app/pull/3069) - Added retry button to check api again in health check [#3109](https://github.com/wazuh/wazuh-kibana-app/pull/3109) - Added `wazuh-statistics` template and a new mapping for these indices [#3111](https://github.com/wazuh/wazuh-kibana-app/pull/3111) -- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126 -- Added lowercase levels in storeError [#3377](https://github.com/wazuh/wazuh-kibana-app/pull/3377) +- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126) +- Fixed Agent Evolution graph showing agents from multiple APIs [#3256](https://github.com/wazuh/wazuh-kibana-app/pull/3256) +- Added Disabled index pattern checks in Health Check [#3311](https://github.com/wazuh/wazuh-kibana-app/pull/3311) ### Changed @@ -58,6 +110,25 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed check for TCP protocol in deploy new agent [#3163](https://github.com/wazuh/wazuh-kibana-app/pull/3163) - Fixed RBAC issue with agent group permissions [#3181](https://github.com/wazuh/wazuh-kibana-app/pull/3181) - Fixed change index pattern from menu doesn't work [#3187](https://github.com/wazuh/wazuh-kibana-app/pull/3187) +- Fixed Alerts Summary of modules for reports [#3303](https://github.com/wazuh/wazuh-kibana-app/pull/3303) + +## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2, 7.11.2 - Revision 4108 + +### Fixed + +- Unable to change selected index pattern from the Wazuh menu [#3330](https://github.com/wazuh/wazuh-kibana-app/pull/3330) + +## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2, 7.11.2 - Revision 4107 + +### Added + +- Support for Kibana 7.11.2 +- Added a warning message for the `Install and enroll the agent` step of `Deploy new agent` guide [#3238](https://github.com/wazuh/wazuh-kibana-app/pull/3238) + +### Fixed + +- Conflict with the creation of the index pattern when performing the Health Check [#3223](https://github.com/wazuh/wazuh-kibana-app/pull/3223) +- Fixing mac os agents add command [#3207](https://github.com/wazuh/wazuh-kibana-app/pull/3207) ## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2 - Revision 4106 diff --git a/common/constants.ts b/common/constants.ts index af603b70cb..6bcfb2121f 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -21,22 +21,20 @@ export const WAZUH_INDEX_SHARDS = 2; export const WAZUH_INDEX_REPLICAS = 0; // Job - Wazuh monitoring - -export const WAZUH_INDEX_TYPE_MONITORING = 'monitoring'; -export const WAZUH_MONITORING_PREFIX = 'wazuh-monitoring-'; -export const WAZUH_MONITORING_PATTERN = 'wazuh-monitoring-*'; -export const WAZUH_MONITORING_TEMPLATE_NAME = 'wazuh-agent'; -export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = WAZUH_INDEX_SHARDS; -export const WAZUH_MONITORING_DEFAULT_CREATION = 'd'; +export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; +export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; +export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; +export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; +export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = 1; +export const WAZUH_MONITORING_DEFAULT_CREATION = 'w'; export const WAZUH_MONITORING_DEFAULT_ENABLED = true; export const WAZUH_MONITORING_DEFAULT_FREQUENCY = 900; export const WAZUH_MONITORING_DEFAULT_CRON_FREQ = '0 * * * * *'; // Job - Wazuh statistics - -export const WAZUH_INDEX_TYPE_STATISTICS = 'statistics'; -export const WAZUH_STATISTICS_DEFAULT_PREFIX = 'wazuh'; -export const WAZUH_STATISTICS_DEFAULT_NAME = 'statistics'; +export const WAZUH_INDEX_TYPE_STATISTICS = "statistics"; +export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; +export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = WAZUH_INDEX_SHARDS; @@ -207,7 +205,7 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'xpack.rbac.enabled': true, 'wazuh.monitoring.enabled': WAZUH_MONITORING_DEFAULT_ENABLED, 'wazuh.monitoring.frequency': WAZUH_MONITORING_DEFAULT_FREQUENCY, - 'wazuh.monitoring.shards': WAZUH_INDEX_SHARDS, + 'wazuh.monitoring.shards': WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, 'wazuh.monitoring.replicas': WAZUH_INDEX_REPLICAS, 'wazuh.monitoring.creation': WAZUH_MONITORING_DEFAULT_CREATION, 'wazuh.monitoring.pattern': WAZUH_MONITORING_PATTERN, @@ -259,7 +257,54 @@ export enum WAZUH_MODULES_ID { GDPR = 'gdpr', } +export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { + MANAGEMENT = 'management', + ADMINISTRATION = 'administration', + RULESET = 'ruleset', + RULES = 'rules', + DECODERS = 'decoders', + CDB_LISTS = 'lists', + GROUPS = 'groups', + CONFIGURATION = 'configuration', + STATUS_AND_REPORTS = 'statusReports', + STATUS = 'status', + CLUSTER = 'monitoring', + LOGS = 'logs', + REPORTING = 'reporting', + STATISTICS = 'statistics', +} + +export enum WAZUH_MENU_TOOLS_SECTIONS_ID { + API_CONSOLE = 'devTools', + RULESET_TEST = 'logtest', +} + +export enum WAZUH_MENU_SECURITY_SECTIONS_ID { + USERS = 'users', + ROLES = 'roles', + POLICIES = 'policies', + ROLES_MAPPING = 'roleMapping', +} + +export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { + SETTINGS = 'settings', + API_CONFIGURATION = 'api', + MODULES = 'modules', + SAMPLE_DATA = 'sample_data', + CONFIGURATION = 'configuration', + LOGS = 'logs', + MISCELLANEOUS = 'miscellaneous', + ABOUT = 'about', +} + export const AUTHORIZED_AGENTS = 'authorized-agents'; + +// Wazuh links +export const WAZUH_LINK_DOCUMENTATION = 'https://documentation.wazuh.com'; +export const WAZUH_LINK_GITHUB = 'https://github.com/wazuh'; +export const WAZUH_LINK_GOOGLE_GROUPS = 'https://groups.google.com/forum/#!forum/wazuh'; +export const WAZUH_LINK_SLACK = 'https://wazuh.com/community/join-us-on-slack'; + export const HEALTH_CHECK = 'health-check'; // Health check diff --git a/common/wazu-menu/wz-menu-management.cy.ts b/common/wazu-menu/wz-menu-management.cy.ts new file mode 100644 index 0000000000..23ca04d08f --- /dev/null +++ b/common/wazu-menu/wz-menu-management.cy.ts @@ -0,0 +1,28 @@ +/* + * Wazuh app - Wazuh Constants file for Cypress + * Copyright (C) 2015-2021 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 enum WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID { + MANAGEMENT = 'menuManagementManagementLink', + ADMINISTRATION = 'menuManagementAdministrationLink', + RULESET = 'menuManagementRulesetLink', + RULES = 'menuManagementRulesLink', + DECODERS = 'menuManagementDecodersLink', + CDB_LISTS = 'menuManagementCdbListsLink', + GROUPS = 'menuManagementGroupsLink', + CONFIGURATION = 'menuManagementConfigurationLink', + STATUS_AND_REPORTS = 'menuManagementStatusReportsLink', + STATUS = 'menuManagementStatusLink', + CLUSTER = 'menuManagementMonitoringLink', + LOGS = 'menuManagementLogsLink', + REPORTING = 'menuManagementReportingLink', + STATISTICS = 'menuManagementStatisticsLink', +} diff --git a/common/wazu-menu/wz-menu-overview.cy.ts b/common/wazu-menu/wz-menu-overview.cy.ts new file mode 100644 index 0000000000..32622ac474 --- /dev/null +++ b/common/wazu-menu/wz-menu-overview.cy.ts @@ -0,0 +1,33 @@ +/* + * Wazuh app - Wazuh Constants file for Cypress + * Copyright (C) 2015-2021 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 enum WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID { + SECURITY_EVENTS = 'menuModulesSecurityEventsLink', + INTEGRITY_MONITORING = 'menuModulesFimLink', + AMAZON_WEB_SERVICES = 'menuModulesAwsLink', + GOOGLE_CLOUD_PLATFORM = 'menuModulesGcpLink', + POLICY_MONITORING = 'menuModulesPolicyMonitoringLink', + SECURITY_CONFIGURATION_ASSESSMENT = 'menuModulesScaLink', + AUDITING = 'menuModulesAuditLink', + OPEN_SCAP = 'menuModulesOpenScapLink', + VULNERABILITIES = 'menuModulesVulsLink', + OSQUERY = 'menuModulesOsqueryLink', + DOCKER = 'menuModulesDockerLink', + MITRE_ATTACK = 'menuModulesMitreLink', + PCI_DSS = 'menuModulesPciLink', + HIPAA = 'menuModulesHipaaLink', + NIST_800_53 = 'menuModulesNistLink', + TSC = 'menuModulesTscLink', + CIS_CAT = 'menuModulesCiscatLink', + VIRUSTOTAL = 'menuModulesVirustotalLink', + GDPR = 'menuModulesGdprLink', +} diff --git a/common/wazu-menu/wz-menu-security.cy.ts b/common/wazu-menu/wz-menu-security.cy.ts new file mode 100644 index 0000000000..f82c425497 --- /dev/null +++ b/common/wazu-menu/wz-menu-security.cy.ts @@ -0,0 +1,18 @@ +/* + * Wazuh app - Wazuh Constants file for Cypress + * Copyright (C) 2015-2021 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 enum WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID { + USERS = 'menuSecurityUsersLink', + ROLES = 'menuSecurityRolesLink', + POLICIES = 'menuSecurityPoliciesLink', + ROLES_MAPPING = 'menuSecurityRoleMappingLink', +} diff --git a/common/wazu-menu/wz-menu-settings.cy.ts b/common/wazu-menu/wz-menu-settings.cy.ts new file mode 100644 index 0000000000..4053ff29e3 --- /dev/null +++ b/common/wazu-menu/wz-menu-settings.cy.ts @@ -0,0 +1,22 @@ +/* + * Wazuh app - Wazuh Constants file for Cypress + * Copyright (C) 2015-2021 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 enum WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID { + SETTINGS = 'menuSettingsSettingsLink', + API_CONFIGURATION = 'menuSettingsApiLink', + MODULES = 'menuSettingsModulesLink', + SAMPLE_DATA = 'menuSettingsSampleDataLink', + CONFIGURATION = 'menuSettingsConfigurationLink', + LOGS = 'menuSettingsLogsLink', + MISCELLANEOUS = 'menuSettingsMiscellaneousLink', + ABOUT = 'menuSettingsAboutLink', +} diff --git a/common/wazu-menu/wz-menu-tools.cy.ts b/common/wazu-menu/wz-menu-tools.cy.ts new file mode 100644 index 0000000000..576a6fe247 --- /dev/null +++ b/common/wazu-menu/wz-menu-tools.cy.ts @@ -0,0 +1,16 @@ +/* + * Wazuh app - Wazuh Constants file for Cypress + * Copyright (C) 2015-2021 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 enum WAZUH_MENU_TOOLS_SECTIONS_CY_TEST_ID { + API_CONSOLE = 'menuToolsDevToolsLink', + RULESET_TEST = 'menuToolsLogtestLink', +} diff --git a/public/app.js b/public/app.js index ee573bafed..c216a01fd6 100644 --- a/public/app.js +++ b/public/app.js @@ -53,9 +53,11 @@ import './factories'; import { checkCurrentSecurityPlatform } from './controllers/management/components/management/configuration/utils/wz-fetch'; import store from './redux/store'; import { updateCurrentPlatform } from './redux/actions/appStateActions'; -import { WzAuthentication } from './react-services/wz-authentication'; +import { WzAuthentication, loadAppConfig } from './react-services'; + +import { getAngularModule} from './kibana-services'; +import { addHelpMenuToAppChrome } from './utils'; -import { getAngularModule } from './kibana-services'; const app = getAngularModule(); app.config([ @@ -81,10 +83,13 @@ app.run([ // Set currentSecurity platform in Redux when app starts. checkCurrentSecurityPlatform().then((item) => { store.dispatch(updateCurrentPlatform(item)) - }).catch(() => {}) + }).catch(() => {}); // Init the process of refreshing the user's token when app start. checkPluginVersion().finally(WzAuthentication.refresh); + + // Load the app state + loadAppConfig(); }, ]); @@ -101,6 +106,9 @@ app.run(function ($rootElement) { `); + // Add plugin help links as extension to Kibana help menu + addHelpMenuToAppChrome(); + // Bind deleteExistentToken on Log out component. $('.euiHeaderSectionItem__button').on('mouseleave', function () { // opendistro diff --git a/public/assets/icon_google_groups.svg b/public/assets/icon_google_groups.svg new file mode 100644 index 0000000000..292fa411a0 --- /dev/null +++ b/public/assets/icon_google_groups.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/public/components/add-modules-data/WzSampleDataWrapper.js b/public/components/add-modules-data/WzSampleDataWrapper.js index 7f00da1c50..16670b1e20 100644 --- a/public/components/add-modules-data/WzSampleDataWrapper.js +++ b/public/components/add-modules-data/WzSampleDataWrapper.js @@ -50,16 +50,12 @@ export class WzSampleDataProvider extends Component {

Sample data

-
-
- - - - Add sample data to modules. + + Add sample data with events to the modules - + diff --git a/public/components/add-modules-data/add-modules-data-main.tsx b/public/components/add-modules-data/add-modules-data-main.tsx index 27947529f1..89b47b5446 100644 --- a/public/components/add-modules-data/add-modules-data-main.tsx +++ b/public/components/add-modules-data/add-modules-data-main.tsx @@ -99,7 +99,7 @@ export default class WzAddModulesData extends Component { - return moment(date).format('YYYY-MM-DD'); - } + // TODO: Change the type suggestions: {[key:string]: IWzSuggestItem[]} = { files: [ {type: 'q', label: 'file', description:"Name of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('file', value, this.props.agent.id, {type:'file'})}, ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'perm', description:"Permisions of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('perm', value, this.props.agent.id)}]: []), - {type: 'q', label: 'mtime', description:"Date the file was modified", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('mtime', value, this.props.agent.id,{}, this.formatDate)}, - {type: 'q', label: 'date', description:"Date of registration of the event", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('date', value, this.props.agent.id, {}, this.formatDate)}, + {type: 'q', label: 'mtime', description:"Date the file was modified", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('mtime', value, this.props.agent.id,{}, formatUIDate)}, + {type: 'q', label: 'date', description:"Date of registration of the event", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('date', value, this.props.agent.id, {}, formatUIDate)}, {type: 'q', label: 'uname', description:"Owner of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('uname', value, this.props.agent.id)}, {type: 'q', label: 'uid', description:"Id of the onwner file", operators:['=','!=', '~'], values: async (value) => getFilterValues('uid', value, this.props.agent.id)}, ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'gname', description:"Name of the group owner file", operators:['=','!=', '~'], values: async (value) => getFilterValues('gname', value, this.props.agent.id)}]: []), @@ -69,4 +67,4 @@ export class FilterBar extends Component { ) } -} \ No newline at end of file +} diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index 5fe311407a..86bbef000f 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -24,6 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; import { filtersToObject } from '../../../wz-search-bar'; +import { formatUIDate } from '../../../../react-services/time-service'; export class RegistryTable extends Component { state: { @@ -185,6 +186,7 @@ export class RegistryTable extends Component { name: 'Last Modified', sortable: true, width: '200px', + render: formatUIDate, } ] } diff --git a/public/components/agents/fim/inventory/registryValues/registryValues.tsx b/public/components/agents/fim/inventory/registryValues/registryValues.tsx index 7ee31f5bc2..1db8d8f9ca 100644 --- a/public/components/agents/fim/inventory/registryValues/registryValues.tsx +++ b/public/components/agents/fim/inventory/registryValues/registryValues.tsx @@ -16,6 +16,7 @@ import React, { useEffect, useState } from 'react'; import valuesMock from './values.json'; import { DIRECTIONS } from '@elastic/eui/src/components/flex/flex_group'; import { emptyFieldHandler } from '../lib'; +import { formatUIDate } from '../../../../../react-services/time-service'; export const RegistryValues = (props) => { const [values, setValues] = useState([]); @@ -47,6 +48,7 @@ export const RegistryValues = (props) => { field: 'date', name: 'Date', sortable: true, + render: formatUIDate }, { field: 'value', diff --git a/public/components/agents/sca/components/rule-text.tsx b/public/components/agents/sca/components/rule-text.tsx index 1233597bd4..47c6d42160 100644 --- a/public/components/agents/sca/components/rule-text.tsx +++ b/public/components/agents/sca/components/rule-text.tsx @@ -2,16 +2,15 @@ import React from 'react' import { EuiText } from '@elastic/eui'; -interface IRuleText { - rulesText: string +interface RuleTextProps { + rules: {type: string, rule: string}[] } -export const RuleText: React.FunctionComponent = ({ rulesText }) => { - const splitRulesText = rulesText.split(' -> '); +export const RuleText: React.FunctionComponent = ({ rules }) => { return (
    - {splitRulesText.map((text, idx) =>
  • {text}
  • )} + {rules.map((rule, idx) =>
  • {rule.rule}
  • )}
) diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 8d3e4e252d..19b8d15f7a 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -355,7 +355,7 @@ export class Inventory extends Component { }, { title: checks, - description: , + description: , }, { title: 'Compliance', diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index c9d6eea64e..a8374e093d 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -69,7 +69,7 @@ export class FlyoutDetail extends Component { render() { const { currentItem } = this.state; - const title = `${currentItem.name} ${currentItem.cve}`; + const title = `${currentItem.cve}`; const id = title.replace(/ /g, '_'); return ( ) ), - withGuard( - (props) => { - const agentData = - props.currentAgentData && props.currentAgentData.id ? props.currentAgentData : props.agent; - return agentData.status !== 'active'; - }, - () => - ), withUserAuthorizationPrompt((props) => { const agentData = props.currentAgentData && props.currentAgentData.id ? props.currentAgentData : props.agent; diff --git a/public/components/common/error-boundary/error-boundary.tsx b/public/components/common/error-boundary/error-boundary.tsx index 3824766ef0..29c4484324 100644 --- a/public/components/common/error-boundary/error-boundary.tsx +++ b/public/components/common/error-boundary/error-boundary.tsx @@ -21,7 +21,7 @@ import { UILogLevel, } from '../../../react-services/error-orchestrator/types'; import { ErrorComponentPrompt } from '../error-boundary-prompt/error-boundary-prompt'; -import { getErrorOrchestrator } from '../../../react-services'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; export default class ErrorBoundary extends Component { constructor(props) { diff --git a/public/components/common/globalBreadcrumb/globalBreadcrumb.scss b/public/components/common/globalBreadcrumb/globalBreadcrumb.scss index 7655ddaf6a..50d1dd6501 100644 --- a/public/components/common/globalBreadcrumb/globalBreadcrumb.scss +++ b/public/components/common/globalBreadcrumb/globalBreadcrumb.scss @@ -1,7 +1,7 @@ .wz-global-breadcrumb { font-size: 14px; font-family: "Inter UI"; - padding: 0px 0px 0px 5px; + padding: 0px; } .wz-global-breadcrumb-btn{ @@ -14,11 +14,6 @@ font-family: "Inter UI"; } -@media only screen and (max-width: 767px){ - .wz-global-breadcrumb { - padding-left: 16px; - } -} @media screen and (-webkit-min-device-pixel-ratio: 0) { _::-webkit-full-page-media, _:future, :root , .wz-global-breadcrumb { diff --git a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx index 5de33a9c42..1762d5fd69 100644 --- a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx +++ b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { EuiBreadcrumbs, EuiToolTip } from '@elastic/eui'; +import { EuiBreadcrumbs, EuiToolTip, EuiSelect } from '@elastic/eui'; import { connect } from 'react-redux'; import './globalBreadcrumb.scss'; import { AppNavigate } from '../../../react-services/app-navigate'; @@ -22,11 +22,9 @@ class WzGlobalBreadcrumb extends Component { $('#breadcrumbNoTitle').attr("title", ""); } render() { - const container = document.getElementsByClassName('euiBreadcrumbs'); return (
{!!this.props.state.breadcrumb.length && ( - ReactDOM.createPortal( ) } : breadcrumb)} aria-label="Wazuh global breadcrumbs" - />, - container[0]) + /> )}
) diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index 81653072c2..a08b1f9c64 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -9,26 +9,15 @@ * * Find more information about this on the LICENSE file. */ -export { withWindowSize } from './withWindowSize'; - -export { withKibanaContext, withKibanaContextExtendsProps } from './withKibanaContext'; - -export { withUserPermissions, withUserPermissionsRequirements, withUserPermissionsPrivate } from './withUserPermissions'; - -export { withUserRoles, withUserRolesRequirements, withUserRolesPrivate } from './withUserRoles'; - -export { withUserAuthorizationPrompt} from './withUserAuthorization'; - -export { withGlobalBreadcrumb } from './withGlobalBreadcrumb'; - -export { withReduxProvider } from './withReduxProvider'; - -export { withGuard } from './withGuard'; - -export { withButtonOpenOnClick } from './withButtonOpenOnClick'; - -export { withAgentSupportModule } from './withAgentSupportModule'; - -export { withUserLogged } from './withUserLogged'; - -export { withErrorBoundary } from './error-boundary/with-error-boundary'; +export * from './withWindowSize'; +export * from './withKibanaContext'; +export * from './withUserPermissions'; +export * from './withUserRoles'; +export * from './withUserAuthorization'; +export * from './withGlobalBreadcrumb'; +export * from './withReduxProvider'; +export * from './withGuard'; +export * from './withButtonOpenOnClick'; +export * from './withAgentSupportModule'; +export * from './withUserLogged'; +export * from './error-boundary/with-error-boundary'; diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 62a22c2b3d..87e5151c2e 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -10,26 +10,17 @@ * Find more information about this on the LICENSE file. */ -export { useFilterManager } from './use-filter-manager'; - -export { useIndexPattern } from './use-index-pattern'; - -export { useKbnLoadingIndicator } from './use-kbn-loading-indicator'; - -export { useQuery } from './use-query'; - -export { useTimeFilter } from './use-time-filter'; - -export { useWindowSize } from './useWindowSize'; - -export { useUserPermissions, useUserPermissionsRequirements, useUserPermissionsPrivate } from './useUserPermissions'; - -export { useUserRoles, useUserRolesRequirements, useUserRolesPrivate } from './useUserRoles'; - -export { useRefreshAngularDiscover } from './useResfreshAngularDiscover'; - -export { useAllowedAgents } from './useAllowedAgents'; - -export { useApiRequest } from './useApiRequest'; - -export * from './use_async_action'; +export * from './use-filter-manager'; +export * from './use-index-pattern'; +export * from './use-kbn-loading-indicator'; +export * from './use-query'; +export * from './use-time-filter'; +export * from './useWindowSize'; +export * from './useUserPermissions'; +export * from './useUserRoles'; +export * from './useResfreshAngularDiscover'; +export * from './useAllowedAgents'; +export * from './useApiRequest'; +export * from './use-app-config'; +export * from './useRootScope'; +export * from './use_async_action'; \ No newline at end of file diff --git a/public/components/common/hooks/use-app-config.ts b/public/components/common/hooks/use-app-config.ts new file mode 100644 index 0000000000..9a323a7050 --- /dev/null +++ b/public/components/common/hooks/use-app-config.ts @@ -0,0 +1,19 @@ +/* + * Wazuh app - React hook for app configuration + * Copyright (C) 2015-2021 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 { AppRootState } from '../../../redux/types'; +import { useSelector } from 'react-redux'; + +export const useAppConfig = () => { + const appConfig = useSelector((state: AppRootState) => state.appConfig); + return appConfig; +} diff --git a/public/components/common/hooks/useRootScope.ts b/public/components/common/hooks/useRootScope.ts new file mode 100644 index 0000000000..de6d0b9628 --- /dev/null +++ b/public/components/common/hooks/useRootScope.ts @@ -0,0 +1,22 @@ +/* + * Wazuh app - React hook to get the AngularJS $rootScope + * Copyright (C) 2015-2021 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 { useEffect, useRef } from 'react'; +import { getAngularModule } from '../../../kibana-services'; + +export function useRootScope(){ + const refRootScope = useRef(); + useEffect(() => { + const app = getAngularModule(); + refRootScope.current = app.$injector.get('$rootScope'); + },[]); + return refRootScope.current; +}; diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index ea0f44496b..8cf7c4bf9b 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -17,9 +17,12 @@ import { EuiFlexItem, EuiButtonIcon, EuiTitle, + EuiToolTip, EuiPopover, EuiBadge, - EuiPopoverTitle + EuiIcon, + EuiText, + EuiPopoverTitle, } from '@elastic/eui'; import '../../common/modules/module.scss'; import { updateGlobalBreadcrumb } from '../../../redux/actions/globalBreadcrumbActions'; @@ -54,32 +57,60 @@ export class MainModuleOverview extends Component { }; } - getBadgeColor(agentStatus){ - if (agentStatus.toLowerCase() === 'active') { return 'secondary'; } - else if (agentStatus.toLowerCase() === 'disconnected') { return '#BD271E'; } - else if (agentStatus.toLowerCase() === 'never connected') { return 'default'; } + getBadgeColor(agentStatus) { + if (agentStatus.toLowerCase() === 'active') { + return 'secondary'; + } else if (agentStatus.toLowerCase() === 'disconnected') { + return '#BD271E'; + } else if (agentStatus.toLowerCase() === 'never connected') { + return 'default'; + } } setGlobalBreadcrumb() { const currentAgent = store.getState().appStateReducers.currentAgentData; - if (WAZUH_MODULES[this.props.currentTab]) { - let breadcrumb = [ + if (WAZUH_MODULES[this.props.section]) { + let breadcrumb:any[] = [ { text: '', }, { - text: currentAgent.id ? (Modules - {AppNavigate.navigateToModule(ev, 'agents', {"tab": "welcome", "agent": currentAgent.id } )}} - color={this.getBadgeColor(currentAgent.status)}> - {currentAgent.id} - ) : 'Modules', - href: "#/overview" - }, - { - text: WAZUH_MODULES[this.props.section].title + text: 'Modules', + href: '#/overview' }, ]; + if (currentAgent.id) { + breadcrumb.push( { + text: ( + { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": currentAgent.id }); this.router.reload(); }} + id="breadcrumbNoTitle" + > + + {currentAgent.name} + + ), + }) + } + breadcrumb.push({ + text: ( + +
+ +
+ {WAZUH_MODULES[this.props.section].title} +
+
+
+ + + +
+ ), + truncate: false, + },) store.dispatch(updateGlobalBreadcrumb(breadcrumb)); } } @@ -91,100 +122,49 @@ export class MainModuleOverview extends Component { async componentDidMount() { const tabView = AppNavigate.getUrlParameter('tabView') || 'panels'; const tab = AppNavigate.getUrlParameter('tab'); - if(tabView && tabView !== this.props.selectView){ - if(tabView === 'panels' && tab=== 'sca' ){ // SCA initial tab is inventory + if (tabView && tabView !== this.props.selectView) { + if (tabView === 'panels' && tab === 'sca') { + // SCA initial tab is inventory this.props.onSelectedTabChanged('inventory'); - }else{ + } else { this.props.onSelectedTabChanged(tabView); } } - - this.setGlobalBreadcrumb(); - } - renderTitle() { - return ( - - - - - - -

-  {WAZUH_MODULES[this.props.section].title}   -

-
- { this.setState({ isDescPopoverOpen: !this.state.isDescPopoverOpen }) }} - /> - } - anchorPosition="rightUp" - isOpen={this.state.isDescPopoverOpen} - closePopover={() => { this.setState({ isDescPopoverOpen: false }) }}> - Module description -
- {WAZUH_MODULES[this.props.section].description} -
-
-
-
- -
-
-
- ); + this.setGlobalBreadcrumb(); } render() { const { section, selectView } = this.props; - const title = this.renderTitle(); return (
-
-
- {title} -
-
-
-
- {(this.props.tabs && this.props.tabs.length) && -
- - {this.props.renderTabs()} - - - - - - {(selectView === 'dashboard') && - this.props.renderReportButton() - } - {(this.props.buttons || []).includes('dashboard') && - this.props.renderDashboardButton() - } - -
- } -
-
-
- +
+ {this.props.tabs && this.props.tabs.length && ( +
+ + {this.props.renderTabs()} + + + + + + {selectView === 'dashboard' && this.props.renderReportButton()} + {(this.props.buttons || []).includes('dashboard') && + this.props.renderDashboardButton()} + +
+ )}
+
); } } -const mapStateToProps = state => ({ - agent: state.appStateReducers.currentAgentData +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, }); const ModuleTabViewer = compose( @@ -192,28 +172,24 @@ const ModuleTabViewer = compose( withAgentSupportModule )((props) => { const { section, selectView } = props; - return <> - {selectView === 'events' && - - } - {selectView === 'loader' && - + {selectView === 'events' && } + {selectView === 'loader' && ( + props.loadSection(section)} - redirect={props.afterLoad}> - } - {selectView === 'dashboard' && - - } - {selectView === 'settings' && - - } - + redirect={props.afterLoad} + > + )} + {selectView === 'dashboard' && } + {selectView === 'settings' && } {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} - {section === 'fim' && selectView==='inventory' && } - {section === 'sca' && selectView==='inventory' && } - - {section === 'vuls' && selectView==='inventory' && } + {section === 'fim' && selectView === 'inventory' && } + {section === 'sca' && selectView === 'inventory' && } + + {section === 'vuls' && selectView === 'inventory' && } {section === 'mitre' && selectView === 'inventory' && } {section === 'mitre' && selectView === 'intelligence' && } @@ -222,4 +198,5 @@ const ModuleTabViewer = compose( )} {/* -------------------------------------------------------------------------- */} -}) + ); +}); \ No newline at end of file diff --git a/public/components/common/modules/main.tsx b/public/components/common/modules/main.tsx index 02e7fa9e0d..275abffa09 100644 --- a/public/components/common/modules/main.tsx +++ b/public/components/common/modules/main.tsx @@ -115,13 +115,24 @@ export const MainModule = compose( this.setState({ loadingReport: true }); const isDarkModeTheme = getUiSettings().get('theme:darkMode'); if (isDarkModeTheme) { + + //Patch to fix white text in dark-mode pdf reports const defaultTextColor = '#DFE5EF'; + + //Patch to fix dark backgrounds in visualizations dark-mode pdf reports + const $labels = $('.euiButtonEmpty__text, .echLegendItem'); + const $vizBackground = $('.echChartBackground'); + const defaultVizBackground = $vizBackground.css('background-color'); + try { - $('.euiButtonEmpty__text').css('color', 'black'); + $labels.css('color', 'black'); + $vizBackground.css('background-color', 'transparent'); await this.startVis2PngByAgent(); - $('.euiButtonEmpty__text').css('color', defaultTextColor); + $vizBackground.css('background-color', defaultVizBackground); + $labels.css('color', defaultTextColor); } catch (e) { - $('.euiButtonEmpty__text').css('color', defaultTextColor); + $labels.css('color', defaultTextColor); + $vizBackground.css('background-color', defaultVizBackground); this.setState({ loadingReport: false }); } } else { diff --git a/public/components/common/modules/overview-current-section.tsx b/public/components/common/modules/overview-current-section.tsx index 1d9aad1cab..e0d79de166 100644 --- a/public/components/common/modules/overview-current-section.tsx +++ b/public/components/common/modules/overview-current-section.tsx @@ -21,7 +21,7 @@ import { updateCurrentTab } from '../../../redux/actions/appStateActions'; import store from '../../../redux/store'; import { connect } from 'react-redux'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { AppNavigate } from '../../../react-services/app-navigate' +import { AppNavigate } from '../../../react-services/app-navigate'; class WzCurrentOverviewSection extends Component { constructor(props) { @@ -43,13 +43,13 @@ class WzCurrentOverviewSection extends Component { if(WAZUH_MODULES[this.props.currentTab]){ const breadcrumb = currentAgent.id ? [ { text: '' }, - { text: 'Modules', href: '/app/wazuh#/overview' }, + { text: 'Modules', href: '#/overview' }, { agent: currentAgent }, { text: WAZUH_MODULES[this.props.currentTab].title}, ] : [ { text: '' }, - { text: 'Modules', href: '/app/wazuh#/overview' }, + { text: 'Modules', href: '#/overview' }, { text: WAZUH_MODULES[this.props.currentTab].title}, diff --git a/public/components/common/util/index.ts b/public/components/common/util/index.ts index 669665dff4..83f035e63f 100644 --- a/public/components/common/util/index.ts +++ b/public/components/common/util/index.ts @@ -15,3 +15,4 @@ export { TruncateHorizontalComponents } from './truncate-horizontal-components/truncate-horizontal-components'; export { GroupingComponents } from './grouping-components'; export * from './markdown/markdown'; + export * from './wz-overlay-mask-interface'; diff --git a/public/components/common/util/wz-overlay-mask-interface.tsx b/public/components/common/util/wz-overlay-mask-interface.tsx new file mode 100644 index 0000000000..871827682a --- /dev/null +++ b/public/components/common/util/wz-overlay-mask-interface.tsx @@ -0,0 +1,121 @@ +/* + * Wazuh app - Simple description for each App tabs + * Copyright (C) 2015-2021 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, { + FunctionComponent, + HTMLAttributes, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; +import { createPortal } from 'react-dom'; +import classNames from 'classnames'; + +export interface WzOverlayMaskInterface { + /** + * Function that applies to clicking the mask itself and not the children + */ + onClick?: () => void; + /** + * ReactNode to render as this component's content + */ + children?: ReactNode; + /** + * Should the mask visually sit above or below the EuiHeader (controlled by z-index) + */ + headerZindexLocation?: 'above' | 'below'; +} + +export type EuiOverlayMaskProps = + Omit< + Partial, string>>, + keyof WzOverlayMaskInterface + > & + WzOverlayMaskInterface; + +export const WzOverlayMask: FunctionComponent = ({ + className, + children, + onClick, + headerZindexLocation = 'above', + ...rest +}) => { + const overlayMaskNode = useRef(document.createElement('div')); + const functionOnClick = useRef(); + const [isPortalTargetReady, setIsPortalTargetReady] = useState(false); + + useEffect(() => { + document.body.classList.add('euiBody-hasOverlayMask'); + + return () => { + document.body.classList.remove('euiBody-hasOverlayMask'); + }; + }, []); + + useEffect(() => { + const portalTarget = overlayMaskNode.current; + document.body.appendChild(overlayMaskNode.current); + setIsPortalTargetReady(true); + + return () => { + if (portalTarget) { + document.body.removeChild(portalTarget); + } + }; + }, []); + + useEffect(() => { + if (!overlayMaskNode.current) return; + Object.keys(rest).forEach(key => { + if (typeof rest[key] !== 'string') { + throw new Error( + `Unhandled property type. EuiOverlayMask property ${key} is not a string.` + ); + } + overlayMaskNode.current.setAttribute(key, rest[key]!); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (!overlayMaskNode.current) return; + overlayMaskNode.current.className = classNames( + 'euiOverlayMask', + `euiOverlayMask--${headerZindexLocation}Header`, + className + ); + }, [className, headerZindexLocation]); + + useEffect(() => { + if (!overlayMaskNode.current || !onClick) return; + const portalTarget = overlayMaskNode.current; + if (functionOnClick.current) { + portalTarget.removeEventListener('click', functionOnClick.current); + } + functionOnClick.current = e => { + if (e.target === overlayMaskNode.current) { + onClick(); + } + } + overlayMaskNode.current.addEventListener('click',functionOnClick.current); + + return () => { + if (portalTarget && onClick) { + portalTarget.removeEventListener('click', functionOnClick.current); + } + }; + }, [onClick]); + + return isPortalTargetReady ? ( + <>{createPortal(children, overlayMaskNode.current!)} + ) : null; +}; \ No newline at end of file diff --git a/public/components/common/welcome/agents-info.js b/public/components/common/welcome/agents-info.js index 4f3bb108d1..5d539088c3 100644 --- a/public/components/common/welcome/agents-info.js +++ b/public/components/common/welcome/agents-info.js @@ -73,7 +73,7 @@ export class AgentInfo extends Component { const osName = os_name === '- -' ? '-' : os_name; return ( - + {this.getPlatformIcon(this.props.agent)} {' '}{osName} @@ -125,6 +125,8 @@ export class AgentInfo extends Component { return field !== undefined || field ? field : '-'; }; const stats = items.map(item => { + // We add tooltipProps, so that the ClusterNode and Operating System fields occupy their space and do not exceed this, overlapping with the one on the right + const tooltipProps = item.description === 'Cluster node' ? { anchorClassName: 'wz-width-100'} : {}; return ( + {checkField(item.title)} ) diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index 6120bee12c..76868ed98f 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFlexGrid, EuiButtonEmpty, EuiSideNav, EuiIcon, EuiButtonIcon } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFlexGrid, EuiSideNav, EuiIcon } from '@elastic/eui'; import { connect } from 'react-redux'; import { AppState } from '../../../../react-services/app-state'; import { hasAgentSupportModule } from '../../../../react-services/wz-agents'; @@ -78,10 +78,14 @@ class WzMenuAgent extends Component { ]; this.auditingItems = [ this.agentSections.pm, + this.agentSections.audit, + this.agentSections.oscap, this.agentSections.ciscat, this.agentSections.sca ]; this.threatDetectionItems = [ + this.agentSections.vuls, + this.agentSections.docker, this.agentSections.virustotal, this.agentSections.osquery, this.agentSections.mitre diff --git a/public/components/eui-loader.js b/public/components/eui-loader.js index ec39ea8ca2..76bbebdf7e 100644 --- a/public/components/eui-loader.js +++ b/public/components/eui-loader.js @@ -30,7 +30,7 @@ import { import { Tabs } from './common/tabs/tabs'; import { MultipleAgentSelector } from './management/groups/multiple-agent-selector'; import { NodeList } from './management/cluster/node-list'; -import { HealthCheck } from './health-check/health-check'; +import { HealthCheck } from './health-check'; import { WzEmptyPromptNoPermissions } from './common/permissions/prompt'; import { getAngularModule } from '../kibana-services'; import { Logtest } from '../directives/wz-logtest/components/logtest'; diff --git a/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap b/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap new file mode 100644 index 0000000000..de13159dd7 --- /dev/null +++ b/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Check result component should render a Check result screen 1`] = ` + + +
+ Check Test +
+
+ +
+

+ + + + + + + + + +

+
+
+
+`; diff --git a/public/components/health-check/components/check-result.test.tsx b/public/components/health-check/components/check-result.test.tsx new file mode 100644 index 0000000000..8b52cbfa77 --- /dev/null +++ b/public/components/health-check/components/check-result.test.tsx @@ -0,0 +1,89 @@ +/* + * Wazuh app - Check Result Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { CheckResult } from './check-result'; +import { waitFor, render } from '@testing-library/react'; + +describe('Check result component', () => { + const validationService = jest.fn(); + const handleErrors = jest.fn(); + const handleCheckReady = jest.fn(); + const cleanErrors = jest.fn(); + + test('should render a Check result screen', () => { + validationService.mockImplementation(() => ({ errors: [] })); + const component = mount( + + ); + + expect(component).toMatchSnapshot(); + }); + + test('should print ready', async () => { + validationService.mockImplementation(() => ({ errors: [] })); + const {queryByLabelText} = render( + + ); + await waitFor(()=>{ + expect(queryByLabelText('ready').tagName).toEqual('svg') + }); + }); + + test('should print error', async () => { + validationService.mockImplementation(() => {throw 'error'}); + const {queryByLabelText} = render( + + ); + await waitFor(()=>{ + expect(queryByLabelText('error_retry').tagName).toEqual('svg') + }); + }); +}); diff --git a/public/components/health-check/components/check-result.tsx b/public/components/health-check/components/check-result.tsx new file mode 100644 index 0000000000..7ae82a7ec1 --- /dev/null +++ b/public/components/health-check/components/check-result.tsx @@ -0,0 +1,142 @@ +/* + * Wazuh app - Check Result Component + * + * Copyright (C) 2015-2021 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, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; +import { + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiCodeBlock, +} from '@elastic/eui'; +import InspectLogButton from './inspect-logs-button'; +import ResultIcons from './result-icons'; + + +type Result = 'loading' | 'ready' | 'error' | 'error_retry' | 'disabled' | 'waiting'; + +export function CheckResult(props) { + const [result, setResult] = useState('waiting'); + const [isCheckStarted, setIsCheckStarted] = useState(false); + const [verboseInfo, setVerboseInfo] = useState([]); + const [verboseIsOpen, setVerboseIsOpen] = useState(false); + const [isCheckFinished, setIsCheckFinished] = useState(false); + const verboseInfoWithNotificationWasOpened = useRef(false); + + useEffect(() => { + if (props.shouldCheck && !props.isLoading && awaitForIsReady()){ + initCheck(); + } else if (props.shouldCheck === false && !props.checksReady[props.name]) { + setResult('disabled'); + setAsReady(); + } + }, [props.shouldCheck, props.isLoading, props.checksReady]); + + useEffect(() => { + if (isCheckFinished){ + const errors = verboseInfo.filter(log => log.type === 'error'); + if(errors.length){ + props.canRetry ? setResult('error_retry') : setResult('error'); + props.handleErrors(props.name, errors.map(({message}) => message)); + }else{ + setResult('ready'); + setAsReady(); + } + } + }, [isCheckFinished]) + + useEffect(() => { + if (isCheckFinished){ + const errors = verboseInfo.filter(log => log.type === 'error'); + if(errors.length){ + props.canRetry ? setResult('error_retry') : setResult('error'); + props.handleErrors(props.name, errors.map(({message}) => message)); + }else{ + setResult('ready'); + setAsReady(); + } + } + }, [isCheckFinished]) + + const setAsReady = () => { + props.handleCheckReady(props.name, true); + }; + + /** + * validate if the current check is not started and if the dependentes checks are ready + */ + const awaitForIsReady = () => { + return !isCheckStarted && (props.awaitFor.length === 0 || props.awaitFor.every((check) => { + return props.checksReady[check]; + })) + } + + const checkLogger = useMemo(() => ({ + _log: (message: string, type: 'info' | 'error' | 'action' ) => { + setVerboseInfo(state => [...state, {message, type}]); + }, + info: (message: string) => checkLogger._log(message, 'info'), + error: (message: string) => checkLogger._log(message, 'error'), + action: (message: string) => checkLogger._log(message, 'action') + }), []); + + const initCheck = async () => { + setIsCheckStarted(true); + setResult('loading'); + setVerboseInfo([]); + props.cleanErrors(props.name); + setIsCheckFinished(false); + try { + await props.validationService(checkLogger); + } catch (error) { + checkLogger.error(error.message || error); + } + setIsCheckFinished(true); + }; + + + const checkDidSomeAction = useMemo(() => verboseInfo.some(log => log.type === 'action'), [verboseInfo]); + const shouldShowNotification = checkDidSomeAction && !verboseInfoWithNotificationWasOpened.current; + const haveLogs = verboseInfo.length > 0; + + const switchVerboseDetails = useCallback(() => { + if(checkDidSomeAction && !verboseInfoWithNotificationWasOpened.current){ + verboseInfoWithNotificationWasOpened.current = true; + }; + setVerboseIsOpen(state => !state); + },[checkDidSomeAction]); + + const showLogButton = (props.showLogButton && isCheckStarted && haveLogs); + + return ( + <> + + {props.title} + + +

+ {showLogButton ? :<>} +

+
+ {verboseInfo.length > 0 && ( + + {verboseInfo.map(log => `${log.type.toUpperCase()}: ${log.message}`).join('\n')} + + )} + + ); +} diff --git a/public/components/health-check/components/inspect-logs-button.tsx b/public/components/health-check/components/inspect-logs-button.tsx new file mode 100644 index 0000000000..f05f47ce94 --- /dev/null +++ b/public/components/health-check/components/inspect-logs-button.tsx @@ -0,0 +1,48 @@ +/* + * Wazuh app - Inspect Logs Button Component + * + * Copyright (C) 2015-2021 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 from 'react'; +import { + EuiIcon, + EuiButtonEmpty, + EuiToolTip, +} from '@elastic/eui'; + +const InspectLogButton = ({ verboseIsOpen, switchVerboseDetails, haveLogs, shouldShowNotification }) => { + + const tooltipVerboseButton = `${verboseIsOpen ? 'Close' : 'Open'} details`; + const LogButton = + + + {shouldShowNotification && ( + + )} + + + + return LogButton; +} + +export default InspectLogButton; \ No newline at end of file diff --git a/public/components/health-check/components/result-icons.tsx b/public/components/health-check/components/result-icons.tsx new file mode 100644 index 0000000000..374a1bccca --- /dev/null +++ b/public/components/health-check/components/result-icons.tsx @@ -0,0 +1,57 @@ +/* + * Wazuh app - Result Icons Component + * + * Copyright (C) 2015-2021 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 from 'react'; +import { + EuiIcon, + EuiButtonIcon, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; +import { resultsPreset } from '../types/result-icons-presets'; + +const ResultIcons = ({ result, children, initCheck }) => { + + + return ( + <>{ + resultsPreset[result].disabled ? <>Disabled : <> + + {resultsPreset[result].spinner ? : + + } + + + {children} + {resultsPreset[result].retry && + + } + + } + + ); + +}; +export default ResultIcons; \ No newline at end of file diff --git a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap new file mode 100644 index 0000000000..af01b7ee3a --- /dev/null +++ b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Health Check container should render a Health check screen 1`] = ` +
+ +
+ + + + + + + + + + + + +
+ +
+`; diff --git a/public/components/health-check/container/health-check.container.test.tsx b/public/components/health-check/container/health-check.container.test.tsx new file mode 100644 index 0000000000..d55fb5e6b6 --- /dev/null +++ b/public/components/health-check/container/health-check.container.test.tsx @@ -0,0 +1,93 @@ +/* + * Wazuh app - Health Check Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount, shallow } from 'enzyme'; +import { HealthCheckTest } from './health-check.container'; + +jest.mock('../../../components/common/hooks', () => ({ + useAppConfig: () => ({ + isReady: true, + isLoading: false, + data: { + 'ip.selector': 1, + 'checks.metaFields': true, + 'checks.timerFilter': true, + 'checks.maxBuckets': true, + 'checks.api': true, + 'checks.setup': true, + 'checks.pattern': true, + 'checks.template': true, + 'checks.fields': true, + }, + }), + useRootScope: () => ({}) +})); + +jest.mock('../services', () => ({ + checkPatternService: (appInfo) => () => undefined, + checkTemplateService: (appInfo) => () => undefined, + checkApiService: (appInfo) => () => undefined, + checkSetupService: (appInfo) => () => undefined, + checkFieldsService: (appInfo) => () => undefined, + checkKibanaSettings: (appInfo) => () => undefined, + checkPatternSupportService: (appInfo) => () => undefined +})); + +jest.mock('../components/check-result', () => ({ + CheckResult: () => () => <>, +})); + +jest.mock('../../../react-services', () => ({ + AppState: { + setPatternSelector: () => {}, + }, + ErrorHandler: { + handle: (error) => error + } +})); + +jest.mock('../../../kibana-services', () => ({ + getHttp: () => ({ + basePath: { + prepend: (str) => str + } + }), + getDataPlugin: () => ({ + query: { + timefilter: { + timefilter: { + setTime: (time: number) => true + } + } + } + }) +})); + +describe('Health Check container', () => { + test('should render a Health check screen', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + test('should render a Health check screen with error', () => { + const component = mount(); + + component.find('CheckResult').at(1).invoke('handleErrors')('setup',['Test error']); // invoke is wrapped with act to await for setState + + const callOutError = component.find('EuiCallOut'); + expect(callOutError.text()).toBe('[API version] Test error'); + }); +}); diff --git a/public/components/health-check/container/health-check.container.tsx b/public/components/health-check/container/health-check.container.tsx new file mode 100644 index 0000000000..442e0f0ac7 --- /dev/null +++ b/public/components/health-check/container/health-check.container.tsx @@ -0,0 +1,258 @@ +/* + * Wazuh app - Health Check Component + * + * Copyright (C) 2015-2021 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 { + EuiButton, + EuiCallOut, + EuiDescriptionList, + EuiSpacer, +} from '@elastic/eui'; +import React, { Fragment, useState, useEffect, useRef } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; +import { AppState, ErrorHandler } from '../../../react-services'; +import { useAppConfig, useRootScope } from '../../../components/common/hooks'; +import { + checkApiService, + checkKibanaSettings, + checkIndexPatternService, + checkPatternSupportService, + checkSetupService, +} from '../services'; +import { CheckResult } from '../components/check-result'; +import { withReduxProvider } from '../../common/hocs'; +import { getHttp } from '../../../kibana-services'; +import { + HEALTH_CHECK_REDIRECTION_TIME, + KIBANA_SETTING_NAME_MAX_BUCKETS, + KIBANA_SETTING_NAME_METAFIELDS, + KIBANA_SETTING_NAME_TIME_FILTER, + WAZUH_INDEX_TYPE_MONITORING, + WAZUH_INDEX_TYPE_STATISTICS, + WAZUH_KIBANA_SETTING_MAX_BUCKETS, + WAZUH_KIBANA_SETTING_METAFIELDS, + WAZUH_KIBANA_SETTING_TIME_FILTER, +} from '../../../../common/constants'; + +import { getDataPlugin } from '../../../kibana-services'; +import { CheckLogger } from '../types/check_logger'; + +const checks = { + api: { + title: 'Check Wazuh API connection', + label: 'API connection', + validator: checkApiService, + awaitFor: [], + canRetry: true, + }, + setup: { + title: 'Check Wazuh API version', + label: 'API version', + validator: checkSetupService, + awaitFor: ["api"], + }, + pattern: { + title: 'Check alerts index pattern', + label: 'Alerts index pattern', + validator: checkIndexPatternService, + awaitFor: [], + shouldCheck: true, + canRetry: true, + }, + patternMonitoring: { + title: 'Check monitoring index pattern', + label: 'Monitoring index pattern', + validator: (appConfig) => checkPatternSupportService(appConfig.data['wazuh.monitoring.pattern'], WAZUH_INDEX_TYPE_MONITORING), + awaitFor: [], + shouldCheck: true, + canRetry: true, + }, + patternStatistics: { + title: 'Check statistics index pattern', + label: 'Statistics index pattern', + validator: (appConfig) => checkPatternSupportService(`${appConfig.data['cron.prefix']}-${appConfig.data['cron.statistics.index.name']}-*`, WAZUH_INDEX_TYPE_STATISTICS), + awaitFor: [], + shouldCheck: true, + canRetry: true, + }, + maxBuckets: { + title: `Check ${KIBANA_SETTING_NAME_MAX_BUCKETS} setting`, + label: `${KIBANA_SETTING_NAME_MAX_BUCKETS} setting`, + validator: checkKibanaSettings(KIBANA_SETTING_NAME_MAX_BUCKETS, WAZUH_KIBANA_SETTING_MAX_BUCKETS), + awaitFor: [], + canRetry: true, + }, + metaFields: { + title: `Check ${KIBANA_SETTING_NAME_METAFIELDS} setting`, + label: `${KIBANA_SETTING_NAME_METAFIELDS} setting`, + validator: checkKibanaSettings(KIBANA_SETTING_NAME_METAFIELDS, WAZUH_KIBANA_SETTING_METAFIELDS), + awaitFor: [], + canRetry: true, + }, + timeFilter: { + title: `Check ${KIBANA_SETTING_NAME_TIME_FILTER} setting`, + label: `${KIBANA_SETTING_NAME_TIME_FILTER} setting`, + validator: checkKibanaSettings(KIBANA_SETTING_NAME_TIME_FILTER, JSON.stringify(WAZUH_KIBANA_SETTING_TIME_FILTER), (checkLogger: CheckLogger, options: {defaultAppValue: any}) => { + getDataPlugin().query.timefilter.timefilter.setTime(WAZUH_KIBANA_SETTING_TIME_FILTER) + && checkLogger.action(`Timefilter set to ${JSON.stringify(options.defaultAppValue)}`); + }), + awaitFor: [], + canRetry: true, + } +}; + +function HealthCheckComponent() { + const [checkErrors, setCheckErrors] = useState<{[key:string]: []}>({}); + const [checksReady, setChecksReady] = useState<{[key: string]: boolean}>({}); + const [isDebugMode, setIsDebugMode] = useState(false); + const appConfig = useAppConfig(); + const checksInitiated = useRef(false); + const $rootScope = useRootScope(); + + const redirectionPassHealthcheck = () => { + const params = $rootScope.previousParams || {}; + const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&'); + const url = '/app/wazuh#' + ($rootScope.previousLocation || '') + '?' + queryString; + window.location.href = getHttp().basePath.prepend(url); + }; + + useEffect(() => { + if (appConfig.isReady && !checksInitiated.current) { + checksInitiated.current = true; + AppState.setPatternSelector(appConfig.data['ip.selector']); + } + }, [appConfig]); + + useEffect(() => { + // Redirect to app when all checks are ready + Object.keys(checks) + .every(check => checksReady[check]) + && !isDebugMode && (() => setTimeout(redirectionPassHealthcheck, HEALTH_CHECK_REDIRECTION_TIME) + )() + }, [checksReady]); + + useEffect(() => { + // Check if Health should not redirect automatically (Debug mode) + setIsDebugMode(window.location.href.includes('debug')); + },[]); + + const handleErrors = (checkID, errors, parsed) => { + const newErrors = parsed + ? errors.map((error) => + ErrorHandler.handle(error, 'Health Check', { warning: false, silent: true }) + ) + : errors; + setCheckErrors((prev) => ({...prev, [checkID]: newErrors})); + }; + + const cleanErrors = (checkID: string) => { + delete checkErrors[checkID]; + setCheckErrors({...checkErrors}); + } + + const handleCheckReady = (checkID, isReady) => { + setChecksReady(prev => ({...prev, [checkID]: isReady})); + } + + const logoUrl = getHttp().basePath.prepend('/plugins/wazuh/assets/icon_blue.svg'); + const thereAreErrors = Object.keys(checkErrors).length > 0; + + const renderChecks = () => { + const showLogButton = (thereAreErrors || isDebugMode); + return Object.keys(checks).map((check, index) => { + return ( + + ); + }); + }; + + const renderErrors = () => { + return Object.keys(checkErrors).map((checkID) => + checkErrors[checkID].map((error, index) => ( + + + + + )) + ) + }; + + return ( +
+ +
+ + {renderChecks()} + +
+ {thereAreErrors && ( + <> + + {renderErrors()} + + )} + {(thereAreErrors || isDebugMode) && ( + <> + + + {thereAreErrors && ( + + + Go to Settings + + + )} + {isDebugMode && Object.keys(checks).every(check => checksReady[check]) && ( + + + Continue + + + )} + + + ) + } + +
+ ); +} + +export const HealthCheck = withReduxProvider(HealthCheckComponent); + +export const HealthCheckTest = HealthCheckComponent; + + diff --git a/public/components/health-check/health-check.tsx b/public/components/health-check/health-check.tsx deleted file mode 100644 index 5eae460b61..0000000000 --- a/public/components/health-check/health-check.tsx +++ /dev/null @@ -1,506 +0,0 @@ -import React, { Component } from 'react'; -import { EuiLoadingSpinner, EuiDescriptionList, EuiIcon, EuiCallOut, EuiButtonIcon, EuiSpacer, EuiButton, EuiToolTip } from '@elastic/eui'; -import { AppState } from '../../react-services/app-state'; -import { PatternHandler } from '../../react-services/pattern-handler'; -import { getAngularModule, getToasts, getHttp, getDataPlugin } from '../../kibana-services'; -import { WazuhConfig } from '../../react-services/wazuh-config'; -import { GenericRequest } from '../../react-services/generic-request'; -import { ApiCheck } from '../../react-services/wz-api-check'; -import { WzRequest } from '../../react-services/wz-request'; -import { SavedObject } from '../../react-services/saved-objects'; -import { ErrorHandler } from '../../react-services/error-handler'; -import { WAZUH_ERROR_DAEMONS_NOT_READY, WAZUH_INDEX_TYPE_STATISTICS, WAZUH_INDEX_TYPE_MONITORING, HEALTH_CHECK } from '../../../common/constants'; -import { checkKibanaSettings, checkKibanaSettingsTimeFilter, checkKibanaSettingsMaxBuckets } from './lib'; -import store from '../../redux/store'; -import { updateWazuhNotReadyYet } from '../../redux/actions/appStateActions.js'; -import { withErrorBoundary } from '../common/hocs'; - -export const HealthCheck = withErrorBoundary (class HealthCheck extends Component { - checkPatternCount = 0; - constructor(props) { - super(props); - this.state = { - checks: [], - results: [], - errors: [] - }; - } - async componentDidMount() { - const app = getAngularModule(); - this.$rootScope = app.$injector.get('$rootScope'); - this.load(); - } - - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time - }); - }; - - /** - * Manage an error - */ - handleError(error) { - let errors = this.state.errors; - errors.push(ErrorHandler.handle(error, 'Health Check', { silent: true })); - this.setState({ errors }); - } - - /** - * Sleep method - * @param time - */ - delay = time => new Promise(res => setTimeout(res, time)); - - async checkDefaultPattern(defaultPattern) { - if (defaultPattern) { - const patternData = await SavedObject.existsIndexPattern(defaultPattern); - patternData.status && getDataPlugin().indexPatterns.setDefault(defaultPattern, true); - } - } - - /** - * This validates a pattern - */ - async checkPatterns() { - this.checkPatternCount++; - if (this.checkPatternCount > 10) return Promise.reject('Error trying to check patterns.'); - try { - let patternTitle = ''; - let results = this.state.results; - let errors = this.state.errors; - const resultIndex = this.state.results.map(item => item.id).indexOf(2); - const currentPattern = AppState.getCurrentPattern(); - - if (this.state.checks.pattern) { - //get patters or create default - const patternList = await PatternHandler.getPatternList(HEALTH_CHECK); - // check selected pattern - const wazuhConfig = new WazuhConfig(); - const { pattern: defaultPattern } = wazuhConfig.getConfig(); - await this.checkDefaultPattern(defaultPattern); - - //check selected pattern - let patternData = currentPattern ? await SavedObject.existsIndexPattern(currentPattern) : false; - if (!patternData) patternData = {}; - patternTitle = patternData.title; - - if (!patternData.status) { - if (patternList.length) { - const indexPatternDefault = patternList.find((indexPattern) => indexPattern.title === defaultPattern); - indexPatternDefault && AppState.setCurrentPattern(indexPatternDefault.id); - await this.delay(3000); - return await this.checkPatterns(); - } else { - errors.push('The selected index-pattern is not present.'); - results[resultIndex].description = Error; - } - } else { - results[resultIndex].description = Ready; - } - this.setState({ results, errors }); - } - - if (this.state.checks.template) { - if (!patternTitle) { - var patternData = await SavedObject.existsIndexPattern(currentPattern); - patternTitle = patternData.title; - } - const i = results.map(item => item.id).indexOf(3); - const templateData = await GenericRequest.request( - 'GET', - `/elastic/template/${patternTitle}` - ); - if (!templateData.data.status) { - errors.push('No template found for the selected index-pattern.'); - results[i].description = Error; - } else { - results[i].description = Ready; - } - this.setState({ results, errors }); - } - return; - } catch (error) { - this.handleError(error); - } - } - - async trySetDefault() { - try { - const response = await GenericRequest.request('GET', '/hosts/apis'); - const hosts = response.data; - const errors = []; - const results = this.state.results; - const maxTries = 5; - let apiId = ''; - - if (hosts.length) { - for (let i = 0; i < hosts.length; i++) { - for (let tries = 0; tries < maxTries; tries++) { - await this.delay(3000); - try { - const API = await ApiCheck.checkApi(hosts[i], true); - if (API && API.data) { - apiId = hosts[i].id; - tries = maxTries; - i = hosts.length - } - } catch (err) { - if (tries) { - results[0].description = Retrying {'.'.repeat(tries) }; - results[1].description = Retrying {'.'.repeat(tries) }; - this.setState({ results }); - } else { - if (err.includes(WAZUH_ERROR_DAEMONS_NOT_READY)) { - const updateNotReadyYet = updateWazuhNotReadyYet(false); - store.dispatch(updateNotReadyYet); - } - errors.push(`Could not connect to API with id: ${hosts[i].id}: ${err.message || err}`); - } - } - } - if (apiId) return apiId; - } - - const updateNotReadyYet = updateWazuhNotReadyYet(false); - store.dispatch(updateNotReadyYet); - - if (errors.length) { - let err = this.state.errors; - errors.forEach(error => err.push(error)); - this.setState({ errors: err }) - return Promise.reject('No API available to connect.'); - } - } - } catch (err) { - return Promise.reject(`Error connecting to API: ${err}`); - } - } - - /** - * This attempts to reconnect with API - */ - reconnectWithAPI() { - let results = this.state.results; - results[0].description = Checking...; - results[1].description = Checking...; - getToasts().toasts$._value.forEach(toast => { - if (toast.text.includes('3000')) - getToasts().remove(toast.id); - }); - - const errors = this.state.errors.filter((error: string) => error.indexOf('API') < 0) - this.setState({ results, errors }); - this.checkApiConnection(); - } - - /** - * This attempts to connect with API - */ - async checkApiConnection() { - let results = this.state.results; - let errors = this.state.errors; - let apiChanged = false; - const buttonRestartApi =
Error - { - this.reconnectWithAPI()} - size="m" - aria-label="Next" - /> - } -
; - - try { - const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); - if (this.state.checks.api && currentApi && currentApi.id) { - let data; - try { - data = await ApiCheck.checkStored(currentApi.id); - // Fix when the app endpoint replies with a valid response but the API is really down - if(data.data.data.apiIsDown){ - throw 'API is down' - }; - } catch (err) { - try { - const newApi = await this.trySetDefault(); - data = await ApiCheck.checkStored(newApi, true); - apiChanged = true; - } catch (err2) { - throw err2 - }; - } - if (apiChanged) { - this.showToast( - 'warning', - 'Selected Wazuh API has been updated', - '', - 3000 - ); - const api = ((data || {}).data || {}).data || {}; - const name = (api.cluster_info || {}).manager || false; - AppState.setCurrentAPI( - JSON.stringify({ name: name, id: api.id }) - ); - } - //update cluster info - const cluster_info = (((data || {}).data || {}).data || {}) - .cluster_info; - if (cluster_info) { - AppState.setClusterInfo(cluster_info); - } - const i = results.map(item => item.id).indexOf(0); - if (data === 3099) { - errors.push('Wazuh not ready yet.'); - results[i].description = Error;; - if (this.checks.setup) { - const i = results.map(item => item.id).indexOf(1); - results[i].description = Error; - } - this.setState({ results, errors }); - } else if (data.data.error || data.data.data.apiIsDown) { - errors.push(data.data.data.apiIsDown ? 'Wazuh API is down.' : `Error connecting to the API.${data.data.error && data.data.error.message ? ` ${data.data.error.message}` : ''}`); - results[i].description = buttonRestartApi; - results[i + 1].description = Error - this.setState({ results, errors }); - } else { - results[i].description = Ready; - this.setState({ results, errors }); - if (this.state.checks.setup) { - const versionData = await WzRequest.apiReq( - 'GET', - '//', - {} - ); - const apiVersion = versionData.data.data.api_version; - const setupData = await GenericRequest.request( - 'GET', - '/api/setup' - ); - if (!setupData.data.data['app-version']) { - errors.push('Error fetching app version'); - }; - if (!apiVersion) { - errors.push('Error fetching Wazuh API version'); - }; - const api = /v?(?\d+)\.(?\d+)\.(?\d+)/.exec(apiVersion); - const appSplit = setupData.data.data['app-version'].split('.'); - - const i = this.state.results.map(item => item.id).indexOf(1); - if (api.groups.version !== appSplit[0] || api.groups.minor !== appSplit[1]) { - errors.push( - 'API version mismatch. Expected v' + - setupData.data.data['app-version'] - ); - results[i].description = Error; - this.setState({ results, errors }); - } else { - results[i].description = Ready; - const updateNotReadyYet = updateWazuhNotReadyYet(false); - store.dispatch(updateNotReadyYet); - this.setState({ results, errors }); - } - } - } - } - return; - } catch (error) { - results[0].description = buttonRestartApi; - results[1].description = Error; - this.setState({ results }); - AppState.removeNavigation(); - if (error && error.data && error.data.code && error.data.code === 3002) { - return error; - } else { - this.handleError(error); - } - } - } - - /** - * This check if the pattern exist then create if not - * @param pattern string - */ - async checkSupportPattern(pattern, itemId, indexType) { - let results = this.state.results; - let errors = this.state.errors; - - const result = await SavedObject.existsIndexPattern(pattern); - if (!result.data) { - const toast = getToasts().addWarning(`${pattern} index pattern was not found and it will be created`) - const fields = await SavedObject.getIndicesFields(pattern, indexType); - try { - await SavedObject.createSavedObject( - 'index-pattern', - pattern, - { - attributes: { - title: pattern, - timeFieldName: 'timestamp' - } - }, - fields - ); - getToasts().remove(toast.id); - getToasts().addSuccess(`${pattern} index pattern created successfully`) - results[itemId].description = Ready; - this.setState({ results }); - } catch (error) { - getToasts().remove(toast.id); - errors.push(`Error trying to create ${pattern} index pattern: ${error.message}`); - results[itemId].description = Error; - this.setState({ results }); - } - } else { - results[itemId].description = Ready; - this.setState({ results }); - } - } - - /** - * On controller loads - */ - async load() { - try { - const wazuhConfig = new WazuhConfig(); - const configuration = wazuhConfig.getConfig(); - checkKibanaSettings(configuration['checks.metaFields']); - checkKibanaSettingsTimeFilter(configuration['checks.timeFilter']); - checkKibanaSettingsMaxBuckets(configuration['checks.maxBuckets']); - AppState.setPatternSelector(configuration['ip.selector']); - let checks = {}; - checks.pattern = configuration['checks.pattern']; - checks.template = configuration['checks.template']; - checks.api = configuration['checks.api']; - checks.setup = configuration['checks.setup']; - checks.fields = configuration['checks.fields']; - const results = [] - results.push( - { - id: 0, - title: 'Check Wazuh API connection', - description: checks.api ? Checking... : 'Disabled' - }, - { - id: 1, - title: 'Check for Wazuh API version', - description: checks.setup ? Checking... : 'Disabled' - }, - { - id: 2, - title: 'Check Elasticsearch index pattern', - description: checks.pattern ? Checking... : 'Disabled' - }, - { - id: 3, - title: 'Check Elasticsearch template', - description: checks.template ? Checking... : 'Disabled' - }, - { - id: 4, - title: 'Check index pattern fields', - description: checks.fields ? Checking... : 'Disabled' - }, - { - id: 5, - title: 'Check Monitoring index pattern', - description: Checking... - }, - { - id: 6, - title: 'Check Statistics index pattern', - description: Checking... - }, - ); - this.setState({ checks, results }, - async () => { - await Promise.all([ - this.checkPatterns(), - this.checkApiConnection(), - this.checkSupportPattern(configuration['wazuh.monitoring.pattern'], 5, WAZUH_INDEX_TYPE_MONITORING), - this.checkSupportPattern(`${configuration['cron.prefix']}-${configuration['cron.statistics.index.name']}-*`, 6, WAZUH_INDEX_TYPE_STATISTICS), - ]); - if (checks.fields) { - const i = results.map(item => item.id).indexOf(4); - try { - await PatternHandler.refreshIndexPattern(); - results[i].description = Ready; - this.setState({ results }); - } catch (error) { - results[i].description = Error; - this.setState({ results }, () => this.handleError(error)); - } - } - - if (!this.state.errors || !this.state.errors.length) { - setTimeout(() => { - const params = this.$rootScope.previousParams || {}; - var queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&'); - const url = 'wazuh#' + (this.$rootScope.previousLocation || '') + '?' + queryString; - window.location.assign( - getHttp().basePath.prepend(url) - ); - return; - }, 300); - } - return; - }); - } catch (error) { - this.handleError(error); - } - } - - goAppSettings() { - window.location.href = '/app/wazuh#/settings'; - } - - render() { - const logo_url = getHttp().basePath.prepend('/plugins/wazuh/assets/icon_blue.svg'); - return ( -
- {!this.state.errors && ( - - )} - -
- -
- - {(this.state.errors || []).map(error => ( - <> - - - - - ))} - - {!!this.state.errors.length && ( - this.goAppSettings()}> - Go to App - - )} -
- ); - } -}); diff --git a/public/components/health-check/index.ts b/public/components/health-check/index.ts new file mode 100644 index 0000000000..4391723e0a --- /dev/null +++ b/public/components/health-check/index.ts @@ -0,0 +1 @@ +export { HealthCheck } from './container/health-check.container'; diff --git a/public/components/health-check/lib/check-kibana-settings.ts b/public/components/health-check/lib/check-kibana-settings.ts deleted file mode 100644 index c54bb937f6..0000000000 --- a/public/components/health-check/lib/check-kibana-settings.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { AxiosResponse } from 'axios'; -import { GenericRequest } from '../../../react-services'; - -type userValue = {userValue: T} - -type kbnSettings = { - buildNum: userValue - metaFields?: userValue, -}; - -type responseKbnSettings = {settings: kbnSettings}; - -export function checkKibanaSettings (removeMetaFields: boolean) { - removeMetaFields && getKibanaSettings() - .then(checkMetafieldSetting) - .then(updateMetaFieldsSetting) - .catch(error => error !== 'Unable to update config' && console.log(error)); -} - -async function getKibanaSettings(): Promise { - const kibanaSettings:AxiosResponse = await GenericRequest.request('GET', '/api/kibana/settings'); - return kibanaSettings.data; -} - -async function checkMetafieldSetting({settings}: responseKbnSettings) { - const { metaFields } = settings; - return !!metaFields && !!metaFields.userValue.length; -} - -async function updateMetaFieldsSetting(isModified:boolean) { - return !isModified && await GenericRequest.request( - 'POST', - '/api/kibana/settings', - {"changes":{"metaFields":[]}} - ); -} \ No newline at end of file diff --git a/public/components/health-check/lib/check-max-buckets.ts b/public/components/health-check/lib/check-max-buckets.ts deleted file mode 100644 index 9c778fb019..0000000000 --- a/public/components/health-check/lib/check-max-buckets.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AxiosResponse } from 'axios'; -import { GenericRequest } from '../../../react-services'; -import { WAZUH_MAX_BUCKETS_DEFAULT } from '../../../../common/constants' -import { getDataPlugin } from '../../../kibana-services'; - -type userValue = { userValue: T } -type kbnSettings = { - buildNum: userValue - timeFilter?: userValue, - maxBuckets?: userValue -}; - - -type responseKbnSettings = { settings: kbnSettings }; - -export function checkKibanaSettingsMaxBuckets(changeMaxBucketsDefaults: boolean) { - checkMaxBuckets && getKibanaSettings() - .then(checkMaxBuckets) - .then(updateMaxBucketsSetting) - .catch(error => error !== 'Unable to update config' && console.log(error)); -} - -async function getKibanaSettings(): Promise { - const kibanaSettings: AxiosResponse = await GenericRequest.request('GET', '/api/kibana/settings'); - return kibanaSettings.data; -} - -async function checkMaxBuckets({ settings }: responseKbnSettings) { - if (!settings["timelion:max_buckets"]) { - return false; - } - - const maxBuckets = settings["timelion:max_buckets"].userValue; - return WAZUH_MAX_BUCKETS_DEFAULT === maxBuckets; -} - -async function updateMaxBucketsSetting(isModified: boolean) { - return !isModified && await GenericRequest.request( - 'POST', - '/api/kibana/settings', - { "changes": { "timelion:max_buckets": WAZUH_MAX_BUCKETS_DEFAULT } } - ) -} diff --git a/public/components/health-check/lib/check-time-filter.ts b/public/components/health-check/lib/check-time-filter.ts deleted file mode 100644 index 03b5fd08a6..0000000000 --- a/public/components/health-check/lib/check-time-filter.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AxiosResponse } from 'axios'; -import { GenericRequest } from '../../../react-services'; -import { WAZUH_TIME_FILTER_DEFAULT } from '../../../../common/constants' -import { getDataPlugin } from '../../../kibana-services'; - -type userValue = { userValue: T } -type kbnSettings = { - buildNum: userValue - timeFilter?: userValue, -}; - - -type responseKbnSettings = { settings: kbnSettings }; - -export function checkKibanaSettingsTimeFilter(changeTimeDefaults: boolean) { - changeTimeDefaults && getKibanaSettings() - .then(checkTimeFilter) - .then(updateTimeFilterSetting) - .catch(error => error !== 'Unable to update config' && console.log(error)); -} - -async function getKibanaSettings(): Promise { - const kibanaSettings: AxiosResponse = await GenericRequest.request('GET', '/api/kibana/settings'); - return kibanaSettings.data; -} - -async function checkTimeFilter({ settings }: responseKbnSettings) { - if (!settings["timepicker:timeDefaults"]) { - return false; - } - - const timeFilter = settings["timepicker:timeDefaults"].userValue; - const timeFilterObject = JSON.parse(timeFilter); - return WAZUH_TIME_FILTER_DEFAULT.from == timeFilterObject.from && WAZUH_TIME_FILTER_DEFAULT.to == timeFilterObject.to; -} - -async function updateTimeFilterSetting(isModified: boolean) { - return !isModified && await GenericRequest.request( - 'POST', - '/api/kibana/settings', - { "changes": { "timepicker:timeDefaults": JSON.stringify(WAZUH_TIME_FILTER_DEFAULT) } } - ) && getDataPlugin().query.timefilter.timefilter.setTime(WAZUH_TIME_FILTER_DEFAULT); -} \ No newline at end of file diff --git a/public/components/health-check/lib/index.ts b/public/components/health-check/lib/index.ts deleted file mode 100644 index 77a9fa5d6f..0000000000 --- a/public/components/health-check/lib/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { checkKibanaSettings } from './check-kibana-settings'; -export { checkKibanaSettingsTimeFilter } from './check-time-filter'; -export { checkKibanaSettingsMaxBuckets } from './check-max-buckets'; diff --git a/public/components/health-check/services/check-api.service.ts b/public/components/health-check/services/check-api.service.ts index 4a0373d563..e3ec28f120 100644 --- a/public/components/health-check/services/check-api.service.ts +++ b/public/components/health-check/services/check-api.service.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - Check Api Service + * Wazuh app - Check APIs service * * Copyright (C) 2015-2021 Wazuh, Inc. * @@ -13,88 +13,88 @@ */ import { getToasts } from '../../../kibana-services'; -import AppState from '../../../react-services/app-state'; -import GenericRequest from '../../../react-services/generic-request'; -import ApiCheck from '../../../react-services/wz-api-check'; +import { ApiCheck, AppState, GenericRequest } from '../../../react-services'; +import { CheckLogger } from '../types/check_logger'; -const trySetDefault = async () => { +const trySetDefault = async (checkLogger: CheckLogger) => { try { + checkLogger.info(`Getting API hosts...`); const response = await GenericRequest.request('GET', '/hosts/apis'); + checkLogger.info(`API hosts found: ${response.data.length}`); const hosts = response.data; const errors = []; if (hosts.length) { for (var i = 0; i < hosts.length; i++) { try { + checkLogger.info(`Checking API host id [${hosts[i].id}]...`); const API = await ApiCheck.checkApi(hosts[i], true); if (API && API.data) { return hosts[i].id; } } catch (err) { - return { - error: `Could not connect to API with id: ${hosts[i].id}: ${err.message || err}`, - }; + checkLogger.info(`Could not connect to API id [${hosts[i].id}]: ${err.message || err}`); + errors.push(`Could not connect to API id [${hosts[i].id}]: ${err.message || err}`); } } if (errors.length) { - return Promise.reject('No API available to connect.'); + return Promise.reject('No API available to connect'); } } - } catch (err) { - return Promise.reject(`Error connecting to API: ${err}`); + } catch (error) { + checkLogger.error(`Error connecting to API: ${error}`); + return Promise.reject(`Error connecting to API: ${error}`); } }; -export const checkApiService = async (): Promise<{ errors: string[] }> => { - let errors: string[] = []; +export const checkApiService = (appInfo: any) => async (checkLogger: CheckLogger) => { let apiChanged = false; try { const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); - + checkLogger.info(`Current API id [${currentApi.id}]`); + checkLogger.info(`Checking current API id [${currentApi.id}]...`); const data = await ApiCheck.checkStored(currentApi.id).catch(async (err) => { - const newApi = await trySetDefault(); + checkLogger.info(`Current API id [${currentApi.id}] has some problem: ${err.message || err}`); + const newApi = await trySetDefault(checkLogger); if (newApi.error) { return { error: newApi.error }; } apiChanged = true; + checkLogger.info(`API host id [${newApi}] available`); return await ApiCheck.checkStored(newApi, true); }); if (apiChanged) { + const api = ((data || {}).data || {}).data || {}; + const name = (api.cluster_info || {}).manager || false; + AppState.setCurrentAPI(JSON.stringify({ name: name, id: api.id })); + checkLogger.info(`Set current API in cookie: id [${api.id}], name [${api.name}]`); getToasts().add({ color: 'warning', title: 'Selected Wazuh API has been updated', text: '', toastLifeTimeMs: 3000, }); - const api = ((data || {}).data || {}).data || {}; - const name = (api.cluster_info || {}).manager || false; - AppState.setCurrentAPI(JSON.stringify({ name: name, id: api.id })); } //update cluster info const cluster_info = (((data || {}).data || {}).data || {}).cluster_info; if (cluster_info) { AppState.setClusterInfo(cluster_info); + checkLogger.info(`Set cluster info in cookie`); } - if (data === 3099) { - errors.push('Wazuh not ready yet.'); + checkLogger.error('Wazuh not ready yet'); } else if (data.data.error || data.data.data.apiIsDown) { - errors.push( - data.data.data.apiIsDown - ? 'Wazuh API is down.' - : `Error connecting to the API.${ - data.data.error && data.data.error.message ? ` ${data.data.error.message}` : '' - }` - ); + const errorMessage = data.data.data.apiIsDown + ? 'Wazuh API is down' + : `Error connecting to the API: ${ + data.data.error && data.data.error.message ? ` ${data.data.error.message}` : '' + }`; + checkLogger.error(errorMessage); } - return { errors }; } catch (error) { AppState.removeNavigation(); - if (error && error.data && error.data.code && error.data.code === 3002) { - return { errors }; - } else { - throw error; - } + checkLogger.info('Removed [navigate] cookie'); + throw error; } }; diff --git a/public/components/health-check/services/check-index-pattern/check-fields.service.ts b/public/components/health-check/services/check-index-pattern/check-fields.service.ts new file mode 100644 index 0000000000..c7193e1634 --- /dev/null +++ b/public/components/health-check/services/check-index-pattern/check-fields.service.ts @@ -0,0 +1,30 @@ +/* + * Wazuh app - Check index pattern fields service + * + * Copyright (C) 2015-2021 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 { AppState, SavedObject } from '../../../../react-services'; +import { getDataPlugin } from '../../../../kibana-services'; +import { CheckLogger } from '../../types/check_logger'; + +export const checkFieldsService = async (appConfig, checkLogger: CheckLogger) => { + const patternId = AppState.getCurrentPattern(); + checkLogger.info(`Index pattern id in cookie: [${patternId}]`); + + checkLogger.info(`Getting index pattern data [${patternId}]...`); + const pattern = await getDataPlugin().indexPatterns.get(patternId); + checkLogger.info(`Index pattern data found: [${pattern ? 'yes' : 'no'}]`); + + checkLogger.info(`Refreshing index pattern fields: title [${pattern.title}], id [${pattern.id}]...`); + await SavedObject.refreshIndexPattern(pattern, null); + checkLogger.action(`Refreshed index pattern fields: title [${pattern.title}], id [${pattern.id}]`); +}; diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts new file mode 100644 index 0000000000..df0bedfc2a --- /dev/null +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts @@ -0,0 +1,103 @@ +/* + * Wazuh app - Check alerts index pattern service + * + * Copyright (C) 2015-2021 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 { AppState, SavedObject } from '../../../../react-services'; +import { getDataPlugin } from '../../../../kibana-services'; +import { HEALTH_CHECK } from '../../../../../common/constants'; +import { CheckLogger } from '../../types/check_logger'; + +export const checkIndexPatternObjectService = async (appConfig, checkLogger: CheckLogger) => { + const patternId: string = AppState.getCurrentPattern(); + const defaultPatternId: string = appConfig.data['pattern']; + const shouldCreateIndex: boolean = appConfig.data['checks.pattern']; + checkLogger.info(`Index pattern id in cookie: ${patternId ? `yes [${patternId}]` : 'no'}`); + + const defaultIndexPatterns: string[] = [ + defaultPatternId, + ...(patternId && patternId !== defaultPatternId ? [patternId] : []) + ]; + checkLogger.info(`Getting list of valid index patterns...`); + let listValidIndexPatterns = await SavedObject.getListOfWazuhValidIndexPatterns(defaultIndexPatterns, HEALTH_CHECK); + checkLogger.info(`Valid index patterns found: ${listValidIndexPatterns.length || 0}`); + + const indexPatternDefaultFound = listValidIndexPatterns.find((indexPattern) => indexPattern.title === defaultPatternId); + checkLogger.info(`Found default index pattern with title [${defaultPatternId}]: ${indexPatternDefaultFound ? 'yes' : 'no'}`); + + if (!indexPatternDefaultFound && defaultPatternId) { + // if no valid index patterns are found we try to create the wazuh-alerts-* + try { + checkLogger.info(`Checking if index pattern [${defaultPatternId}] exists...`); + const existDefaultIndexPattern = await SavedObject.getExistingIndexPattern(defaultPatternId); + checkLogger.info(`Index pattern id [${defaultPatternId}] exists: ${existDefaultIndexPattern ? 'yes' : 'no'}`); + if (existDefaultIndexPattern) { + checkLogger.info(`Refreshing index pattern fields [${defaultPatternId}]...`); + await SavedObject.refreshIndexPattern(defaultPatternId); + checkLogger.action(`Refreshed index pattern fields [${defaultPatternId}]`); + } else if(shouldCreateIndex) { + checkLogger.info(`Creating index pattern [${defaultPatternId}]...`); + await SavedObject.createWazuhIndexPattern(defaultPatternId); + checkLogger.action(`Created index pattern [${defaultPatternId}]`); + }else{ + // show error + checkLogger.error(`Default index pattern not found`); + } + checkLogger.info(`Getting list of valid index patterns [${patternId}]...`); + listValidIndexPatterns = await SavedObject.getListOfWazuhValidIndexPatterns(defaultIndexPatterns, HEALTH_CHECK); + checkLogger.info(`Valid index patterns found: ${listValidIndexPatterns.length || 0}`); + if(!AppState.getCurrentPattern()){ + AppState.setCurrentPattern(defaultPatternId); + checkLogger.info(`Index pattern set in cookie: [${defaultPatternId}]`); + } + } catch (error) { + AppState.removeCurrentPattern(); + checkLogger.info(`Removed current pattern from cookie: [${patternId}]`); + + throw error; + } + } + + if (AppState.getCurrentPattern() && listValidIndexPatterns.length) { + const indexPatternToSelect = listValidIndexPatterns.find(item => item.id === AppState.getCurrentPattern()); + if (!indexPatternToSelect){ + AppState.setCurrentPattern(indexPatternToSelect.id); + checkLogger.action(`Set index pattern id in cookie: [${indexPatternToSelect.id}]`); + } + } + + checkLogger.info(`Checking the app default pattern exists: id [${defaultPatternId}]...`); + const existsDefaultPattern = await SavedObject.existsIndexPattern(defaultPatternId); + checkLogger.info(`Default pattern with id [${defaultPatternId}] exists: ${existsDefaultPattern.status ? 'yes' : 'no'}`); + + existsDefaultPattern.status + && getDataPlugin().indexPatterns.setDefault(defaultPatternId, true) + && checkLogger.action(`Default pattern id [${defaultPatternId}] set as default index pattern`); + + patternId && checkLogger.info(`Checking the index pattern id [${patternId}] exists...`); + const patternData = patternId ? (await SavedObject.existsIndexPattern(patternId)) || {} : {} ; + patternId && checkLogger.info(`Index pattern id exists [${patternId}]: ${patternData.status ? 'yes': 'no'}`); + + if (!patternData.status) { + if (listValidIndexPatterns.length) { + const indexPatternDefaultFound = listValidIndexPatterns.find((indexPattern) => indexPattern.title === defaultPatternId); + checkLogger.info(`Index pattern id exists [${defaultPatternId}]: ${indexPatternDefaultFound ? 'yes': 'no'}`); + if(indexPatternDefaultFound){ + AppState.setCurrentPattern(indexPatternDefaultFound.id); + checkLogger.action(`Index pattern set in cookie: [${indexPatternDefaultFound.id}]`); + } + checkLogger.info('Retrying the check...'); + return await checkIndexPatternObjectService(appConfig, checkLogger); + } else { + checkLogger.error('The selected index-pattern is not present'); + } + } +}; diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts new file mode 100644 index 0000000000..64f0bd3040 --- /dev/null +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts @@ -0,0 +1,31 @@ +/* + * Wazuh app - Check alerts index pattern service + * + * Copyright (C) 2015-2021 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 { CheckLogger } from '../../types/check_logger'; +import { checkFieldsService } from './check-fields.service'; +import { checkIndexPatternObjectService } from './check-index-pattern-object.service'; +import { checkTemplateService } from './check-template.service'; + +export const checkIndexPatternService = (appConfig) => async (checkLogger: CheckLogger) => await checkPattern(appConfig, checkLogger); + +const checkPattern = async (appConfig, checkLogger: CheckLogger) => { + if(appConfig.data['check.pattern'] === 'false'){ + checkLogger.info('Index pattern check Disabled'); + await checkIndexPatternObjectService(appConfig, checkLogger); + }else{ + await checkIndexPatternObjectService(appConfig, checkLogger); + await checkTemplateService(appConfig, checkLogger); + await checkFieldsService(appConfig, checkLogger); + } + return; +}; diff --git a/public/components/health-check/services/check-index-pattern/check-template.service.ts b/public/components/health-check/services/check-index-pattern/check-template.service.ts new file mode 100644 index 0000000000..7489c8cb4e --- /dev/null +++ b/public/components/health-check/services/check-index-pattern/check-template.service.ts @@ -0,0 +1,36 @@ +/* + * Wazuh app - Check template for alerts index pattern service + * + * Copyright (C) 2015-2021 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 { AppState, GenericRequest, SavedObject } from '../../../../react-services'; +import { CheckLogger } from '../../types/check_logger'; + +export const checkTemplateService = async (appConfig, checkLogger: CheckLogger) => { + const patternId = AppState.getCurrentPattern(); + checkLogger.info(`Index pattern id in cookie: ${patternId ? `yes [${patternId}]` : 'no'}`); + + checkLogger.info(`Checking if the index pattern id [${patternId}] exists...`); + const patternData = patternId ? (await SavedObject.existsIndexPattern(patternId)) : null; + checkLogger.info(`Index pattern id [${patternId}] found: ${patternData.title ? `yes title [${patternData.title}]`: 'no'}`); + + if (patternData.title){ + checkLogger.info(`Checking if exists a template compatible with the index pattern title [${patternData.title}]`); + const templateData = await GenericRequest.request('GET', `/elastic/template/${patternData.title}`); + checkLogger.info(`Template found for the selected index-pattern title [${patternData.title}]: ${templateData.data.status ? 'yes': 'no'}`); + if (!templateData.data.status) { + checkLogger.error(`No template found for the selected index-pattern title [${patternData.title}]`); + }; + }else{ + checkLogger.error(`Index pattern id [${patternId}] found: no`); + }; +}; diff --git a/public/components/health-check/services/check-kibana-settings.service.ts b/public/components/health-check/services/check-kibana-settings.service.ts new file mode 100644 index 0000000000..136cf111a4 --- /dev/null +++ b/public/components/health-check/services/check-kibana-settings.service.ts @@ -0,0 +1,54 @@ +/* + * Wazuh app - Check Kibana settings service + * + * Copyright (C) 2015-2021 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 { AxiosResponse } from 'axios'; +import { GenericRequest } from '../../../react-services'; +import { CheckLogger } from '../types/check_logger'; +import _ from 'lodash'; + +type userValue = { userValue: T }; + +type kbnSettings = { + buildNum: userValue; + maxBuckets?: userValue; + metaFields?: userValue; + timeFilter?: userValue; +}; + +type responseKbnSettings = { settings: kbnSettings }; + +export const checkKibanaSettings = (kibanaSettingName: string, defaultAppValue: any, callback?: (checkLogger: CheckLogger, options: {defaultAppValue: any}) => void) => (appConfig: any) => async (checkLogger: CheckLogger) => { + checkLogger.info('Getting settings...'); + const kibanaSettingsResponse: AxiosResponse = await GenericRequest.request('GET', '/api/kibana/settings'); + checkLogger.info('Got Kibana settings'); + const valueKibanaSetting = kibanaSettingsResponse.data?.settings?.[kibanaSettingName]?.userValue; + const settingsAreDifferent = !_.isEqual(valueKibanaSetting, defaultAppValue); + checkLogger.info(`Check Kibana setting [${kibanaSettingName}]: ${stringifySetting(valueKibanaSetting)}`); + checkLogger.info(`App setting [${kibanaSettingName}]: ${stringifySetting(defaultAppValue)}`); + checkLogger.info(`Settings mismatch [${kibanaSettingName}]: ${settingsAreDifferent ? 'yes' : 'no'}`); + if ( !valueKibanaSetting && settingsAreDifferent ){ + checkLogger.info(`Updating [${kibanaSettingName}] setting...`); + await GenericRequest.request('POST', '/api/kibana/settings', { changes: { [kibanaSettingName]: defaultAppValue } }); + checkLogger.action(`Updated [${kibanaSettingName}] setting to: ${stringifySetting(defaultAppValue)}`); + callback && callback(checkLogger,{ defaultAppValue }); + } +} + +function stringifySetting(setting: any){ + try{ + return JSON.stringify(setting); + }catch(error){ + return setting; + }; +}; \ No newline at end of file diff --git a/public/components/health-check/services/check-pattern-support.service.ts b/public/components/health-check/services/check-pattern-support.service.ts new file mode 100644 index 0000000000..60ead0b0b6 --- /dev/null +++ b/public/components/health-check/services/check-pattern-support.service.ts @@ -0,0 +1,49 @@ +/* + * Wazuh app - Check index pattern support service + * + * Copyright (C) 2015-2021 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 { SavedObject } from '../../../react-services'; +import { CheckLogger } from '../types/check_logger'; + +export const checkPatternSupportService = (pattern: string, indexType : string) => async (checkLogger: CheckLogger) => { + checkLogger.info(`Checking index pattern id [${pattern}] exists...`); + const result = await SavedObject.existsIndexPattern(pattern); + checkLogger.info(`Exist index pattern id [${pattern}]: ${result.data ? 'yes' : 'no'}`); + + if (!result.data) { + checkLogger.info(`Getting indices fields for the index pattern id [${pattern}]...`); + const fields = await SavedObject.getIndicesFields(pattern, indexType); + checkLogger.info(`Fields for index pattern id [${pattern}] found: ${fields.length}`); + + try { + checkLogger.info(`Creating saved object for the index pattern with id [${pattern}]. + title: ${pattern} + id: ${pattern} + timeFieldName: timestamp + fields: ${fields.length}`); + await SavedObject.createSavedObject( + 'index-pattern', + pattern, + { + attributes: { + title: pattern, + timeFieldName: 'timestamp' + } + }, + fields + ); + checkLogger.action(`Created the saved object for the index pattern id [${pattern}]`); + } catch (error) { + checkLogger.error(`Error creating index pattern id [${pattern}]: ${error.message || error}`); + } + }; +} diff --git a/public/components/health-check/services/check-setup.service.ts b/public/components/health-check/services/check-setup.service.ts new file mode 100644 index 0000000000..30c85dcfc8 --- /dev/null +++ b/public/components/health-check/services/check-setup.service.ts @@ -0,0 +1,50 @@ +/* + * Wazuh app - Check setup service + * + * Copyright (C) 2015-2021 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 { AppState, GenericRequest, WzRequest } from '../../../react-services'; +import { CheckLogger } from '../types/check_logger'; + +export const checkSetupService = appInfo => async (checkLogger: CheckLogger) => { + const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); + if (currentApi && currentApi.id) { + checkLogger.info(`Current API in cookie: [${currentApi.id}]`); + checkLogger.info(`Getting API version data...`); + const versionData = await WzRequest.apiReq('GET', '//', {}); + const apiVersion = versionData.data.data['api_version']; + checkLogger.info(`API version: [${apiVersion}]`); + checkLogger.info(`Getting the app version...`); + const setupData = await GenericRequest.request('GET', '/api/setup'); + if (!setupData.data.data['app-version']) { + checkLogger.info('Error fetching app version'); + }else{ + checkLogger.info(`App version: [${setupData.data.data['app-version']}]`); + }; + + if (!apiVersion) { + checkLogger.info('Error fetching Wazuh API version'); + } else { + const api = /v?(?\d+)\.(?\d+)\.(?\d+)/.exec(apiVersion); + const appSplit = setupData.data.data['app-version'].split('.'); + if ( + !api || + !api.groups || + api.groups.version !== appSplit[0] || + api.groups.minor !== appSplit[1] + ) { + checkLogger.error(`API and APP version mismatch. API version: ${apiVersion}. App version: ${setupData.data.data['app-version']}. At least, major and minor should match`); + } + } + } + +}; diff --git a/public/components/health-check/services/index.ts b/public/components/health-check/services/index.ts new file mode 100644 index 0000000000..a61b274eb6 --- /dev/null +++ b/public/components/health-check/services/index.ts @@ -0,0 +1,19 @@ +/* + * Wazuh app - Health check services + * + * Copyright (C) 2015-2021 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 * from './check-api.service'; +export * from './check-kibana-settings.service'; +export * from './check-index-pattern/check-index-pattern.service'; +export * from './check-pattern-support.service'; +export * from './check-setup.service'; diff --git a/public/components/health-check/types/check_logger.ts b/public/components/health-check/types/check_logger.ts new file mode 100644 index 0000000000..75220289b4 --- /dev/null +++ b/public/components/health-check/types/check_logger.ts @@ -0,0 +1,7 @@ +export type CheckLog = (message: string) => void; + +export interface CheckLogger { + info: CheckLog + error: CheckLog + action: CheckLog +}; diff --git a/public/components/health-check/types/result-icons-presets.ts b/public/components/health-check/types/result-icons-presets.ts new file mode 100644 index 0000000000..09d457bd62 --- /dev/null +++ b/public/components/health-check/types/result-icons-presets.ts @@ -0,0 +1,49 @@ +export type ResultIconProps = { + tooltipText?: string + iconColor?: string + iconType?: string + disabled?: boolean + spinner?: boolean + retry?: boolean +}; + +export type ResultIconsPreset = { + disabled: ResultIconProps, + loading: ResultIconProps, + ready: ResultIconProps, + error: ResultIconProps, + error_retry: ResultIconProps, + waiting: ResultIconProps +}; + +export const resultsPreset: ResultIconsPreset = { + disabled: { + disabled: true + }, + loading: { + tooltipText: 'Checking...', + spinner: true, + iconType: '' + }, + ready: { + tooltipText: 'Ready', + iconColor: 'secondary', + iconType: 'check' + }, + error: { + tooltipText: 'Error', + iconColor: 'danger', + iconType: 'alert' + }, + error_retry: { + tooltipText: 'Error', + iconColor: 'danger', + iconType: 'alert', + retry: true + }, + waiting: { + tooltipText: 'On hold...', + iconColor: '#999999', + iconType: 'clock' + } +} \ No newline at end of file diff --git a/public/components/overview/compliance-table/components/requirements/requirements.tsx b/public/components/overview/compliance-table/components/requirements/requirements.tsx index 71a8a5a774..77cf4be801 100644 --- a/public/components/overview/compliance-table/components/requirements/requirements.tsx +++ b/public/components/overview/compliance-table/components/requirements/requirements.tsx @@ -182,7 +182,6 @@ export class ComplianceRequirements extends Component { button={( this.onGearButtonClick()}>)} isOpen={this.state.isPopoverOpen} panelPaddingSize="none" - withTitle closePopover={() => this.closePopover()}> diff --git a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index 13a2673695..0a5d5b6a4c 100644 --- a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -195,7 +195,6 @@ export class ComplianceSubrequirements extends Component { closePopover={() => { }} panelPaddingSize="none" style={{ width: "100%" }} - withTitle anchorPosition="downLeft">xxx
diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index 709a554c8c..939bc5da9b 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -24,6 +24,7 @@ import { getElasticAlerts, getIndexPattern } from '../mitre/lib'; import { ModulesHelper } from '../../common/modules/modules-helper' import { getDataPlugin } from '../../../kibana-services'; import { withAllowedAgents } from '../../common/hocs/withAllowedAgents'; +import { formatUIDate } from '../../../react-services'; export const Metrics = withAllowedAgents(class Metrics extends Component { _isMount = false; @@ -91,7 +92,7 @@ export const Metrics = withAllowedAgents(class Metrics extends Component { { name: "Last scan not checked", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.notchecked" } } } } }, color: "subdued"}, { name: "Last scan pass", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.pass" } } } } }, color: "secondary"}, { name: "Last scan score", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.score" } } } } }}, - { name: "Last scan date", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.timestamp" } } } } }, color: "secondary"}, + { name: "Last scan date", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.timestamp" } } } } }, color: "secondary", transformValue:formatUIDate}, { name: "Last scan errors", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.error" } } } } }, color: "danger"}, { name: "Last scan fails", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.fail" } } } } }, color: "danger"}, { name: "Last scan unknown", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.unknown" } } } } }, color: "subdued"}, @@ -288,7 +289,7 @@ export const Metrics = withAllowedAgents(class Metrics extends Component { 20 ? 3 : 1} key={`${item.name}`}> 20 ? "2rem" : "2.25rem" }}>{this.state.results[item.name]}} + 20 ? "2rem" : "2.25rem" }}>{item.transformValue ? item.transformValue(this.state.results[item.name]) : this.state.results[item.name]}} description={item.name} titleColor={this.metricsList[section][idx].color || 'primary'} isLoading={this.state.loading} diff --git a/public/components/overview/mitre/components/tactics/tactics.tsx b/public/components/overview/mitre/components/tactics/tactics.tsx index c7931f86b8..4a9df58c9e 100644 --- a/public/components/overview/mitre/components/tactics/tactics.tsx +++ b/public/components/overview/mitre/components/tactics/tactics.tsx @@ -290,7 +290,6 @@ export class Tactics extends Component { button={( this.onGearButtonClick()} aria-label={'tactics options'}>)} isOpen={this.state.isPopoverOpen} panelPaddingSize="none" - withTitle closePopover={() => this.closePopover()}> diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 3a4f1a3cc3..8d32c76646 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -303,7 +303,6 @@ export const Techniques = withWindowSize(class Techniques extends Component { closePopover={() => this.closeActionsMenu()} panelPaddingSize="none" style={{width: "100%"}} - withTitle anchorPosition="downLeft"> diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx new file mode 100644 index 0000000000..706ab88de3 --- /dev/null +++ b/public/components/security/policies/create-policy.tsx @@ -0,0 +1,431 @@ +import React, { useState, useEffect } from 'react'; +import { + EuiButton, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiInMemoryTable, + EuiConfirmModal, + EuiOverlayMask, + EuiFieldText, + EuiText, +} from '@elastic/eui'; +import { WzRequest } from '../../../react-services/wz-request'; +import { ErrorHandler } from '../../../react-services/error-handler'; +import { WzOverlayMask } from '../../common/util'; + +export const CreatePolicyFlyout = ({ closeFlyout }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [resources, setResources] = useState([]); + const [resourceValue, setResourceValue] = useState(''); + const [resourceIdentifierValue, setResourceIdentifierValue] = useState(''); + const [availableResources, setAvailableResources] = useState([]); + const [addedActions, setAddedActions] = useState([]); + const [availableActions, setAvailableActions] = useState([]); + const [addedResources, setAddedResources] = useState([]); + const [actions, setActions] = useState([]); + const [actionValue, setActionValue] = useState(''); + const [policyName, setPolicyName] = useState(''); + const [policies, setPolicies] = useState(''); + const [loading, setLoading] = useState(false); + const [effectValue, setEffectValue] = useState(); + const [hasChanges, setHasChanges] = useState(false); + + const actions_columns = [ + { + field: 'action', + name: 'Actions', + sortable: true, + truncateText: true, + }, + { + name: '', + actions: [ + { + name: 'Remove', + description: 'Remove this action', + type: 'icon', + color: 'danger', + icon: 'trash', + onClick: (action) => removeAction(action), + }, + ], + }, + ]; + + const resources_columns = [ + { + field: 'resource', + name: 'Resources', + sortable: true, + truncateText: true, + }, + { + name: '', + actions: [ + { + name: 'Remove', + description: 'Remove this resource', + type: 'icon', + color: 'danger', + icon: 'trash', + onClick: (resource) => removeResource(resource), + }, + ], + }, + ]; + + const effectOptions = [ + { + value: 'allow', + inputDisplay: 'Allow', + }, + { + value: 'deny', + inputDisplay: 'Deny', + }, + ]; + + async function getData() { + const resources_request = await WzRequest.apiReq('GET', '/security/resources', {}); + const actions_request = await WzRequest.apiReq('GET', '/security/actions', {}); + const resources_data = ((resources_request || {}).data || []).data || {}; + setAvailableResources(resources_data); + + const actions_data = ((actions_request || {}).data || []).data || {}; + setAvailableActions(actions_data); + const actions = Object.keys(actions_data).map((x, idx) => { + return { + id: idx, + value: x, + inputDisplay: x, + dropdownDisplay: ( + <> + {x} + +

{actions_data[x].description}

+
+ + ), + }; + }); + setActions(actions); + } + + const loadResources = () => { + let allResources = []; + addedActions.forEach((x) => { + const res = (availableActions[x.action] || {})['resources']; + allResources = allResources.concat(res); + }); + const allResourcesSet = new Set(allResources); + const resources = Array.from(allResourcesSet).map((x, idx) => { + return { + id: idx, + value: x, + inputDisplay: x, + dropdownDisplay: ( + <> + {x} + +

{availableResources[x].description}

+
+ + ), + }; + }); + setResources(resources); + }; + + const removeAction = (action) => { + setAddedActions(addedActions.filter((x) => x !== action)); + }; + + const getPolicies = async () => { + setLoading(true); + const request = await WzRequest.apiReq('GET', '/security/policies', {}); + const policies = (((request || {}).data || {}).data || {}).affected_items || []; + setPolicies(policies); + setLoading(false); + }; + + const createPolicy = async () => { + try { + const result = await WzRequest.apiReq( + 'POST', + '/security/policies', + + { + name: policyName, + policy: { + actions: addedActions.map((x) => x.action), + resources: addedResources.map((x) => x.resource), + effect: effectValue, + }, + } + ); + const resultData = (result.data || {}).data; + if (resultData.failed_items && resultData.failed_items.length) { + return; + } + ErrorHandler.info('Policy was successfully created', ''); + await getPolicies(); + setPolicyName(''); + setAddedActions([]); + setAddedResources([]); + setEffectValue(null); + } catch (error) { + ErrorHandler.handle(error, 'Error creating policy'); + return; + } + closeFlyout(); + }; + + const getIdentifier = () => { + const keys = Object.keys(availableResources) || []; + return (keys[resourceValue] || ':').split(':')[1]; + }; + + const addResource = () => { + if ( + !addedResources.filter((x) => x.resource === `${resourceValue}:${resourceIdentifierValue}`) + .length + ) { + setAddedResources((addedResources) => [ + ...addedResources, + { resource: `${resourceValue}:${resourceIdentifierValue}` }, + ]); + } + setResourceIdentifierValue(''); + }; + + const addAction = () => { + if (!addedActions.filter((x) => x.action === actionValue).length) { + setAddedActions((addedActions) => [...addedActions, { action: actionValue }]); + } + setActionValue(''); + }; + + const removeResource = (resource) => { + setAddedResources(addedResources.filter((x) => x !== resource)); + }; + + const onChangePolicyName = (e) => { + setPolicyName(e.target.value); + }; + + const onChangeResourceValue = async (value) => { + setResourceValue(value); + setResourceIdentifierValue(''); + }; + + const onChangeActionValue = async (value) => { + setActionValue(value); + }; + + const onEffectValueChange = (value) => { + setEffectValue(value); + }; + + const onChangeResourceIdentifierValue = async (e) => { + setResourceIdentifierValue(e.target.value); + }; + + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + getData(); + }, []); + + useEffect(() => { + loadResources(); + }, [addedActions]); + + useEffect(() => { + getPolicies(); + }, []); + + useEffect(() => { + if (policyName.length || actionValue.length || addedActions.length || addedResources.length || effectValue) { + setHasChanges(true); + } else { + setHasChanges(false); + } + }, [policyName, actionValue, addedActions, addedResources, effectValue]); + + return ( + <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + + +

New policy

+
+
+ + + + onChangePolicyName(e)} + aria-label="" + /> + + + + + + onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + + + + + + addAction()} + iconType="plusInCircle" + disabled={!actionValue} + > + Add + + + + + {!!addedActions.length && ( + <> + + + + + + + + )} + + + + + onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length} + /> + + + + + onChangeResourceIdentifierValue(e)} + disabled={!resourceValue} + /> + + + + + addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue} + > + Add + + + + + {!!addedResources.length && ( + <> + + + + + + + + )} + + + onEffectValueChange(value)} + /> + + + { + createPolicy(); + }} + fill + > + Create policy + + + +
+
+ {modal} + + ); +}; diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index b73398c250..c91a152c36 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -1,398 +1,430 @@ - import React, { useState, useEffect } from 'react'; import { - EuiButton, - EuiTitle, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiForm, - EuiFormRow, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiBadge, - EuiSuperSelect, - EuiText, - EuiInMemoryTable, - EuiFieldText, + EuiButton, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiSuperSelect, + EuiText, + EuiInMemoryTable, + EuiFieldText, + EuiConfirmModal, + EuiOverlayMask, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; +import { WzOverlayMask } from '../../common/util'; +import _ from 'lodash'; export const EditPolicyFlyout = ({ policy, closeFlyout }) => { - const isReserved = WzAPIUtils.isReservedID(policy.id); - const [actionValue, setActionValue] = useState(''); - const [addedActions, setAddedActions] = useState([]); - const [availableResources, setAvailableResources] = useState([]); - const [availableActions, setAvailableActions] = useState([]); - const [actions, setActions] = useState([]); - const [addedResources, setAddedResources] = useState([]); - const [resources, setResources] = useState([]); - const [resourceValue, setResourceValue] = useState(''); - const [resourceIdentifierValue, setResourceIdentifierValue] = useState(''); - const [effectValue, setEffectValue] = useState(); - - useEffect(() => { - getData(); - initData(); - }, []); - - useEffect(() => { loadResources() }, [addedActions,availableActions]); + const isReserved = WzAPIUtils.isReservedID(policy.id); + const [actionValue, setActionValue] = useState(''); + const [initialActionValue] = useState(''); + const [addedActions, setAddedActions] = useState([]); + const [initialAddedActions, setInitialAddedActions] = useState([]); + const [availableResources, setAvailableResources] = useState([]); + const [availableActions, setAvailableActions] = useState([]); + const [actions, setActions] = useState([]); + const [addedResources, setAddedResources] = useState([]); + const [initialAddedResources, setInitialAddedResources] = useState([]); + const [resources, setResources] = useState([]); + const [resourceValue, setResourceValue] = useState(''); + const [initialResourceValue] = useState(''); + const [resourceIdentifierValue, setResourceIdentifierValue] = useState(''); + const [effectValue, setEffectValue] = useState(); + const [initialEffectValue, setInitialEffectValue] = useState(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [hasChanges, setHasChanges] = useState(false); - const updatePolicy = async() => { - try{ - const actions = addedActions.map(item => item.action) - const resources = addedResources.map(item => item.resource) - const response = await WzRequest.apiReq( - 'PUT', - `/security/policies/${policy.id}`, - { - "policy": { - "actions": actions, - "resources": resources, - "effect": effectValue - } - } - ); - - const data = (response.data || {}).data; - if (data.failed_items && data.failed_items.length) { - return; - } - ErrorHandler.info('Role was successfully updated with the selected policies'); - closeFlyout(); - }catch(error){ - ErrorHandler.handle(error, 'Unexpected error'); - } - } + useEffect(() => { + getData(); + initData(); + }, []); + useEffect(() => { + loadResources(); + }, [addedActions, availableActions]); - async function getData() { - const resources_request = await WzRequest.apiReq( - 'GET', - '/security/resources', - {} - ); - const actions_request = await WzRequest.apiReq( - 'GET', - '/security/actions', - {} - ); - const resources_data = ((resources_request || {}).data || {}).data || {}; - setAvailableResources(resources_data); - - const actions_data = ((actions_request || {}).data || {}).data || {}; - setAvailableActions(actions_data); - const actions = Object.keys(actions_data) - .map((x, idx) => { - return { - id: idx, - value: x, - inputDisplay: x, - dropdownDisplay: ( - <> - {x} - -

- {actions_data[x].description} -

-
- - ) - } - }); - setActions(actions); - } + const updatePolicy = async () => { + try { + const actions = addedActions.map((item) => item.action); + const resources = addedResources.map((item) => item.resource); + const response = await WzRequest.apiReq('PUT', `/security/policies/${policy.id}`, { + policy: { + actions: actions, + resources: resources, + effect: effectValue, + }, + }); - const loadResources = () => { - let allResources = []; - addedActions.forEach(x => { - const res = (availableActions[x.action] || {})['resources']; - allResources = allResources.concat(res); - }); - const allResourcesSet = new Set(allResources); - const resources = Array.from(allResourcesSet).map((x, idx) => { - return { - id: idx, - value: x, - inputDisplay: x, - dropdownDisplay: ( - <> - {x} - -

- {(availableResources[x] || {}).description} -

-
- - ) - } - }); - setResources(resources); + const data = (response.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; + } + ErrorHandler.info('Role was successfully updated with the selected policies'); + closeFlyout(); + } catch (error) { + ErrorHandler.handle(error, 'Unexpected error'); } + }; - const initData = () => { - const policies = ((policy || {}).policy || {}).actions || []; - const initPolicies = policies.map(item => { - return {action: item} - }) - setAddedActions(initPolicies) - - const resources = ((policy || {}).policy || {}).resources || []; - const initResources = resources.map(item => { - return {resource: item} - }) - setAddedResources(initResources) + async function getData() { + const resources_request = await WzRequest.apiReq('GET', '/security/resources', {}); + const actions_request = await WzRequest.apiReq('GET', '/security/actions', {}); + const resources_data = ((resources_request || {}).data || {}).data || {}; + setAvailableResources(resources_data); - setEffectValue(policy.policy.effect) - }; + const actions_data = ((actions_request || {}).data || {}).data || {}; + setAvailableActions(actions_data); + const actions = Object.keys(actions_data).map((x, idx) => { + return { + id: idx, + value: x, + inputDisplay: x, + dropdownDisplay: ( + <> + {x} + +

{actions_data[x].description}

+
+ + ), + }; + }); + setActions(actions); + } + const loadResources = () => { + let allResources = []; + addedActions.forEach((x) => { + const res = (availableActions[x.action] || {})['resources']; + allResources = allResources.concat(res); + }); + const allResourcesSet = new Set(allResources); + const resources = Array.from(allResourcesSet).map((x, idx) => { + return { + id: idx, + value: x, + inputDisplay: x, + dropdownDisplay: ( + <> + {x} + +

{(availableResources[x] || {}).description}

+
+ + ), + }; + }); + setResources(resources); + }; - const onEffectValueChange = value => { - setEffectValue(value); - }; + const initData = () => { + const policies = ((policy || {}).policy || {}).actions || []; + const initPolicies = policies.map((item) => { + return { action: item }; + }); + setAddedActions(initPolicies); + setInitialAddedActions(initPolicies); - const effectOptions = [ - { - value: 'allow', - inputDisplay: 'Allow' - }, - { - value: 'deny', - inputDisplay: 'Deny' - }, - ]; + const resources = ((policy || {}).policy || {}).resources || []; + const initResources = resources.map((item) => { + return { resource: item }; + }); + setAddedResources(initResources); + setInitialAddedResources(initResources); + setEffectValue(policy.policy.effect); + setInitialEffectValue(policy.policy.effect) + }; + const onEffectValueChange = (value) => { + setEffectValue(value); + }; + const effectOptions = [ + { + value: 'allow', + inputDisplay: 'Allow', + }, + { + value: 'deny', + inputDisplay: 'Deny', + }, + ]; - const onChangeActionValue = async (value) => { - setActionValue(value); - }; + const onChangeActionValue = async (value) => { + setActionValue(value); + }; - const addAction = () => { - if (!addedActions.filter(x => x.action === actionValue).length) { - setAddedActions(addedActions => - [...addedActions, - { action: actionValue } - ]) - } - setActionValue(''); - }; + const addAction = () => { + if (!addedActions.filter((x) => x.action === actionValue).length) { + setAddedActions((addedActions) => [...addedActions, { action: actionValue }]); + } + setActionValue(''); + }; - const removeAction = (action) => { - setAddedActions(addedActions.filter(x => x !== action)); - }; + const removeAction = (action) => { + setAddedActions(addedActions.filter((x) => x !== action)); + }; - const actions_columns = [ + const actions_columns = [ + { + field: 'action', + name: 'Actions', + sortable: true, + truncateText: true, + }, + { + name: '', + actions: [ { - field: 'action', - name: 'Actions', - sortable: true, - truncateText: true, + name: 'Remove', + description: 'Remove this action', + type: 'icon', + enabled: () => !isReserved, + color: 'danger', + icon: 'trash', + onClick: (action) => removeAction(action), }, - { - name: '', - actions: [ - { - name: 'Remove', - description: 'Remove this action', - type: 'icon', - enabled: () => !isReserved, - color: 'danger', - icon: 'trash', - onClick: (action) => removeAction(action), - }, - ], - } - ]; + ], + }, + ]; - const resources_columns = [ + const resources_columns = [ + { + field: 'resource', + name: 'Resources', + sortable: true, + truncateText: true, + }, + { + name: '', + actions: [ { - field: 'resource', - name: 'Resources', - sortable: true, - truncateText: true, + name: 'Remove', + description: 'Remove this resource', + type: 'icon', + color: 'danger', + enabled: () => !isReserved, + icon: 'trash', + onClick: (resource) => removeResource(resource), }, - { - name: '', - actions: [ - { - name: 'Remove', - description: 'Remove this resource', - type: 'icon', - color: 'danger', - enabled: () => !isReserved, - icon: 'trash', - onClick: (resource) => removeResource(resource), - }, - ], - } - ]; + ], + }, + ]; + const onChangeResourceValue = async (value) => { + setResourceValue(value); + setResourceIdentifierValue(''); + }; - const onChangeResourceValue = async (value) => { - setResourceValue(value); - setResourceIdentifierValue(''); - }; + const getIdentifier = () => { + const keys = Object.keys(availableResources) || []; + return (keys[resourceValue] || ':').split(':')[1]; + }; - const getIdentifier = () => { - const keys = (Object.keys(availableResources) || []); - return (keys[resourceValue] || ':').split(':')[1]; - } + const onChangeResourceIdentifierValue = async (e) => { + setResourceIdentifierValue(e.target.value); + }; - const onChangeResourceIdentifierValue = async (e) => { - setResourceIdentifierValue(e.target.value); - }; + const removeResource = (resource) => { + setAddedResources(addedResources.filter((x) => x !== resource)); + }; - const removeResource = (resource) => { - setAddedResources(addedResources.filter(x => x !== resource)); + const addResource = () => { + if ( + !addedResources.filter((x) => x.resource === `${resourceValue}:${resourceIdentifierValue}`) + .length + ) { + setAddedResources((addedResources) => [ + ...addedResources, + { resource: `${resourceValue}:${resourceIdentifierValue}` }, + ]); } + setResourceIdentifierValue(''); + }; - - const addResource = () => { - if (!addedResources.filter(x => x.resource === `${resourceValue}:${resourceIdentifierValue}`).length) { - setAddedResources(addedResources => - [...addedResources, - { resource: `${resourceValue}:${resourceIdentifierValue}` } - ]) - } - setResourceIdentifierValue(''); + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + useEffect(() => { + if (initialActionValue != actionValue || !_.isEqual(addedResources, initialAddedResources) || + !_.isEqual(addedActions, initialAddedActions) || initialResourceValue != resourceValue || + initialEffectValue != effectValue) { + setHasChanges(true); + } else { + setHasChanges(false); } + }, [actionValue, addedResources, addedActions, resourceValue, effectValue]); - return ( - closeFlyout(false)}> - - -

- Edit policy {policy.name}   - {isReserved && - Reserved - } -

-
-
- - - { }} - aria-label="" + return ( + <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }}> + + +

+ Edit policy {policy.name}   + {isReserved && Reserved} +

+
+
+ + + + {}} + aria-label="" + /> + + + + + + onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers /> - - - - - - onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - - - - - - addAction()} - iconType="plusInCircle" - disabled={!actionValue || isReserved} - >Add - - - - {!!addedActions.length && - <> - - - - - - - - } - - +
+
+ + + + addAction()} + iconType="plusInCircle" + disabled={!actionValue || isReserved} + > + Add + + + + + {!!addedActions.length && ( + <> + + - - onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length || isReserved} - /> - + - - - onChangeResourceIdentifierValue(e)} - disabled={!resourceValue || isReserved} - /> - - - - - addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue || isReserved} - >Add - - - - {!!addedResources.length && - <> - - - - - - - - } - - + + + )} + + + + onEffectValueChange(value)} + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length || isReserved} /> - - - - Apply - + + + + + onChangeResourceIdentifierValue(e)} + disabled={!resourceValue || isReserved} + /> + + + + + addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue || isReserved} + > + Add + + + + + {!!addedResources.length && ( + <> + + + + + + + + )} + + + onEffectValueChange(value)} + /> + + + + Apply + - - ) -}; \ No newline at end of file + + + + {modal} + + ); +}; diff --git a/public/components/security/policies/policies-table.tsx b/public/components/security/policies/policies-table.tsx index e8609159fd..8907b0f3cd 100644 --- a/public/components/security/policies/policies-table.tsx +++ b/public/components/security/policies/policies-table.tsx @@ -10,16 +10,20 @@ import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; import { WzButtonModalConfirm } from '../../common/buttons'; +import { CreatePolicyFlyout } from './create-policy'; -export const PoliciesTable = ({policies, loading, editPolicy, updatePolicies}) => { +export const PoliciesTable = ({policies, loading, editPolicy, createPolicy,updatePolicies}) => { - const getRowProps = item => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - onClick: () => editPolicy(item), - }; + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + onClick: () => { + editPolicy(item); + createPolicy(item) + }, }; + }; const columns = [ diff --git a/public/components/security/policies/policies.tsx b/public/components/security/policies/policies.tsx index aec0a7e160..3caccd9c11 100644 --- a/public/components/security/policies/policies.tsx +++ b/public/components/security/policies/policies.tsx @@ -22,26 +22,22 @@ import { } from '@elastic/eui'; import { PoliciesTable } from './policies-table'; import { WzRequest } from '../../../react-services/wz-request'; -import { WazuhSecurity } from '../../../factories/wazuh-security' -import { ErrorHandler } from '../../../react-services/error-handler'; import { EditPolicyFlyout } from './edit-policy'; +import { CreatePolicyFlyout } from './create-policy'; + export const Policies = () => { - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [resources, setResources] = useState([]); - const [resourceValue, setResourceValue] = useState(''); - const [resourceIdentifierValue, setResourceIdentifierValue] = useState(''); const [availableResources, setAvailableResources] = useState([]); const [availableActions, setAvailableActions] = useState([]); const [addedActions, setAddedActions] = useState([]); - const [addedResources, setAddedResources] = useState([]); const [actions, setActions] = useState([]); - const [actionValue, setActionValue] = useState(''); - const [policyName, setPolicyName] = useState(''); const [policies, setPolicies] = useState(''); const [loading, setLoading] = useState(false); + const [isCreatingPolicy, setIsCreatingPolicy] = useState(false); const [isEditingPolicy, setIsEditingPolicy] = useState(false); const [editingPolicy, setEditingPolicy] = useState(''); + const [creatingPolicy, setCreatingPolicy] = useState(''); useEffect(() => { loadResources() }, [addedActions]); @@ -66,52 +62,11 @@ export const Policies = () => { setIsEditingPolicy(true); } - - const createPolicy = async () => { - try { - const result = await WzRequest.apiReq( - 'POST', - '/security/policies', - - { - name: policyName, - policy: { - actions: addedActions.map(x => x.action), - resources: addedResources.map(x => x.resource), - effect: effectValue - } - } - ); - const resultData = (result.data || {}).data; - if (resultData.failed_items && resultData.failed_items.length) { - return; - } - ErrorHandler.info('Policy was successfully created', ''); - await getPolicies(); - setPolicyName(""); - setAddedActions([]); - setAddedResources([]); - setEffectValue(null); - } catch (error) { - ErrorHandler.handle(error, 'Error creating policy'); - return; - } - setIsFlyoutVisible(false); + const createPolicy = (item) => { + setCreatingPolicy(item); + setIsCreatingPolicy(true); } - const onChangePolicyName = e => { - setPolicyName(e.target.value); - }; - - const onChangeResourceValue = async (value) => { - setResourceValue(value); - setResourceIdentifierValue(''); - }; - - const onChangeResourceIdentifierValue = async (e) => { - setResourceIdentifierValue(e.target.value); - }; - const loadResources = () => { let allResources = []; addedActions.forEach(x => { @@ -139,9 +94,6 @@ export const Policies = () => { setResources(resources); } - const onChangeActionValue = async (value) => { - setActionValue(value); - }; async function getData() { const resources_request = await WzRequest.apiReq( 'GET', @@ -183,245 +135,27 @@ export const Policies = () => { getData(); }, []); - const effectOptions = [ - { - value: 'allow', - inputDisplay: 'Allow' - }, - { - value: 'deny', - inputDisplay: 'Deny' - }, - ]; - - const [effectValue, setEffectValue] = useState(); - - const onEffectValueChange = value => { - setEffectValue(value); - }; - - const getIdentifier = () => { - const keys = (Object.keys(availableResources) || []); - return (keys[resourceValue] || ':').split(':')[1]; - } - - const addResource = () => { - if (!addedResources.filter(x => x.resource === `${resourceValue}:${resourceIdentifierValue}`).length) { - setAddedResources(addedResources => - [...addedResources, - { resource: `${resourceValue}:${resourceIdentifierValue}` } - ]) - } - setResourceIdentifierValue(''); - } - - const addAction = () => { - if (!addedActions.filter(x => x.action === actionValue).length) { - setAddedActions(addedActions => - [...addedActions, - { action: actionValue } - ]) - } - setActionValue(''); - } - - const removeAction = (action) => { - setAddedActions(addedActions.filter(x => x !== action)); - } - - const removeResource = (resource) => { - setAddedResources(addedResources.filter(x => x !== resource)); - } - - const actions_columns = [ - { - field: 'action', - name: 'Actions', - sortable: true, - truncateText: true, - }, - { - name: '', - actions: [ - { - name: 'Remove', - description: 'Remove this action', - type: 'icon', - color: 'danger', - icon: 'trash', - onClick: (action) => removeAction(action), - }, - ], - } - ]; - - const resources_columns = [ - { - field: 'resource', - name: 'Resources', - sortable: true, - truncateText: true, - }, - { - name: '', - actions: [ - { - name: 'Remove', - description: 'Remove this resource', - type: 'icon', - color: 'danger', - icon: 'trash', - onClick: (resource) => removeResource(resource), - }, - ], - } - ]; - const closeEditingFlyout = async () => { setIsEditingPolicy(false); await getPolicies(); }; + + const closeCreatingFlyout = async () => { + setIsCreatingPolicy(false); + await getPolicies(); + }; let editFlyout; if (isEditingPolicy) { - editFlyout = ( { closeEditingFlyout() }} > + editFlyout = ( - ) + ) } let flyout; - if (isFlyoutVisible) { + if (isCreatingPolicy) { flyout = ( - { setIsFlyoutVisible(false) }}> - setIsFlyoutVisible(false)}> - - -

New policy

-
-
- - - - onChangePolicyName(e)} - aria-label="" - /> - - - - - - onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - - - - - - addAction()} - iconType="plusInCircle" - disabled={!actionValue} - >Add - - - - {!!addedActions.length && - <> - - - - - - - - } - - - - - onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length} - /> - - - - - onChangeResourceIdentifierValue(e)} - disabled={!resourceValue} - /> - - - - - addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue} - >Add - - - - {!!addedResources.length && - <> - - - - - - - - } - - - onEffectValueChange(value)} - /> - - - createPolicy()} - fill> - Create policy - - - -
-
+ ); } @@ -439,7 +173,7 @@ export const Policies = () => { &&
setIsFlyoutVisible(true)}> + onClick={() => setIsCreatingPolicy(true)}> Create policy {flyout} @@ -449,7 +183,7 @@ export const Policies = () => { - + ); diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 4befcdb7ec..5dd081c093 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { EuiTitle, EuiFlyout, @@ -11,11 +11,14 @@ import { EuiFlexItem, EuiComboBox, EuiFieldText, + EuiOverlayMask, + EuiConfirmModal, } from '@elastic/eui'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { RuleEditor } from './rule-editor'; import RulesServices from '../../rules/services'; import RolesServices from '../../roles/services'; +import { WzOverlayMask } from '../../../common/util' export const RolesMappingCreate = ({ closeFlyout, @@ -28,18 +31,22 @@ export const RolesMappingCreate = ({ const [selectedRoles, setSelectedRoles] = useState([]); const [ruleName, setRuleName] = useState(''); const [isLoading, setIsLoading] = useState(false); - - const getRolesList = roles => { - const list = roles.map(item => { + const [hasChangeMappingRules, setHasChangeMappingRules] = useState(false); + const [initialSelectedRoles] = useState([]); + const [initialRuleName] = useState(''); + const [isModalVisible, setIsModalVisible] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const getRolesList = () => { + const list = roles.map((item) => { return { label: rolesEquivalences[item.id], id: item.id }; }); return list; }; - const createRule = async toSaveRule => { + const createRule = async (toSaveRule) => { try { setIsLoading(true); - const formattedRoles = selectedRoles.map(item => { + const formattedRoles = selectedRoles.map((item) => { return item.id; }); const newRule = await RulesServices.CreateRule({ @@ -47,7 +54,7 @@ export const RolesMappingCreate = ({ rule: toSaveRule, }); await Promise.all( - formattedRoles.map(async role => await RolesServices.AddRoleRules(role, [newRule.id])) + formattedRoles.map(async (role) => await RolesServices.AddRoleRules(role, [newRule.id])) ); ErrorHandler.info('Role mapping was successfully created'); } catch (error) { @@ -57,60 +64,108 @@ export const RolesMappingCreate = ({ closeFlyout(false); }; + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + if(initialSelectedRoles.length != selectedRoles.length || initialRuleName != ruleName || hasChangeMappingRules){ + setHasChanges(true); + }else{ + setHasChanges(false) + } + }, [selectedRoles, ruleName, hasChangeMappingRules]); + return ( - closeFlyout(false)}> - - -

Create new role mapping  

-
-
- - - - setRuleName(e.target.value)} - /> - - - { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - - - - - - createRule(rule)} - initialRule={false} - isReserved={false} - isLoading={isLoading} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - > - - - -
+ <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }}> + + +

Create new role mapping  

+
+
+ + + + setRuleName(e.target.value)} + /> + + + { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + + + + + + createRule(rule)} + initialRule={false} + isReserved={false} + isLoading={isLoading} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => { + setHasChangeMappingRules(hasChange); + }} + > + + + +
+
+ {modal} + ); }; diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index dccac8d571..cbc9f9acf0 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiTitle, EuiFlyout, @@ -11,6 +11,8 @@ import { EuiFlexItem, EuiBadge, EuiComboBox, + EuiOverlayMask, + EuiConfirmModal, EuiFieldText, } from '@elastic/eui'; import { ErrorHandler } from '../../../../react-services/error-handler'; @@ -18,6 +20,8 @@ import { RuleEditor } from './rule-editor'; import RulesServices from '../../rules/services'; import RolesServices from '../../roles/services'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; +import { WzOverlayMask } from '../../../common/util' +import _ from 'lodash'; export const RolesMappingEdit = ({ rule, @@ -28,8 +32,8 @@ export const RolesMappingEdit = ({ onSave, currentPlatform, }) => { - const getEquivalences = roles => { - const list = roles.map(item => { + const getEquivalences = (roles) => { + const list = roles.map((item) => { return { label: rolesEquivalences[item], id: item }; }); return list; @@ -38,18 +42,21 @@ export const RolesMappingEdit = ({ const [selectedRoles, setSelectedRoles] = useState(getEquivalences(rule.roles)); const [ruleName, setRuleName] = useState(rule.name); const [isLoading, setIsLoading] = useState(false); + const [hasChangeMappingRules, setHasChangeMappingRules] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [hasChanges, setHasChanges] = useState(false); - const getRolesList = roles => { - const list = roles.map(item => { + const getRolesList = (roles) => { + const list = roles.map((item) => { return { label: rolesEquivalences[item.id], id: item.id }; }); return list; }; - const editRule = async toSaveRule => { + const editRule = async (toSaveRule) => { try { setIsLoading(true); - const formattedRoles = selectedRoles.map(item => { + const formattedRoles = selectedRoles.map((item) => { return item.id; }); @@ -58,16 +65,16 @@ export const RolesMappingEdit = ({ rule: toSaveRule, }); - const toAdd = formattedRoles.filter(value => !rule.roles.includes(value)); - const toRemove = rule.roles.filter(value => !formattedRoles.includes(value)); + const toAdd = formattedRoles.filter((value) => !rule.roles.includes(value)); + const toRemove = rule.roles.filter((value) => !formattedRoles.includes(value)); await Promise.all( - toAdd.map(async role => { + toAdd.map(async (role) => { return RolesServices.AddRoleRules(role, [rule.id]); }) ); await Promise.all( - toRemove.map(async role => { + toRemove.map(async (role) => { return RolesServices.RemoveRoleRules(role, [rule.id]); }) ); @@ -81,65 +88,114 @@ export const RolesMappingEdit = ({ closeFlyout(false); }; + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + defaultFocusedButton="confirm" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + const initialRoles = getEquivalences(rule.roles); + if (rule.name != ruleName || !_.isEqual(initialRoles, selectedRoles) || hasChangeMappingRules) { + setHasChanges(true); + } else { + setHasChanges(false); + } + }, [selectedRoles, ruleName, hasChangeMappingRules]); + return ( - closeFlyout(false)}> - - -

- Edit {rule.name}   - {WzAPIUtils.isReservedID(rule.id) && Reserved} -

-
-
- - - - setRuleName(e.target.value)} - aria-label="" - /> - - - { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - - - - - - editRule(rule)} - initialRule={rule.rule} - isLoading={isLoading} - isReserved={WzAPIUtils.isReservedID(rule.id)} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - > - - - -
+ <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + + +

+ Edit {rule.name}   + {WzAPIUtils.isReservedID(rule.id) && Reserved} +

+
+
+ + + + setRuleName(e.target.value)} + aria-label="" + /> + + + { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + + + + + + editRule(rule)} + initialRule={rule.rule} + isLoading={isLoading} + isReserved={WzAPIUtils.isReservedID(rule.id)} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} + > + + + +
+
+ {modal} + ); }; diff --git a/public/components/security/roles-mapping/components/rule-editor.tsx b/public/components/security/roles-mapping/components/rule-editor.tsx index 9c77fc12de..e535bb15d2 100644 --- a/public/components/security/roles-mapping/components/rule-editor.tsx +++ b/public/components/security/roles-mapping/components/rule-editor.tsx @@ -31,19 +31,25 @@ import 'brace/mode/json'; import 'brace/snippets/json'; import 'brace/ext/language_tools'; import "brace/ext/searchbox"; +import _ from 'lodash'; -export const RuleEditor = ({ save, initialRule, isLoading, isReserved, internalUsers, currentPlatform }) => { +export const RuleEditor = ({ save, initialRule, isLoading, isReserved, internalUsers, currentPlatform,onFormChange }) => { const [logicalOperator, setLogicalOperator] = useState('OR'); const [isLogicalPopoverOpen, setIsLogicalPopoverOpen] = useState(false); const [isJsonEditor, setIsJsonEditor] = useState(false); const [ruleJson, setRuleJson] = useState('{\n\t\n}'); const [hasWrongFormat, setHasWrongFormat] = useState(false); const [rules, setRules] = useState([]); + const [initialRules, setInitialRules] = useState([]); + const [initialInternalUserRules, setInitialInternalUserRules] = useState([]); const [internalUserRules, setInternalUserRules] = useState([]); const [internalUsersOptions, setInternalUsersOptions] = useState[]>( [] ); const [selectedUsers, setSelectedUsers] = useState[]>([]); + const [initialSelectedUsers, setInitialSelectedUsers] = useState[]>([]); + const [initialLogicalOperator, setInitialLogicalOperator] = useState('OR'); + const searchOperationOptions = [ { value: 'FIND', text: 'FIND' }, { value: 'FIND$', text: 'FIND$' }, @@ -56,6 +62,12 @@ export const RuleEditor = ({ save, initialRule, isLoading, isReserved, internalU useEffect(() => { if (initialRule) { setStateFromRule(JSON.stringify(initialRule)); + const rulesResult = getRulesFromJson(JSON.stringify(initialRule)); + const _selectedUsers = getSelectedUsersFromRules(rulesResult.internalUsersRules); + setInitialLogicalOperator(rulesResult.logicalOperator); + setInitialRules(rulesResult.customRules); + setInitialInternalUserRules(rulesResult.internalUsersRules); + setInitialSelectedUsers(_selectedUsers); } }, []); @@ -79,6 +91,7 @@ export const RuleEditor = ({ save, initialRule, isLoading, isReserved, internalU setIsJsonEditor(true); } }; + const onButtonClick = () => setIsLogicalPopoverOpen(isLogicalPopoverOpen => !isLogicalPopoverOpen); const closeLogicalPopover = () => setIsLogicalPopoverOpen(false); @@ -261,6 +274,19 @@ export const RuleEditor = ({ save, initialRule, isLoading, isReserved, internalU setInternalUserRules(tmpInternalUsersRules); }; + useEffect(() => { + if ( + !_.isEqual(initialSelectedUsers, selectedUsers) || + !_.isEqual(initialRules, rules) || + !_.isEqual(initialInternalUserRules, internalUserRules)|| + !_.isEqual(initialLogicalOperator, logicalOperator) + ){ + onFormChange(true) + } else{ + onFormChange(false); + } + }, [selectedUsers, rules, internalUserRules, logicalOperator]); + return ( <> diff --git a/public/components/security/roles-mapping/roles-mapping.tsx b/public/components/security/roles-mapping/roles-mapping.tsx index 495e7154cb..526c54a326 100644 --- a/public/components/security/roles-mapping/roles-mapping.tsx +++ b/public/components/security/roles-mapping/roles-mapping.tsx @@ -7,6 +7,13 @@ import { EuiButton, EuiTitle, EuiOverlayMask, + EuiSpacer, + EuiText, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, } from '@elastic/eui'; import { RolesMappingTable } from './components/roles-mapping-table'; import { RolesMappingEdit } from './components/roles-mapping-edit'; @@ -47,7 +54,7 @@ export const RolesMapping = () => { ErrorHandler.handle('There was an error loading roles'); } }, [rolesLoading]); - + const getInternalUsers = async () => { try { const wazuhSecurity = new WazuhSecurity(); @@ -88,19 +95,13 @@ export const RolesMapping = () => { const updateRoles = async () => { await getRules(); }; - + let editFlyout; if (isEditingRule) { editFlyout = ( - { - setIsEditingRule(false); - }} - > { + closeFlyout={(isVisible) => { setIsEditingRule(isVisible); initData(); }} @@ -110,20 +111,13 @@ export const RolesMapping = () => { onSave={async () => await updateRoles()} currentPlatform={currentPlatform} /> - ); } let createFlyout; if (isCreatingRule) { editFlyout = ( - { - setIsCreatingRule(false); - }} - > { + closeFlyout={(isVisible) => { setIsCreatingRule(isVisible); initData(); }} @@ -133,10 +127,8 @@ export const RolesMapping = () => { onSave={async () => await updateRoles()} currentPlatform={currentPlatform} /> - ); } - return ( @@ -150,7 +142,7 @@ export const RolesMapping = () => { !loadingTable &&
- setIsCreatingRule(true)}>Create Role mapping + {setIsCreatingRule(true)}}>Create Role mapping {createFlyout} {editFlyout}
diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index b6219a8d99..c8cc6f1aee 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -7,147 +7,184 @@ import { EuiFlyoutBody, EuiForm, EuiFieldText, + EuiOverlayMask, EuiFormRow, EuiSpacer, - EuiComboBox + EuiComboBox, + EuiConfirmModal } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; +import { WzOverlayMask } from '../../common/util' export const CreateRole = ({ closeFlyout }) => { - const [policies, setPolicies] = useState([]); - const [roleName, setRoleName] = useState(''); - const [roleNameError, setRoleNameError] = useState(false); - const [selectedPolicies, setSelectedPolicies] = useState([]); - const [selectedPoliciesError, setSelectedPoliciesError] = useState(false); - - async function getData() { - const policies_request = await WzRequest.apiReq( - 'GET', - '/security/policies', - {} - ); - const policies = ((((policies_request || {}).data || {}).data || {}).affected_items || []) - .map(x => { return { 'label': x.name, id: x.id} }); - setPolicies(policies); + const [policies, setPolicies] = useState([]); + const [roleName, setRoleName] = useState(''); + const [roleNameError, setRoleNameError] = useState(false); + const [selectedPolicies, setSelectedPolicies] = useState([]); + const [selectedPoliciesError, setSelectedPoliciesError] = useState(false); + const [initialSelectedPolies] = useState([]); + const [initialRoleName] = useState(''); + const [isModalVisible, setIsModalVisible] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + async function getData() { + const policies_request = await WzRequest.apiReq('GET', '/security/policies', {}); + const policies = ((((policies_request || {}).data || {}).data || {}).affected_items || []).map( + (x) => { + return { label: x.name, id: x.id }; + } + ); + setPolicies(policies); + } + + useEffect(() => { + getData(); + }, []); + + const createUser = async () => { + if (!roleName) { + setRoleNameError(true); + return; + } else if (roleNameError) { + setRoleNameError(false); } - - useEffect(() => { - getData(); - }, []); - - - - const createUser = async () => { - if (!roleName) { - setRoleNameError(true); - return; - } else if (roleNameError) { - setRoleNameError(false); - } - if (!selectedPolicies.length) { - setSelectedPoliciesError(true); - return; - } else if (selectedPoliciesError) { - setSelectedPoliciesError(false); - } - - try { - const result = await WzRequest.apiReq( - 'POST', - '/security/roles', - { - "name": roleName - } - ); - const data = (result.data || {}).data; - if(data.failed_items && data.failed_items.length){ - return; - } - let roleId = ""; - if(data.affected_items && data.affected_items){ - roleId = data.affected_items[0].id; - } - const policiesId = selectedPolicies.map(policy => { - return policy.id; - }) - const policyResult = await WzRequest.apiReq( - 'POST', - `/security/roles/${roleId}/policies`, - { - params:{ - policy_ids: policiesId.toString() - } - } - ); - - const policiesData = (policyResult.data || {}).data; - if(policiesData.failed_items && policiesData.failed_items.length){ - return; - } - ErrorHandler.info('Role was successfully created with the selected policies'); - - } catch (error) { - ErrorHandler.handle(error, "There was an error"); - } - closeFlyout(false) + if (!selectedPolicies.length) { + setSelectedPoliciesError(true); + return; + } else if (selectedPoliciesError) { + setSelectedPoliciesError(false); } - - const onChangeRoleName = e => { - setRoleName(e.target.value); - }; - - - const onChangePolicies = selectedPolicies => { - setSelectedPolicies(selectedPolicies); - }; - - - return ( - closeFlyout(false)}> - - -

New role

-
-
- - - - onChangeRoleName(e)} - aria-label="" - /> - - - - - - - Create role - + try { + const result = await WzRequest.apiReq('POST', '/security/roles', { + name: roleName, + }); + const data = (result.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; + } + let roleId = ''; + if (data.affected_items && data.affected_items) { + roleId = data.affected_items[0].id; + } + const policiesId = selectedPolicies.map((policy) => { + return policy.id; + }); + const policyResult = await WzRequest.apiReq('POST', `/security/roles/${roleId}/policies`, { + params: { + policy_ids: policiesId.toString(), + }, + }); + + const policiesData = (policyResult.data || {}).data; + if (policiesData.failed_items && policiesData.failed_items.length) { + return; + } + ErrorHandler.info('Role was successfully created with the selected policies'); + } catch (error) { + ErrorHandler.handle(error, 'There was an error'); + } + closeFlyout(false); + }; + + const onChangeRoleName = (e) => { + setRoleName(e.target.value); + }; + + const onChangePolicies = (selectedPolicies) => { + setSelectedPolicies(selectedPolicies); + }; + + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + if (initialSelectedPolies.length != selectedPolicies.length || initialRoleName != roleName) { + setHasChanges(true); + } else { + setHasChanges(false); + } + }, [selectedPolicies, roleName]); + + return ( + <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }}> + + +

New role

+
+
+ + + + onChangeRoleName(e)} + aria-label="" + /> + + + + + + + Create role + - +
- - ) + + {modal} + + ); }; \ No newline at end of file diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 73ff2d80d7..1e957ca736 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -13,162 +13,205 @@ import { EuiFlexItem, EuiBadge, EuiComboBox, + EuiOverlayMask, + EuiConfirmModal } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { EditRolesTable } from './edit-role-table'; +import { WzOverlayMask } from '../../common/util' const reservedRoles = ['administrator', 'readonly', 'users_admin', 'agents_readonly', 'agents_admin', 'cluster_readonly', 'cluster_admin']; export const EditRole = ({ role, closeFlyout }) => { - const [isLoading, setIsLoading] = useState(true); - const [currentRole, setCurrentRole] = useState({}); - const [isReserved, setIsReserved] = useState(reservedRoles.includes(role.name)) - const [policies, setPolicies] = useState([]); - const [selectedPolicies, setSelectedPolicies] = useState([]); - const [selectedPoliciesError, setSelectedPoliciesError] = useState(false); - const [assignedPolicies, setAssignedPolicies] = useState([]); - - async function getData() { - try{ - setIsLoading(true); - const roleDataResponse = await WzRequest.apiReq( - 'GET', - '/security/roles', - { - params: { - role_ids: role.id - } - } - ); - const roleData = (((roleDataResponse.data || {}).data || {}).affected_items || [])[0]; - setCurrentRole(roleData); - const policies_request = await WzRequest.apiReq( - 'GET', - '/security/policies', - {} - ); - const selectedPoliciesCopy = []; - const policies = ((((policies_request || {}).data || {}).data || {}).affected_items || []) - .map(x => { - const currentPolicy = { 'label': x.name, id: x.id, roles: x.roles, policy: x.policy }; - if (roleData.policies.includes(x.id)) { - selectedPoliciesCopy.push(currentPolicy); - return false; - } else { - return currentPolicy - } - }); - const filteredPolicies = policies.filter(item => item !== false); - setAssignedPolicies(selectedPoliciesCopy) - setPolicies(filteredPolicies); - }catch(error){ - ErrorHandler.handle( error, 'Error'); + const [isLoading, setIsLoading] = useState(true); + const [currentRole, setCurrentRole] = useState({}); + const [isReserved, setIsReserved] = useState(reservedRoles.includes(role.name)); + const [policies, setPolicies] = useState([]); + const [selectedPolicies, setSelectedPolicies] = useState([]); + const [selectedPoliciesError, setSelectedPoliciesError] = useState(false); + const [assignedPolicies, setAssignedPolicies] = useState([]); + const [initialSelectedPolicies] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + + async function getData() { + try { + setIsLoading(true); + const roleDataResponse = await WzRequest.apiReq('GET', '/security/roles', { + params: { + role_ids: role.id, + }, + }); + const roleData = (((roleDataResponse.data || {}).data || {}).affected_items || [])[0]; + setCurrentRole(roleData); + const policies_request = await WzRequest.apiReq('GET', '/security/policies', {}); + const selectedPoliciesCopy = []; + const policies = ( + (((policies_request || {}).data || {}).data || {}).affected_items || [] + ).map((x) => { + const currentPolicy = { label: x.name, id: x.id, roles: x.roles, policy: x.policy }; + if (roleData.policies.includes(x.id)) { + selectedPoliciesCopy.push(currentPolicy); + return false; + } else { + return currentPolicy; } - setIsLoading(false); + }); + const filteredPolicies = policies.filter((item) => item !== false); + setAssignedPolicies(selectedPoliciesCopy); + setPolicies(filteredPolicies); + } catch (error) { + ErrorHandler.handle(error, 'Error'); } - - useEffect(() => { - getData(); - }, []); - - - - const addPolicy = async () => { - if (!selectedPolicies.length) { - setSelectedPoliciesError(true); - return; - } else if (selectedPoliciesError) { - setSelectedPoliciesError(false); - } - - try { - let roleId = currentRole.id; - - const policiesId = selectedPolicies.map(policy => { - return policy.id; - }) - const policyResult = await WzRequest.apiReq( - 'POST', - `/security/roles/${roleId}/policies`, - { - params: { - policy_ids: policiesId.toString() - } - } - ); - - const policiesData = (policyResult.data || {}).data; - if (policiesData.failed_items && policiesData.failed_items.length) { - return; - } - ErrorHandler.info('Role was successfully updated with the selected policies'); - setSelectedPolicies([]) - await update(); - } catch (error) { - ErrorHandler.handle(error, "There was an error"); - } + setIsLoading(false); + } + + useEffect(() => { + getData(); + }, []); + + const addPolicy = async () => { + if (!selectedPolicies.length) { + setSelectedPoliciesError(true); + return; + } else if (selectedPoliciesError) { + setSelectedPoliciesError(false); } - const update = async() => { - await getData(); + try { + let roleId = currentRole.id; + + const policiesId = selectedPolicies.map((policy) => { + return policy.id; + }); + const policyResult = await WzRequest.apiReq('POST', `/security/roles/${roleId}/policies`, { + params: { + policy_ids: policiesId.toString(), + }, + }); + + const policiesData = (policyResult.data || {}).data; + if (policiesData.failed_items && policiesData.failed_items.length) { + return; + } + ErrorHandler.info('Role was successfully updated with the selected policies'); + setSelectedPolicies([]); + await update(); + } catch (error) { + ErrorHandler.handle(error, 'There was an error'); } - - - const onChangePolicies = selectedPolicies => { - setSelectedPolicies(selectedPolicies); - }; - - - return ( - closeFlyout(false)}> - - -

- Edit {role.name} role   - {isReserved && - Reserved - } -

-
-
- - - - - - - - - - Add policy - - - - - - - + }; + + const update = async () => { + await getData(); + }; + + const onChangePolicies = (selectedPolicies) => { + setSelectedPolicies(selectedPolicies); + }; + + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + return ( + <> + { + if (initialSelectedPolicies.length != selectedPolicies.length) { + setIsModalVisible(true); + } else { + closeFlyout(false); + } + }} + > + { + if (initialSelectedPolicies.length != selectedPolicies.length) { + setIsModalVisible(true); + } else { + closeFlyout(false); + } + }} + > + + +

+ Edit {role.name} role   + {isReserved && Reserved} +

+
+
+ + + + + + + + + + + Add policy + + + + + -
- -
-
+
+ +
+
- - ) + + {modal} + + ); }; \ No newline at end of file diff --git a/public/components/security/roles/roles.tsx b/public/components/security/roles/roles.tsx index 8b7ec2c228..f181f1bb77 100644 --- a/public/components/security/roles/roles.tsx +++ b/public/components/security/roles/roles.tsx @@ -53,22 +53,13 @@ export const Roles = () => { getData(); }, []); - - - const closeFlyout = async () => { - setIsFlyoutVisible(false); - await getData(); - }; - - let flyout; if (isFlyoutVisible) { flyout = ( - {setIsFlyoutVisible(false) }}> - - + { + setIsFlyoutVisible(isVisible); + await getData(); + }} /> ); } @@ -77,20 +68,14 @@ export const Roles = () => { setIsEditFlyoutVisible(true); } - - const closeEditingFlyout = async () => { - setIsEditFlyoutVisible(false); - await getData(); - }; - let editFlyout; if (isEditFlyoutVisible) { editFlyout = ( - {await closeEditingFlyout(); }}> - - + { + setIsEditFlyoutVisible(isVisible); + await getData(); + }} /> ); } diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index 9f454d9cc8..75965f1e86 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -11,11 +11,11 @@ import { EuiFormRow, EuiSpacer, EuiComboBox, - EuiSwitch, EuiFieldPassword, - EuiText, EuiFieldText, + EuiOverlayMask, EuiPanel, + EuiConfirmModal, } from '@elastic/eui'; import { useApiService } from '../../../common/hooks/useApiService'; @@ -26,12 +26,13 @@ import RolesServices from '../../roles/services'; import { WzButtonPermissions } from '../../../common/permissions/button'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; +import { WzOverlayMask } from '../../../common/util' export const CreateUser = ({ closeFlyout }) => { const [selectedRoles, setSelectedRole] = useState([]); const [rolesLoading, roles, rolesError] = useApiService(RolesServices.GetRoles, {}); const rolesOptions: any = roles - ? roles.map(item => { + ? roles.map((item) => { return { label: item.name, id: item.id }; }) : []; @@ -39,6 +40,11 @@ export const CreateUser = ({ closeFlyout }) => { const [isLoading, setIsLoading] = useState(false); const [userName, setUserName] = useState(''); const [password, setPassword] = useState(''); + const [initialSelectedRoles] = useState([]); + const [initialUserName] = useState(''); + const [initialPassword] = useState(''); + const [isModalVisible, setIsModalVisible] = useState(false); + const [hasChanges, setHasChanges] = useState(false); const [confirmPassword, setConfirmPassword] = useState(''); const [allowRunAs, setAllowRunAs] = useState(false); const [formErrors, setFormErrors] = useState({ @@ -89,7 +95,7 @@ export const CreateUser = ({ closeFlyout }) => { const validations = { userName: [ { fn: () => (userName.trim() === '' ? 'The user name is required' : '') }, - { fn: () => (userName.trim().includes(' ')? 'The user name cannot contain spaces' : '') }, + { fn: () => (userName.trim().includes(' ') ? 'The user name cannot contain spaces' : '') }, { fn: () => !userName.match(/^.{4,20}$/) @@ -115,7 +121,7 @@ export const CreateUser = ({ closeFlyout }) => { const validateFields = (fields, showErrors = true) => { const _formErrors = { ...formErrors }; let isValid = true; - fields.forEach(field => { + fields.forEach((field) => { const error = validations[field].reduce((currentError, validation) => { return !!currentError ? currentError : validation.fn(); }, ''); @@ -144,11 +150,10 @@ export const CreateUser = ({ closeFlyout }) => { password: password, }; - try { + try { const user = await UsersServices.CreateUser(userData); await addRoles(user.id); - if (allowRunAsData) - await UsersServices.UpdateAllowRunAs(user.id, allowRunAsData); + if (allowRunAsData) await UsersServices.UpdateAllowRunAs(user.id, allowRunAsData); ErrorHandler.info('User was successfully created'); closeFlyout(true); @@ -158,128 +163,174 @@ export const CreateUser = ({ closeFlyout }) => { } }; - const addRoles = async userId => { - const formattedRoles = selectedRoles.map(item => { + const addRoles = async (userId) => { + const formattedRoles = selectedRoles.map((item) => { return item.id; }); if (formattedRoles.length > 0) await UsersServices.AddUserRoles(userId, formattedRoles); }; - const onChangeRoles = selectedRoles => { + const onChangeRoles = (selectedRoles) => { setSelectedRole(selectedRoles); }; - const onChangeUserName = e => { + const onChangeUserName = (e) => { setUserName(e.target.value); }; - const onChangePassword = e => { + const onChangePassword = (e) => { setPassword(e.target.value); }; - const onChangeConfirmPassword = e => { + const onChangeConfirmPassword = (e) => { setConfirmPassword(e.target.value); }; - const onChangeAllowRunAs = e => { + const onChangeAllowRunAs = (e) => { setAllowRunAs(e.target.checked); }; + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + if ( + initialSelectedRoles.length != selectedRoles.length || initialPassword != password || + initialPassword != confirmPassword || initialUserName != userName || allowRunAs + ) { + setHasChanges(true); + } else { + setHasChanges(false); + } + }, [selectedRoles, userName, password, confirmPassword, allowRunAs]); + return ( - closeFlyout()}> - - -

Create new user

-
-
- - - - -

User data

-
- - - onChangeUserName(e)} - aria-label="" - isInvalid={!!formErrors.userName} - /> - - - onChangePassword(e)} - aria-label="" - isInvalid={!!formErrors.password} - /> - - - onChangeConfirmPassword(e)} - aria-label="" - isInvalid={!!formErrors.confirmPassword} - /> - - - onChangeAllowRunAs(e)} - aria-label="" - /> - -
- - - -

User roles

+ <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }}> + + +

Create new user

- - - -
- - - - - Apply - - - -
-
-
+ + + + + +

User data

+
+ + + onChangeUserName(e)} + aria-label="" + isInvalid={!!formErrors.userName} + /> + + + onChangePassword(e)} + aria-label="" + isInvalid={!!formErrors.password} + /> + + + onChangeConfirmPassword(e)} + aria-label="" + isInvalid={!!formErrors.confirmPassword} + /> + + + onChangeAllowRunAs(e)} + aria-label="" + /> + +
+ + + +

User roles

+
+ + + +
+ + + + + Apply + + + +
+
+ + + {modal} + ); }; diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index d00d498f1b..22c82d25cf 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { EuiButton, EuiTitle, @@ -12,8 +12,9 @@ import { EuiSpacer, EuiBadge, EuiComboBox, - EuiSwitch, EuiFieldPassword, + EuiOverlayMask, + EuiConfirmModal, EuiPanel, } from '@elastic/eui'; @@ -26,16 +27,18 @@ import { WzButtonPermissions } from '../../../common/permissions/button'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; +import { WzOverlayMask } from '../../../common/util' +import _ from 'lodash'; export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { const userRolesFormatted = currentUser.roles && currentUser.roles.length - ? currentUser.roles.map(item => ({ label: rolesObject[item], id: item })) + ? currentUser.roles.map((item) => ({ label: rolesObject[item], id: item })) : []; const [selectedRoles, setSelectedRole] = useState(userRolesFormatted); const [rolesLoading, roles, rolesError] = useApiService(RolesServices.GetRoles, {}); const rolesOptions: any = roles - ? roles.map(item => { + ? roles.map((item) => { return { label: item.name, id: item.id }; }) : []; @@ -43,7 +46,10 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { const [isLoading, setIsLoading] = useState(false); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [initialPassword] = useState(''); + const [hasChanges, setHasChanges] = useState(false); const [allowRunAs, setAllowRunAs] = useState(currentUser.allow_run_as); + const [isModalVisible, setIsModalVisible] = useState(false); const [formErrors, setFormErrors] = useState({ password: '', confirmPassword: '', @@ -76,7 +82,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { isValidForm(false) && (allowRunAs !== currentUser.allow_run_as || password !== '' || - Object.values(getRolesDiff()).some(i => i.length)); + Object.values(getRolesDiff()).some((i) => i.length)); setShowApply(_showApply); }, @@ -100,7 +106,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { const validateFields = (fields, showErrors = true) => { const _formErrors = { ...formErrors }; let isValid = true; - fields.forEach(field => { + fields.forEach((field) => { const error = validations[field].reduce((currentError, validation) => { return !!currentError ? currentError : validation.fn(); }, ''); @@ -148,10 +154,10 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { }; const getRolesDiff = () => { - const formattedRoles = selectedRoles.map(item => item.id); - const _userRolesFormatted = userRolesFormatted.map(role => role.id); - const toAdd = formattedRoles.filter(value => !_userRolesFormatted.includes(value)); - const toRemove = _userRolesFormatted.filter(value => !formattedRoles.includes(value)); + const formattedRoles = selectedRoles.map((item) => item.id); + const _userRolesFormatted = userRolesFormatted.map((role) => role.id); + const toAdd = formattedRoles.filter((value) => !_userRolesFormatted.includes(value)); + const toRemove = _userRolesFormatted.filter((value) => !formattedRoles.includes(value)); return { toAdd, toRemove }; }; @@ -161,121 +167,169 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { if (toRemove.length) await UsersServices.RemoveUserRoles(currentUser.id, toRemove); }; - const onChangeRoles = selectedRoles => { + const onChangeRoles = (selectedRoles) => { setSelectedRole(selectedRoles); }; - const onChangePassword = e => { + const onChangePassword = (e) => { setPassword(e.target.value); }; - const onChangeConfirmPassword = e => { + const onChangeConfirmPassword = (e) => { setConfirmPassword(e.target.value); }; - const onChangeAllowRunAs = e => { + const onChangeAllowRunAs = (e) => { setAllowRunAs(e.target.checked); }; + let modal; + if (isModalVisible) { + modal = ( + + { + setIsModalVisible(false); + closeFlyout(false); + setHasChanges(false); + }} + onCancel={() => setIsModalVisible(false)} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } + + useEffect(() => { + if ( + initialPassword != password || initialPassword != confirmPassword || + !_.isEqual(userRolesFormatted, selectedRoles) || allowRunAs + ) { + setHasChanges(true); + } else { + setHasChanges(false); + } + }, [selectedRoles, password, confirmPassword, allowRunAs]); + return ( - closeFlyout()}> - - -

- Edit {currentUser.username} user     - {WzAPIUtils.isReservedID(currentUser.id) && Reserved} -

-
-
- - - - -

Run as

-
- - onChangeAllowRunAs(e)} - aria-label="" - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - -
- - - -

Password

-
- - onChangePassword(e)} - aria-label="" - isInvalid={!!formErrors.password} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - - - onChangeConfirmPassword(e)} - aria-label="" - isInvalid={!!formErrors.confirmPassword} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - -
- - - -

Roles

+ <> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + > + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }}> + + +

+ Edit {currentUser.username} user     + {WzAPIUtils.isReservedID(currentUser.id) && ( + Reserved + )} +

- - - -
+ + + + + +

Run as

+
+ + onChangeAllowRunAs(e)} + aria-label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + +
+ + + +

Password

+
+ + onChangePassword(e)} + aria-label="" + isInvalid={!!formErrors.password} + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + + + onChangeConfirmPassword(e)} + aria-label="" + isInvalid={!!formErrors.confirmPassword} + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + +
+ + + +

Roles

+
+ + + +
- - - - - Apply - - - -
-
-
+ + + + + Apply + + + + + + + + {modal} + ); }; diff --git a/public/components/settings/miscellaneous/miscellaneous.tsx b/public/components/settings/miscellaneous/miscellaneous.tsx new file mode 100644 index 0000000000..211ab517e6 --- /dev/null +++ b/public/components/settings/miscellaneous/miscellaneous.tsx @@ -0,0 +1,101 @@ +/* + * Wazuh app - React component for Settings > Miscellaneous + * + * Copyright (C) 2015-2021 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, { Fragment, useCallback } from 'react'; + +import { + EuiButton, + EuiDescribedFormGroup, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPage, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { getHttp } from '../../../kibana-services'; + +export const SettingsMiscellaneous = () => { + + const redirectHealthCheckDebugMode = useCallback(() => { + window.location.href = getHttp().basePath.prepend('/app/wazuh#/health-check?debug'); + }, []); + + return ( + + + + + +

Miscellaneous

+
+ + App utils + +
+
+ + + + Run + + + ) + } + ]} + /> +
+
+ ) +}; + + +const SettingsMiscellaneousCategory = ({title, description = '', actions}) => ( + + <> + +

{title}

+
+ {description && ( + + {description} + + )} + + {actions.map(action => ( + + {action.title}} + titleSize='s' + description={action.description} + > + {action.render} + + + ))} + +
+) \ No newline at end of file diff --git a/public/components/settings/settings-logs/logs.js b/public/components/settings/settings-logs/logs.js index bdb642d32c..369f40efaf 100644 --- a/public/components/settings/settings-logs/logs.js +++ b/public/components/settings/settings-logs/logs.js @@ -110,14 +110,10 @@ class SettingsLogs extends Component { - - - - Log file located at - /usr/share/kibana/data/wazuh/logs/wazuhapp.log - - - + + Log file located at + /usr/share/kibana/data/wazuh/logs/wazuhapp.log + {this.state.refreshingEntries && ( )} diff --git a/public/components/wz-menu/wz-menu-management.js b/public/components/wz-menu/wz-menu-management.js index 8ac265429b..3d7105a8f1 100644 --- a/public/components/wz-menu/wz-menu-management.js +++ b/public/components/wz-menu/wz-menu-management.js @@ -14,6 +14,8 @@ import { EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiIcon, EuiButtonEmpty, EuiTool import { WzRequest } from '../../react-services/wz-request'; import { connect } from 'react-redux'; import { AppNavigate } from '../../react-services/app-navigate' +import { WAZUH_MENU_MANAGEMENT_SECTIONS_ID } from '../../../common/constants'; +import { WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID } from '../../../common/wazu-menu/wz-menu-management.cy'; class WzMenuManagement extends Component { constructor(props) { @@ -24,20 +26,76 @@ class WzMenuManagement extends Component { }; this.managementSections = { - management: { id: 'management', text: 'Management' }, - administration: { id: 'administration', text: 'Administration' }, - ruleset: { id: 'ruleset', text: 'Ruleset' }, - rules: { id: 'rules', text: 'Rules' }, - decoders: { id: 'decoders', text: 'Decoders' }, - lists: { id: 'lists', text: 'CDB lists' }, - groups: { id: 'groups', text: 'Groups' }, - configuration: { id: 'configuration', text: 'Configuration' }, - statusReports: { id: 'statusReports', text: 'Status and reports' }, - status: { id: 'status', text: 'Status' }, - cluster: { id: 'monitoring', text: 'Cluster' }, - logs: { id: 'logs', text: 'Logs' }, - reporting: { id: 'reporting', text: 'Reporting' }, - statistics: { id: 'statistics', text: 'Statistics' }, + management: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.MANAGEMENT, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.MANAGEMENT, + text: 'Management', + }, + administration: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.ADMINISTRATION, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.ADMINISTRATION, + text: 'Administration', + }, + ruleset: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.RULESET, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.RULESET, + text: 'Ruleset', + }, + rules: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.RULES, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.RULES, + text: 'Rules', + }, + decoders: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.DECODERS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.DECODERS, + text: 'Decoders', + }, + lists: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.CDB_LISTS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.CDB_LISTS, + text: 'CDB lists', + }, + groups: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.GROUPS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.GROUPS, + text: 'Groups', + }, + configuration: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.CONFIGURATION, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.CONFIGURATION, + text: 'Configuration', + }, + statusReports: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.STATUS_AND_REPORTS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.STATUS_AND_REPORTS, + text: 'Status and reports', + }, + status: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.STATUS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.STATUS, + text: 'Status', + }, + cluster: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.CLUSTER, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.CLUSTER, + text: 'Cluster', + }, + logs: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.LOGS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.LOGS, + text: 'Logs', + }, + reporting: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.REPORTING, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.REPORTING, + text: 'Reporting', + }, + statistics: { + id: WAZUH_MENU_MANAGEMENT_SECTIONS_ID.STATISTICS, + cyTestId: WAZUH_MENU_MANAGEMENT_SECTIONS_CY_TEST_ID.STATISTICS, + text: 'Statistics', + }, }; this.paths = { @@ -67,6 +125,7 @@ class WzMenuManagement extends Component { ...data, id: item.id, name: item.text, + 'data-test-subj': item.cyTestId, isSelected: this.props.state.section === item.id, onClick: () => { }, onMouseDown: (ev) => this.clickMenuItem(ev, item.id) diff --git a/public/components/wz-menu/wz-menu-overview.js b/public/components/wz-menu/wz-menu-overview.js index 328d5b4b68..fd5c046ec6 100644 --- a/public/components/wz-menu/wz-menu-overview.js +++ b/public/components/wz-menu/wz-menu-overview.js @@ -19,6 +19,7 @@ import { AppNavigate } from '../../react-services/app-navigate'; import { getAngularModule } from '../../kibana-services'; import { hasAgentSupportModule } from '../../react-services/wz-agents'; import { WAZUH_MODULES_ID } from '../../../common/constants'; +import { WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID } from '../../../common/wazu-menu/wz-menu-overview.cy'; class WzMenuOverview extends Component { constructor(props) { @@ -31,36 +32,112 @@ class WzMenuOverview extends Component { this.overviewSections = { securityInformation: { id: 'securityInformation', - text: 'Security information management' + text: 'Security information management', }, auditing: { id: 'auditing', text: 'Auditing and Policy Monitoring' }, threatDetection: { id: 'threatDetection', - text: 'Threat detection and response' + text: 'Threat detection and response', }, regulatoryCompliance: { id: 'regulatoryCompliance', - text: 'Regulatory Compliance' + text: 'Regulatory Compliance', + }, + general: { + id: WAZUH_MODULES_ID.SECURITY_EVENTS, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.SECURITY_EVENTS, + text: 'Security Events', + }, + fim: { + id: WAZUH_MODULES_ID.INTEGRITY_MONITORING, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.INTEGRITY_MONITORING, + text: 'Integrity Monitoring', + }, + aws: { + id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.AMAZON_WEB_SERVICES, + text: 'Amazon AWS', + }, + gcp: { + id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.GOOGLE_CLOUD_PLATFORM, + text: 'Google Cloud Platform', + }, + pm: { + id: WAZUH_MODULES_ID.POLICY_MONITORING, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.POLICY_MONITORING, + text: 'Policy Monitoring', + }, + sca: { + id: WAZUH_MODULES_ID.SECURITY_CONFIGURATION_ASSESSMENT, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.SECURITY_CONFIGURATION_ASSESSMENT, + text: 'Security configuration assessment', + }, + audit: { + id: WAZUH_MODULES_ID.AUDITING, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.AUDITING, + text: 'System Auditing', + }, + oscap: { + id: WAZUH_MODULES_ID.OPEN_SCAP, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.OPEN_SCAP, + text: 'OpenSCAP', + }, + ciscat: { + id: WAZUH_MODULES_ID.CIS_CAT, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.CIS_CAT, + text: 'CIS-CAT', + }, + vuls: { + id: WAZUH_MODULES_ID.VULNERABILITIES, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.VULNERABILITIES, + text: 'Vulnerabilities', + }, + virustotal: { + id: WAZUH_MODULES_ID.VIRUSTOTAL, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.VIRUSTOTAL, + text: 'VirusTotal', + }, + osquery: { + id: WAZUH_MODULES_ID.OSQUERY, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.OSQUERY, + text: 'Osquery', + }, + docker: { + id: WAZUH_MODULES_ID.DOCKER, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.DOCKER, + text: 'Docker Listener', + }, + mitre: { + id: WAZUH_MODULES_ID.MITRE_ATTACK, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.MITRE_ATTACK, + text: 'MITRE ATT&CK', + }, + pci: { + id: WAZUH_MODULES_ID.PCI_DSS, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.PCI_DSS, + text: 'PCI DSS', + }, + gdpr: { + id: WAZUH_MODULES_ID.GDPR, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.GDPR, + text: 'GDPR', + }, + hipaa: { + id: WAZUH_MODULES_ID.HIPAA, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.HIPAA, + text: 'HIPAA', + }, + nist: { + id: WAZUH_MODULES_ID.NIST_800_53, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.NIST_800_53, + text: 'NIST 800-53', + }, + tsc: { + id: WAZUH_MODULES_ID.TSC, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.TSC, + text: 'TSC', }, - general: { id: WAZUH_MODULES_ID.SECURITY_EVENTS, text: 'Security Events' }, - fim: { id: WAZUH_MODULES_ID.INTEGRITY_MONITORING, text: 'Integrity Monitoring' }, - aws: { id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS' }, - gcp: { id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, text: 'Google Cloud Platform' }, - pm: { id: WAZUH_MODULES_ID.POLICY_MONITORING, text: 'Policy Monitoring' }, - sca: { id: WAZUH_MODULES_ID.SECURITY_CONFIGURATION_ASSESSMENT, text: 'Security configuration assessment' }, - audit: { id: WAZUH_MODULES_ID.AUDITING, text: 'System Auditing' }, - oscap: { id: WAZUH_MODULES_ID.OPEN_SCAP, text: 'OpenSCAP' }, - ciscat: { id: WAZUH_MODULES_ID.CIS_CAT, text: 'CIS-CAT' }, - vuls: { id: WAZUH_MODULES_ID.VULNERABILITIES, text: 'Vulnerabilities' }, - virustotal: { id: WAZUH_MODULES_ID.VIRUSTOTAL, text: 'VirusTotal' }, - osquery: { id: WAZUH_MODULES_ID.OSQUERY, text: 'Osquery' }, - docker: { id: WAZUH_MODULES_ID.DOCKER, text: 'Docker Listener' }, - mitre: { id: WAZUH_MODULES_ID.MITRE_ATTACK, text: 'MITRE ATT&CK' }, - pci: { id: WAZUH_MODULES_ID.PCI_DSS, text: 'PCI DSS' }, - gdpr: { id: WAZUH_MODULES_ID.GDPR, text: 'GDPR' }, - hipaa: { id: WAZUH_MODULES_ID.HIPAA, text: 'HIPAA' }, - nist: { id: WAZUH_MODULES_ID.NIST_800_53, text: 'NIST 800-53' }, - tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC' } }; this.securityInformationItems = [ @@ -137,6 +214,7 @@ class WzMenuOverview extends Component { ...data, id: item.id, name: item.text, + 'data-test-subj': item.cyTestId, isSelected: this.props.currentTab === item.id, onClick: () => { }, onMouseDown: (ev) => this.clickMenuItem(ev, item.id) diff --git a/public/components/wz-menu/wz-menu-security.js b/public/components/wz-menu/wz-menu-security.js index 44c6ca795b..b79cd4fcd7 100644 --- a/public/components/wz-menu/wz-menu-security.js +++ b/public/components/wz-menu/wz-menu-security.js @@ -14,6 +14,8 @@ import { EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiIcon } from '@elastic/eui'; import { WzRequest } from '../../react-services/wz-request'; import { connect } from 'react-redux'; import { AppNavigate } from '../../react-services/app-navigate'; +import { WAZUH_MENU_SECURITY_SECTIONS_ID } from '../../../common/constants'; +import { WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID } from '../../../common/wazu-menu/wz-menu-security.cy'; class WzMenuSecurity extends Component { constructor(props) { @@ -34,11 +36,27 @@ class WzMenuSecurity extends Component { avaibleRenderSettings() { return [ - this.createItem({ id: 'users', text: 'Users' }), - this.createItem({ id: 'roles', text: 'Roles' }), - this.createItem({ id: 'policies', text: 'Policies' }), - this.createItem({ id: 'roleMapping', text: 'Roles mapping' }), - ] + this.createItem({ + id: WAZUH_MENU_SECURITY_SECTIONS_ID.USERS, + cyTestId: WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID.USERS, + text: 'Users', + }), + this.createItem({ + id: WAZUH_MENU_SECURITY_SECTIONS_ID.ROLES, + cyTestId: WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID.ROLES, + text: 'Roles', + }), + this.createItem({ + id: WAZUH_MENU_SECURITY_SECTIONS_ID.POLICIES, + cyTestId: WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID.POLICIES, + text: 'Policies', + }), + this.createItem({ + id: WAZUH_MENU_SECURITY_SECTIONS_ID.ROLES_MAPPING, + cyTestId: WAZUH_MENU_SECURITY_SECTIONS_CY_TEST_ID.ROLES_MAPPING, + text: 'Roles mapping', + }), + ]; } clickMenuItem = async (ev, section) => { @@ -52,6 +70,7 @@ class WzMenuSecurity extends Component { ...data, id: item.id, name: item.text, + 'data-test-subj': item.cyTestId, isSelected: window.location.href.includes('/security') && this.props.state.selected_security_section === item.id, onClick: () => { }, onMouseDown: (ev) => this.clickMenuItem(ev, item.id) diff --git a/public/components/wz-menu/wz-menu-settings.js b/public/components/wz-menu/wz-menu-settings.js index b98848b069..5198b76bce 100644 --- a/public/components/wz-menu/wz-menu-settings.js +++ b/public/components/wz-menu/wz-menu-settings.js @@ -15,6 +15,8 @@ import { WzRequest } from '../../react-services/wz-request'; import { connect } from 'react-redux'; import { AppNavigate } from '../../react-services/app-navigate'; import { getAngularModule } from '../../kibana-services'; +import { WAZUH_MENU_SETTINGS_SECTIONS_ID } from '../../../common/constants'; +import { WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID } from '../../../common/wazu-menu/wz-menu-settings.cy'; class WzMenuSettings extends Component { constructor(props) { @@ -35,13 +37,45 @@ class WzMenuSettings extends Component { availableSettings() { let auxSettings = { - settings: { id: 'settings', text: 'Settings' }, - api: { id: 'api', text: 'API configuration' }, - modules: { id: 'modules', text: 'Modules' }, - sample_data: { id: 'sample_data', text: 'Sample data' }, - configuration: { id: 'configuration', text: 'Configuration' }, - logs: { id: 'logs', text: 'Logs' }, - about: { id: 'about', text: 'About' }, + settings: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.SETTINGS, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.SETTINGS, + text: 'Settings', + }, + api: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.API_CONFIGURATION, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.API_CONFIGURATION, + text: 'API configuration', + }, + modules: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.MODULES, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.MODULES, + text: 'Modules', + }, + sample_data: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.SAMPLE_DATA, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.SAMPLE_DATA, + text: 'Sample data', + }, + configuration: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.CONFIGURATION, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.CONFIGURATION, + text: 'Configuration', + }, + logs: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.LOGS, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.LOGS, + text: 'Logs' }, + miscellaneous: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.MISCELLANEOUS, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.MISCELLANEOUS, + text: 'Miscellaneous', + }, + about: { + id: WAZUH_MENU_SETTINGS_SECTIONS_ID.ABOUT, + cyTestId: WAZUH_MENU_SETTINGS_SECTIONS_CY_TEST_ID.ABOUT, + text: 'About', + }, }; return (auxSettings); } @@ -54,6 +88,7 @@ class WzMenuSettings extends Component { this.createItem(availableSettings.sample_data), this.createItem(availableSettings.configuration), this.createItem(availableSettings.logs), + this.createItem(availableSettings.miscellaneous), this.createItem(availableSettings.about), ] return (auxItems); @@ -75,6 +110,7 @@ class WzMenuSettings extends Component { ...data, id: item.id, name: item.text, + 'data-test-subj': item.cyTestId, isSelected: window.location.href.includes('/settings') && this.props.state.selected_settings_section === item.id, onClick: () => { }, onMouseDown: (ev) => this.clickMenuItem(ev, item.id) diff --git a/public/components/wz-menu/wz-menu-tools.js b/public/components/wz-menu/wz-menu-tools.js index b72b2d02f2..7b5533d129 100644 --- a/public/components/wz-menu/wz-menu-tools.js +++ b/public/components/wz-menu/wz-menu-tools.js @@ -14,6 +14,8 @@ import { EuiFlexItem, EuiFlexGroup, EuiSideNav, EuiIcon } from '@elastic/eui'; import { WzRequest } from '../../react-services/wz-request'; import { connect } from 'react-redux'; import { AppNavigate } from '../../react-services/app-navigate'; +import { WAZUH_MENU_TOOLS_SECTIONS_ID } from '../../../common/constants'; +import { WAZUH_MENU_TOOLS_SECTIONS_CY_TEST_ID } from '../../../common/wazu-menu/wz-menu-tools.cy'; class WzMenuTools extends Component { constructor(props) { @@ -34,9 +36,17 @@ class WzMenuTools extends Component { avaibleRenderSettings() { return [ - this.createItem({ id: 'devTools', text: 'API Console' }), - this.createItem({ id: 'logtest', text: 'Ruleset Test' }), - ] + this.createItem({ + id: WAZUH_MENU_TOOLS_SECTIONS_ID.API_CONSOLE, + cyTestId: WAZUH_MENU_TOOLS_SECTIONS_CY_TEST_ID.API_CONSOLE, + text: 'API Console', + }), + this.createItem({ + id: WAZUH_MENU_TOOLS_SECTIONS_ID.RULESET_TEST, + cyTestId: WAZUH_MENU_TOOLS_SECTIONS_CY_TEST_ID.RULESET_TEST, + text: 'Ruleset Test', + }), + ]; } clickMenuItem = async (ev, section) => { @@ -50,6 +60,7 @@ class WzMenuTools extends Component { ...data, id: item.id, name: item.text, + 'data-test-subj': item.cyTestId, isSelected: window.location.href.includes('/wazuh-dev') && this.props.state.selected_tools_section === item.id, onClick: () => { }, onMouseDown: (ev) => this.clickMenuItem(ev, item.id) diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 56465f78ba..2c8f69b162 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -17,7 +17,9 @@ import { EuiPopover, EuiIcon, EuiButtonEmpty, + EuiText, EuiCallOut, + EuiPageHeader, EuiToolTip, EuiLoadingSpinner, EuiFormRow, @@ -43,6 +45,8 @@ import { WzGlobalBreadcrumbWrapper } from '../common/globalBreadcrumb/globalBrea import { AppNavigate } from '../../react-services/app-navigate'; import WzTextWithTooltipIfTruncated from '../../components/common/wz-text-with-tooltip-if-truncated'; import { getDataPlugin } from '../../kibana-services'; +import { withWindowSize } from '../../components/common/hocs/withWindowSize'; + const sections = { 'overview': 'overview', @@ -55,7 +59,7 @@ const sections = { 'security': 'security' }; -class WzMenu extends Component { +export const WzMenu = withWindowSize(class WzMenu extends Component { constructor(props) { super(props); this.state = { @@ -169,16 +173,22 @@ class WzMenu extends Component { async componentDidUpdate(prevProps) { + if (this.state.APIlist && !this.state.APIlist.length) { this.loadApiList(); } const { id: apiId } = JSON.parse(AppState.getCurrentAPI()); const { currentAPI } = this.state; const currentTab = this.getCurrentTab(); + if (currentTab !== this.state.currentMenuTab) { this.setState({ currentMenuTab: currentTab }); } + if(this.props.windowSize){ + this.showSelectorsInPopover = this.props.windowSize.width < 1100; + } + if ( prevProps.state.showMenu !== this.props.state.showMenu || (this.props.state.showMenu === true && this.state.showMenu === false) @@ -196,6 +206,7 @@ class WzMenu extends Component { this.setState({ currentAPI: this.props.state.currentAPI }); } } + } async load() { @@ -203,7 +214,8 @@ class WzMenu extends Component { this.setState({ showMenu: true, isOverviewPopoverOpen: false, - isManagementPopoverOpen: false + isManagementPopoverOpen: false, + isSelectorsPopoverOpen: false }); const currentTab = this.getCurrentTab(); @@ -211,7 +223,7 @@ class WzMenu extends Component { this.setState({ currentMenuTab: currentTab, hover: currentTab }); } const list = await PatternHandler.getPatternList('api'); - if (!list) return; + if (!list || (list && !list.length)) return; // Abort if we have disabled the pattern selector if (!AppState.getPatternSelector()) return; @@ -241,26 +253,38 @@ class WzMenu extends Component { }); } } catch (error) { - this.showToast('danger', 'Error', error, 4000); + this.showToast('danger', 'Error', error.message || error, 4000); } this.isLoading = false; } changePattern = async (event) => { try { - const newPattern = event.target.value; + const newPattern = event.target; if (!AppState.getPatternSelector()) return; - await PatternHandler.changePattern(newPattern); - this.setState({ currentSelectedPattern: newPattern }); + await PatternHandler.changePattern(newPattern.value); + this.setState({ currentSelectedPattern: newPattern.value }); if (this.state.currentMenuTab !== 'wazuh-dev') { this.router.reload(); } - this.switchMenuOpened(); + + if (newPattern?.id === 'selectIndexPatternBar') { + this.updatePatternAndApi(); + } else { + this.switchMenuOpened(); + } } catch (error) { this.showToast('danger', 'Error', error, 4000); } }; + updatePatternAndApi = () => { + this.setState({ menuOpened: false, hover: this.state.currentMenuTab }, async () => { + await this.loadApiList(); + await this.loadIndexPatternsList(); + }); + } + /** * @param {String} id * @param {Object} clusterInfo @@ -279,24 +303,27 @@ class WzMenu extends Component { changeAPI = async event => { try { - const apiId = event.target.value; + const apiId = event.target[event.target.selectedIndex]; const apiEntry = this.state.APIlist.filter(item => { - return item.id === apiId; + return item.id === apiId.value; }); const response = await ApiCheck.checkApi(apiEntry[0]); const clusterInfo = response.data || {}; const apiData = this.state.APIlist.filter(item => { - return item.id === apiId; + return item.id === apiId.value; }); - this.updateClusterInfoInRegistry(apiId, clusterInfo); + this.updateClusterInfoInRegistry(apiId.value, clusterInfo); apiData[0].cluster_info = clusterInfo; AppState.setClusterInfo(apiData[0].cluster_info); AppState.setCurrentAPI( - JSON.stringify({ name: apiData[0].manager, id: apiId }) + JSON.stringify({ name: apiData[0].manager, id: apiId.value }) ); - this.switchMenuOpened(); + if (apiId?.id !== 'selectAPIBar') { + this.switchMenuOpened(); + } + if (this.state.currentMenuTab !== 'wazuh-dev') { this.router.reload(); } @@ -393,6 +420,7 @@ class WzMenu extends Component { isManagementPopoverOpen: false, isSecurityPopoverOpen: false, isSettingsPopoverOpen: false, + isSelectorsPopoverOpen: false }; }); } @@ -408,6 +436,7 @@ class WzMenu extends Component { isManagementPopoverOpen: false, isSecurityPopoverOpen: false, isToolsPopoverOpen: false, + isSelectorsPopoverOpen: false }; }); } @@ -423,6 +452,7 @@ class WzMenu extends Component { isManagementPopoverOpen: false, isSettingsPopoverOpen: false, isToolsPopoverOpen: false, + isSelectorsPopoverOpen: false }; }); } @@ -438,6 +468,7 @@ class WzMenu extends Component { isSettingsPopoverOpen: false, isSecurityPopoverOpen: false, isToolsPopoverOpen: false, + isSelectorsPopoverOpen: false }; }); } @@ -453,6 +484,7 @@ class WzMenu extends Component { isSettingsPopoverOpen: false, isSecurityPopoverOpen: false, isToolsPopoverOpen: false, + isSelectorsPopoverOpen: false }; }); } @@ -495,6 +527,7 @@ class WzMenu extends Component { isManagementPopoverOpen: false, isSettingsPopoverOpen: false, isToolsPopoverOpen: false, + isSelectorsPopoverOpen: false }); } @@ -504,7 +537,8 @@ class WzMenu extends Component { this.state.isManagementPopoverOpen || this.state.isSettingsPopoverOpen || this.state.isSecurityPopoverOpen || - this.state.isToolsPopoverOpen + this.state.isToolsPopoverOpen || + this.state.isSelectorsPopoverOpen ); } @@ -525,10 +559,8 @@ class WzMenu extends Component { } this.setState({ menuOpened: !this.state.menuOpened, kibanaMenuBlockedOrOpened, hover: this.state.currentMenuTab }, async () => { - if (this.state.menuOpened) { - await this.loadApiList(); - await this.loadIndexPatternsList(); - }; + await this.loadApiList(); + await this.loadIndexPatternsList(); }); }; @@ -598,14 +630,86 @@ class WzMenu extends Component { || (!this.state.currentAPI) || (AppState.getPatternSelector() && this.state.theresPattern && this.state.patternList && this.state.patternList.length > 1)) } + + getApiSelectorComponent() { + let style = { maxWidth: 100 }; + if (this.showSelectorsInPopover){ + style = { width: '100%', minWidth: 200 }; + } + + return ( + <> + +

API

+
+ +
+ { + return { value: item.id, text: item.id } + }) + } + value={this.state.currentAPI} + onChange={this.changeAPI} + aria-label="API selector" + /> +
+
+ + ) + } + + getIndexPatternSelectorComponent(){ + + let style = { maxWidth: 200, maxHeight: 50 }; + if (this.showSelectorsInPopover){ + style = { width: '100%', maxHeight: 50, minWidth: 200 }; + } + + return( + <> + +

Index pattern

+
+ + +
+ { + return { value: item.id, text: item.title } + }) + } + value={this.state.currentSelectedPattern} + onChange={this.changePattern} + aria-label="Index pattern selector" + /> +
+
+ + + ) + } + + switchSelectorsPopOver(){ + this.setState({ isSelectorsPopoverOpen: !this.state.isSelectorsPopoverOpen }) + } + + render() { const currentAgent = store.getState().appStateReducers.currentAgentData; const thereAreSelectors = this.thereAreSelectors(); + const menu = (
- { this.setState({ hover: "overview" }) }} className={ 'wz-menu-button ' + @@ -625,7 +729,7 @@ class WzMenu extends Component { )} - { this.setState({ hover: "manager" }) }} className={ 'wz-menu-button ' + @@ -644,7 +748,7 @@ class WzMenu extends Component { )} - Agents - - )} -
- - {thereAreSelectors && ( -
- {AppState.getAPISelector() && - this.state.currentAPI && - this.state.APIlist && - this.state.APIlist.length > 1 && - this.buildApiSelector()} - {!this.state.currentAPI && No API } - {AppState.getPatternSelector() && - this.state.theresPattern && - this.state.patternList && - this.state.patternList.length > 1 && - this.buildPatternSelector()} -
- )}
@@ -753,7 +841,7 @@ class WzMenu extends Component { > )} - { this.state.isToolsPopoverOpen && ( + {this.state.isToolsPopoverOpen && ( this.setState({ menuOpened: false })} @@ -815,7 +903,7 @@ class WzMenu extends Component { const logotype_url = getHttp().basePath.prepend('/plugins/wazuh/assets/logotype.svg'); const mainButton = ( -
{this.props.resultState === 'loading' && ( -
+
) } diff --git a/public/controllers/agent/components/agents-preview.scss b/public/controllers/agent/components/agents-preview.scss new file mode 100644 index 0000000000..1195d3440f --- /dev/null +++ b/public/controllers/agent/components/agents-preview.scss @@ -0,0 +1,42 @@ +.flex-column { + flex-direction: column; +} + +.mt-0 { + margin-top: 0px; +} + +.loading-chart { + margin: 75px auto; +} + +.eui-panel { + padding-bottom: 0px; + min-height: 168px; + min-width: 350px; +} + +.align-items-center { + align-items: center; +} + +.group-details { + padding: 12px 0px; +} + +.white-space-nowrap { + white-space: nowrap; +} + +.pb-12 { + padding-bottom: 12px; +} + +.p-30 { +} + +.loading-chart-xl { + display: block; + text-align: center; + padding: 30px; +} diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 085689fb37..1abd76464c 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -32,7 +32,8 @@ import { EuiCallOut, EuiSpacer, EuiProgress, - EuiCode + EuiCode, + EuiLink } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { withErrorBoundary } from '../../../components/common/hocs' @@ -287,7 +288,7 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp // macos doesnt need = param if (this.state.selectedOS === 'macos') { - return deployment.replaceAll('=', ' '); + return deployment.replace(/=/g, ' '); } return deployment; @@ -414,13 +415,13 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp }; const customTexts = { rpmText: `sudo ${this.optionalDeploymentVariables()}yum install ${this.optionalPackages()}`, - debText: `curl -so wazuh-agent.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent.deb`, - macosText: `curl -so wazuh-agent.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${ + debText: `curl -so wazuh-agent-${this.state.wazuhVersion}.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent-${this.state.wazuhVersion}.deb`, + macosText: `curl -so wazuh-agent-${this.state.wazuhVersion}.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${ this.state.wazuhVersion - }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent.pkg -target /`, + }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${this.state.wazuhVersion}.pkg -target /`, winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${ this.state.wazuhVersion - }-1.msi -OutFile wazuh-agent.msi; ./wazuh-agent.msi /q ${this.optionalDeploymentVariables()}`, + }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${this.state.wazuhVersion}.msi /q ${this.optionalDeploymentVariables()}`, }; const field = `${this.state.selectedOS}Text`; @@ -444,9 +445,13 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp
{this.state.selectedOS && ( -

- You can use this command to install and enroll the Wazuh agent in one or more hosts. -

+

You can use this command to install and enroll the Wazuh agent in one or more hosts.

+ Running this command on a host with an agent already installed upgrades the agent package without enrolling the agent. To enroll it, see the Wazuh documentation.} + iconType="iInCircle" + /> + {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} @@ -516,6 +521,7 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp children: ( this.selectOS(os)} @@ -529,6 +535,7 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp children: ( this.setVersion(version)} @@ -544,6 +551,7 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp children: ( this.setArchitecture(architecture)} @@ -560,6 +568,7 @@ export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Comp children: ( this.setArchitecture(architecture)} diff --git a/public/controllers/management/components/management/configuration/configuration-main.js b/public/controllers/management/components/management/configuration/configuration-main.js index b5993a92a8..7b7dc0af08 100644 --- a/public/controllers/management/components/management/configuration/configuration-main.js +++ b/public/controllers/management/components/management/configuration/configuration-main.js @@ -26,7 +26,7 @@ export default compose( if (props.agent.id === '000') { breadcrumb = [ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Configuration' }, ]; } else { diff --git a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js index e189e1d1c7..a3271873c6 100644 --- a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js +++ b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js @@ -59,13 +59,16 @@ class WzEditConfiguration extends Component { this.state = { xml: '', editorValue: '', + initialValue: '', restart: false, restarting: false, saving: false, + hasChanges: false, infoChangesAfterRestart: false, disableSaveRestartButtons: false }; } + addToast(toast){ getToasts().add(toast); } @@ -137,11 +140,18 @@ class WzEditConfiguration extends Component { this.setState({ editorValue }); } onDidMount(xmlFetched, errorXMLFetched) { - this.setState({ editorValue: xmlFetched, disableSaveRestartButtons: errorXMLFetched}); + this.setState({ editorValue: xmlFetched, disableSaveRestartButtons: errorXMLFetched,initialValue: xmlFetched}); } onLoadingConfiguration(disableSaveRestartButtons) { this.setState({ disableSaveRestartButtons }); } + + componentDidUpdate(prevProps, prevState) { + if (prevState.editorValue !== this.state.editorValue) { + this.setState({ hasChanges: this.state.editorValue !== this.state.initialValue }); + } + } + async confirmRestart() { try { this.setState({ restarting: true, saving: true, infoChangesAfterRestart: false }); @@ -216,6 +226,7 @@ class WzEditConfiguration extends Component { } render() { const { restart, restarting, saving, editorValue, disableSaveRestartButtons } = this.state; + const initialValue = editorValue; const { clusterNodeSelected, agent } = this.props; const xmlError = editorValue && validateXML(editorValue); return ( @@ -223,6 +234,7 @@ class WzEditConfiguration extends Component { diff --git a/public/controllers/management/components/management/configuration/util-components/configuration-path.js b/public/controllers/management/components/management/configuration/util-components/configuration-path.js index 0e25462a29..853da89e98 100644 --- a/public/controllers/management/components/management/configuration/util-components/configuration-path.js +++ b/public/controllers/management/components/management/configuration/util-components/configuration-path.js @@ -22,15 +22,20 @@ import { EuiSpacer, EuiTitle, EuiText, + EuiConfirmModal, EuiIcon } from '@elastic/eui'; import WzBadge from '../util-components/badge'; import WzClusterSelect from './configuration-cluster-selector'; +import { WzOverlayMask } from '../../../../../../components/common/util'; class WzConfigurationPath extends Component { constructor(props) { super(props); + this.state = { + isModalVisible: false, + }; } render() { const { @@ -39,8 +44,34 @@ class WzConfigurationPath extends Component { icon, updateConfigurationSection, badge, + hasChanges, children } = this.props; + + const closeModal = () => this.setState({ isModalVisible: false }); + const showModal = () => this.setState({ isModalVisible: true }); + + let modal; + if (this.state.isModalVisible) { + modal = ( + + { + closeModal; + updateConfigurationSection(''); + }} + onCancel={closeModal} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } return ( @@ -53,7 +84,13 @@ class WzConfigurationPath extends Component { style={{ padding: 0 }} iconType="arrowLeft" iconSize="l" - onClick={() => updateConfigurationSection('')} + onClick={() => { + if (hasChanges) { + showModal(); + } else { + updateConfigurationSection(''); + } + }} aria-label="back to configuration" /> @@ -83,6 +120,7 @@ class WzConfigurationPath extends Component { )} + {modal} ); } @@ -93,6 +131,7 @@ WzConfigurationPath.propTypes = { description: PropTypes.string, icon: PropTypes.string, updateConfigurationSection: PropTypes.func, + hasChanges: PropTypes.bool, badge: PropTypes.bool }; diff --git a/public/controllers/management/components/management/groups/groups-editor.js b/public/controllers/management/components/management/groups/groups-editor.js index 9c0a68d801..b0b92e79a8 100644 --- a/public/controllers/management/components/management/groups/groups-editor.js +++ b/public/controllers/management/components/management/groups/groups-editor.js @@ -25,6 +25,7 @@ import { EuiToolTip, EuiButtonIcon, EuiCodeEditor, + EuiConfirmModal, EuiPanel, EuiCodeBlock } from '@elastic/eui'; @@ -34,6 +35,7 @@ import GroupsHandler from './utils/groups-handler'; import { getToasts } from '../../../../../kibana-services'; import { validateXML } from '../configuration/utils/xml'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; +import { WzOverlayMask } from '../../../../../components/common/util'; import 'brace/theme/textmate'; import 'brace/mode/xml'; import 'brace/snippets/xml'; @@ -48,7 +50,7 @@ class WzGroupsEditor extends Component { fontSize: '14px', enableBasicAutocompletion: true, enableSnippets: true, - enableLiveAutocompletion: true + enableLiveAutocompletion: true, }; this.groupsHandler = GroupsHandler; const { fileContent } = this.props.state; @@ -60,8 +62,11 @@ class WzGroupsEditor extends Component { isSaving: false, content, name, + isModalVisible: false, + hasChanges: false, isEditable, - groupName: groupName + initContent: content, + groupName: groupName, }; } @@ -70,6 +75,12 @@ class WzGroupsEditor extends Component { this.forceUpdate(); }; + componentDidUpdate(prevProps, prevState) { + if (prevState.content !== this.state.content) { + this.setState({ hasChanges: this.state.content !== this.state.initContent }); + } + } + componentWillUnmount() { // When the component is going to be unmounted its info is clear this._isMounted = false; @@ -88,7 +99,6 @@ class WzGroupsEditor extends Component { * @param {String} name */ async save(name) { - if (!this._isMounted) { return; } @@ -101,7 +111,7 @@ class WzGroupsEditor extends Component { await validateConfigAfterSent(); } catch (error) { const warning = Object.assign(error, { - savedMessage: `File ${name} saved, but there were found several error while validating the configuration.` + savedMessage: `File ${name} saved, but there were found several error while validating the configuration.`, }); this.setState({ isSaving: false }); this.showToast('warning', warning.savedMessage, error, 3000); @@ -112,12 +122,7 @@ class WzGroupsEditor extends Component { this.showToast('success', 'Success', textSuccess, 3000); } catch (error) { this.setState({ error, isSaving: false }); - this.showToast( - 'danger', - 'Error', - 'Error saving group configuration: ' + error, - 3000 - ); + this.showToast('danger', 'Error', 'Error saving group configuration: ' + error, 3000); } } @@ -126,7 +131,7 @@ class WzGroupsEditor extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; @@ -136,88 +141,121 @@ class WzGroupsEditor extends Component { const xmlError = validateXML(content); const saveButton = ( this.save(name)} > - {(isEditable && xmlError) ? 'XML format error' : 'Save'} + {isEditable && xmlError ? 'XML format error' : 'Save'} ); + const closeModal = () => this.setState({ isModalVisible: false }); + const showModal = () => this.setState({ isModalVisible: true }); + + let modal; + if (this.state.isModalVisible) { + modal = ( + + { + closeModal; + this.props.cleanFileContent(); + }} + onCancel={closeModal} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } return ( - - - - - {/* File name and back button */} - - - - - - this.props.cleanFileContent()} - /> - - {name} of {groupName} group - - - - - {isEditable && ( - {saveButton} + <> + + + + + {/* File name and back button */} + + + + + + { + if (this.state.hasChanges) { + showModal(); + } else { + this.props.cleanFileContent(); + } + }} + /> + + {name} of {groupName}{' '} + group + + + + + {isEditable && {saveButton}} + + + {xmlError && ( + + {xmlError} + + )} - - - {xmlError && ( - - {xmlError} - - - )} - - - - - {(isEditable && ( - - this.setState({content: newContent}) - } - mode="xml" - wrapEnabled - setOptions={this.codeEditorOptions} - aria-label="Code Editor" - /> - )) || ( - - {content} - - )} - - - - - - - - + + + + + {(isEditable && ( + this.setState({ content: newContent })} + mode="xml" + wrapEnabled + setOptions={this.codeEditorOptions} + aria-label="Code Editor" + /> + )) || ( + + {content} + + )} + + + + +
+ + + + {modal} + ); } } diff --git a/public/controllers/management/components/management/groups/groups-main.js b/public/controllers/management/components/management/groups/groups-main.js index 0e33a91319..0464938323 100644 --- a/public/controllers/management/components/management/groups/groups-main.js +++ b/public/controllers/management/components/management/groups/groups-main.js @@ -24,6 +24,7 @@ import { } from '../../../../../redux/actions/groupsActions'; import { connect } from 'react-redux'; import { updateGlobalBreadcrumb } from '../../../../../redux/actions/globalBreadcrumbActions'; +import { WzRequest } from '../../../../../react-services/wz-request'; class WzGroups extends Component { constructor(props) { @@ -33,14 +34,25 @@ class WzGroups extends Component { setGlobalBreadcrumb() { const breadcrumb = [ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Groups' } ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); } - componentDidMount() { + async componentDidMount() { this.setGlobalBreadcrumb(); + // Check if there is a group in the URL + const [_, group] = window.location.href.match(new RegExp('group=' + '([^&]*)')) || []; + window.location.href = window.location.href.replace(new RegExp('group=' + '[^&]*'), ''); + if(group){ + try{ + // Try if the group can be accesed + const responseGroup = await WzRequest.apiReq('GET', '/groups', {params: {groups_list: group}}); + const dataGroup = responseGroup?.data?.data?.affected_items?.[0]; + this.props.updateGroupDetail(dataGroup); + }catch(error){}; + }; } UNSAFE_componentWillReceiveProps(nextProps) { @@ -87,7 +99,8 @@ const mapDispatchToProps = dispatch => { return { resetGroup: () => dispatch(resetGroup()), updateShowAddAgents: showAddAgents => - dispatch(updateShowAddAgents(showAddAgents)) + dispatch(updateShowAddAgents(showAddAgents)), + updateGroupDetail: groupDetail => dispatch(updateGroupDetail(groupDetail)) }; }; export default connect( diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index 87abf53fb3..a8b61e9612 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -39,7 +39,7 @@ import { WzFieldSearch } from '../../../../../components/wz-field-search-bar/wz- export default compose( withGlobalBreadcrumb([ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Logs' } ]), withUserAuthorizationPrompt([{action: 'cluster:status', resource: '*:*:*'}, {action: 'cluster:read', resource: 'node:id:*'}]) diff --git a/public/controllers/management/components/management/reporting/reporting-main.js b/public/controllers/management/components/management/reporting/reporting-main.js index 35775d138c..af225e5aeb 100644 --- a/public/controllers/management/components/management/reporting/reporting-main.js +++ b/public/controllers/management/components/management/reporting/reporting-main.js @@ -26,7 +26,7 @@ class WzReporting extends Component { setGlobalBreadcrumb() { const breadcrumb = [ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Reporting' } ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index ddeea389d0..01fe585374 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -29,6 +29,7 @@ import { EuiButtonIcon, EuiButtonEmpty, EuiFieldText, + EuiConfirmModal, EuiCodeEditor, EuiPanel, } from '@elastic/eui'; @@ -47,6 +48,8 @@ import 'brace/snippets/xml'; import 'brace/ext/language_tools'; import "brace/ext/searchbox"; import { showFlyoutLogtest } from '../../../../../redux/actions/appStateActions'; +import { WzOverlayMask } from '../../../../../components/common/util'; +import _ from 'lodash'; class WzRulesetEditor extends Component { _isMounted = false; @@ -60,23 +63,23 @@ class WzRulesetEditor extends Component { animatedScroll: true, enableBasicAutocompletion: true, enableSnippets: true, - enableLiveAutocompletion: false + enableLiveAutocompletion: false, }; this.rulesetHandler = new RulesetHandler(this.props.state.section); const { fileContent, addingRulesetFile } = this.props.state; - const { name, content, path } = fileContent - ? fileContent - : addingRulesetFile; + const { name, content, path } = fileContent ? fileContent : addingRulesetFile; this.state = { isSaving: false, error: false, inputValue: '', + initialInputValue: '', showWarningRestart: false, - content, + isModalVisible: false, initContent: content, + content, name, - path + path, }; } @@ -90,6 +93,7 @@ class WzRulesetEditor extends Component { this._isMounted = true; } + /** * Save the new content * @param {String} name @@ -127,7 +131,7 @@ class WzRulesetEditor extends Component { toast.toastMessage += '\nThe content file was restored to previous state.'; } - getToasts().addError({stack: error, message: toast.toastMessage}, toast); + getToasts().addError({ stack: error, message: toast.toastMessage }, toast); return; } @@ -138,16 +142,15 @@ class WzRulesetEditor extends Component { if (overwrite) { textSuccess = 'File successfully edited'; } - this.setState({ showWarningRestart: true }); + this.setState({ + showWarningRestart: true, + initialInputValue: this.state.inputValue, + initContent: content, + }); this.showToast('success', 'Success', textSuccess, 3000); } catch (error) { this.setState({ error, isSaving: false }); - this.showToast( - 'danger', - 'Error', - 'Error saving file: ' + error, - 3000 - ); + this.showToast('danger', 'Error', 'Error saving file: ' + error, 3000); } } @@ -156,11 +159,11 @@ class WzRulesetEditor extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; - goToEdit = name => { + goToEdit = (name) => { const { content, path } = this.state; const file = { name: name, content: content, path: path }; this.props.updateFileContent(file); @@ -169,18 +172,14 @@ class WzRulesetEditor extends Component { /** * onChange the input value in case adding new file */ - onChange = e => { + onChange = (e) => { this.setState({ - inputValue: e.target.value + inputValue: e.target.value, }); }; render() { - const { - section, - addingRulesetFile, - fileContent - } = this.props.state; + const { section, addingRulesetFile, fileContent } = this.props.state; const { wazuhNotReadyYet } = this.props; const { name, content, path, showWarningRestart } = this.state; const isRules = path.includes('rules') ? 'Ruleset Test' : 'Decoders Test'; @@ -189,9 +188,7 @@ class WzRulesetEditor extends Component { ? true : path !== 'ruleset/rules' && path !== 'ruleset/decoders'; let nameForSaving = addingRulesetFile ? this.state.inputValue : name; - nameForSaving = nameForSaving.endsWith('.xml') - ? nameForSaving - : `${nameForSaving}.xml`; + nameForSaving = nameForSaving.endsWith('.xml') ? nameForSaving : `${nameForSaving}.xml`; const overwrite = fileContent ? true : false; const xmlError = validateXML(content); @@ -199,7 +196,7 @@ class WzRulesetEditor extends Component { const onClickOpenLogtest = () => { this.props.logtestProps.openCloseFlyout(); this.props.showFlyoutLogtest(true); - } + }; const buildLogtestButton = () => { return ( @@ -229,7 +226,7 @@ class WzRulesetEditor extends Component { fill iconType={isEditable && xmlError ? 'alert' : 'save'} isLoading={this.state.isSaving} - isDisabled={nameForSaving.length <= 4 || (!!(isEditable && xmlError))} + isDisabled={nameForSaving.length <= 4 || !!(isEditable && xmlError)} onClick={() => this.save(nameForSaving, overwrite)} > {isEditable && xmlError ? 'XML format error' : 'Save'} @@ -237,106 +234,157 @@ class WzRulesetEditor extends Component { ); + const closeModal = () => this.setState({ isModalVisible: false }); + const showModal = () => this.setState({ isModalVisible: true }); + + let modal; + if (this.state.isModalVisible) { + modal = ( + + { + closeModal; + this.props.cleanInfo(); + }} + onCancel={closeModal} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, do it" + > +

+ There are unsaved changes. Are you sure you want to proceed? +

+
+
+ ); + } return ( - - - - - {/* File name and back button */} - - - {(!fileContent && ( - - - - this.props.cleanInfo()} + <> + + + + + {/* File name and back button */} + + + {(!fileContent && ( + + + + { + if ( + this.state.content !== this.state.initContent || + this.state.inputValue !== this.state.initialInputValue + ) { + showModal(); + } else { + this.props.cleanInfo(); + } + }} + /> + + + + - - - - - - - )) || ( + + + )) || ( - + this.props.cleanInfo()} + onClick={() => { + if ( + this.state.content !== this.state.initContent || + this.state.inputValue !== this.state.initialInputValue + ) { + showModal(); + } else { + this.props.cleanInfo(); + } + }} /> {nameForSaving} )} - - - {/* This flex item is for separating between title and save button */} - {isEditable && ( - {headerButtons} - )} - - - {this.state.showWarningRestart && ( - - this.setState({ showWarningRestart: false })} - onRestartedError={() => this.setState({ showWarningRestart: true })} - /> - - - )} - {xmlError && ( - - {xmlError} - - - )} - - - - - {this.setState({ content: newContent })}} - mode="xml" - isReadOnly={!isEditable} - wrapEnabled - setOptions={this.codeEditorOptions} - aria-label="Code Editor" - /> + + + {/* This flex item is for separating between title and save button */} + {isEditable && ( + + {headerButtons} - - - - - - - + )} + + + {this.state.showWarningRestart && ( + + this.setState({ showWarningRestart: false })} + onRestartedError={() => this.setState({ showWarningRestart: true })} + /> + + + )} + {xmlError && ( + + {xmlError} + + + )} + + + + + { + this.setState({ content: newContent }); + }} + mode="xml" + isReadOnly={!isEditable} + wrapEnabled + setOptions={this.codeEditorOptions} + aria-label="Code Editor" + /> + + + + + + + + + {modal} + ); } } diff --git a/public/controllers/management/components/management/ruleset/ruleset-overview.js b/public/controllers/management/components/management/ruleset/ruleset-overview.js index 81781fe793..d6d31a2924 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-overview.js +++ b/public/controllers/management/components/management/ruleset/ruleset-overview.js @@ -109,7 +109,7 @@ export default compose( } return [ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: sectionNames[props.state.section] } ]; }), diff --git a/public/controllers/management/components/management/statistics/statistics-overview.js b/public/controllers/management/components/management/statistics/statistics-overview.js index ec9833855e..a29a8c6b44 100644 --- a/public/controllers/management/components/management/statistics/statistics-overview.js +++ b/public/controllers/management/components/management/statistics/statistics-overview.js @@ -237,7 +237,7 @@ export class WzStatisticsOverview extends Component { export default compose( withGlobalBreadcrumb([ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Statistics' } ]), withGuard(props => { diff --git a/public/controllers/management/components/management/status/status-overview.js b/public/controllers/management/components/management/status/status-overview.js index b444f27189..4c649d70cc 100644 --- a/public/controllers/management/components/management/status/status-overview.js +++ b/public/controllers/management/components/management/status/status-overview.js @@ -250,7 +250,7 @@ const mapDispatchToProps = dispatch => { export default compose( withGlobalBreadcrumb([ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Status' } ]), withUserAuthorizationPrompt([ diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index f66fe387a1..30d2670441 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -148,6 +148,20 @@ export class ManagementController { : this.restartManager(); }); + this.$rootScope.timeoutIsReady; + this.$rootScope.$watch('resultState', () => { + if(this.$rootScope.timeoutIsReady){ + clearTimeout(this.$rootScope.timeoutIsReady); + } + if(this.$rootScope.resultState === 'ready'){ + this.$scope.isReady = true; + } + else + { + this.$rootScope.timeoutIsReady = setTimeout(() => this.$scope.isReady = false, 1000); + } + }) + this.welcomeCardsProps = { switchTab: (tab, setNav) => this.switchTab(tab, setNav) }; @@ -186,6 +200,7 @@ export class ManagementController { }, logtestProps: this.logtestProps, }; + } /** @@ -410,7 +425,7 @@ export class ManagementController { openCloseFlyout() { this.logtestOpened = !this.logtestOpened; - this.logtestProps.isRuleset = this.tab, + this.logtestProps.isRuleset = this.tab; this.$scope.$applyAsync(); } diff --git a/public/controllers/management/monitoring.js b/public/controllers/management/monitoring.js index 51e14f1857..12d3e9fded 100644 --- a/public/controllers/management/monitoring.js +++ b/public/controllers/management/monitoring.js @@ -319,7 +319,7 @@ export function ClusterController( const breadcrumb = [ { text: '' }, - { text: 'Management', href: '/app/wazuh#/manager' }, + { text: 'Management', href: '#/manager' }, { text: 'Cluster' } ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); diff --git a/public/controllers/settings/index.js b/public/controllers/settings/index.js index c191d1b29f..2188922d2e 100644 --- a/public/controllers/settings/index.js +++ b/public/controllers/settings/index.js @@ -16,6 +16,7 @@ import { ApiIsDown } from '../../components/settings/api/api-is-down'; import { EnableModules } from '../../components/settings/modules/modules'; import { WzConfigurationSettings } from '../../components/settings/configuration/configuration'; import SettingsLogs from '../../components/settings/settings-logs/logs'; +import { SettingsMiscellaneous } from '../../components/settings/miscellaneous/miscellaneous'; import {WzSampleDataWrapper} from '../../components/add-modules-data/WzSampleDataWrapper' import { getAngularModule } from '../../kibana-services'; @@ -26,6 +27,7 @@ app .value('WzSampleDataWrapper', WzSampleDataWrapper) .value('WzConfigurationSettings', WzConfigurationSettings) .value('SettingsLogs', SettingsLogs) + .value('SettingsMiscelaneous', SettingsMiscellaneous) .value('ApiTable', ApiTable) .value('AddApi', AddApi) .value('ApiIsDown', ApiIsDown); diff --git a/public/controllers/settings/settings.js b/public/controllers/settings/settings.js index 3246180c41..ca614c72ba 100644 --- a/public/controllers/settings/settings.js +++ b/public/controllers/settings/settings.js @@ -126,6 +126,7 @@ export class SettingsController { { id: 'sample_data', name: 'Sample data' }, { id: 'configuration', name: 'Configuration' }, { id: 'logs', name: 'Logs' }, + { id: 'miscellaneous', name: 'Miscellaneous'}, { id: 'about', name: 'About' } ]; this.settingsTabsProps = { diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index adf4f91a3a..d8c7d62bae 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -29,6 +29,9 @@ import { import { WzRequest } from '../../../react-services'; import { withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; import { compose } from 'redux'; +import { useSelector, useDispatch } from 'react-redux'; +import { updateLogtestToken } from '../../../redux/actions/appStateActions'; +import { WzButtonPermissionsModalConfirm } from '../../../components/common/buttons'; type LogstestProps = { openCloseFlyout: () => {}; @@ -42,12 +45,14 @@ export const Logtest = compose( withReduxProvider, withUserAuthorizationPrompt([{ action: 'logtest:run', resource: `*:*:*` }]) )((props: LogstestProps) => { - const [value, setValue] = useState([]); + const [events, setEvents] = useState([]); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(''); + const dispatch = useDispatch(); + const sessionToken = useSelector((state)=> state.appStateReducers.logtestToken); const onChange = (e) => { - setValue(e.target.value.split('\n').filter((item) => item)); + setEvents(e.target.value.split('\n').filter((item) => item)); }; const formatResult = (result, alert) => { @@ -85,21 +90,28 @@ export const Logtest = compose( const runAllTests = async () => { setTestResult(''); setTesting(true); + let token = sessionToken; + const responses = []; + let gotToken = Boolean(token); + try { - const responsesLogtest = await Promise.all( - value.map(async (event) => { - const body = { - log_format: 'syslog', - location: 'logtest', - event: event, - }; - return await WzRequest.apiReq('PUT', '/logtest', body); - }) - ); - const testResults = responsesLogtest.map((response) => + for (let event of events) { + const response = await WzRequest.apiReq('PUT', '/logtest', { + log_format: 'syslog', + location: 'logtest', + event, + ...(token ? { token }: {}) + }); + token = response.data.data.token; + !sessionToken && !gotToken && token && dispatch(updateLogtestToken(token)); + token && (gotToken = true); + responses.push(response); + }; + + const testResults = responses.map((response) => response.data.data.output.rule || '' ? formatResult(response.data.data.output, response.data.data.alert) - : `No result found for: ${response.data.data.output.full_log} \n\n\n` + : `No result found for: ${response.data.data.output.full_log} \n\n\n` ); setTestResult(testResults); } finally { @@ -113,6 +125,17 @@ export const Logtest = compose( } }; + const deleteToken = async() =>{ + try { + const response = await WzRequest.apiReq('DELETE', `/logtest/sessions/${sessionToken}`, {}); + dispatch(updateLogtestToken('')); + setTestResult(''); + } + catch(error) { + this.showToast('danger', 'Error', `Error trying to delete logtest token due to: ${error.message || error}`); + } + } + const buildLogtest = () => { return ( @@ -125,11 +148,12 @@ export const Logtest = compose( onKeyPress={handleKeyPress} /> + + + { + deleteToken(); + }} + color="danger" + modalTitle={`Do you want to clear current session?`} + modalProps={{ + buttonColor: 'danger', + children: 'Clearing the session means the logs execution history is removed. This affects to rules that fire an alert when similar logs are executed in a specific range of time.' + }} + > + Clear session + + + (((item || {}).vis || {}).type || {}).name === 'table' ); for (let i = 0; i < tables.length; i++) { + let tablesData = []; const columns = []; const title = tables[i].vis.title || 'Table'; const item = await tables[i].handler.execution.getData(); - for (const table of item.value.visData.tables) { + + //Normalize visData tables structure for 7.12 retro-compatibility + if(item.value.visData.tables?.length) + tablesData = item.value.visData.tables; + else if(item.value.visData.table) + tablesData.push(item.value.visData.table); + + for (const table of tablesData) { columns.push(...table.columns.map(t => t.name)); } + tables[i] = !!( - ((((item || {}).value || {}).visData || {}).tables || [])[0] || {} + tablesData[0] || {} ).rows ? { - rows: item.value.visData.tables[0].rows.map(x => { + rows: tablesData[0].rows.map(x => { return Object.values(x); }), title, diff --git a/public/kibana-integrations/kibana-discover.js b/public/kibana-integrations/kibana-discover.js index 9058c4b4e1..0adde99731 100644 --- a/public/kibana-integrations/kibana-discover.js +++ b/public/kibana-integrations/kibana-discover.js @@ -30,6 +30,7 @@ import { subscribeWithScope, tabifyAggResponse, getHeaderActionMenuMounter, + setUiActions } from './discover/kibana_services'; import indexTemplateLegacy from './discover/application/angular/discover_legacy.html'; @@ -114,6 +115,7 @@ appDiscover.run(async () => { () => getAngularModule().$injector ); setServices(services); + setUiActions(getPlugins().uiActions); }); const wazuhApp = getAngularModule(); diff --git a/public/kibana-integrations/kibana-vis.js b/public/kibana-integrations/kibana-vis.js index 096e937868..cef5cf94d3 100644 --- a/public/kibana-integrations/kibana-vis.js +++ b/public/kibana-integrations/kibana-vis.js @@ -9,21 +9,21 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from "react"; - -import $ from "jquery"; -import dateMath from "@elastic/datemath"; -import { DiscoverPendingUpdates } from "../factories/discover-pending-updates"; -import { connect } from "react-redux"; -import { LoadedVisualizations } from "../factories/loaded-visualizations"; -import { RawVisualizations } from "../factories/raw-visualizations"; -import { VisHandlers } from "../factories/vis-handlers"; -import { WzRequest } from '../react-services/wz-request'; -import { TabVisualizations } from "../factories/tab-visualizations"; -import store from "../redux/store"; -import { updateMetric } from "../redux/actions/visualizationsActions"; -import { GenericRequest } from "../react-services/generic-request"; -import { createSavedVisLoader } from "./visualizations/saved_visualizations"; +import React, { Component } from 'react'; + +import $ from 'jquery'; +import dateMath from '@elastic/datemath'; +import { DiscoverPendingUpdates } from '../factories/discover-pending-updates'; +import { connect } from 'react-redux'; +import { LoadedVisualizations } from '../factories/loaded-visualizations'; +import { RawVisualizations } from '../factories/raw-visualizations'; +import { VisHandlers } from '../factories/vis-handlers'; +import { AppState } from '../react-services'; +import { TabVisualizations } from '../factories/tab-visualizations'; +import store from '../redux/store'; +import { updateMetric } from '../redux/actions/visualizationsActions'; +import { GenericRequest } from '../react-services/generic-request'; +import { createSavedVisLoader } from './visualizations/saved_visualizations'; import { EuiLoadingChart, EuiLoadingSpinner, @@ -31,21 +31,29 @@ import { EuiIcon, EuiFlexItem, EuiFlexGroup, -} from "@elastic/eui"; -import { getAngularModule, getToasts, getVisualizationsPlugin, getSavedObjects, getDataPlugin, getChrome, getOverlays } from '../kibana-services'; -import { KnownFields } from "../utils/known-fields"; +} from '@elastic/eui'; +import { + getAngularModule, + getToasts, + getVisualizationsPlugin, + getSavedObjects, + getDataPlugin, + getChrome, + getOverlays, + getPlugins, +} from '../kibana-services'; +import { KnownFields } from '../utils/known-fields'; import { union } from 'lodash'; import { getFilterWithAuthorizedAgents } from '../react-services/filter-authorization-agents'; import { AUTHORIZED_AGENTS } from '../../common/constants'; - class KibanaVis extends Component { _isMounted = false; constructor(props) { super(props); this.lockFields = false; - this.implicitFilter = ""; + this.implicitFilter = ''; this.rawFilters = []; this.rendered = false; this.visualization = null; @@ -62,24 +70,22 @@ class KibanaVis extends Component { this.tabVisualizations = new TabVisualizations(); this.state = { visRefreshingIndex: false, - }; + }; const services = { savedObjectsClient: getSavedObjects().client, indexPatterns: getDataPlugin().indexPatterns, search: getDataPlugin().search, chrome: getChrome(), overlays: getOverlays(), + savedObjects: getPlugins().savedObjects, }; const servicesForVisualizations = { ...services, ...{ visualizationTypes: getVisualizationsPlugin() }, }; - this.savedObjectLoaderVisualize = createSavedVisLoader( - servicesForVisualizations - ); + this.savedObjectLoaderVisualize = createSavedVisLoader(servicesForVisualizations); this.visID = this.props.visID; this.tab = this.props.tab; - } showToast = (color, title, text, time) => { @@ -94,7 +100,7 @@ class KibanaVis extends Component { componentDidMount() { this._isMounted = true; const app = getAngularModule(); - this.$rootScope = app.$injector.get("$rootScope"); + this.$rootScope = app.$injector.get('$rootScope'); } componentWillUnmount() { @@ -130,13 +136,12 @@ class KibanaVis extends Component { data.value && data.value.visData && data.value.visData.rows && - this.props.state[this.visID] !== - data.value.visData.rows["0"]["col-0-1"] + this.props.state[this.visID] !== data.value.visData.rows['0']['col-0-1'] ) { store.dispatch( this.updateMetric({ name: this.visID, - value: data.value.visData.rows["0"]["col-0-1"], + value: data.value.visData.rows['0']['col-0-1'], }) ); } @@ -148,22 +153,21 @@ class KibanaVis extends Component { data.value.visData && data.value.visData.tables && data.value.visData.tables.length && - data.value.visData.tables["0"] && - data.value.visData.tables["0"].rows && - data.value.visData.tables["0"].rows["0"] && - this.props.state[this.visID] !== - data.value.visData.tables["0"].rows["0"]["col-0-2"] + data.value.visData.tables['0'] && + data.value.visData.tables['0'].rows && + data.value.visData.tables['0'].rows['0'] && + this.props.state[this.visID] !== data.value.visData.tables['0'].rows['0']['col-0-2'] ) { store.dispatch( this.updateMetric({ name: this.visID, - value: data.value.visData.tables["0"].rows["0"]["col-0-2"], + value: data.value.visData.tables['0'].rows['0']['col-0-2'], }) ); } } } catch (error) { - this.showToast("danger", "Error", error.message || error, 4000); + this.showToast('danger', 'Error', error.message || error, 4000); } } @@ -180,7 +184,7 @@ class KibanaVis extends Component { setSearchSource = (discoverList) => { try { - const isCluster = this.visID.includes("Cluster"); + const isCluster = this.visID.includes('Cluster'); if (isCluster) { // Checks for cluster.name or cluster.node filter existence const monitoringFilter = discoverList[1].filter( @@ -188,17 +192,16 @@ class KibanaVis extends Component { item && item.meta && item.meta.key && - (item.meta.key.includes("cluster.name") || - item.meta.key.includes("cluster.node")) + (item.meta.key.includes('cluster.name') || item.meta.key.includes('cluster.node')) ); // Applying specific filter to cluster monitoring vis if (Array.isArray(monitoringFilter) && monitoringFilter.length) { - this.visualization.searchSource.setField("filter", monitoringFilter); + this.visualization.searchSource.setField('filter', monitoringFilter); } } } catch (error) { - this.showToast("danger", "Error", error.message || error, 4000); + this.showToast('danger', 'Error', error.message || error, 4000); } }; @@ -206,16 +209,39 @@ class KibanaVis extends Component { const timefilter = getDataPlugin().query.timefilter.timefilter; try { const discoverList = this.discoverPendingUpdates.getList(); - const isAgentStatus = - this.visID === "Wazuh-App-Overview-General-Agents-status"; - const timeFilterSeconds = this.calculateTimeFilterSeconds( - timefilter.getTime() - ); + const isAgentStatus = this.visID === 'Wazuh-App-Overview-General-Agents-status'; + const timeFilterSeconds = this.calculateTimeFilterSeconds(timefilter.getTime()); const timeRange = isAgentStatus && timeFilterSeconds < 900 - ? { from: "now-15m", to: "now", mode: "quick" } + ? { from: 'now-15m', to: 'now', mode: 'quick' } : timefilter.getTime(); - let filters = isAgentStatus ? [] : discoverList[1] || []; + let filters = isAgentStatus + ? [ + { + meta: { + index: 'wazuh-monitoring-*', + alias: null, + negate: false, + disabled: false, + }, + query: { + bool: { + should: [ + { + term: + AppState.getClusterInfo().status === 'enabled' + ? { 'cluster.name': AppState.getClusterInfo().cluster } + : { 'manager.keyword': AppState.getClusterInfo().manager }, + }, + ], + }, + }, + $state: { + store: 'appState', + }, + }, + ] + : discoverList[1] || []; const query = !isAgentStatus ? discoverList[0] : {}; const rawVis = raw ? raw.filter((item) => item && item.id === this.visID) : []; @@ -223,7 +249,8 @@ class KibanaVis extends Component { if (rawVis.length && discoverList.length) { let vizPattern; try { - vizPattern = JSON.parse(rawVis[0].attributes.kibanaSavedObjectMeta.searchSourceJSON).index; + vizPattern = JSON.parse(rawVis[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) + .index; } catch (ex) { console.warn(`kibana-vis exception: ${ex.message || ex}`); } @@ -236,7 +263,7 @@ class KibanaVis extends Component { const visInput = { timeRange, filters, - query + query, }; // There are pending updates from the discover (which is the one who owns the true app state) @@ -244,17 +271,19 @@ class KibanaVis extends Component { if (!this.visualization && !this.rendered && !this.renderInProgress) { // There's no visualization object -> create it with proper filters this.renderInProgress = true; - this.visualization = await this.savedObjectLoaderVisualize.get( - this.visID, - rawVis[0] - ); + this.visualization = await this.savedObjectLoaderVisualize.get(this.visID, rawVis[0]); this.visualization.searchSource = await getDataPlugin().search.searchSource.create(); // Visualization doesn't need the "_source" - this.visualization.searchSource.setField("source", false); + this.visualization.searchSource.setField('source', false); // Visualization doesn't need "hits" this.visualization.searchSource.setField('size', 0); - const visState = await getVisualizationsPlugin().convertToSerializedVis(this.visualization); - const vis = await getVisualizationsPlugin().createVis(this.visualization.visState.type, visState); + const visState = await getVisualizationsPlugin().convertToSerializedVis( + this.visualization + ); + const vis = await getVisualizationsPlugin().createVis( + this.visualization.visState.type, + visState + ); this.visHandler = await getVisualizationsPlugin().__LEGACY.createVisEmbeddableFromObject( vis, visInput @@ -271,18 +300,14 @@ class KibanaVis extends Component { } this.rendered = true; - this.$rootScope.rendered = "true"; + this.$rootScope.rendered = 'true'; this.visHandler.updateInput(visInput); this.setSearchSource(discoverList); } if (this.state.visRefreshingIndex) this.setState({ visRefreshingIndex: false }); } } catch (error) { - if ( - ((error || {}).message || "").includes( - "not locate that index-pattern-field" - ) - ) { + if (((error || {}).message || '').includes('not locate that index-pattern-field')) { if (this.deadField) { this.tabVisualizations.addDeadVis(); return this.renderComplete(); @@ -297,9 +322,10 @@ class KibanaVis extends Component { this.renderInProgress = false; this.rendered = false; - // if there's a field name it looks for known fields structures - const foundField = (match[1] && KnownFields.find(field => field.name === match[1].trim())); - + // if there's a field name it looks for known fields structures + const foundField = + match[1] && KnownFields.find((field) => field.name === match[1].trim()); + await this.props.refreshKnownFields(foundField); } this.renderInProgress = false; @@ -315,11 +341,11 @@ class KibanaVis extends Component { destroyAll = () => { try { this.visualization.destroy(); - } catch (error) { } // eslint-disable-line + } catch (error) {} // eslint-disable-line try { this.visHandler.destroy(); this.visHandler = null; - } catch (error) { } // eslint-disable-line + } catch (error) {} // eslint-disable-line }; renderComplete = async () => { @@ -334,22 +360,18 @@ class KibanaVis extends Component { this.loadedVisualizations.addItem(true); const currentLoaded = this.loadedVisualizations.getList().length; - const deadVis = - this.props.tab === "ciscat" ? 0 : this.tabVisualizations.getDeadVis(); - const totalTabVis = - this.tabVisualizations.getItem(this.props.tab) - deadVis; - this.$rootScope.loadingStatus = "Fetching data..."; + const deadVis = this.props.tab === 'ciscat' ? 0 : this.tabVisualizations.getDeadVis(); + const totalTabVis = this.tabVisualizations.getItem(this.props.tab) - deadVis; + this.$rootScope.loadingStatus = 'Fetching data...'; if (totalTabVis < 1) { - this.$rootScope.resultState = "none"; + this.$rootScope.resultState = 'none'; } else { const currentCompleted = Math.round((currentLoaded / totalTabVis) * 100); if (currentCompleted >= 100) { - this.$rootScope.rendered = "true"; - if (visId.includes("AWS-geo")) { - const canvas = $( - ".visChart.leaflet-container .leaflet-control-zoom-in" - ); + this.$rootScope.rendered = 'true'; + if (visId.includes('AWS-geo')) { + const canvas = $('.visChart.leaflet-container .leaflet-control-zoom-in'); setTimeout(() => { if (!this.mapClicked) { this.mapClicked = true; @@ -357,43 +379,38 @@ class KibanaVis extends Component { } }, 1000); } - } else if (this.visID !== "Wazuh-App-Overview-General-Agents-status") { - this.$rootScope.rendered = "false"; + } else if (this.visID !== 'Wazuh-App-Overview-General-Agents-status') { + this.$rootScope.rendered = 'false'; } } }; render() { - const height = this.props.resultState === "loading" ? 0 : "100%"; + const isLoading = this.props.resultState === 'loading'; return ( this.visID && (
- - + Refreshing Index Pattern. - - +
@@ -402,12 +419,8 @@ class KibanaVis extends Component {
@@ -416,15 +429,18 @@ class KibanaVis extends Component { position="top" content={ - No alerts were found with the field:{" "} - {this.deadField} + No alerts were found with the field: {this.deadField} } >
-
+
) ); @@ -434,7 +450,7 @@ class KibanaVis extends Component { const mapStateToProps = (state) => { return { state: state.visualizationsReducers, - allowedAgents: state.appStateReducers.allowedAgents + allowedAgents: state.appStateReducers.allowedAgents, }; }; diff --git a/public/kibana-integrations/visualizations/_saved_vis.ts b/public/kibana-integrations/visualizations/_saved_vis.ts index 57edc4253d..46c1db6c33 100644 --- a/public/kibana-integrations/visualizations/_saved_vis.ts +++ b/public/kibana-integrations/visualizations/_saved_vis.ts @@ -24,16 +24,13 @@ * * NOTE: It's a type of SavedObject, but specific to visualizations. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../../src/plugins/saved_objects/public'; +import { SavedObject } from '../../../../../src/plugins/saved_objects/public'; // @ts-ignore import { extractReferences, injectReferences } from './saved_visualization_references'; import { IIndexPattern } from '../../../../../src/plugins/data/public'; import { ISavedVis, SerializedVis, updateOldState } from '../../../../../src/plugins/visualizations/public'; import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; +import { getPlugins } from '../../kibana-services'; export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; @@ -72,11 +69,10 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; }; -export function createSavedVisClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); +export function createSavedVisClass(services) { const savedSearch = createSavedSearchesLoader(services); - class SavedVis extends SavedObjectClass { + class SavedVis extends getPlugins().savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { title: 'text', diff --git a/public/plugin.ts b/public/plugin.ts index 167d9743b6..71a02b84f1 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -1,4 +1,5 @@ -import { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { BehaviorSubject } from 'rxjs'; +import { AppMountParameters, CoreSetup, CoreStart, AppUpdater, Plugin, PluginInitializerContext } from 'kibana/public'; import { setDataPlugin, setHttp, @@ -24,7 +25,7 @@ import { } from './types'; import { Cookies } from 'react-cookie'; import { AppState } from './react-services/app-state'; -import { setErrorOrchestrator } from './react-services'; +import { setErrorOrchestrator } from './react-services/common-services'; import { ErrorOrchestratorService } from './react-services/error-orchestrator/error-orchestrator.service'; const innerAngularName = 'app/wazuh'; @@ -33,7 +34,8 @@ export class WazuhPlugin implements Plugin void; private innerAngularInitialized: boolean = false; - + private stateUpdater = new BehaviorSubject(() => ({})); + public setup(core: CoreSetup, plugins: WazuhSetupPlugins): WazuhSetup { core.application.register({ id: `wazuh`, @@ -48,6 +50,7 @@ export class WazuhPlugin implements Plugin { + if(response.isWazuhDisabled) unmount(); + return { status: response.isWazuhDisabled } + }) return () => { unmount(); }; @@ -68,6 +80,7 @@ export class WazuhPlugin implements Plugin('ErrorOrchestratorService'); diff --git a/public/react-services/generic-request.js b/public/react-services/generic-request.js index adc2aa0846..ab86e54522 100644 --- a/public/react-services/generic-request.js +++ b/public/react-services/generic-request.js @@ -19,7 +19,7 @@ import { OdfeUtils } from '../utils'; import { getHttp, getDataPlugin } from '../kibana-services'; export class GenericRequest { - static async request(method, path, payload = null) { + static async request(method, path, payload = null, returnError = false) { try { if (!method || !path) { throw new Error('Missing parameters'); @@ -100,10 +100,11 @@ export class GenericRequest { wzMisc.setApiIsDown(true); if (!window.location.hash.includes('#/settings')) { - window.location.href = '/app/wazuh#/health-check'; + window.location.href = getHttp().basePath.prepend('/app/wazuh#/health-check'); } } } + if (returnError) return Promise.reject(err); return (((err || {}).response || {}).data || {}).message || false ? Promise.reject(err.response.data.message) : Promise.reject(err || 'Server did not respond'); diff --git a/public/react-services/index.ts b/public/react-services/index.ts index 5b03c3fcaa..8363f3664f 100644 --- a/public/react-services/index.ts +++ b/public/react-services/index.ts @@ -1,12 +1,24 @@ -import { ErrorOrchestratorService } from './error-orchestrator/error-orchestrator.service'; -import { createGetterSetter } from '../utils/create-getter-setter'; - -export { GenericRequest } from './generic-request'; -export { WzRequest } from './wz-request'; -export { ErrorHandler } from './error-handler'; -export { formatUIDate } from './time-service'; - -export const [getErrorOrchestrator, setErrorOrchestrator] = createGetterSetter< - ErrorOrchestratorService ->('ErrorOrchestratorService'); - +export * from './action-agents'; +export * from './app-navigate'; +export * from './app-state'; +export * from './check-daemons-status'; +export * from './error-handler'; +export * from './filter-authorization-agents'; +export * from './generic-request'; +export * from './group-handler'; +export * from './load-app-config.service'; +export * from './pattern-handler'; +export * from './reporting'; +export * from './saved-objects'; +export * from './time-service'; +export * from './toast-notifications' +export * from './vis-factory-handler'; +export * from './wazuh-config'; +export * from './wz-agents'; +export * from './wz-api-check'; +export * from './wz-authentication'; +export * from './wz-csv'; +export * from './wz-request'; +export * from './wz-security-opendistro'; +export * from './wz-security-xpack'; +export * from './wz-user-permissions'; diff --git a/public/react-services/load-app-config.service.ts b/public/react-services/load-app-config.service.ts new file mode 100644 index 0000000000..8e09d9b6e1 --- /dev/null +++ b/public/react-services/load-app-config.service.ts @@ -0,0 +1,41 @@ +/* + * Wazuh app - Load App config service + * Copyright (C) 2015-2021 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 { GenericRequest } from './generic-request'; +import store from '../redux/store'; +import { + setAppConfigIsLoading, + setAppConfigHasError, + updateAppConfig, +} from '../redux/actions/appConfigActions'; + + +/** + * Retunrs the wazuh app config + */ +export const loadAppConfig = async () => { + try { + store.dispatch(setAppConfigIsLoading()); + const config = await GenericRequest.request('GET', '/utils/configuration', {}); + + if (!config || !config.data || !config.data.data) { + throw new Error('No config available'); + } + + const ymlContent = config.data.data; + store.dispatch(updateAppConfig(ymlContent)) + } catch (error) { + store.dispatch(setAppConfigHasError()); + console.error('Error parsing wazuh.yml, using default values.'); // eslint-disable-line + console.error(error.message || error); // eslint-disable-line + } +}; diff --git a/public/react-services/pattern-handler.js b/public/react-services/pattern-handler.js index 95d650c4ba..29d76bda81 100644 --- a/public/react-services/pattern-handler.js +++ b/public/react-services/pattern-handler.js @@ -9,11 +9,9 @@ * * Find more information about this on the LICENSE file. */ -import { GenericRequest } from './generic-request'; import { AppState } from './app-state'; -import { WzMisc } from '../factories/misc'; import { SavedObject } from './saved-objects'; -import { getDataPlugin, getToasts } from '../kibana-services'; +import { getDataPlugin, getToasts, getHttp } from '../kibana-services'; import { WazuhConfig } from '../react-services/wazuh-config'; import { HEALTH_CHECK } from '../../common/constants'; @@ -23,53 +21,19 @@ export class PatternHandler { */ static async getPatternList(origin) { try { - var patternList = await SavedObject.getListOfWazuhValidIndexPatterns(); + const wazuhConfig = new WazuhConfig(); + const { pattern } = wazuhConfig.getConfig(); - if (origin == HEALTH_CHECK) { - const wazuhConfig = new WazuhConfig(); - const { pattern } = wazuhConfig.getConfig(); - const indexPatternFound = patternList.find((indexPattern) => indexPattern.title === pattern); - - if (!indexPatternFound && pattern) { - // if no valid index patterns are found we try to create the wazuh-alerts-* - try { - - getToasts().add({ - color: 'warning', - title: - `No ${pattern} index pattern was found, proceeding to create it.`, - toastLifeTimeMs: 5000 - }); - - await SavedObject.createWazuhIndexPattern(pattern); - getToasts().addSuccess(`${pattern} index pattern created successfully`); - !AppState.getCurrentPattern() && AppState.setCurrentPattern(pattern); - } catch (err) { - getToasts().addDanger({ - title: 'Error creating the index pattern.', - text: err.message || err, - toastLifeTimeMs: 3000 - }); - AppState.removeCurrentPattern(); - - return; - } - } - patternList = await SavedObject.getListOfWazuhValidIndexPatterns(); - } - if (AppState.getCurrentPattern() && patternList.length) { - let filtered = patternList.filter( - item => item.id === AppState.getCurrentPattern() - ); - if (!filtered.length) AppState.setCurrentPattern(patternList[0].id); - } + const defaultPatterns = [pattern]; + const selectedPattern = AppState.getCurrentPattern(); + if (selectedPattern && selectedPattern !== pattern) defaultPatterns.push(selectedPattern); + let patternList = await SavedObject.getListOfWazuhValidIndexPatterns(defaultPatterns, origin); return patternList; } catch (error) { console.error("getPatternList", error) throw new Error('Error Pattern Handler (getPatternList)'); } - return; } /** diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index cbd92b002c..acf286650f 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -10,11 +10,16 @@ * Find more information about this on the LICENSE file. */ -import { GenericRequest } from './generic-request'; -import { KnownFields } from '../utils/known-fields'; -import { FieldsStatistics } from '../utils/statistics-fields'; -import { FieldsMonitoring } from '../utils/monitoring-fields'; -import { WAZUH_INDEX_TYPE_MONITORING, WAZUH_INDEX_TYPE_STATISTICS, WAZUH_INDEX_TYPE_ALERTS } from '../../common/constants'; +import {GenericRequest} from './generic-request'; +import {KnownFields} from '../utils/known-fields'; +import {FieldsStatistics} from '../utils/statistics-fields'; +import {FieldsMonitoring} from '../utils/monitoring-fields'; +import { + HEALTH_CHECK, + WAZUH_INDEX_TYPE_ALERTS, + WAZUH_INDEX_TYPE_MONITORING, + WAZUH_INDEX_TYPE_STATISTICS +} from '../../common/constants'; export class SavedObject { /** @@ -25,11 +30,9 @@ export class SavedObject { try { const result = await GenericRequest.request( 'GET', - `/api/saved_objects/_find?type=index-pattern&search_fields=title` + `/api/saved_objects/_find?type=index-pattern&fields=title&fields=fields&per_page=9999` ); - const indexPatterns = ((result || {}).data || {}).saved_objects || []; - - return indexPatterns; + return ((result || {}).data || {}).saved_objects || []; } catch (error) { return ((error || {}).data || {}).message || false ? error.data.message @@ -42,37 +45,26 @@ export class SavedObject { * Returns the full list of index patterns that are valid * An index is valid if its fields contain at least these 4 fields: 'timestamp', 'rule.groups', 'agent.id' and 'manager.name' */ - static async getListOfWazuhValidIndexPatterns() { + static async getListOfWazuhValidIndexPatterns(defaultIndexPatterns, where) { try { - const list = await this.getListOfIndexPatterns(); - const result = list.filter(item => { - if (item.attributes && item.attributes.fields) { - const fields = JSON.parse(item.attributes.fields); - const minimum = { - timestamp: true, - 'rule.groups': true, - 'manager.name': true, - 'agent.id': true - }; - let validCount = 0; - - fields.map(currentField => { - if (minimum[currentField.name]) { - validCount++; - } - }); + let result = []; + if (where === HEALTH_CHECK) { + const list = await Promise.all( + defaultIndexPatterns.map( + async (pattern) => await SavedObject.getExistingIndexPattern(pattern) + ) + ); + result = this.validateIndexPatterns(list); + } - if (validCount === 4) { - return true; - } - } - return false; - }); + if (!result.length) { + const list = await this.getListOfIndexPatterns(); + result = this.validateIndexPatterns(list); + } - const validIndexPatterns = result.map(item => { + return result.map((item) => { return { id: item.id, title: item.attributes.title }; }); - return validIndexPatterns; } catch (error) { return ((error || {}).data || {}).message || false ? error.data.message @@ -80,6 +72,25 @@ export class SavedObject { } } + static validateIndexPatterns(list) { + const requiredFields = [ + 'timestamp', + 'rule.groups', + 'manager.name', + 'agent.id', + ]; + + return list.filter(item => { + if (item.attributes && item.attributes.fields) { + const fields = JSON.parse(item.attributes.fields); + return requiredFields.every((reqField => { + return fields.find(field => field.name === reqField); + })); + } + return false; + }); + } + static async existsOrCreateIndexPattern(patternID) { const result = await SavedObject.existsIndexPattern(patternID); if (!result.data) { @@ -98,6 +109,23 @@ export class SavedObject { } } + /** + * + * Given an index pattern ID, checks if it exists + */ + static async getExistingIndexPattern(patternID) { + try { + const result = await GenericRequest.request( + 'GET', + `/api/saved_objects/index-pattern/${patternID}?fields=title&fields=fields` + ); + + return result.data; + } catch (error) { + if (error && error.response && error.response.status == 404) return false; + return ((error || {}).data || {}).message || false ? error.data.message : error.message || false; + } + } /** * @@ -107,16 +135,18 @@ export class SavedObject { try { const result = await GenericRequest.request( 'GET', - `/api/saved_objects/index-pattern/${patternID}` + `/api/saved_objects/index-pattern/${patternID}?fields=title&fields=fields` ); const title = (((result || {}).data || {}).attributes || {}).title; + const fields = (((result || {}).data || {}).attributes || {}).fields; if (title) { return { data: 'Index pattern found', status: true, statusCode: 200, - title: title + title, + fields }; } } catch (error) { @@ -195,7 +225,7 @@ export class SavedObject { /** * Checks the field has a proper structure - * @param {index-pattern-field} field + * @param {index-pattern-field} field */ static isValidField(field) { diff --git a/public/react-services/wazuh-config.js b/public/react-services/wazuh-config.js index 14af752e99..02199c5788 100644 --- a/public/react-services/wazuh-config.js +++ b/public/react-services/wazuh-config.js @@ -10,6 +10,9 @@ * Find more information about this on the LICENSE file. */ +import store from "../redux/store"; +import { updateAppConfig } from "../redux/actions/appConfigActions"; + export class WazuhConfig { constructor() { if (!!WazuhConfig.instance) { @@ -17,7 +20,6 @@ export class WazuhConfig { } WazuhConfig.instance = this; - this.config = {}; return this; } @@ -27,20 +29,20 @@ export class WazuhConfig { * @param {Object} cfg */ setConfig(cfg) { - this.config = { ...cfg }; + store.dispatch(updateAppConfig({...cfg})); } /** * Get configuration */ getConfig() { - return this.config; + return store.getState().appConfig.data; } /** * Returns true if debug level is enabled, otherwise it returns false. */ isDebug() { - return ((this.config || {})['logs.level'] || false) === 'debug'; + return ((this.getConfig() || {})['logs.level'] || false) === 'debug'; } } diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 48fa97c0ef..5dcb361d96 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -65,11 +65,10 @@ export class WzRequest { } catch (error) { const wzMisc = new WzMisc(); wzMisc.setApiIsDown(true); - if (!window.location.hash.includes('#/settings')) { - window.location.href = '/app/wazuh#/health-check'; + window.location.href = getHttp().basePath.prepend('/app/wazuh#/health-check'); } - return; + throw new Error(error); } } const errorMessage = diff --git a/public/redux/actions/appConfigActions.ts b/public/redux/actions/appConfigActions.ts new file mode 100644 index 0000000000..bff5cd4cc1 --- /dev/null +++ b/public/redux/actions/appConfigActions.ts @@ -0,0 +1,34 @@ +/* + * Wazuh app - App Config Actions + * Copyright (C) 2015-2021 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 { ResolverAction } from '../types'; + +export const setAppConfigIsLoading = (): ResolverAction => { + return { + type: 'UPDATE_APP_CONFIG_SET_IS_LOADING', + payload: null + }; +} + +export const setAppConfigHasError = (): ResolverAction => { + return { + type: 'UPDATE_APP_CONFIG_SET_HAS_ERROR', + payload: null + }; +} + +export const updateAppConfig = (data: object): ResolverAction => { + return { + type: 'UPDATE_APP_CONFIG_DATA', + payload: data + }; +} diff --git a/public/redux/actions/appStateActions.js b/public/redux/actions/appStateActions.js index d38da02f05..c0de02f987 100644 --- a/public/redux/actions/appStateActions.js +++ b/public/redux/actions/appStateActions.js @@ -224,4 +224,15 @@ export const updateAllowedAgents = allowedAgents => { type: 'GET_ALLOWED_AGENTS', allowedAgents }; +}; + +/** + * Updates logtestToken in the appState store + * @param logtestToken + */ +export const updateLogtestToken = (logtestToken) => { + return { + type: 'UPDATE_LOGTEST_TOKEN', + logtestToken: logtestToken + }; }; \ No newline at end of file diff --git a/public/redux/reducers/appConfigReducers.ts b/public/redux/reducers/appConfigReducers.ts new file mode 100644 index 0000000000..2a5f408648 --- /dev/null +++ b/public/redux/reducers/appConfigReducers.ts @@ -0,0 +1,56 @@ +/* + * Wazuh app - App Config Reducer + * Copyright (C) 2015-2021 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 { Reducer } from 'redux'; +import { WAZUH_DEFAULT_APP_CONFIG } from '../../../common/constants'; +import { AppConfigState, ResolverAction } from '../types'; + +const initialState: AppConfigState = { + isLoading: false, + isReady: false, + hasError: false, + data: WAZUH_DEFAULT_APP_CONFIG, +}; + +const appConfigReducer: Reducer = ( + state = initialState, + action +) => { + switch (action.type) { + case 'UPDATE_APP_CONFIG_SET_IS_LOADING': + return { + ...state, + isLoading: true, + isReady: false, + hasError: false + }; + case 'UPDATE_APP_CONFIG_SET_HAS_ERROR': + return { + ...state, + isLoading: false, + isReady: false, + hasError: true + }; + case 'UPDATE_APP_CONFIG_DATA': + return { + ...state, + isLoading: false, + isReady: true, + hasError: false, + data: {...state.data, ...action.payload}, + }; + default: + return state; + } +}; + +export default appConfigReducer; diff --git a/public/redux/reducers/appStateReducers.js b/public/redux/reducers/appStateReducers.js index 0e3700bc4f..7d5b6ba7c5 100644 --- a/public/redux/reducers/appStateReducers.js +++ b/public/redux/reducers/appStateReducers.js @@ -28,8 +28,9 @@ const initialState = { status: false, contextConfigServer: 'manager', }, - withUserLogged: false, - allowedAgents: [], + withUserLogged: false, + allowedAgents: [], + logtestToken: '', }; const appStateReducers = (state = initialState, action) => { @@ -147,6 +148,13 @@ const appStateReducers = (state = initialState, action) => { }; } + if (action.type === 'UPDATE_LOGTEST_TOKEN') { + return { + ...state, + logtestToken: action.logtestToken + }; + } + return state; }; diff --git a/public/redux/reducers/rootReducers.js b/public/redux/reducers/rootReducers.js index 1f505d5cac..9ddfeb621b 100644 --- a/public/redux/reducers/rootReducers.js +++ b/public/redux/reducers/rootReducers.js @@ -22,6 +22,7 @@ import visualizationsReducers from './visualizationsReducers'; import globalBreadcrumbReducers from './globalBreadcrumbReducers'; import securityReducers from './securityReducers'; import toolsReducers from './toolsReducers'; +import appConfig from './appConfigReducers'; export default combineReducers({ rulesetReducers, @@ -35,4 +36,5 @@ export default combineReducers({ globalBreadcrumbReducers, securityReducers, toolsReducers, + appConfig }); diff --git a/public/redux/types.ts b/public/redux/types.ts new file mode 100644 index 0000000000..d847da1999 --- /dev/null +++ b/public/redux/types.ts @@ -0,0 +1,17 @@ +import { DefaultRootState } from 'react-redux'; + +export type ResolverAction = { + type: string; + payload: any; +}; + +export type AppConfigState = { + isLoading: boolean; + isReady: boolean; + hasError: boolean; + data: { [key: string]: any }; +}; + +export type AppRootState = DefaultRootState & { + appConfig: AppConfigState, +}; diff --git a/public/services/resolves/get-config.js b/public/services/resolves/get-config.js index 83fb2989d9..eaef3fc064 100644 --- a/public/services/resolves/get-config.js +++ b/public/services/resolves/get-config.js @@ -14,6 +14,7 @@ import { WAZUH_ALERTS_PATTERN, WAZUH_INDEX_REPLICAS, WAZUH_INDEX_SHARDS, + WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, WAZUH_MONITORING_PATTERN, WAZUH_SAMPLE_ALERT_PREFIX } from "../../../common/constants"; @@ -50,9 +51,9 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { 'xpack.rbac.enabled': true, 'wazuh.monitoring.enabled': true, 'wazuh.monitoring.frequency': 900, - 'wazuh.monitoring.shards': WAZUH_INDEX_SHARDS, + 'wazuh.monitoring.shards': WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, 'wazuh.monitoring.replicas': WAZUH_INDEX_REPLICAS, - 'wazuh.monitoring.creation': 'd', + 'wazuh.monitoring.creation': 'w', 'wazuh.monitoring.pattern': WAZUH_MONITORING_PATTERN, 'cron.prefix': 'wazuh', 'cron.statistics.status': true, @@ -93,6 +94,5 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { console.log('Error parsing wazuh.yml, using default values.'); // eslint-disable-line console.log(error.message || error); // eslint-disable-line } - return $q.resolve(defaultConfig); } diff --git a/public/services/resolves/get-saved-search.js b/public/services/resolves/get-saved-search.js index 5b0a52892e..25ed1ad3f1 100644 --- a/public/services/resolves/get-saved-search.js +++ b/public/services/resolves/get-saved-search.js @@ -11,59 +11,58 @@ */ import { healthCheck } from './health-check'; import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; -import { getToasts, getChrome, getOverlays, getDataPlugin, getSavedObjects } from '../../kibana-services'; +import { + getToasts, + getChrome, + getOverlays, + getDataPlugin, + getSavedObjects, + getPlugins, +} from '../../kibana-services'; -export function getSavedSearch( - $location, - $window, - $route -) { - const services = { - savedObjectsClient: getSavedObjects().client, - indexPatterns: getDataPlugin().indexPatterns, - search: getDataPlugin().search, - chrome: getChrome(), - overlays: getOverlays(), - }; +export function getSavedSearch($location, $window, $route) { + try { + const services = { + savedObjectsClient: getSavedObjects().client, + indexPatterns: getDataPlugin().indexPatterns, + search: getDataPlugin().search, + chrome: getChrome(), + overlays: getOverlays(), + savedObjects: getPlugins().savedObjects, //for kibana ^7.11 + }; - const savedSearches = createSavedSearchesLoader(services); - const currentParams = $location.search(); - const targetedAgent = - currentParams && (currentParams.agent || currentParams.agent === '000'); - const targetedRule = - currentParams && currentParams.tab === 'ruleset' && currentParams.ruleid; - if (!targetedAgent && !targetedRule && healthCheck($window)) { - $location.path('/health-check'); - return Promise.reject(); - } else { - const savedSearchId = $route.current.params.id; - return savedSearches - .get(savedSearchId) - .then(savedSearch => { - if (savedSearchId) { - getChrome().recentlyAccessed.add( - savedSearch.getFullPath(), - savedSearch.title, - savedSearchId - ); - } - return savedSearch; - }) - .catch(() => { - getToasts().addWarning({ - title: 'Saved object is missing', - text: (element) => { - ReactDOM.render({error.message}, element); - return () => ReactDOM.unmountComponentAtNode(element); - }, + const savedSearches = createSavedSearchesLoader(services); + const currentParams = $location.search(); + const targetedAgent = currentParams && (currentParams.agent || currentParams.agent === '000'); + const targetedRule = currentParams && currentParams.tab === 'ruleset' && currentParams.ruleid; + if (!targetedAgent && !targetedRule && healthCheck($window)) { + $location.path('/health-check'); + return Promise.reject(); + } else { + const savedSearchId = $route.current.params.id; + return savedSearches + .get(savedSearchId) + .then((savedSearch) => { + if (savedSearchId) { + getChrome().recentlyAccessed.add( + savedSearch.getFullPath(), + savedSearch.title, + savedSearchId + ); + } + return savedSearch; + }) + .catch(() => { + getToasts().addWarning({ + title: 'Saved object is missing', + text: (element) => { + ReactDOM.render({error.message}, element); + return () => ReactDOM.unmountComponentAtNode(element); + }, + }); }); - - /* redirectWhenMissing({ - search: '/discover', - 'index-pattern': - '/management/kibana/objects/savedSearches/' + - $route.current.params.id - }) */ - }); + } + } catch (error) { + console.error(error); } } diff --git a/public/services/routes.js b/public/services/routes.js index ba2ff00639..bc139b79f6 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -137,68 +137,84 @@ app.config(($routeProvider) => { $routeProvider .when('/health-check', { template: healthCheckTemplate, - resolve: { apiCount, wzConfig, ip } + resolve: { apiCount, wzConfig, ip }, + outerAngularWrapperRoute: true }) .when('/agents/:agent?/:tab?/:tabView?', { template: agentsTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, reloadOnSearch: false, + outerAngularWrapperRoute: true }) .when('/agents-preview/', { template: agentsPrevTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, reloadOnSearch: false, + outerAngularWrapperRoute: true }) .when('/manager/', { template: managementTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, reloadOnSearch: false, + outerAngularWrapperRoute: true }) .when('/manager/:tab?', { template: managementTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId } + resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, + outerAngularWrapperRoute: true }) .when('/overview/', { template: overviewTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, reloadOnSearch: false, + outerAngularWrapperRoute: true }) .when('/settings', { template: settingsTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - reloadOnSearch: false + reloadOnSearch: false, + outerAngularWrapperRoute: true }) .when('/security', { template: securityTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch } + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + outerAngularWrapperRoute: true }) .when('/visualize/create?', { redirectTo: function () { }, - resolve: { wzConfig, wzKibana } + resolve: { wzConfig, wzKibana }, + outerAngularWrapperRoute: true }) .when('/context/:pattern?/:type?/:id?', { redirectTo: function () { }, - resolve: { wzKibana } + resolve: { wzKibana }, + outerAngularWrapperRoute: true }) .when('/doc/:pattern?/:index?/:type?/:id?', { redirectTo: function () { }, - resolve: { wzKibana } + resolve: { wzKibana }, + outerAngularWrapperRoute: true }) .when('/wazuh-dev', { template: toolsTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch } + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + outerAngularWrapperRoute: true }) .when('/blank-screen', { template: blankScreenTemplate, - resolve: { enableWzMenu, wzConfig } + resolve: { enableWzMenu, wzConfig }, + outerAngularWrapperRoute: true }) .when('/', { - redirectTo: '/overview/' + redirectTo: '/overview/', + outerAngularWrapperRoute: true }) .when('', { - redirectTo: '/overview/' + redirectTo: '/overview/', + outerAngularWrapperRoute: true }) .otherwise({ - redirectTo: '/overview' + redirectTo: '/overview', + outerAngularWrapperRoute: true }); }); diff --git a/public/styles/common.scss b/public/styles/common.scss index 17316a71b9..10e7d3bf6e 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1034,11 +1034,40 @@ wz-xml-file-editor { .health-check{ padding-top: 5%; - width: 500px; + max-width: 700px; margin: 0 auto; text-align: center; } +.health-check dl.euiDescriptionList dd{ + white-space: nowrap; +} + +.health-check dl.euiDescriptionList dd>span:first-child{ + display: inline-block; + width: 26px; +} + +.health-check .euiCodeBlock{ + max-height: 0; + margin-top: 0; + transition: all 0.25s ease-out; + overflow: hidden; + border: 0px solid #0001; +} + +.health-check .euiCodeBlock.visible{ + border: 1px solid #0001; + max-height: 700px; + margin-top: 16px; + visibility: visible; +} + +.wz-hover-transform-y1:hover{ + -webkit-transform: translateY(-1px) !important; + transform: translateY(-1px) !important; +} + @keyframes rotation { from { transform: rotate(0deg); @@ -1700,3 +1729,11 @@ iframe.width-changed { width: 20px; margin: 0 auto; } + +.custom-charts-bar span.euiLoadingChart__bar{ + width: 15px; +} + +.wz-width-100{ + width: 100%; +} \ No newline at end of file diff --git a/public/styles/component.scss b/public/styles/component.scss index bc25dc77c5..32a091a795 100644 --- a/public/styles/component.scss +++ b/public/styles/component.scss @@ -109,4 +109,13 @@ kbn-dis doc-table .kbnDocViewer__warning { display: none; +} + +/* Custom Breadcrumb styles*/ +.header__breadcrumbsWithExtensionContainer .euiHeaderBreadcrumbs { + flex-grow: 1; + margin-right: 12px; +} +.header__breadcrumbsWithExtensionContainer .header__breadcrumbsAppendExtension { + flex-grow: 0; } \ No newline at end of file diff --git a/public/styles/dark_theme/wz_theme_dark.scss b/public/styles/dark_theme/wz_theme_dark.scss index 210b376f69..01f07decda 100644 --- a/public/styles/dark_theme/wz_theme_dark.scss +++ b/public/styles/dark_theme/wz_theme_dark.scss @@ -361,9 +361,6 @@ md-divider.md-default-theme, md-divider { background: #1a1b20; } -.wz-menu-selectors { - border-top: 1px solid #343741!important; -} .wz-module-header-agent, .wz-module-header-nav { border-bottom: 1px solid #343741!important; diff --git a/public/templates/management/management.html b/public/templates/management/management.html index 20cdecb83f..9e8586a415 100644 --- a/public/templates/management/management.html +++ b/public/templates/management/management.html @@ -220,14 +220,20 @@

-
+
+ + + + + + Top 5 nodesTop 5 nodes
+ +
+ +
+
diff --git a/public/types.ts b/public/types.ts index a595eb7067..0d3a246213 100644 --- a/public/types.ts +++ b/public/types.ts @@ -6,6 +6,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugi import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; import { SecurityOssPluginStart } from '../../../src/plugins/security_oss/public/'; +import { SavedObjectsStart } from '../../../src/plugins/saved_objects/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; @@ -13,7 +14,8 @@ export interface AppPluginStartDependencies { visualizations: VisualizationsStart; discover: DiscoverStart; charts: ChartsPluginStart - securityOss: SecurityOssPluginStart + securityOss: SecurityOssPluginStart, + savedObjects: SavedObjectsStart } export interface AppDependencies { core: CoreStart; diff --git a/public/utils/add_help_menu_to_app.tsx b/public/utils/add_help_menu_to_app.tsx new file mode 100644 index 0000000000..f5519ea391 --- /dev/null +++ b/public/utils/add_help_menu_to_app.tsx @@ -0,0 +1,54 @@ +/* + * Wazuh app - Add the plugin help links as extension in Kibana help menu + * Copyright (C) 2015-2021 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 from 'react'; +import { + EuiIcon, +} from '@elastic/eui'; +import { version } from '../../package.json'; +import { getChrome, getHttp} from '../kibana-services'; +import { + WAZUH_LINK_DOCUMENTATION, + WAZUH_LINK_GITHUB, + WAZUH_LINK_GOOGLE_GROUPS, + WAZUH_LINK_SLACK +} from '../../common/constants'; + +const appVersionMajorDotMinor = version.split('.').slice(0, 2).join('.'); + +export function addHelpMenuToAppChrome(){ + getChrome().setHelpExtension({ + appName: 'Wazuh support', + links: [ + { + linkType: 'custom', + href: `${WAZUH_LINK_DOCUMENTATION}/${appVersionMajorDotMinor}`, + content: Documentation + }, + { + linkType: 'custom', + href: WAZUH_LINK_SLACK, + content: Slack channel + }, + { + linkType: 'custom', + href: WAZUH_LINK_GITHUB, + content: Projects on Github + }, + { + linkType: 'custom', + href: WAZUH_LINK_GOOGLE_GROUPS, + content: Google Group + } + ] + }); +} diff --git a/public/utils/config-equivalences.js b/public/utils/config-equivalences.js index ef0b62e92b..0326020e6c 100644 --- a/public/utils/config-equivalences.js +++ b/public/utils/config-equivalences.js @@ -90,7 +90,7 @@ export const nameEquivalence = { 'wazuh.monitoring.frequency': 'Frequency', 'wazuh.monitoring.shards': 'Index shards', 'wazuh.monitoring.replicas': 'Index replicas', - 'wazuh.monitoring.creation': 'Interval creation', + 'wazuh.monitoring.creation': 'Index creation', 'wazuh.monitoring.pattern': 'Index pattern', hideManagerAlerts: 'Hide manager alerts', 'logs.level': 'Log level', diff --git a/public/utils/index.ts b/public/utils/index.ts index e70cb71836..3691c70d1c 100644 --- a/public/utils/index.ts +++ b/public/utils/index.ts @@ -1,3 +1,5 @@ export * as OdfeUtils from './odfe-utils'; -export { checkPluginVersion } from './check-plugin-version'; \ No newline at end of file +export { checkPluginVersion } from './check-plugin-version'; + +export { addHelpMenuToAppChrome } from './add_help_menu_to_app'; \ No newline at end of file diff --git a/public/utils/wz-logo-menu.js b/public/utils/wz-logo-menu.js index f6813fb06a..e39c1efd7e 100644 --- a/public/utils/wz-logo-menu.js +++ b/public/utils/wz-logo-menu.js @@ -13,10 +13,10 @@ // Remove Kibana Wazuh name and breadcrumb export const changeWazuhNavLogo = () => { const interval = setInterval(() => { - const nav = $('nav'); - if (nav.length) { + const nav = document.querySelector('[data-test-subj="breadcrumbs"] > .euiBreadcrumb'); + if (nav) { clearInterval(interval); + nav.style.display = 'none'; } - $('.euiHeader > .euiBreadcrumbs > .euiBreadcrumb').hide(); - }, 100); + }, 200); }; diff --git a/server/controllers/wazuh-api.ts b/server/controllers/wazuh-api.ts index 9c9a42dafa..1d8fb153bf 100644 --- a/server/controllers/wazuh-api.ts +++ b/server/controllers/wazuh-api.ts @@ -25,13 +25,14 @@ import jwtDecode from 'jwt-decode'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; import { APIUserAllowRunAs, CacheInMemoryAPIUserAllowRunAs, API_USER_STATUS_RUN_AS } from '../lib/cache-api-user-has-run-as'; import { getCookieValueByName } from '../lib/cookie'; +import { SecurityObj } from '../lib/security-factory'; +import { getConfiguration } from '../lib/get-configuration'; export class WazuhApiCtrl { manageHosts: ManageHosts updateRegistry: UpdateRegistry constructor() { - // this.monitoringInstance = new Monitoring(server, true); this.manageHosts = new ManageHosts(); this.updateRegistry = new UpdateRegistry(); } @@ -206,19 +207,18 @@ export class WazuhApiCtrl { // If we have an invalid response from the Wazuh API throw new Error(responseManagerInfo.data.detail || `${api.url}:${api.port} is unreachable`); } catch (error) { - log('wazuh-api:checkStoredAPI', error.message || error); if (error.code === 'EPROTO') { return response.ok({ body: { statusCode: 200, - data: { password: '****', apiIsDown: true }, + data: { apiIsDown: true }, } }); } else if (error.code === 'ECONNREFUSED') { return response.ok({ body: { statusCode: 200, - data: { password: '****', apiIsDown: true }, + data: { apiIsDown: true }, } }); } else { @@ -251,8 +251,10 @@ export class WazuhApiCtrl { } catch (error) { } // eslint-disable-line } } catch (error) { + log('wazuh-api:checkStoredAPI', error.message || error); return ErrorResponse(error.message || error, 3020, 500, response); } + log('wazuh-api:checkStoredAPI', error.message || error); return ErrorResponse(error.message || error, 3002, 500, response); } } @@ -1043,4 +1045,29 @@ export class WazuhApiCtrl { return ErrorResponse(error.message || error, 3035, 500, response); } } + /** + * Check if user assigned roles disable Wazuh Plugin + * @param context + * @param request + * @param response + * @returns {object} Returns { isWazuhDisabled: boolean parsed integer } + */ + async isWazuhDisabled(context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) { + try { + + const disabledRoles = ( await getConfiguration() )['disabled_roles'] || []; + const wazuhSecurity = SecurityObj(context.wazuh.plugins); + const data = (await wazuhSecurity.getCurrentUser(request, context)).authContext; + + const isWazuhDisabled = +(data.roles || []).some((role) => disabledRoles.includes(role)); + + return response.ok({ + body: { isWazuhDisabled } + }); + } catch (error) { + log('wazuh-api:isWazuhDisabled', error.message || error); + return ErrorResponse(error.message || error, 3035, 500, response); + } + + } } diff --git a/server/controllers/wazuh-reporting.ts b/server/controllers/wazuh-reporting.ts index 0397e6791b..5fda542130 100644 --- a/server/controllers/wazuh-reporting.ts +++ b/server/controllers/wazuh-reporting.ts @@ -30,10 +30,10 @@ import { KeyEquivalence } from '../../common/csv-key-equivalence'; import { AgentConfiguration } from '../lib/reporting/agent-configuration'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; import { ReportPrinter } from '../lib/reporting/printer'; - import { log } from '../lib/logger'; import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH, WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH, AUTHORIZED_AGENTS } from '../../common/constants'; import { createDirectoryIfNotExists, createDataDirectoryIfNotExists } from '../lib/filesystem'; +import moment from 'moment'; export class WazuhReportingCtrl { @@ -73,7 +73,11 @@ export class WazuhReportingCtrl { str += `${ type === 'range' ? `${params.gte}-${params.lt}` - : !!value + : type === 'phrases' + ? '(' + params.join(" OR ") + ')' + : type === 'exists' + ? '*' + : !!value ? value : (params || {}).query }`; @@ -188,6 +192,7 @@ export class WazuhReportingCtrl { * @param {String} apiId API id */ private async buildAgentsTable(context, printer: ReportPrinter, agentIDs: string[], apiId: string, multi = false) { + const dateFormat = await context.core.uiSettings.client.get('dateFormat'); if (!agentIDs || !agentIDs.length) return; log( 'reporting:buildAgentsTable', @@ -232,7 +237,9 @@ export class WazuhReportingCtrl { agentRows.push({ ...agent, manager: agent.manager || agent.manager_host, - os: (agent.os && agent.os.name && agent.os.version) ? `${agent.os.name} ${agent.os.version}` : '' + os: (agent.os && agent.os.name && agent.os.version) ? `${agent.os.name} ${agent.os.version}` : '', + lastKeepAlive: moment(agent.lastKeepAlive).format(dateFormat), + dateAdd: moment(agent.dateAdd).format(dateFormat) }); } catch (error) { log( @@ -1136,11 +1143,9 @@ export class WazuhReportingCtrl { rows }); } - nestedData.forEach(nest => { this.getConfigTables(nest, section, tab + 1, array); }); - return array; } @@ -1541,6 +1546,7 @@ export class WazuhReportingCtrl { 'debug' ); for (let section of config.sections) { + let titleOfSubsection = false; if ( components[idxComponent] && (section.config || section.wodle) @@ -1562,71 +1568,70 @@ export class WazuhReportingCtrl { 'GET', `/agents/${agentID}/config/${conf.component}/${conf.configuration}`, {}, - {apiHostID: apiId} + { apiHostID: apiId } ); } else { for (let wodle of wmodulesResponse.data.data['wmodules']) { if (Object.keys(wodle)[0] === conf['name']) { agentConfigResponse.data = { - data: wodle + data: wodle, }; - }; + } } } - const agentConfig = agentConfigResponse && agentConfigResponse.data && agentConfigResponse.data.data; + const agentConfig = + agentConfigResponse && agentConfigResponse.data && agentConfigResponse.data.data; if (!titleOfSection) { printer.addContent({ text: config.title, style: 'h1', - margin: [0, 0, 0, 15] + margin: [0, 0, 0, 15], }); titleOfSection = true; - }; - printer.addContent({ - text: section.subtitle, - style: 'h4' - }); - printer.addContent({ - text: section.desc, - style: { fontSize: 12, color: '#000' }, - margin: [0, 0, 0, 10] - }); - if ( - agentConfig - ) { + } + if (!titleOfSubsection) { + printer.addContent({ + text: section.subtitle, + style: 'h4', + }); + printer.addContent({ + text: section.desc, + style: { fontSize: 12, color: '#000' }, + margin: [0, 0, 0, 10], + }); + titleOfSubsection = true; + } + if (agentConfig) { for (let agentConfigKey of Object.keys(agentConfig)) { if (Array.isArray(agentConfig[agentConfigKey])) { /* LOG COLLECTOR */ if (conf.filterBy) { let groups = []; - agentConfig[agentConfigKey].forEach(obj => { + agentConfig[agentConfigKey].forEach((obj) => { if (!groups[obj.logformat]) { groups[obj.logformat] = []; } groups[obj.logformat].push(obj); }); - Object.keys(groups).forEach(group => { + Object.keys(groups).forEach((group) => { let saveidx = 0; groups[group].forEach((x, i) => { if ( - Object.keys(x).length > - Object.keys(groups[group][saveidx]).length + Object.keys(x).length > Object.keys(groups[group][saveidx]).length ) { saveidx = i; } }); - const columns = Object.keys( - groups[group][saveidx] - ); - const rows = groups[group].map(x => { + const columns = Object.keys(groups[group][saveidx]); + const rows = groups[group].map((x) => { let row = []; - columns.forEach(key => { + columns.forEach((key) => { row.push( typeof x[key] !== 'object' ? x[key] : Array.isArray(x[key]) - ? x[key].map(x => { + ? x[key].map((x) => { return x + '\n'; }) : JSON.stringify(x[key]) @@ -1635,62 +1640,50 @@ export class WazuhReportingCtrl { return row; }); columns.forEach((col, i) => { - columns[i] = - col[0].toUpperCase() + col.slice(1); + columns[i] = col[0].toUpperCase() + col.slice(1); }); tables.push({ title: section.labels[0][group], type: 'table', columns, - rows + rows, }); }); } else if (agentConfigKey.configuration !== 'socket') { tables.push( - ...this.getConfigTables( - agentConfig[agentConfigKey], - section, - idx - ) + ...this.getConfigTables(agentConfig[agentConfigKey], section, idx) ); } else { for (let _d2 of agentConfig[agentConfigKey]) { - tables.push( - ...this.getConfigTables(_d2, section, idx) - ); + tables.push(...this.getConfigTables(_d2, section, idx)); } } } else { /*INTEGRITY MONITORING MONITORED DIRECTORIES */ if (conf.matrix) { - const directories = agentConfig[agentConfigKey].directories; - delete agentConfig[agentConfigKey].directories; + const {directories,diff,synchronization,file_limit,...rest} = agentConfig[agentConfigKey]; tables.push( - ...this.getConfigTables( - agentConfig[agentConfigKey], - section, - idx - ) + ...this.getConfigTables(rest, section, idx), + ...(diff && diff.disk_quota ? this.getConfigTables(diff.disk_quota, {tabs:['Disk quota']}, 0 ): []), + ...(diff && diff.file_size ? this.getConfigTables(diff.file_size, {tabs:['File size']}, 0 ): []), + ...(synchronization ? this.getConfigTables(synchronization, {tabs:['Synchronization']}, 0 ): []), + ...(file_limit ? this.getConfigTables(file_limit, {tabs:['File limit']}, 0 ): []), ); let diffOpts = []; - Object.keys(section.opts).forEach(x => { + Object.keys(section.opts).forEach((x) => { diffOpts.push(x); }); const columns = [ '', - ...diffOpts.filter( - x => x !== 'check_all' && x !== 'check_sum' - ) + ...diffOpts.filter((x) => x !== 'check_all' && x !== 'check_sum'), ]; let rows = []; - directories.forEach(x => { + directories.forEach((x) => { let row = []; row.push(x.dir); - columns.forEach(y => { + columns.forEach((y) => { if (y !== '') { - row.push( - x.opts.indexOf(y) > -1 ? 'yes' : 'no' - ); + row.push(x.opts.indexOf(y) > -1 ? 'yes' : 'no'); } }); row.push(x.recursion_level); @@ -1704,15 +1697,11 @@ export class WazuhReportingCtrl { title: 'Monitored directories', type: 'table', columns, - rows + rows, }); } else { tables.push( - ...this.getConfigTables( - agentConfig[agentConfigKey], - section, - idx - ) + ...this.getConfigTables(agentConfig[agentConfigKey], section, idx) ); } } @@ -1725,10 +1714,10 @@ export class WazuhReportingCtrl { { text: `${section.subtitle.toLowerCase()} configuration.`, link: section.docuLink, - style: { fontSize: 12, color: '#1a0dab' } - } + style: { fontSize: 12, color: '#1a0dab' }, + }, ], - margin: [0, 0, 0, 20] + margin: [0, 0, 0, 20], }); } } catch (error) { diff --git a/server/integration-files/visualizations/agents/agents-audit.ts b/server/integration-files/visualizations/agents/agents-audit.ts index d6eae8d1c6..cb46a51761 100644 --- a/server/integration-files/visualizations/agents/agents-audit.ts +++ b/server/integration-files/visualizations/agents/agents-audit.ts @@ -14,274 +14,626 @@ export default [ _id: 'Wazuh-App-Agents-Audit-New-files-metric', _source: { title: 'New files metric', - visState: - '{"title":"New files metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"New files"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'New files metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'New files' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.id", - "value": "80790", - "params": { - "query": "80790", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.id": { - "query": "80790", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.id', + value: '80790', + params: { + query: '80790', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.id': { + query: '80790', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Read-files-metric', _source: { title: 'Read files metric', - visState: - '{"title":"Read files metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Read files"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Read files metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Read files' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.id", - "value": "80784", - "params": { - "query": "80784", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.id": { - "query": "80784", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.id', + value: '80784', + params: { + query: '80784', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.id': { + query: '80784', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Modified-files-metric', _source: { title: 'Modified files metric', - visState: - '{"title":"Modified files metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Modified files"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Modified files metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Modified files' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "type": "phrases", - "key": "rule.id", - "value": "80781, 80787", - "params": [ - "80781", - "80787" - ], - "negate": false, - "disabled": false, - "alias": null - }, - "query": { - "bool": { - "should": [ - { - "match_phrase": { - "rule.id": "80781" - } - }, - { - "match_phrase": { - "rule.id": "80787" - } - } - ], - "minimum_should_match": 1 - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + type: 'phrases', + key: 'rule.id', + value: '80781, 80787', + params: ['80781', '80787'], + negate: false, + disabled: false, + alias: null, + }, + query: { + bool: { + should: [ + { + match_phrase: { + 'rule.id': '80781', + }, + }, + { + match_phrase: { + 'rule.id': '80787', + }, + }, + ], + minimum_should_match: 1, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Removed-files-metric', _source: { title: 'Removed files metric', - visState: - '{"title":"Removed files metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Removed files"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Removed files metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Removed files' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.id", - "value": "80791", - "params": { - "query": "80791", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.id": { - "query": "80791", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.id', + value: '80791', + params: { + query: '80791', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.id': { + query: '80791', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Groups', _source: { title: 'Groups', - visState: - '{"title":"Groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.groups', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Files', _source: { title: 'Files', - visState: - '{"title":"Files","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.audit.file.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.audit.file.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Alerts-over-time', _source: { title: 'Alerts over time', - visState: - '{"title":"Alerts over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Alerts over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Commands', _source: { title: 'Commands', - visState: - '{"title":"Commands","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.audit.command","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Commands', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.audit.command', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-Audit-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Event"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.audit.exe","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Command"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.audit.type","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Type"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Event', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.audit.exe', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Command', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.audit.type', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Type', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-aws.ts b/server/integration-files/visualizations/agents/agents-aws.ts index cfbcfa7822..ad69d0b038 100644 --- a/server/integration-files/visualizations/agents/agents-aws.ts +++ b/server/integration-files/visualizations/agents/agents-aws.ts @@ -15,162 +15,620 @@ export default [ _type: 'visualization', _source: { title: 'Top rules', - visState: - '{"title":"Top rules","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","size":500,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule ID"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Event"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Top rules', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showtoolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + size: 500, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Event', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-geo', _type: 'visualization', _source: { title: 'Geolocation map', - visState: - '{"title":"Geolocation map","type":"tile_map","params":{"colorSchema":"Green to Red","mapType":"Scaled Circle Markers","isDesaturated":false,"addTooltip":true,"heatClusterSize":1.5,"legendPosition":"bottomright","mapZoom":1,"mapCenter":[0,0],"wms":{"enabled":false,"options":{"format":"image/png","transparent":true}},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"geohash":{"accessor":0,"format":{"id":"string"},"params":{"precision":2,"useGeocentroid":true},"aggType":"geohash_grid"},"geocentroid":{"accessor":2,"format":{"id":"string"},"params":{},"aggType":"geo_centroid"}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"geohash_grid","schema":"segment","params":{"field":"GeoLocation.location","autoPrecision":true,"precision":2,"useGeocentroid":true,"isFilteredByCollar":true,"mapZoom":1,"mapCenter":[0,0]}}]}', - uiStateJSON: - '{"mapZoom":2,"mapCenter":[38.685509760012025,-31.816406250000004]}', + visState: JSON.stringify({ + title: 'Geolocation map', + type: 'tile_map', + params: { + colorSchema: 'Green to Red', + mapType: 'Scaled Circle Markers', + isDesaturated: false, + addTooltip: true, + heatClusterSize: 1.5, + legendPosition: 'bottomright', + mapZoom: 1, + mapCenter: [0, 0], + wms: { enabled: false, options: { format: 'image/png', transparent: true } }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + geohash: { + accessor: 0, + format: { id: 'string' }, + params: { precision: 2, useGeocentroid: true }, + aggType: 'geohash_grid', + }, + geocentroid: { + accessor: 2, + format: { id: 'string' }, + params: {}, + aggType: 'geo_centroid', + }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'GeoLocation.location', + autoPrecision: true, + precision: 2, + useGeocentroid: true, + isFilteredByCollar: true, + mapZoom: 1, + mapCenter: [0, 0], + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + mapZoom: 2, + mapCenter: [38.685509760012025, -31.816406250000004], + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Events-by-source', _type: 'visualization', _source: { title: 'Events by source over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"left","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.aws.source","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'left', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.aws.source', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Top-accounts', _type: 'visualization', _source: { title: 'Accounts', - visState: - '{"title":"Accounts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.accountId","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Accounts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.accountId', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Top-sources', _type: 'visualization', _source: { title: 'Sources', - visState: - '{"title":"Sources","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.source","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Sources', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.source', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Top-buckets', _type: 'visualization', _source: { title: 'Buckets', - visState: - '{"title":"Buckets","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.log_info.s3bucket","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Buckets', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.log_info.s3bucket', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Top-regions', _type: 'visualization', _source: { title: 'Regions', - visState: - '{"title":"Regions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.region","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Regions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.region', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, - /*{ - _id: 'Wazuh-App-Agents-AWS-Service-selector', - _type: 'visualization', - _source: { - title: 'Service selector', - visState: - '{"title":"Service selector","type":"input_control_vis","params":{"controls":[{"id":"1543502225381","indexPattern":"wazuh-alerts","fieldName":"data.aws.service.serviceName","parent":"","label":"Service","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"}}],"updateFiltersOnChange":true,"useTimeFilter":true,"pinFilters":false},"aggs":[]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - },*/ { _id: 'Wazuh-App-Agents-AWS-Events-by-s3-bucket', _type: 'visualization', _source: { title: 'Events by S3 bucket over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.aws.log_info.s3bucket","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.aws.log_info.s3bucket', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-AWS-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/agents/agents-ciscat.ts b/server/integration-files/visualizations/agents/agents-ciscat.ts index 4bc65db553..2b10754fbf 100644 --- a/server/integration-files/visualizations/agents/agents-ciscat.ts +++ b/server/integration-files/visualizations/agents/agents-ciscat.ts @@ -15,184 +15,678 @@ export default [ _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"count"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.rule_title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule title"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.group","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Group"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.result","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Result"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'count', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.rule_title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule title', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.group', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Group', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.result', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Result', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-not-checked', _type: 'visualization', _source: { title: 'Last scan not checked', - visState: - '{"title":"Last scan not checked","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.notchecked","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan not checked', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.notchecked', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-score', _type: 'visualization', _source: { title: 'Last scan score', - visState: - '{"title":"Last scan score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.score","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.score', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-pass', _type: 'visualization', _source: { title: 'Last scan pass', - visState: - '{"title":"Last scan pass","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.pass","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan pass', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.pass', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-fail', _type: 'visualization', _source: { title: 'Last scan fail', - visState: - '{"title":"Last scan fail","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.fail","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan fail', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.fail', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-timestamp', _type: 'visualization', _source: { title: 'Last scan timestamp', - visState: - '{"title":"Last scan timestamp","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.timestamp","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan timestamp', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.timestamp', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-error', _type: 'visualization', _source: { title: 'Last scan error', - visState: - '{"title":"Last scan error","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.error","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan error', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.error', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-benchmark', _type: 'visualization', _source: { title: 'Last scan benchmark', - visState: - '{"title":"Last scan benchmark","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.benchmark","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan benchmark', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.benchmark', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-last-scan-unknown', _type: 'visualization', _source: { title: 'Last scan unknown', - visState: - '{"title":"Last scan unknown","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.unknown","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan unknown', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.unknown', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-top-5-groups', _type: 'visualization', _source: { title: 'Top 5 groups', - visState: - '{"title":"Top 5 groups","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"},"valueAxis":null},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":25,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.cis.group","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Group"}}]}', + visState: JSON.stringify({ + title: 'Top 5 groups', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' }, valueAxis: null }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, truncate: 25, filter: true, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.cis.group', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Group', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Agents-CISCAT-scan-result-evolution', _type: 'visualization', _source: { title: 'Scan result evolution', - visState: - '{"title":"Scan result evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.cis.result","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Result"}}]}', + visState: JSON.stringify({ + title: 'Scan result evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, truncate: 100, filter: true, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.cis.result', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Result', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-docker.ts b/server/integration-files/visualizations/agents/agents-docker.ts index 8aa3490810..c2d4a74937 100644 --- a/server/integration-files/visualizations/agents/agents-docker.ts +++ b/server/integration-files/visualizations/agents/agents-docker.ts @@ -15,80 +15,394 @@ export default [ _type: 'visualization', _source: { title: 'Top 5 actions', - visState: - '{"title":"Top 5 actions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.docker.Action","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 actions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.docker.Action', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Docker-top-5-images', _type: 'visualization', _source: { title: 'Top 5 images', - visState: - '{"title":"Top 5 images","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.docker.Actor.Attributes.image","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 images', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.docker.Actor.Attributes.image', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Docker-Events-summary', _type: 'visualization', _source: { title: 'Events summary', - visState: - '{"title":"Events summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.docker.Actor.Attributes.name","size":50,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Container"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.docker.Action","size":20,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"timestamp","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Date"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Events summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.docker.Actor.Attributes.name', + size: 50, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Container', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.docker.Action', + size: 20, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'timestamp', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Date', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Docker-Types-over-time', _type: 'visualization', _source: { title: 'Types over time', - visState: - '{"title":"Types over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.docker.Type","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Types over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.docker.Type', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Docker-Actions-over-time', _type: 'visualization', _source: { title: 'Actions over time', - visState: - '{"title":"Actions over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Events"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Events","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Events"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{},"customLabel":""}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.docker.Action","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}}]}', + visState: JSON.stringify({ + title: 'Actions over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Events' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Events', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Events' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + customLabel: '', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.docker.Action', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-fim.ts b/server/integration-files/visualizations/agents/agents-fim.ts index 3e2ef92933..2fa1835b30 100644 --- a/server/integration-files/visualizations/agents/agents-fim.ts +++ b/server/integration-files/visualizations/agents/agents-fim.ts @@ -14,212 +14,492 @@ export default [ _id: 'Wazuh-App-Agents-FIM-Users', _source: { title: 'Most active users', - visState: - '{"title":"Most active users","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.uname_after","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Most active users', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'syscheck.uname_after', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Actions', _source: { title: 'Actions', - visState: - '{"title":"Actions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.event","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Actions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'syscheck.event', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Events', _source: { title: 'Events', - visState: - '{ "title": "Unique events", "type": "line", "params": { "type": "line", "grid": { "categoryLines": false }, "categoryAxes": [ { "id": "CategoryAxis-1", "type": "category", "position": "bottom", "show": true, "style": {}, "scale": { "type": "linear" }, "labels": { "show": true, "truncate": 100 }, "title": {} } ], "valueAxes": [ { "id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "left", "show": true, "style": {}, "scale": { "type": "linear", "mode": "normal" }, "labels": { "show": true, "rotate": 0, "filter": false, "truncate": 100 }, "title": { "text": "Count" } } ], "seriesParams": [ { "show": "true", "type": "line", "mode": "normal", "data": { "label": "Count", "id": "1" }, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": true, "showCircles": true } ], "addTooltip": true, "addLegend": true, "legendPosition": "right", "times": [], "addTimeMarker": false, "dimensions": { "x": { "accessor": 0, "format": { "id": "terms", "params": { "id": "string", "otherBucketLabel": "Other", "missingBucketLabel": "Missing" } }, "params": {}, "aggType": "terms" }, "y": [ { "accessor": 2, "format": { "id": "number" }, "params": {}, "aggType": "count" } ], "series": [ { "accessor": 1, "format": { "id": "terms", "params": { "id": "string", "otherBucketLabel": "Other", "missingBucketLabel": "Missing" } }, "params": {}, "aggType": "terms" } ] } }, "aggs": [ { "id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {} }, { "id": "2", "enabled": true, "type": "date_histogram", "schema": "segment", "params": { "field": "timestamp", "useNormalizedEsInterval": true, "interval": "auto", "drop_partials": false, "min_doc_count": 1, "extended_bounds": {} } }, { "id": "3", "enabled": true, "type": "terms", "schema": "group", "params": { "field": "syscheck.event", "order": "desc", "size": 5, "orderBy": "1", "otherBucket": false, "otherBucketLabel": "Other", "missingBucket": false, "missingBucketLabel": "Missing" } } ] }', + visState: JSON.stringify({ + title: 'Unique events', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'syscheck.event', + order: 'desc', + size: 5, + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Files-added', _source: { title: 'Files added', - visState: - '{"title":"Files added","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.path","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Files added', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'syscheck.path', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "type": "phrases", - "key": "syscheck.event", - "value": "added, readded", - "params": [ - "added", - "readded" - ], - "negate": false, - "disabled": false, - "alias": null - }, - "query": { - "bool": { - "should": [ - { - "match_phrase": { - "syscheck.event": "added" - } - }, - { - "match_phrase": { - "syscheck.event": "readded" - } - } - ], - "minimum_should_match": 1 - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + type: 'phrases', + key: 'syscheck.event', + value: 'added, readded', + params: ['added', 'readded'], + negate: false, + disabled: false, + alias: null, + }, + query: { + bool: { + should: [ + { + match_phrase: { + 'syscheck.event': 'added', + }, + }, + { + match_phrase: { + 'syscheck.event': 'readded', + }, + }, + ], + minimum_should_match: 1, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Files-modified', _source: { title: 'Files modified', - visState: - '{"title":"Files modified","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.path","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Files modified', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'syscheck.path', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "syscheck.event", - "value": "modified", - "params": { - "query": "modified", - "type": "phrase" - } - }, - "query": { - "match": { - "syscheck.event": { - "query": "modified", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'syscheck.event', + value: 'modified', + params: { + query: 'modified', + type: 'phrase', + }, + }, + query: { + match: { + 'syscheck.event': { + query: 'modified', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Files-deleted', _source: { title: 'Files deleted', - visState: - '{"title":"Files deleted","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.path","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Files deleted', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'syscheck.path', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "syscheck.event", - "value": "deleted", - "params": { - "query": "deleted", - "type": "phrase" - } - }, - "query": { - "match": { - "syscheck.event": { - "query": "deleted", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'syscheck.event', + value: 'deleted', + params: { + query: 'deleted', + type: 'phrase', + }, + }, + query: { + match: { + 'syscheck.event': { + query: 'deleted', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-FIM-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"syscheck.path","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"File"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'syscheck.path', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'File', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-gcp.ts b/server/integration-files/visualizations/agents/agents-gcp.ts index ecd483b649..2ce2678ea4 100644 --- a/server/integration-files/visualizations/agents/agents-gcp.ts +++ b/server/integration-files/visualizations/agents/agents-gcp.ts @@ -16,126 +16,646 @@ export default [ _type: 'visualization', _source: { title: 'Top 5 rules', - visState: - '{"title":"Top 5 rules","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","size":500,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule ID"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Event"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Top 5 rules', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + size: 500, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Event', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Event-Query-Name', _type: 'visualization', _source: { title: 'Top query events', - visState: - '{"title":"Wazuh-App-Agents-GCP-Query-Name","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.jsonPayload.queryName","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-Query-Name', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.jsonPayload.queryName', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Tag-Severities', _type: 'visualization', _source: { title: 'Severities count', - visState: - '{"title":"Wazuh-App-Agents-GCP-Tag-Severities","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":true,"metric":{"type":"vis_dimension","accessor":1,"format":{"id":"string","params":{}}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.severity","orderBy":"1","order":"desc","size":5,"otherBucket":true,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Severities"}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-Tag-Severities', + type: 'tagcloud', + params: { + scale: 'linear', + orientation: 'single', + minFontSize: 18, + maxFontSize: 72, + showLabel: true, + metric: { type: 'vis_dimension', accessor: 1, format: { id: 'string', params: {} } }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: { customLabel: '' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.severity', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: true, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severities', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Top-5-instances', _type: 'visualization', _source: { title: 'Top 5 instances', - visState: - '{"title":"Wazuh-App-Agents-GCP-Top-5-instances","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.jsonPayload.vmInstanceId","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-Top-5-instances', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.jsonPayload.vmInstanceId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Top-5-resource-type', _type: 'visualization', _source: { title: 'Top 5 Events type', - visState: - '{"title":"Wazuh-App-Agents-GCP-Top-5-resource-type","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.type","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-Top-5-resource-type', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.type', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-authAnswer-Bar', _type: 'visualization', _source: { title: 'Auth answer count', - visState: - '{"title":"Wazuh-App-Agents-GCP-authAnswer-Bar","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.jsonPayload.authAnswer","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"authAnswer"}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-authAnswer-Bar', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.jsonPayload.authAnswer', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'authAnswer', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Events-Over-Time', _type: 'visualization', _source: { title: 'GCP alerts evolution', - visState: - '{"title":"Wazuh-App-Agents-GCP-Events-Over-Time","type":"line","params":{"type":"line","grid":{"categoryLines":false,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"labels":{},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","format":"YYYY-MM-DD","bounds":{"min":"2019-09-07T14:30:14.047Z","max":"2019-11-07T14:19:07.505Z"}},"aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-2M","to":"2019-11-07T14:19:07.505Z"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{},"customLabel":""}}]}', + visState: JSON.stringify({ + title: 'Wazuh-App-Agents-GCP-Events-Over-Time', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + labels: {}, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + params: { + date: true, + interval: 'P1D', + format: 'YYYY-MM-DD', + bounds: { min: '2019-09-07T14:30:14.047Z', max: '2019-11-07T14:19:07.505Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-2M', to: '2019-11-07T14:19:07.505Z' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + customLabel: '', + }, + }, + ], + }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-GCP-Top-ResourceType-By-Project-Id', _source: { title: 'Resource type by project id', - visState: - '{"title":"Top resource type by project","type":"horizontal_bar","params":{"addLegend":true,"addTimeMarker":false,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":false,"rotate":0,"show":true,"truncate":200},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"grid":{"categoryLines":false},"labels":{},"legendPosition":"right","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"mode":"normal","show":true,"showCircles":true,"type":"histogram","valueAxis":"ValueAxis-1"}],"times":[],"type":"histogram","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":true,"rotate":75,"show":true,"truncate":100},"name":"LeftAxis-2","position":"left","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.project_id","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Project ID"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.gcp.resource.type","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Resource type"}}]}', + visState: JSON.stringify({ + title: 'Top resource type by project', + type: 'horizontal_bar', + params: { + addLegend: true, + addTimeMarker: false, + addTooltip: true, + categoryAxes: [ + { + id: 'CategoryAxis-1', + labels: { filter: false, rotate: 0, show: true, truncate: 200 }, + position: 'bottom', + scale: { type: 'linear' }, + show: true, + style: {}, + title: {}, + type: 'category', + }, + ], + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + grid: { categoryLines: false }, + labels: {}, + legendPosition: 'right', + seriesParams: [ + { + data: { id: '1', label: 'Count' }, + drawLinesBetweenPoints: true, + mode: 'normal', + show: true, + showCircles: true, + type: 'histogram', + valueAxis: 'ValueAxis-1', + }, + ], + times: [], + type: 'histogram', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { filter: true, rotate: 75, show: true, truncate: 100 }, + name: 'LeftAxis-2', + position: 'left', + scale: { mode: 'normal', type: 'linear' }, + show: true, + style: {}, + title: { text: 'Count' }, + type: 'value', + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.project_id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Project ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.gcp.resource.type', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Resource type', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -144,14 +664,125 @@ export default [ _id: 'Wazuh-App-Agents-GCP-Top-ProjectId-By-SourceType', _source: { title: 'Top project id by sourcetype', - visState: - '{"title":"top project id by source type","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":4,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"4","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.location","customLabel":"Location","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.project_id","customLabel":"Project ID","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.source_type","customLabel":"Source type","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'top project id by source type', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 4, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.location', + customLabel: 'Location', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.project_id', + customLabel: 'Project ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.source_type', + customLabel: 'Source type', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -161,16 +792,85 @@ export default [ _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, -]; \ No newline at end of file +]; diff --git a/server/integration-files/visualizations/agents/agents-gdpr.ts b/server/integration-files/visualizations/agents/agents-gdpr.ts index 0652c17b8f..84a3a30512 100644 --- a/server/integration-files/visualizations/agents/agents-gdpr.ts +++ b/server/integration-files/visualizations/agents/agents-gdpr.ts @@ -14,97 +14,320 @@ export default [ _id: 'Wazuh-App-Agents-GDPR-Groups', _source: { title: 'Top 5 rule groups', - visState: - '{"title":"Top 5 rule groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rule groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.groups', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-GDPR-Rule', _source: { title: 'Top 5 rules', - visState: - '{"title":"Top 5 rules","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rules', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.description', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-GDPR-Requirement', _source: { title: 'Top 5 requirements', - visState: - '{"title":"Top 5 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gdpr","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.gdpr', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-GDPR-Rule-level-distribution', _source: { title: 'Rule level distribution', - visState: - '{"title":"Rule level distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","size":15,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'Rule level distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + size: 15, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: false } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-GDPR-Requirements', _source: { title: 'Requirements', - visState: - '{"title":"Requirements","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.gdpr","size":5,"order":"desc","orderBy":"1","customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gdpr","size":10,"order":"desc","orderBy":"1","customLabel":"GDPR requirements"}}]}', + visState: JSON.stringify({ + title: 'Requirements', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter:true,truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'rule.gdpr', size: 5, order: 'desc', orderBy: '1', customLabel: '' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.gdpr', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'GDPR requirements', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-GDPR-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.gdpr","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.gdpr', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-general.ts b/server/integration-files/visualizations/agents/agents-general.ts index 1068fc8172..e8993cf1f5 100644 --- a/server/integration-files/visualizations/agents/agents-general.ts +++ b/server/integration-files/visualizations/agents/agents-general.ts @@ -14,281 +14,794 @@ export default [ _id: 'Wazuh-App-Agents-General-Top-5-alerts', _source: { title: 'Top 5 alerts', - visState: - '{"title":"Top 5 alerts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Top 5 alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Metric-alerts', _source: { title: 'Metric alerts', - visState: - '{"title":"Metric Alerts","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Alerts"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Metric Alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Level-12-alerts', _source: { title: 'Level 12 alerts', - visState: - '{"title":"Count Level 12 Alerts","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Level 12 or above alerts"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Level 12 Alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Level 12 or above alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "$state": { - "store": "appState" - }, - "meta": { - "alias": null, - "disabled": false, - "index": "wazuh-alerts", - "key": "rule.level", - "negate": false, - "params": { - "gte": 12, - "lt": null - }, - "type": "range", - "value": "12 to +∞" - }, - "range": { - "rule.level": { - "gte": 12, - "lt": null - } - } - } - ], - "query":{ "query": "", "language": "lucene" } - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + index: 'wazuh-alerts', + key: 'rule.level', + negate: false, + params: { + gte: 12, + lt: null, + }, + type: 'range', + value: '12 to +∞', + }, + range: { + 'rule.level': { + gte: 12, + lt: null, + }, + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Authentication-failure', _source: { title: 'Authentication failure', - visState: - '{"title":"Count Authentication Failure","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Authentication failure"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Authentication Failure', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication failure' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "type": "phrases", - "key": "rule.groups", - "value": "win_authentication_failed, authentication_failed, authentication_failures", - "params": [ - "win_authentication_failed", - "authentication_failed", - "authentication_failures" - ], - "negate": false, - "disabled": false, - "alias": null - }, - "query": { - "bool": { - "should": [ - { - "match_phrase": { - "rule.groups": "win_authentication_failed" - } - }, - { - "match_phrase": { - "rule.groups": "authentication_failed" - } - }, - { - "match_phrase": { - "rule.groups": "authentication_failures" - } - } - ], - "minimum_should_match": 1 - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + type: 'phrases', + key: 'rule.groups', + value: 'win_authentication_failed, authentication_failed, authentication_failures', + params: [ + 'win_authentication_failed', + 'authentication_failed', + 'authentication_failures', + ], + negate: false, + disabled: false, + alias: null, + }, + query: { + bool: { + should: [ + { + match_phrase: { + 'rule.groups': 'win_authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failures', + }, + }, + ], + minimum_should_match: 1, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Authentication-success', _source: { title: 'Authentication success', - visState: - '{"title":"Count Authentication Success","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Authentication success"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Authentication Success', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication success' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "authentication_success", - "params": { - "query": "authentication_success", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "authentication_success", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'authentication_success', + params: { + query: 'authentication_success', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'authentication_success', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Top-10-groups', _source: { title: 'Top 5 rule groups', - visState: - '{"title":"Top 5 rule groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Top 5 rule groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.groups', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Top-5-PCI-DSS-Requirements', _source: { title: 'Top 5 PCI DSS requirements', - visState: - '{"title":"Top 5 PCI DSS requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 PCI DSS requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{"vis":{"legendOpen":true}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Alert-groups-evolution', _source: { title: 'Alert groups evolution', - visState: - '{"title":"Alerts by group over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by group over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.groups', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Alerts', _source: { title: 'Alerts', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.level","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.level', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-General-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-General-Groups-summary', _type: 'visualization', _source: { title: 'Groups summary', - visState: - '{"title":"Groups summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":1,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.groups","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Group"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":1,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Groups summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 1, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.groups', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Group', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 1, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-hipaa.ts b/server/integration-files/visualizations/agents/agents-hipaa.ts index 3823a2a910..d92ed6816d 100644 --- a/server/integration-files/visualizations/agents/agents-hipaa.ts +++ b/server/integration-files/visualizations/agents/agents-hipaa.ts @@ -14,97 +14,637 @@ export default [ _id: 'Wazuh-App-Agents-HIPAA-Burbles', _source: { title: 'HIPAA requirements', - visState: - '{"title":"HIPAA requirements","type":"line","params":{"type":"line","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":false,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT12H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-07-24T10:27:37.970Z","max":"2019-08-23T10:27:37.970Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"z":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"radiusRatio":20},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-30d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{},"customLabel":"Timestampt"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + visState: JSON.stringify({ + title: 'HIPAA requirements', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: false, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT12H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-07-24T10:27:37.970Z', max: '2019-08-23T10:27:37.970Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + z: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + radiusRatio: 20, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-30d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + customLabel: 'Timestampt', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { id: '4', enabled: true, type: 'count', schema: 'radius', params: {} }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-HIPAA-Distributed-By-Level', _source: { title: 'Requirements distribution by level', - visState: - '{"title":"Requirements distribution by level","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"orderBucketsBySum":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Level"}}]}', + visState: JSON.stringify({ + title: 'Requirements distribution by level', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + orderBucketsBySum: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Level', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-HIPAA-Most-Common', _source: { title: 'Most common alerts', - visState: - '{"title":"Most common alerts","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":15,"maxFontSize":25,"showLabel":true,"metric":{"type":"vis_dimension","accessor":1,"format":{"id":"string","params":{}}},"bucket":{"type":"vis_dimension","accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Most common alerts', + type: 'tagcloud', + params: { + scale: 'linear', + orientation: 'single', + minFontSize: 15, + maxFontSize: 25, + showLabel: true, + metric: { type: 'vis_dimension', accessor: 1, format: { id: 'string', params: {} } }, + bucket: { + type: 'vis_dimension', + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-HIPAA-top-10', _source: { title: 'Top 10 requirements', - visState: - '{"title":"Top 10 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Top 10 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-HIPAA-Requirements-Stacked-Overtime', _source: { title: 'Requirements over time', - visState: - '{"title":"Requirements over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT1H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-08-19T09:19:10.911Z","max":"2019-08-23T09:19:10.911Z"}},"aggType":"date_histogram"},"y":[{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-4d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{},"customLabel":"Timestampt"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Requirements over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT1H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-08-19T09:19:10.911Z', max: '2019-08-23T09:19:10.911Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-4d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + customLabel: 'Timestampt', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-HIPAA-Last-alerts', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum","dimensions":{"metrics":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":1,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","orderBy":"1","order":"desc","size":200,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + dimensions: { + metrics: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 200, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-mitre.ts b/server/integration-files/visualizations/agents/agents-mitre.ts index 6d92c45af3..6d6f043d70 100644 --- a/server/integration-files/visualizations/agents/agents-mitre.ts +++ b/server/integration-files/visualizations/agents/agents-mitre.ts @@ -14,129 +14,766 @@ export default [ _id: 'Wazuh-App-Agents-MITRE', _source: { title: 'Mitre attack count', - visState: - '{"aggs":[{"enabled":true,"id":"1","params":{},"schema":"metric","type":"count"},{"enabled":true,"id":"2","params":{"field":"rule.mitre.id","customLabel":"Attack ID","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":244},"schema":"bucket","type":"terms"}],"params":{"dimensions":{"buckets":[],"metrics":[{"accessor":0,"aggType":"count","format":{"id":"number"},"params":{}}]},"perPage":10,"percentageCol":"","showMetricsAtAllLevels":false,"showPartialRows":false,"showTotal":false,"sort":{"columnIndex":null,"direction":null},"totalFunc":"sum"},"title":"mitre","type":"table"}', + visState: JSON.stringify({ + aggs: [ + { enabled: true, id: '1', params: {}, schema: 'metric', type: 'count' }, + { + enabled: true, + id: '2', + params: { + field: 'rule.mitre.id', + customLabel: 'Attack ID', + missingBucket: false, + missingBucketLabel: 'Missing', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + size: 244, + }, + schema: 'bucket', + type: 'terms', + }, + ], + params: { + dimensions: { + buckets: [], + metrics: [{ accessor: 0, aggType: 'count', format: { id: 'number' }, params: {} }], + }, + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + showToolbar: true, + sort: { columnIndex: null, direction: null }, + totalFunc: 'sum', + }, + title: 'mitre', + type: 'table', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Alerts-Evolution', _source: { title: 'Mitre alerts evolution', - visState: - '{"title":"Alert Evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true,"lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT3H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-11-07T15:45:45.770Z","max":"2019-11-14T15:45:45.770Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-7d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.mitre.technique","customLabel":"Attack ID","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":""}}]}', + visState: JSON.stringify({ + title: 'Alert Evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + lineWidth: 2, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-11-07T15:45:45.770Z', max: '2019-11-14T15:45:45.770Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + customLabel: 'Attack ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Attacks-By-Agent', _source: { title: 'Attacks count by agent', - visState: - '{"title":"Attacks count by agent","type":"pie","params":{"addLegend":true,"addTooltip":true,"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"isDonut":true,"labels":{"last_level":true,"show":false,"truncate":100,"values":true},"legendPosition":"right","type":"pie"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","customLabel":"Agent name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.id","customLabel":"Attack ID", "orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Attacks count by agent', + type: 'pie', + params: { + addLegend: true, + addTooltip: true, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + isDonut: true, + labels: { last_level: true, show: false, truncate: 100, values: true }, + legendPosition: 'right', + type: 'pie', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + customLabel: 'Agent name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.id', + customLabel: 'Attack ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Level-By-Tactic', _source: { title: 'Alerts level by tactic', - visState: - '{"title":"Alerts level by tactic","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Attack ID"}},{"id":"4","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}}]}', + visState: JSON.stringify({ + title: 'Alerts level by tactic', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Attack ID', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Level-By-Attack', _source: { title: 'Alerts level by attack', - visState: - '{"title":"Alerts level by attack","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":4,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.technique","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Attack ID"}},{"id":"4","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}}]}', + visState: JSON.stringify({ + title: 'Alerts level by attack', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 4, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Attack ID', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Attacks-By-Tactic', _source: { title: 'Top tactics', - visState: - '{"title":"Attacks by tactic","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"dimensions":{"x":null,"y":[{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.mitre.technique","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Attacks by tactic', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: null, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Top-Tactics', _source: { title: 'Top tactics pie', - visState: - '{"title":"Top tactics PIE2","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top tactics PIE2', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-MITRE-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/agents/agents-nist.ts b/server/integration-files/visualizations/agents/agents-nist.ts index c9a1e77591..1330ba7042 100644 --- a/server/integration-files/visualizations/agents/agents-nist.ts +++ b/server/integration-files/visualizations/agents/agents-nist.ts @@ -14,97 +14,551 @@ export default [ _id: 'Wazuh-App-Agents-NIST-Stats', _source: { title: 'Stats', - visState: - '{"title":"Stats","type":"metric","params":{"metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"type":"range","from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}},"dimensions":{"metrics":[{"type":"vis_dimension","accessor":0,"format":{"id":"number","params":{}}},{"type":"vis_dimension","accessor":1,"format":{"id":"number","params":{}}}]},"addTooltip":true,"addLegend":false,"type":"metric"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total alerts"}},{"id":"3","enabled":true,"type":"max","schema":"metric","params":{"field":"rule.level","customLabel":"Max rule level"}}]}', + visState: JSON.stringify({ + title: 'Stats', + type: 'metric', + params: { + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ type: 'range', from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + dimensions: { + metrics: [ + { type: 'vis_dimension', accessor: 0, format: { id: 'number', params: {} } }, + { type: 'vis_dimension', accessor: 1, format: { id: 'number', params: {} } }, + ], + }, + addTooltip: true, + addLegend: false, + type: 'metric', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total alerts' }, + }, + { + id: '3', + enabled: true, + type: 'max', + schema: 'metric', + params: { field: 'rule.level', customLabel: 'Max rule level' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-NIST-top-10-requirements', _source: { title: 'Top 10 requirements', - visState: - '{"title":"Top 10 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":0,"format":{"id":"number"},"params":{},"aggType":"count"}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Top 10 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 0, format: { id: 'number' }, params: {}, aggType: 'count' }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-NIST-Requirement-by-level', _source: { title: 'Requirements distributed by level', - visState: - '{"title":"Requirements distributed by level","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"left","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":200},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"bottom","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":75,"filter":true,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"labels":{"show":false}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Level"}}]}', + visState: JSON.stringify({ + title: 'Requirements distributed by level', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, rotate: 0, filter: true, truncate: 200 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 75, filter: true, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + labels: { show: false }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Level', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-NIST-Rule-level-distribution', _source: { title: 'Rule level distribution', - visState: - '{"title":"Rule level distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","size":15,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'Rule level distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + size: 15, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: false } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-NIST-Requirements-stacked-overtime', _source: { title: 'Requirements over time', - visState: - '{"title":"Requirements over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT1H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-08-19T09:46:35.795Z","max":"2019-08-23T09:46:35.795Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-4d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{},"customLabel":"Timestamp"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Requirements over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT1H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-08-19T09:46:35.795Z', max: '2019-08-23T09:46:35.795Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-4d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + customLabel: 'Timestamp', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-NIST-Last-alerts', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum","dimensions":{"metrics":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":1,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","orderBy":"1","order":"desc","size":200,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + dimensions: { + metrics: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 200, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-oscap.ts b/server/integration-files/visualizations/agents/agents-oscap.ts index 4493a459e7..777b8effd8 100644 --- a/server/integration-files/visualizations/agents/agents-oscap.ts +++ b/server/integration-files/visualizations/agents/agents-oscap.ts @@ -14,556 +14,942 @@ export default [ _id: 'Wazuh-App-Agents-OSCAP-Higher-score-metric', _source: { title: 'Higher score metric', - visState: - '{"title":"Higher score metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"data.oscap.scan.score","customLabel":"Higher score"}}]}', + visState: JSON.stringify({ + title: 'Higher score metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'max', + schema: 'metric', + params: { field: 'data.oscap.scan.score', customLabel: 'Higher score' }, + }, + ], + }), uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Lower-score-metric', _source: { title: 'Lower score metric', - visState: - '{"title":"Lower score metric","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"min","schema":"metric","params":{"field":"data.oscap.scan.score","customLabel":"Lower score"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Lower score metric', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'min', + schema: 'metric', + params: { field: 'data.oscap.scan.score', customLabel: 'Lower score' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Last-score', _source: { title: 'Last score', - visState: - '{"title":"Last score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.score","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.scan.score', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Last-scan-profile', _source: { title: 'Last scan profile', - visState: - '{"title":"Last scan profile","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.profile.title","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan profile', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.scan.profile.title', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Scans', _source: { title: 'Scans', - visState: - '{"title":"Scans","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.scan.id","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Scans', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.scan.id', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Profiles', _source: { title: 'Profiles', - visState: - '{"title":"Profiles","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.scan.profile.title","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Profiles', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.oscap.scan.profile.title', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Content', _source: { title: 'Content', - visState: - '{"title":"Content","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.scan.content","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Content', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.scan.content', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Severity', _source: { title: 'Severity', - visState: - '{"title":"Severity","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.severity","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Severity', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.severity', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Daily-scans-evolution', _source: { title: 'Daily scans evolution', - visState: - '{"title":"Daily scans evolution","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{},"customLabel":"Daily scans"}}]}', + visState: JSON.stringify({ + title: 'Daily scans evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + customLabel: 'Daily scans', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Top-5-Alerts', _source: { title: 'Top 5 Alerts', - visState: - '{"title":"Top 5 Alerts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.title","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 Alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.title', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Top-5-High-risk-alerts', _source: { title: 'Top 5 High risk alerts', - visState: - '{"title":"Top 5 High risk alerts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.title","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 High risk alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.title', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.severity", - "value": "high", - "params": { - "query": "high", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.severity": { - "query": "high", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.severity', + value: 'high', + params: { + query: 'high', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.severity': { + query: 'high', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Top-alert', _source: { title: 'Top alert', - visState: - '{"title":"Top alert","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.check.title","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Top alert', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.check.title', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-OSCAP-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.check.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Title"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.profile.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Profile"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.check.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Title', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.scan.profile.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Profile', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-osquery.ts b/server/integration-files/visualizations/agents/agents-osquery.ts index 2f6c2d5215..fc0308b1a4 100644 --- a/server/integration-files/visualizations/agents/agents-osquery.ts +++ b/server/integration-files/visualizations/agents/agents-osquery.ts @@ -15,145 +15,693 @@ export default [ _type: 'visualization', _source: { title: 'Most common rules being fired', - visState: - '{"title":"Most common rules being fired","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","size":1,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Most common rules being fired', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-added', _type: 'visualization', _source: { title: 'Top 5 added', - visState: - '{"title":"Top 5 added","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 added', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"data.osquery.action","value":"added","params":{"query":"added","type":"phrase"}},"query":{"match":{"data.osquery.action":{"query":"added","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.osquery.action', + value: 'added', + params: { query: 'added', type: 'phrase' }, + }, + query: { match: { 'data.osquery.action': { query: 'added', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-removed', _type: 'visualization', _source: { title: 'Top 5 removed', - visState: - '{"title":"Top 5 removed","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 removed', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"data.osquery.action","value":"removed","params":{"query":"removed","type":"phrase"}},"query":{"match":{"data.osquery.action":{"query":"removed","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.osquery.action', + value: 'removed', + params: { query: 'removed', type: 'phrase' }, + }, + query: { match: { 'data.osquery.action': { query: 'removed', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-Evolution', _type: 'visualization', _source: { title: 'Evolution over time', - visState: - '{"title":"Evolution over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.osquery.name","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Evolution over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.osquery.name', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-top-5-packs-being-used', _type: 'visualization', _source: { title: 'Top 5 packs being used', - visState: - '{"title":"Top 5 packs being used","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.pack","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 packs being used', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.pack', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-most-common-osquery-actions', _type: 'visualization', _source: { title: 'Most common Osquery actions', - visState: - '{"title":"Most common Osquery actions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.action","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Most common Osquery actions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.action', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-events-per-pack-over-time', _type: 'visualization', _source: { title: 'Events per pack over time', - visState: - '{"title":"Events per pack over time","type":"line","params":{"type":"line","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"data.osquery.pack","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Events per pack over time', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.osquery.pack', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-events-over-time', _type: 'visualization', _source: { title: 'Osquery events over time', - visState: - '{"title":"Osquery events over time","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Osquery events over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"table","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":5,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.name","size":20,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Name"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.action","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.pack","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":true,"missingBucketLabel":"-","customLabel":"Pack"}},{"id":"6","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.calendarTime","size":2,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Date"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":5,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'table', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 5, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.name', + size: 20, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Name', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.action', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.pack', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: true, + missingBucketLabel: '-', + customLabel: 'Pack', + }, + }, + { + id: '6', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.calendarTime', + size: 2, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Date', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 5, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-pci.ts b/server/integration-files/visualizations/agents/agents-pci.ts index c57f9af758..b824764592 100644 --- a/server/integration-files/visualizations/agents/agents-pci.ts +++ b/server/integration-files/visualizations/agents/agents-pci.ts @@ -14,97 +14,326 @@ export default [ _id: 'Wazuh-App-Agents-PCI-Groups', _source: { title: 'Top 5 rule groups', - visState: - '{"title":"Top 5 rule groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rule groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.groups', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PCI-Rule', _source: { title: 'Top 5 rules', - visState: - '{"title":"Top 5 rules","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rules', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.description', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PCI-Requirement', _source: { title: 'Top 5 requirements', - visState: - '{"title":"Top 5 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.pci_dss', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PCI-Rule-level-distribution', _source: { title: 'Rule level distribution', - visState: - '{"title":"Rule level distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","size":15,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'Rule level distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + size: 15, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: false } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PCI-Requirements', _source: { title: 'Requirements', - visState: - '{"title":"Requirements","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.pci_dss","size":5,"order":"desc","orderBy":"1","customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","size":10,"order":"desc","orderBy":"1","customLabel":"PCI DSS Requirements"}}]}', + visState: JSON.stringify({ + title: 'Requirements', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.pci_dss', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: '', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'PCI DSS Requirements', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PCI-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.pci_dss","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.pci_dss', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-pm.ts b/server/integration-files/visualizations/agents/agents-pm.ts index aca2902cfd..6cea6ea484 100644 --- a/server/integration-files/visualizations/agents/agents-pm.ts +++ b/server/integration-files/visualizations/agents/agents-pm.ts @@ -15,64 +15,316 @@ export default [ _type: 'visualization', _source: { title: 'Events over time', - visState: - '{"title":"Events over time","type":"area","params":{"scale":"linear","yAxis":{},"smoothLines":true,"addTimeMarker":false,"interpolate":"linear","addLegend":true,"shareYAxis":true,"mode":"overlap","defaultYExtents":false,"setYExtents":false,"addTooltip":true,"times":[],"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal","setYExtents":false,"defaultYExtents":false},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"normal","data":{"label":"Count","id":"1"},"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"legendPosition":"right"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Events over time', + type: 'area', + params: { + scale: 'linear', + yAxis: {}, + smoothLines: true, + addTimeMarker: false, + interpolate: 'linear', + addLegend: true, + shareYAxis: true, + mode: 'overlap', + defaultYExtents: false, + setYExtents: false, + addTooltip: true, + times: [], + type: 'area', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal', setYExtents: false, defaultYExtents: false }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'normal', + data: { label: 'Count', id: '1' }, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + legendPosition: 'right', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + { + id: '3', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-PM-Top-5-rules', _type: 'visualization', _source: { title: 'Top 5 rules', - visState: - '{"title":"Export rule distr","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"rule.level"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Export rule distr', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'sum', + schema: 'metric', + params: { field: 'rule.level' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-PM-Events-per-agent-evolution', _source: { title: 'Events per control type evolution', - visState: - '{"title":"Events per control type evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.title","size":5,"order":"desc","orderBy":"1"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Events per control type evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'data.title', size: 5, order: 'desc', orderBy: '1' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-PM-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Control"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Control', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-tsc.ts b/server/integration-files/visualizations/agents/agents-tsc.ts index 973b26d7bb..256b40e586 100644 --- a/server/integration-files/visualizations/agents/agents-tsc.ts +++ b/server/integration-files/visualizations/agents/agents-tsc.ts @@ -14,97 +14,320 @@ export default [ _id: 'Wazuh-App-Agents-TSC-Groups', _source: { title: 'Top 5 rule groups', - visState: - '{"title":"Top 5 rule groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rule groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.groups', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-TSC-Rule', _source: { title: 'Top 5 rules', - visState: - '{"title":"Top 5 rules","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 rules', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.description', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-TSC-Requirement', _source: { title: 'Top 5 requirements', - visState: - '{"title":"Top 5 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.tsc","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.tsc', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-TSC-Rule-level-distribution', _source: { title: 'Rule level distribution', - visState: - '{"title":"Rule level distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":false,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.level","size":15,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'Rule level distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: false, + legendPosition: 'right', + isDonut: true, + labels: { show: true, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.level', + size: 15, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: false } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-TSC-Requirements', _source: { title: 'Requirements', - visState: - '{"title":"Requirements","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.tsc","size":5,"order":"desc","orderBy":"1","customLabel":""}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.tsc","size":10,"order":"desc","orderBy":"1","customLabel":"TSC Requirements"}}]}', + visState: JSON.stringify({ + title: 'Requirements', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'rule.tsc', size: 5, order: 'desc', orderBy: '1', customLabel: '' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.tsc', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'TSC Requirements', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Agents-TSC-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.tsc","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.tsc', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/agents/agents-virustotal.ts b/server/integration-files/visualizations/agents/agents-virustotal.ts index 253012c274..a6ffe63281 100644 --- a/server/integration-files/visualizations/agents/agents-virustotal.ts +++ b/server/integration-files/visualizations/agents/agents-virustotal.ts @@ -15,256 +15,555 @@ export default [ _type: 'visualization', _source: { title: 'Last files', - visState: - '{"title":"Last files","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Files"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.virustotal.source.file","size":5,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Last files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Files' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.virustotal.source.file', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Files-Table', _type: 'visualization', _source: { title: 'Files', - visState: - '{"title":"Files","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Count"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.virustotal.source.file","size":10,"order":"desc","orderBy":"1","customLabel":"File"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.virustotal.permalink","size":1,"order":"desc","orderBy":"1", "missingBucket":true, "missingBucketLabel":"-", "customLabel":"Link"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Files', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Count' }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.source.file', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'File', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.permalink', + size: 1, + order: 'desc', + orderBy: '1', + missingBucket: true, + missingBucketLabel: '-', + customLabel: 'Link', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Total-Malicious', _type: 'visualization', _source: { title: 'Total Malicious', - visState: - '{"title":"Total Malicious","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total malicious files"}}]}', + visState: JSON.stringify({ + title: 'Total Malicious', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total malicious files' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.malicious", - "value": "1", - "params": { - "query": "1", - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.malicious": { - "query": "1", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '1', + params: { + query: '1', + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: '1', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Total-Positives', _type: 'visualization', _source: { title: 'Total Positives', - visState: - '{"title":"Total Positives","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total positive files"}}]}', + visState: JSON.stringify({ + title: 'Total Positives', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total positive files' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal.positives", - "value": "exists" - }, - "exists": { - "field": "data.virustotal.positives" - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.positives", - "value": "0", - "params": { - "query": 0, - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.positives": { - "query": 0, - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Malicious-Evolution', _type: 'visualization', _source: { title: 'Malicious Evolution', - visState: - '{"title":"Malicious Evolution","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Malicious"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Malicious","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Malicious"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Malicious Evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Malicious' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Malicious', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Malicious' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal.positives", - "value": "exists" - }, - "exists": { - "field": "data.virustotal.positives" - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.positives", - "value": "0", - "params": { - "query": 0, - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.positives": { - "query": 0, - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Total', _type: 'visualization', _source: { title: 'Total', - visState: - '{"title":"Total","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total scans"}}]}', + visState: JSON.stringify({ + title: 'Total', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total scans' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[{ - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal", - "value": "exists" - }, - "exists": { - "field": "data.virustotal" - }, - "$state": { - "store": "appState" - } - }], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal', + value: 'exists', + }, + exists: { + field: 'data.virustotal', + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Virustotal-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/agents/agents-vuls.ts b/server/integration-files/visualizations/agents/agents-vuls.ts index 2d3f4a67ba..baf546717d 100644 --- a/server/integration-files/visualizations/agents/agents-vuls.ts +++ b/server/integration-files/visualizations/agents/agents-vuls.ts @@ -15,361 +15,1090 @@ export default [ _type: 'visualization', _source: { title: 'Alerts severity over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.severity","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Alert-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"vulnerability","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":4,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.severity","size":5,"order":"asc","orderBy":"_key","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Severity"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.title","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Title"}},{"id":"6","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.published","size":2,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Published"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.cve","size":1,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"CVE"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":4,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'vulnerability', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 4, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'asc', + orderBy: '_key', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severity', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.title', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Title', + }, + }, + { + id: '6', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.published', + size: 2, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Published', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.cve', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'CVE', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 4, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Commonly-affected-packages', _type: 'visualization', _source: { title: 'Top 5 affected packages', - visState: - '{"title":"Top 5 affected packages","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.package.name","size":5,"order":"desc","orderBy":"1","customLabel":"Affected package"}}]}', + visState: JSON.stringify({ + title: 'Top 5 affected packages', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.package.name', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Affected package', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Metric-Critical-severity', _type: 'visualization', _source: { title: 'Metric Critical severity', - visState: - '{"title":"Metric Critical severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Critical severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Critical severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Critical severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Critical", - "params": { - "query": "Critical", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Critical", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Critical', + params: { + query: 'Critical', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Critical', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Metric-High-severity', _type: 'visualization', _source: { title: 'Metric High severity', - visState: - '{"title":"Metric High severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"High severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric High severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'High severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "High", - "params": { - "query": "High", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "High", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'High', + params: { + query: 'High', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'High', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Metric-Medium-severity', _type: 'visualization', _source: { title: 'Metric Medium severity', - visState: - '{"title":"Metric Medium severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Medium severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Medium severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Medium severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Medium", - "params": { - "query": "Medium", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Medium", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Medium', + params: { + query: 'Medium', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Medium', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Metric-Low-severity', _type: 'visualization', _source: { title: 'Metric Low severity', - visState: - '{"title":"Metric Low severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Low severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Low severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Low severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Low", - "params": { - "query": "Low", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Low", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Low', + params: { + query: 'Low', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Low', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Top-Agents-severity', _type: 'visualization', _source: { title: 'Top Agents severity', - visState: - '{"title":"Top Agents severity","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.severity","size":5,"order":"desc","orderBy":"1","customLabel":"Severity"}}]}', + visState: JSON.stringify({ + title: 'Top Agents severity', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Severity', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Most-common-rules', _type: 'visualization', _source: { title: 'Most common rules', - visState: - "{\"type\":\"table\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"rule.id\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":20,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Rule ID\"}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"bucket\",\"params\":{\"field\":\"rule.description\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":20,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\",\"customLabel\":\"Description\"}}],\"params\":{\"perPage\":5,\"showPartialRows\":false,\"showMetricsAtAllLevels\":false,\"sort\":{\"columnIndex\":null,\"direction\":null},\"showTotal\":false,\"totalFunc\":\"sum\",\"percentageCol\":\"\"},\"title\":\"common rules\"}", - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + type: 'table', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Description', + }, + }, + ], + params: { + perPage: 7, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + percentageCol: '', + }, + title: 'common rules', + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Vulnerability-severity-distribution', _type: 'visualization', _source: { title: 'Severity distribution', - visState: - '{"title":"Severity distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.severity","size":5,"order":"desc","orderBy":"1","customLabel":"Severity"}}]}', + visState: JSON.stringify({ + title: 'Severity distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Severity', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Vulnerability-Most-common-CVEs', _type: 'visualization', _source: { title: 'Most common CVEs', - visState: - '{"title":"Most common CVEs","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cve","size":5,"order":"desc","orderBy":"1","customLabel":"CVE"}}]}', + visState: JSON.stringify({ + title: 'Most common CVEs', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cve', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'CVE', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-top-CWEs', _type: 'visualization', _source: { title: 'Top CWEs', - visState: - '{"type":"table","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.cwe_reference","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"CWE"}}],"params":{"perPage":5,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum","percentageCol":"","row":true},"title":"CWE table"}', + visState: JSON.stringify({ + type: 'table', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.cwe_reference', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'CWE', + }, + }, + ], + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + percentageCol: '', + row: true, + }, + title: 'CWE table', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-evolution-affected-packages', _type: 'visualization', _source: { title: 'Alerts evolution: Commonly affected packages', - visState: - '{"title":"Alerts evolution: Commonly affected packages","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.package.name","size":5,"order":"desc","orderBy":"1"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Alerts evolution: Commonly affected packages', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.package.name', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-Most-common-CWEs', _type: 'visualization', _source: { title: 'Most common CWEs', - visState: - '{"title":"Most common CWEs","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cwe_reference","size":5,"order":"desc","orderBy":"1","customLabel":"Severity"}}]}', + visState: JSON.stringify({ + title: 'Most common CWEs', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cwe_reference', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Severity', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Agents-vuls-packages-CVEs', _type: 'visualization', _source: { title: 'Top affected packages by CVEs', - visState: - '{"type":"histogram","mode":"stacked","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cve","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.package.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}],"params":{"type":"area","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"lineWidth":2,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#E7664C"},"labels":{}},"title":"top packages by CVE"}', + visState: JSON.stringify({ + type: 'histogram', + mode: 'stacked', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cve', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.package.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + labels: {}, + }, + title: 'top packages by CVE', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/agents/agents-welcome.ts b/server/integration-files/visualizations/agents/agents-welcome.ts index f60c1a8236..6b976268da 100644 --- a/server/integration-files/visualizations/agents/agents-welcome.ts +++ b/server/integration-files/visualizations/agents/agents-welcome.ts @@ -11,123 +11,591 @@ */ export default [ { - _id: "Wazuh-App-Agents-Welcome-Top-PCI", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-PCI', + _type: 'visualization', _source: { - title: "Top 5 rules", - visState: - '{"title":"top pci requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 rules', + visState: JSON.stringify({ + title: 'top pci requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Top-GDPR", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-GDPR', + _type: 'visualization', _source: { - title: "Top 5 GDPR", - visState: - '{"title":"top gdpr requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gdpr","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 GDPR', + visState: JSON.stringify({ + title: 'top gdpr requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.gdpr', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Top-HIPAA", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-HIPAA', + _type: 'visualization', _source: { - title: "Top 5 HIPAA", - visState: - '{"title":"top hipaa requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 HIPAA', + visState: JSON.stringify({ + title: 'top hipaa requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Top-NIST-800-53", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-NIST-800-53', + _type: 'visualization', _source: { - title: "Top 5 NIST-800-53", - visState: - '{"title":"top NIST-800-53 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 NIST-800-53', + visState: JSON.stringify({ + title: 'top NIST-800-53 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Top-GPG-13", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-GPG-13', + _type: 'visualization', _source: { - title: "Top 5 GPG-13", - visState: - '{"title":"top GPG-13 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gpg13","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 GPG-13', + visState: JSON.stringify({ + title: 'top GPG-13 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.gpg13', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Top-TSC", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Top-TSC', + _type: 'visualization', _source: { - title: "Top 5 TSC", - visState: - '{"title":"top TSC requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing","parsedUrl":{"origin":"http://172.16.1.2:5601","pathname":"/app/kibana","basePath":""}}},"params":{},"label":"Requirement","aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.tsc","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Top 5 TSC', + visState: JSON.stringify({ + title: 'top TSC requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { + accessor: 1, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://172.16.1.2:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Requirement', + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.tsc', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, { - _id: "Wazuh-App-Agents-Welcome-Events-Evolution", - _type: "visualization", + _id: 'Wazuh-App-Agents-Welcome-Events-Evolution', + _type: 'visualization', _source: { - title: "Events evolution", - visState: - '{"title":"event evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"lineWidth":2,"interpolate":"linear","showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#E7664C"},"dimensions":{"x":null,"y":[{"accessor":0,"format":{"id":"number"},"params":{},"label":"Count","aggType":"count"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","useNormalizedEsInterval":true,"scaleMetricValues":false,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', - description: "", + title: 'Events evolution', + visState: JSON.stringify({ + title: 'event evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + dimensions: { + x: null, + y: [ + { + accessor: 0, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), + description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), }, }, }, - ]; diff --git a/server/integration-files/visualizations/cluster/monitoring.ts b/server/integration-files/visualizations/cluster/monitoring.ts index 3072514250..61fff106eb 100644 --- a/server/integration-files/visualizations/cluster/monitoring.ts +++ b/server/integration-files/visualizations/cluster/monitoring.ts @@ -15,64 +15,178 @@ export default [ _type: 'visualization', _source: { title: 'Wazuh App Cluster Overview', - visState: - '{"title":"Wazuh App Cluster Overview","type":"timelion","params":{"expression":".es(*)","interval":"auto"},"aggs":[]}', + visState: JSON.stringify({ + title: 'Wazuh App Cluster Overview', + type: 'timelion', + params: { expression: '.es(*)', interval: 'auto' }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Cluster-monitoring-Overview-Manager', _type: 'visualization', _source: { title: 'Wazuh App Cluster Overview Manager', - visState: - '{"title":"Wazuh App Cluster Overview Manager","type":"timelion","params":{"expression":".es(q=agent.id:000)","interval":"auto"},"aggs":[]}', + visState: JSON.stringify({ + title: 'Wazuh App Cluster Overview Manager', + type: 'timelion', + params: { expression: '.es(q=agent.id:000)', interval: 'auto' }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Cluster-monitoring-Overview-Node', _source: { title: 'Wazuh App Cluster Overview Node', - visState: - '{"title":"Wazuh App Cluster Overview Node","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', - uiStateJSON: - '{"spy":{"mode":{"name":null,"fill":false}},"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'Wazuh App Cluster Overview Node', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + spy: { mode: { name: null, fill: false } }, + vis: { legendOpen: false }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Cluster-monitoring-Overview-Node-Pie', _type: 'visualization', _source: { title: 'Wazuh App Cluster Overview Node Pie', - visState: - '{"title":"Wazuh App Cluster Overview Node Pie","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"cluster.node","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: '{"spy":{"mode":{"name":"table"}}}', + visState: JSON.stringify({ + title: 'Wazuh App Cluster Overview Node Pie', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'cluster.node', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ spy: { mode: { name: 'table' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/cluster/statistics.ts b/server/integration-files/visualizations/cluster/statistics.ts index 4f3187afb9..f16bd6cef7 100644 --- a/server/integration-files/visualizations/cluster/statistics.ts +++ b/server/integration-files/visualizations/cluster/statistics.ts @@ -15,176 +15,297 @@ export default [ _type: 'visualization', _source: { title: 'Wazuh App Statistics remoted Recv bytes', - visState: - `{"title":"Wazuh App Statistics remoted Recv bytes","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.recv_bytes, q='*').label(recv_bytes),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.recv_bytes, q='*').trend().label(Trend).lines(width=1.5)","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics remoted Recv bytes', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.recv_bytes, q='*').label(recv_bytes),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.recv_bytes, q='*').trend().label(Trend).lines(width=1.5)", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-remoted-event-count', _type: 'visualization', _source: { title: 'Wazuh App Statistics remoted event count', - visState: - `{"title":"Wazuh App Statistics remoted event count","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.evt_count, q='*').label(evt_count),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.evt_count, q='*').trend().label(Trend).lines(width=1.5)","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics remoted event count', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.evt_count, q='*').label(evt_count),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.evt_count, q='*').trend().label(Trend).lines(width=1.5)", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-remoted-messages', _type: 'visualization', _source: { title: 'Wazuh App Statistics remoted messages', - visState: - `{"title":"Wazuh App Statistics remoted messages","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.msg_sent, q='*').label(msg_sent),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.ctrl_msg_count, q='*').label(ctrl_msg_count),.es(index=wazuh-statistics-*,timefield=timestamp,metric=avg:remoted.discarded_count).label(discarded_count),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.dequeued_after_close, q='*').label(dequeued_after_close)","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics remoted messages', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.msg_sent, q='*').label(msg_sent),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.ctrl_msg_count, q='*').label(ctrl_msg_count),.es(index=wazuh-statistics-*,timefield=timestamp,metric=avg:remoted.discarded_count).label(discarded_count),.es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:remoted.dequeued_after_close, q='*').label(dequeued_after_close)", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-remoted-tcp-sessions', _type: 'visualization', _source: { title: 'Wazuh App Statistics remoted tcp sessions', - visState: - `{"title":"Wazuh App Statistics remoted tcp sessions","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:remoted.tcp_sessions, q='*').label(tcp_sessions)","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics remoted tcp sessions', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:remoted.tcp_sessions, q='*').label(tcp_sessions)", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Overview-Events-Decoded', _type: 'visualization', _source: { title: 'Wazuh App Statistics Overview events decoded', - visState: - `{"title":"Wazuh App Statistics Overview events decode","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_events_decoded, q='*').label('Syscheck Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck, q='*').label('Syscollector Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_events_decoded, q='*').label('Rootcheck Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_events_decoded, q='*').label('SCA Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_events_decoded, q='*').label('Other Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_events_decoded, q='*').label('Host Info Events Decoded').bars(stack=true)","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Overview events decode', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_events_decoded, q='*').label('Syscheck Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck, q='*').label('Syscollector Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_events_decoded, q='*').label('Rootcheck Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_events_decoded, q='*').label('SCA Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_events_decoded, q='*').label('Other Events Decoded').bars(stack=true), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_events_decoded, q='*').label('Host Info Events Decoded').bars(stack=true)", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Syscheck', _type: 'visualization', _source: { title: 'Wazuh App Statistics Syscheck', - visState: - `{"title":"Wazuh App Statistics Syscheck","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_events_decoded, q='*').label('Syscheck Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_edps, q='*').label('Syscheck EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Syscheck', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_events_decoded, q='*').label('Syscheck Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_edps, q='*').label('Syscheck EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscheck_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Syscollector', _type: 'visualization', _source: { title: 'Wazuh App Statistics Syscollector', - visState: - `{"title":"Wazuh App Statistics Syscollector","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_events_decoded, q='*').label('syscollector Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_edps, q='*').label('syscollector EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Syscollector', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_events_decoded, q='*').label('syscollector Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_edps, q='*').label('syscollector EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.syscollector_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Rootcheck', _type: 'visualization', _source: { title: 'Wazuh App Statistics Rootcheck', - visState: - `{"title":"Wazuh App Statistics Rootcheck","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_events_decoded, q='*').label('Rootcheck Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_edps, q='*').label('Rootcheck EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage) ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Rootcheck', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_events_decoded, q='*').label('Rootcheck Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_edps, q='*').label('Rootcheck EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rootcheck_queue_usage) ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-SCA', _type: 'visualization', _source: { title: 'Wazuh App Statistics SCA', - visState: - `{"title":"Wazuh App Statistics SCA","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_events_decoded, q='*').label('SCA Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_edps, q='*').label('SCA EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics SCA', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_events_decoded, q='*').label('SCA Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_edps, q='*').label('SCA EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.sca_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-HostInfo', _type: 'visualization', _source: { title: 'Wazuh App Statistics HostInfo', - visState: - `{"title":"Wazuh App Statistics HostInfo","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_events_decoded, q='*').label('Host info Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_edps, q='*').label('Host info EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics HostInfo', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_events_decoded, q='*').label('Host info Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_edps, q='*').label('Host info EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.hostinfo_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Other', _type: 'visualization', _source: { title: 'Wazuh App Statistics Other', - visState: - `{"title":"Wazuh App Statistics Other","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_events_decoded, q='*').label('Host info Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_edps, q='*').label('Host info EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Other', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_events_decoded, q='*').label('Host info Events Decoded'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_edps, q='*').label('Host info EDPS'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ).label('Queue Usage').color('green'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*').if(gte, 0.7, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ), null) .color('#FFCC11').label('Queue Usage 70%+'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*').if(gte, 0.9, .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.other_queue_usage, q='*') ), null) .color('red').label('Queue Usage 90%+')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { @@ -192,51 +313,100 @@ export default [ _type: 'visualization', _source: { title: 'Wazuh App Statistics Events by Node', - visState: - `{"title":"Wazuh App Statistics Events by Node","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*') .label('Total'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*', split=nodeName.keyword:5).label('Events processed by Node: $1','^.* > nodeName.keyword:(\\\\S+) > .*')","interval":"5m"},"aggs":[]}`, - visStateByNode: - `{"title":"Wazuh App Statistics Events by Node","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*') .label('Events processed by Node: NODE_NAME')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Events by Node', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*') .label('Total'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*', split=nodeName.keyword:5).label('Events processed by Node: $1','^.* > nodeName.keyword:(\\\\S+) > .*')", + interval: '5m', + }, + aggs: [], + }), + visStateByNode: JSON.stringify({ + title: 'Wazuh App Statistics Events by Node', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_processed, q='*') .label('Events processed by Node: NODE_NAME')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Events-Dropped-By-Node', _type: 'visualization', _source: { title: 'Wazuh App Statistics Events Dropped by Node', - visState: - `{"title":"Wazuh App Statistics Events Dropped by Node","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*') .label('Total'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*', split=nodeName.keyword:5).label('Events dropped by Node: $1','^.* > nodeName.keyword:(\\\\S+) > .*')","interval":"5m"},"aggs":[]}`, - visStateByNode: - `{"title":"Wazuh App Statistics Events by Node","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*') .label('Events dropped by Node: NODE_NAME')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Events Dropped by Node', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*') .label('Total'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*', split=nodeName.keyword:5).label('Events dropped by Node: $1','^.* > nodeName.keyword:(\\\\S+) > .*')", + interval: '5m', + }, + aggs: [], + }), + visStateByNode: JSON.stringify({ + title: 'Wazuh App Statistics Events by Node', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=sum:analysisd.events_dropped, q='*') .label('Events dropped by Node: NODE_NAME')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Statistics-Analysisd-Queues-Usage', _type: 'visualization', _source: { title: 'Wazuh App Statistics Queues Usage', - visState: - `{"title":"Wazuh App Statistics Queues Usage","type":"timelion","params":{"expression":".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.event_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.event_queue_usage, q='*') ).label('Event queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rule_matching_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rule_matching_queue_usage, q='*') ).label('Rule matching queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.alerts_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.alerts_queue_usage, q='*') ).label('Alerts log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.firewall_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.firewall_queue_usage, q='*') ).label('Firewall log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.statistical_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.statistical_queue_usage, q='*') ).label('Statistical log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.archives_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.archives_queue_usage, q='*') ).label('Statistical log queue usage')","interval":"5m"},"aggs":[]}`, + visState: JSON.stringify({ + title: 'Wazuh App Statistics Queues Usage', + type: 'timelion', + params: { + expression: + ".es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.event_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.event_queue_usage, q='*') ).label('Event queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rule_matching_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.rule_matching_queue_usage, q='*') ).label('Rule matching queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.alerts_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.alerts_queue_usage, q='*') ).label('Alerts log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.firewall_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.firewall_queue_usage, q='*') ).label('Firewall log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.statistical_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.statistical_queue_usage, q='*') ).label('Statistical log queue usage'), .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.archives_queue_size, q='*').multiply( .es(index=wazuh-statistics-*, timefield=timestamp,metric=avg:analysisd.archives_queue_usage, q='*') ).label('Statistical log queue usage')", + interval: '5m', + }, + aggs: [], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-statistics-*","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-statistics-*', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/overview/overview-audit.ts b/server/integration-files/visualizations/overview/overview-audit.ts index fdbec44a35..36aea01dbe 100644 --- a/server/integration-files/visualizations/overview/overview-audit.ts +++ b/server/integration-files/visualizations/overview/overview-audit.ts @@ -14,97 +14,338 @@ export default [ _id: 'Wazuh-App-Overview-Audit-Groups', _source: { title: 'Groups', - visState: - '{"title":"Groups","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.groups","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Groups', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'rule.groups', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-Audit-Agents', _source: { title: 'Agents', - visState: - '{"title":"Agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-Audit-Commands', _source: { title: 'Commands', - visState: - '{"title":"Commands","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.audit.command","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Commands', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.audit.command', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-Audit-Files', _source: { title: 'Files', - visState: - '{"title":"Files","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.audit.file.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.audit.file.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-Audit-Alerts-over-time', _source: { title: 'Alerts over time', - visState: - '{"title":"Alerts over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Alerts over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-Audit-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Event"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.audit.exe","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Command"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Event', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.audit.exe', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Command', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-aws.ts b/server/integration-files/visualizations/overview/overview-aws.ts index 5c19ab920f..7dfc90c3bf 100644 --- a/server/integration-files/visualizations/overview/overview-aws.ts +++ b/server/integration-files/visualizations/overview/overview-aws.ts @@ -15,162 +15,620 @@ export default [ _type: 'visualization', _source: { title: 'Top rules', - visState: - '{"title":"Top rules","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","size":500,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule ID"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Event"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Top rules', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + size: 500, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Event', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-geo', _type: 'visualization', _source: { title: 'Geolocation map', - visState: - '{"title":"Geolocation map","type":"tile_map","params":{"colorSchema":"Green to Red","mapType":"Scaled Circle Markers","isDesaturated":false,"addTooltip":true,"heatClusterSize":1.5,"legendPosition":"bottomright","mapZoom":1,"mapCenter":[0,0],"wms":{"enabled":false,"options":{"format":"image/png","transparent":true}},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"geohash":{"accessor":0,"format":{"id":"string"},"params":{"precision":2,"useGeocentroid":true},"aggType":"geohash_grid"},"geocentroid":{"accessor":2,"format":{"id":"string"},"params":{},"aggType":"geo_centroid"}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"geohash_grid","schema":"segment","params":{"field":"GeoLocation.location","autoPrecision":true,"precision":2,"useGeocentroid":true,"isFilteredByCollar":true,"mapZoom":1,"mapCenter":[0,0]}}]}', - uiStateJSON: - '{"mapZoom":2,"mapCenter":[38.685509760012025,-31.816406250000004]}', + visState: JSON.stringify({ + title: 'Geolocation map', + type: 'tile_map', + params: { + colorSchema: 'Green to Red', + mapType: 'Scaled Circle Markers', + isDesaturated: false, + addTooltip: true, + heatClusterSize: 1.5, + legendPosition: 'bottomright', + mapZoom: 1, + mapCenter: [0, 0], + wms: { enabled: false, options: { format: 'image/png', transparent: true } }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + geohash: { + accessor: 0, + format: { id: 'string' }, + params: { precision: 2, useGeocentroid: true }, + aggType: 'geohash_grid', + }, + geocentroid: { + accessor: 2, + format: { id: 'string' }, + params: {}, + aggType: 'geo_centroid', + }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'GeoLocation.location', + autoPrecision: true, + precision: 2, + useGeocentroid: true, + isFilteredByCollar: true, + mapZoom: 1, + mapCenter: [0, 0], + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + mapZoom: 2, + mapCenter: [38.685509760012025, -31.816406250000004], + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Events-by-source', _type: 'visualization', _source: { title: 'Events by source over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"left","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.aws.source","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'left', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.aws.source', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Top-accounts', _type: 'visualization', _source: { title: 'Accounts', - visState: - '{"title":"Accounts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.accountId","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Accounts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.accountId', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Top-sources', _type: 'visualization', _source: { title: 'Sources', - visState: - '{"title":"Sources","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.source","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Sources', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.source', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Top-buckets', _type: 'visualization', _source: { title: 'Buckets', - visState: - '{"title":"Buckets","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.log_info.s3bucket","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Buckets', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.log_info.s3bucket', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Top-regions', _type: 'visualization', _source: { title: 'Regions', - visState: - '{"title":"Regions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.aws.region","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Regions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.aws.region', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, - /*{ - _id: 'Wazuh-App-Overview-AWS-Service-selector', - _type: 'visualization', - _source: { - title: 'Service selector', - visState: - '{"title":"Service selector","type":"input_control_vis","params":{"controls":[{"id":"1543502225381","indexPattern":"wazuh-alerts","fieldName":"data.aws.service.serviceName","parent":"","label":"Service","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"}}],"updateFiltersOnChange":true,"useTimeFilter":true,"pinFilters":false},"aggs":[]}', - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - },*/ { _id: 'Wazuh-App-Overview-AWS-Events-by-s3-bucket', _type: 'visualization', _source: { title: 'Events by S3 bucket over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.aws.log_info.s3bucket","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.aws.log_info.s3bucket', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-AWS-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-ciscat.ts b/server/integration-files/visualizations/overview/overview-ciscat.ts index 4c6539a280..f589d87719 100644 --- a/server/integration-files/visualizations/overview/overview-ciscat.ts +++ b/server/integration-files/visualizations/overview/overview-ciscat.ts @@ -15,184 +15,678 @@ export default [ _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":4,"direction":"desc"},"showTotal":false,"totalFunc":"count"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.rule_title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule title"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.group","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Group"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.result","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1","customLabel":"Result"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":4,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 4, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'count', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.rule_title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule title', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.group', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Group', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.result', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Result', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 4, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-not-checked', _type: 'visualization', _source: { title: 'Last scan not checked', - visState: - '{"title":"Last scan not checked","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.notchecked","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan not checked', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.notchecked', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-score', _type: 'visualization', _source: { title: 'Last scan score', - visState: - '{"title":"Last scan score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.score","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.score', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-pass', _type: 'visualization', _source: { title: 'Last scan pass', - visState: - '{"title":"Last scan pass","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.pass","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan pass', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.pass', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-fail', _type: 'visualization', _source: { title: 'Last scan fail', - visState: - '{"title":"Last scan fail","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.fail","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan fail', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.fail', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-timestamp', _type: 'visualization', _source: { title: 'Last scan timestamp', - visState: - '{"title":"Last scan timestamp","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.timestamp","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan timestamp', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.timestamp', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-error', _type: 'visualization', _source: { title: 'Last scan error', - visState: - '{"title":"Last scan error","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.error","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan error', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.error', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-benchmark', _type: 'visualization', _source: { title: 'Last scan benchmark', - visState: - '{"title":"Last scan benchmark","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.benchmark","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan benchmark', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.benchmark', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-last-scan-unknown', _type: 'visualization', _source: { title: 'Last scan unknown', - visState: - '{"title":"Last scan unknown","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.cis.unknown","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan unknown', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.cis.unknown', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-top-5-groups', _type: 'visualization', _source: { title: 'Top 5 groups', - visState: - '{"title":"Top 5 groups","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"},"valueAxis":null},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":25,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.cis.group","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Group"}}]}', - uiStateJSON: '{}', + visState: JSON.stringify({ + title: 'Top 5 groups', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' }, valueAxis: null }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 25, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.cis.group', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Group', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({}), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-app-Overview-CISCAT-scan-result-evolution', _type: 'visualization', _source: { title: 'Scan result evolution', - visState: - '{"title":"Scan result evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.cis.result","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Result"}}]}', - uiStateJSON: '{}', + visState: JSON.stringify({ + title: 'Scan result evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.cis.result', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Result', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({}), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-docker.ts b/server/integration-files/visualizations/overview/overview-docker.ts index a5e3517b68..a53625588a 100644 --- a/server/integration-files/visualizations/overview/overview-docker.ts +++ b/server/integration-files/visualizations/overview/overview-docker.ts @@ -15,80 +15,392 @@ export default [ _type: 'visualization', _source: { title: 'Top 5 actions', - visState: - '{"title":"Top 5 actions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.docker.Action","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 actions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.docker.Action', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Docker-top-5-images', _type: 'visualization', _source: { title: 'Top 5 images', - visState: - '{"title":"Top 5 images","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.docker.Actor.Attributes.image","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 images', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.docker.Actor.Attributes.image', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Docker-Events-summary', _type: 'visualization', _source: { title: 'Events summary', - visState: - '{"title":"Events summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.docker.Actor.Attributes.name","size":50,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Container"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.docker.Action","size":20,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"timestamp","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Date"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Events summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.docker.Actor.Attributes.name', + size: 50, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Container', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.docker.Action', + size: 20, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'timestamp', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Date', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Docker-Types-over-time', _type: 'visualization', _source: { title: 'Types over time', - visState: - '{"title":"Types over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.docker.Type","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Types over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.docker.Type', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Docker-Actions-over-time', _type: 'visualization', _source: { title: 'Actions over time', - visState: - '{"title":"Actions over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Events"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Events","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Events"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{},"customLabel":""}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.docker.Action","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}}]}', + visState: JSON.stringify({ + title: 'Actions over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Events' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Events', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Events' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + customLabel: '', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.docker.Action', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-fim.ts b/server/integration-files/visualizations/overview/overview-fim.ts index 90935565a7..9f9d843cfd 100644 --- a/server/integration-files/visualizations/overview/overview-fim.ts +++ b/server/integration-files/visualizations/overview/overview-fim.ts @@ -15,112 +15,521 @@ export default [ _type: 'visualization', _source: { title: 'Events summary', - visState: - '{"title":"Events summary","type":"line","params":{"type":"line","grid":{"categoryLines":true,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Alerts"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Alerts","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Alerts"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp"}}]}', + visState: JSON.stringify({ + title: 'Events summary', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: true, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Alerts' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Alerts', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Alerts' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { field: 'timestamp' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-FIM-Top-5-rules', _type: 'visualization', _source: { title: 'Top 5 rules', - visState: - '{"title":"Export rule distr","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"field":"rule.level"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Export rule distr', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { field: 'rule.level' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-FIM-Top-5-agents-pie', _type: 'visualization', _source: { title: 'Top 5 agents pie', - visState: - '{"title":"Top 5 agents pie","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 agents pie', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"language":"lucene","query":""},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { language: 'lucene', query: '' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-FIM-Common-actions', _type: 'visualization', _source: { title: 'Common actions', - visState: - '{"title":"Common actions","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"syscheck.event","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Common actions', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'syscheck.event', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"language":"lucene","query":""},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { language: 'lucene', query: '' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-FIM-Alerts-by-action-over-time', _source: { title: 'Alerts by action over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"left","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"syscheck.event","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true,truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'left', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'syscheck.event', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ "index":"wazuh-alerts", "filter":[], "query":{"query":"","language":"lucene"} }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-FIM-top-agents-user', _type: 'visualization', _source: { title: 'Top users', - visState: - '{"title":"Top users","type":"table","params":{"perPage":5,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"syscheck.uname_after","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Top user"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.id","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent name"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Top users', + type: 'table', + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'syscheck.uname_after', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Top user', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.id', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent name', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"rule.groups","value":"syscheck","params":{"query":"syscheck","type":"phrase"}},"query":{"match":{"rule.groups":{"query":"syscheck","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syscheck', + params: { query: 'syscheck', type: 'phrase' }, + }, + query: { match: { 'rule.groups': { query: 'syscheck', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-FIM-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"syscheck.path","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Path"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"syscheck.event","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Action"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'syscheck.path', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Path', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'syscheck.event', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Action', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-gcp.ts b/server/integration-files/visualizations/overview/overview-gcp.ts index 4e3f3b836c..b68e7f7c31 100644 --- a/server/integration-files/visualizations/overview/overview-gcp.ts +++ b/server/integration-files/visualizations/overview/overview-gcp.ts @@ -16,14 +16,104 @@ export default [ _id: 'Wazuh-App-Overview-GCP-Alerts-Evolution-By-AuthAnswer', _source: { title: 'Events over time by auth answer', - visState: - '{"title":"Alert evolution by auth result","type":"area","params":{"type":"area","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"labels":{},"dimensions":{"x":null,"y":[{"accessor":0,"format":{"id":"number"},"params":{},"aggType":"count"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.gcp.jsonPayload.authAnswer","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alert evolution by auth result', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + labels: {}, + dimensions: { + x: null, + y: [{ accessor: 0, format: { id: 'number' }, params: {}, aggType: 'count' }], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.gcp.jsonPayload.authAnswer', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -32,14 +122,95 @@ export default [ _id: 'Wazuh-App-Overview-GCP-Top-vmInstances-By-ResponseCode', _source: { title: 'Top instances by response code', - visState: - '{"title":"Top VM instances by response code","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.jsonPayload.vmInstanceName","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"VM Instance Name"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.jsonPayload.responseCode","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Response Code"}}]}', + visState: JSON.stringify({ + title: 'Top VM instances by response code', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.jsonPayload.vmInstanceName', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'VM Instance Name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.jsonPayload.responseCode', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Response Code', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -48,14 +219,129 @@ export default [ _id: 'Wazuh-App-Overview-GCP-Top-ResourceType-By-Project-Id', _source: { title: 'Resource type by project id', - visState: - '{"title":"Top resource type by project","type":"horizontal_bar","params":{"addLegend":true,"addTimeMarker":false,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":false,"rotate":0,"show":true,"truncate":200},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"grid":{"categoryLines":false},"labels":{},"legendPosition":"right","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"mode":"normal","show":true,"showCircles":true,"type":"histogram","valueAxis":"ValueAxis-1"}],"times":[],"type":"histogram","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":true,"rotate":75,"show":true,"truncate":100},"name":"LeftAxis-2","position":"left","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.project_id","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Project ID"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.gcp.resource.type","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Resource type"}}]}', + visState: JSON.stringify({ + title: 'Top resource type by project', + type: 'horizontal_bar', + params: { + addLegend: true, + addTimeMarker: false, + addTooltip: true, + categoryAxes: [ + { + id: 'CategoryAxis-1', + labels: { filter: false, rotate: 0, show: true, truncate: 200 }, + position: 'bottom', + scale: { type: 'linear' }, + show: true, + style: {}, + title: {}, + type: 'category', + }, + ], + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + grid: { categoryLines: false }, + labels: {}, + legendPosition: 'right', + seriesParams: [ + { + data: { id: '1', label: 'Count' }, + drawLinesBetweenPoints: true, + mode: 'normal', + show: true, + showCircles: true, + type: 'histogram', + valueAxis: 'ValueAxis-1', + }, + ], + times: [], + type: 'histogram', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { filter: true, rotate: 75, show: true, truncate: 100 }, + name: 'LeftAxis-2', + position: 'left', + scale: { mode: 'normal', type: 'linear' }, + show: true, + style: {}, + title: { text: 'Count' }, + type: 'value', + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.project_id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Project ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.gcp.resource.type', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Resource type', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -64,14 +350,125 @@ export default [ _id: 'Wazuh-App-Overview-GCP-Top-ProjectId-By-SourceType', _source: { title: 'Top project id by sourcetype', - visState: - '{"title":"top project id by source type","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":4,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"4","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.location","customLabel":"Location","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.project_id","customLabel":"Project ID","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.gcp.resource.labels.source_type","customLabel":"Source type","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'top project id by source type', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 4, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.location', + customLabel: 'Location', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.project_id', + customLabel: 'Project ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.gcp.resource.labels.source_type', + customLabel: 'Source type', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -80,14 +477,67 @@ export default [ _id: 'Wazuh-App-Overview-GCP-Map-By-SourceIp', _source: { title: 'Top 5 Map by source ip', - visState: - '{"title":"Map GCP source IP","type":"tile_map","params":{"colorSchema":"Green to Red","mapType":"Scaled Circle Markers","isDesaturated":false,"addTooltip":true,"heatClusterSize":1.5,"legendPosition":"bottomright","mapZoom":2,"mapCenter":[0,0],"wms":{"enabled":false,"options":{"format":"image/png","transparent":true}},"dimensions":{"metric":{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"},"geohash":{"accessor":1,"format":{"id":"string"},"params":{"precision":2,"useGeocentroid":true},"aggType":"geohash_grid"},"geocentroid":{"accessor":3,"format":{"id":"string"},"params":{},"aggType":"geo_centroid"}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"geohash_grid","schema":"segment","params":{"field":"GeoLocation.location","autoPrecision":true,"precision":2,"useGeocentroid":true,"isFilteredByCollar":true,"mapZoom":3,"mapCenter":{"lon":1.3183593750000002,"lat":18.06231230454674},"mapBounds":{"bottom_right":{"lat":-50.736455137010644,"lon":125.68359375000001},"top_left":{"lat":68.72044056989829,"lon":-123.04687500000001}}}}]}', + visState: JSON.stringify({ + title: 'Map GCP source IP', + type: 'tile_map', + params: { + colorSchema: 'Green to Red', + mapType: 'Scaled Circle Markers', + isDesaturated: false, + addTooltip: true, + heatClusterSize: 1.5, + legendPosition: 'bottomright', + mapZoom: 2, + mapCenter: [0, 0], + wms: { enabled: false, options: { format: 'image/png', transparent: true } }, + dimensions: { + metric: { accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }, + geohash: { + accessor: 1, + format: { id: 'string' }, + params: { precision: 2, useGeocentroid: true }, + aggType: 'geohash_grid', + }, + geocentroid: { + accessor: 3, + format: { id: 'string' }, + params: {}, + aggType: 'geo_centroid', + }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'GeoLocation.location', + autoPrecision: true, + precision: 2, + useGeocentroid: true, + isFilteredByCollar: true, + mapZoom: 3, + mapCenter: { lon: 1.3183593750000002, lat: 18.06231230454674 }, + mapBounds: { + bottom_right: { lat: -50.736455137010644, lon: 125.68359375000001 }, + top_left: { lat: 68.72044056989829, lon: -123.04687500000001 }, + }, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), }, }, _type: 'visualization', @@ -97,16 +547,83 @@ export default [ _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 100, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, -]; \ No newline at end of file +]; diff --git a/server/integration-files/visualizations/overview/overview-gdpr.ts b/server/integration-files/visualizations/overview/overview-gdpr.ts index ba7f6d2e1b..c611d84417 100644 --- a/server/integration-files/visualizations/overview/overview-gdpr.ts +++ b/server/integration-files/visualizations/overview/overview-gdpr.ts @@ -14,115 +14,622 @@ export default [ _id: 'Wazuh-App-Overview-GDPR-Requirements-heatmap', _source: { title: 'GDPR requirements over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.gdpr","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.gdpr', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-GDPR-Requirements-Agents-heatmap', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Greens","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"#555"}}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gdpr","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agents"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 13":"rgb(247,252,245)","13 - 26":"rgb(233,247,228)","26 - 39":"rgb(211,238,205)","39 - 52":"rgb(184,227,177)","52 - 65":"rgb(152,213,148)","65 - 78":"rgb(116,196,118)","78 - 91":"rgb(75,176,98)","91 - 104":"rgb(47,152,79)","104 - 117":"rgb(21,127,59)","117 - 130":"rgb(0,100,40)"}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'heatmap', + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 10, + colorSchema: 'Greens', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { type: 'linear', defaultYExtents: false }, + labels: { show: false, rotate: 0, overwriteColor: false, color: '#555' }, + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.gdpr', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agents', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 13': 'rgb(247,252,245)', + '13 - 26': 'rgb(233,247,228)', + '26 - 39': 'rgb(211,238,205)', + '39 - 52': 'rgb(184,227,177)', + '52 - 65': 'rgb(152,213,148)', + '65 - 78': 'rgb(116,196,118)', + '78 - 91': 'rgb(75,176,98)', + '91 - 104': 'rgb(47,152,79)', + '104 - 117': 'rgb(21,127,59)', + '117 - 130': 'rgb(0,100,40)', + }, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-GDPR-requirements', _source: { title: 'GDPR requirements', - visState: - '{"title":"GDPR requirements","type":"line","params":{"type":"line","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":false,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","format":"YYYY-MM-DD"},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"z":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"radiusRatio":50},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.gdpr","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + visState: JSON.stringify({ + title: 'GDPR requirements', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: false, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + params: { date: true, interval: 'P1D', format: 'YYYY-MM-DD' }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + z: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + radiusRatio: 50, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.gdpr', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { id: '4', enabled: true, type: 'count', schema: 'radius', params: {} }, + ], + }), uiStateJSON: '{"vis":{"legendOpen":false}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-GDPR-Agents', _source: { title: 'GDPR Agents', - visState: - '{"title":"GDPR Agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":10,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'GDPR Agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 10, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-GDPR-Requirements-by-agent', _source: { title: 'GDPR Requirements by agent', - visState: - '{"title":"GDPR Requirements by agent","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":51},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.gdpr","size":5,"order":"desc","orderBy":"1","customLabel":"GDPR Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'GDPR Requirements by agent', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + radiusRatio: 51, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.gdpr', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'GDPR Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-GDPR-Last-alerts', _type: 'visualization', _source: { title: 'GDPR Last alerts', - visState: - '{"title":"GDPR Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.gdpr","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'GDPR Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.gdpr', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-GDPR-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.gdpr","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.gdpr', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-general.ts b/server/integration-files/visualizations/overview/overview-general.ts index cbfbba8298..3719764f21 100644 --- a/server/integration-files/visualizations/overview/overview-general.ts +++ b/server/integration-files/visualizations/overview/overview-general.ts @@ -14,282 +14,882 @@ export default [ _id: 'Wazuh-App-Overview-General-Agents-status', _source: { title: 'Agents status', - visState: - '{"title":"Agents Status","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"mode":"normal","type":"line","drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","lineWidth":3.5,"data":{"id":"4","label":"Unique count of id"},"valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"2","enabled":true,"type":"date_histogram","interval":"1ms","schema":"segment","params":{"field":"timestamp","interval":"1ms","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"status","size":5,"order":"desc","orderBy":"_term"}},{"id":"4","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"id"}}]}', - uiStateJSON: - '{"vis":{"colors":{"never_connected":"#447EBC","active":"#E5AC0E"}}}', + visState: JSON.stringify({ + title: 'Agents Status', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + mode: 'normal', + type: 'line', + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + lineWidth: 3.5, + data: { id: '4', label: 'Unique count of id' }, + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '2', + enabled: true, + type: 'date_histogram', + interval: '1ms', + schema: 'segment', + params: { + field: 'timestamp', + interval: '1ms', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'status', size: 5, order: 'desc', orderBy: '_term' }, + }, + { + id: '4', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { field: 'id' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { colors: { never_connected: '#447EBC', active: '#E5AC0E' } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-monitoring","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-monitoring', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Metric-alerts', _source: { title: 'Metric alerts', - visState: - '{"title":"Metric Alerts","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Alerts"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Metric Alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Level-12-alerts', _source: { title: 'Level 12 alerts', - visState: - '{"title":"Count Level 12 Alerts","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Level 12 or above alerts"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Level 12 Alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Level 12 or above alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "$state": { - "store": "appState" - }, - "meta": { - "alias": null, - "disabled": false, - "index": "wazuh-alerts", - "key": "rule.level", - "negate": false, - "params": { - "gte": 12, - "lt": null - }, - "type": "range", - "value": "12 to +∞" - }, - "range": { - "rule.level": { - "gte": 12, - "lt": null - } - } - } - ], - "query":{ "query": "", "language": "lucene" } - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + index: 'wazuh-alerts', + key: 'rule.level', + negate: false, + params: { + gte: 12, + lt: null, + }, + type: 'range', + value: '12 to +∞', + }, + range: { + 'rule.level': { + gte: 12, + lt: null, + }, + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Authentication-failure', _source: { title: 'Authentication failure', - visState: - '{"title":"Count Authentication Failure","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Authentication failure"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Authentication Failure', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication failure' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "type": "phrases", - "key": "rule.groups", - "value": "win_authentication_failed, authentication_failed, authentication_failures", - "params": [ - "win_authentication_failed", - "authentication_failed", - "authentication_failures" - ], - "negate": false, - "disabled": false, - "alias": null - }, - "query": { - "bool": { - "should": [ - { - "match_phrase": { - "rule.groups": "win_authentication_failed" - } - }, - { - "match_phrase": { - "rule.groups": "authentication_failed" - } - }, - { - "match_phrase": { - "rule.groups": "authentication_failures" - } - } - ], - "minimum_should_match": 1 - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + type: 'phrases', + key: 'rule.groups', + value: 'win_authentication_failed, authentication_failed, authentication_failures', + params: [ + 'win_authentication_failed', + 'authentication_failed', + 'authentication_failures', + ], + negate: false, + disabled: false, + alias: null, + }, + query: { + bool: { + should: [ + { + match_phrase: { + 'rule.groups': 'win_authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failures', + }, + }, + ], + minimum_should_match: 1, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Authentication-success', _source: { title: 'Authentication success', - visState: - '{"title":"Count Authentication Success","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"gauge","gauge":{"verticalSplit":false,"autoExtend":false,"percentageMode":false,"gaugeType":"Metric","gaugeStyle":"Full","backStyle":"Full","orientation":"vertical","colorSchema":"Green to Red","gaugeColorMode":"None","useRange":false,"colorsRange":[{"from":0,"to":100}],"invertColors":false,"labels":{"show":true,"color":"black"},"scale":{"show":false,"labels":false,"color":"#333","width":2},"type":"simple","style":{"fontSize":20,"bgColor":false,"labelColor":false,"subText":""}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Authentication success"}}]}', - uiStateJSON: '{"vis":{"defaultColors":{"0 - 100":"rgb(0,104,55)"}}}', + visState: JSON.stringify({ + title: 'Count Authentication Success', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication success' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "authentication_success", - "params": { - "query": "authentication_success", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "authentication_success", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'authentication_success', + params: { + query: 'authentication_success', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'authentication_success', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Alert-level-evolution', _source: { title: 'Alert level evolution', - visState: - '{"title":"Alert level evolution","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.level","size":"15","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alert level evolution', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.level', + size: '15', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Alerts-Top-Mitre', _source: { title: 'Alerts', - visState: - "{\"type\":\"pie\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"rule.mitre.technique\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":20,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}],\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100}},\"title\":\"mitre top\"}", - uiStateJSON: - '{}', + visState: JSON.stringify({ + type: 'pie', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + title: 'mitre top', + }), + uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Top-5-agents', _source: { title: 'Top 5 agents', - visState: - '{"title":"Top 5 agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Top 5 agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Top-5-agents-Evolution', _source: { title: 'Top 5 rule groups', - visState: - "{\"type\":\"histogram\",\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"date_histogram\",\"schema\":\"segment\",\"params\":{\"field\":\"timestamp\",\"timeRange\":{\"from\":\"2020-07-19T16:18:13.637Z\",\"to\":\"2020-07-28T13:58:33.357Z\"},\"useNormalizedEsInterval\":true,\"scaleMetricValues\":false,\"interval\":\"auto\",\"drop_partials\":false,\"min_doc_count\":1,\"extended_bounds\":{}}},{\"id\":\"3\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"group\",\"params\":{\"field\":\"agent.name\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}],\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"filter\":true,\"truncate\":100},\"title\":{}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":true,\"type\":\"histogram\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"lineWidth\":2,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false,\"thresholdLine\":{\"show\":false,\"value\":10,\"width\":1,\"style\":\"full\",\"color\":\"#E7664C\"},\"labels\":{}},\"title\":\"top 5 agents evolution\"}", - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + type: 'histogram', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: '2020-07-19T16:18:13.637Z', to: '2020-07-28T13:58:33.357Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + labels: {}, + }, + title: 'top 5 agents evolution', + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-General-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-General-Alerts-evolution-Top-5-agents', _type: 'visualization', _source: { title: 'Alerts evolution Top 5 agents', - visState: - '{"title":"Alerts evolution Top 5 agents","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Alerts evolution Top 5 agents', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-hipaa.ts b/server/integration-files/visualizations/overview/overview-hipaa.ts index d3cd06b776..65a64bfc3d 100644 --- a/server/integration-files/visualizations/overview/overview-hipaa.ts +++ b/server/integration-files/visualizations/overview/overview-hipaa.ts @@ -14,130 +14,755 @@ export default [ _id: 'Wazuh-App-Overview-HIPAA-Tag-cloud', _source: { title: 'Most common alerts', - visState: - '{"title":"Most common alerts","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":10,"maxFontSize":30,"showLabel":false,"metric":{"type":"vis_dimension","accessor":1,"format":{"id":"string","params":{}}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'Most common alerts', + type: 'tagcloud', + params: { + scale: 'linear', + orientation: 'single', + minFontSize: 10, + maxFontSize: 30, + showLabel: false, + metric: { type: 'vis_dimension', accessor: 1, format: { id: 'string', params: {} } }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Top-10-requirements', _source: { title: 'Top 10 requirements', - visState: - '{"title":"Top 10 requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 10 requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Top-10-agents', _source: { title: 'Most active agents', - visState: - '{"title":"Most active agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","customLabel":"Agent","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Most active agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + customLabel: 'Agent', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Metrics', _source: { title: 'Stats', - visState: - '{"title":"Stats","type":"metric","params":{"metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"type":"range","from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}},"dimensions":{"metrics":[{"type":"vis_dimension","accessor":0,"format":{"id":"number","params":{}}},{"type":"vis_dimension","accessor":1,"format":{"id":"number","params":{}}}]},"addTooltip":true,"addLegend":false,"type":"metric"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total alerts"}},{"id":"2","enabled":true,"type":"max","schema":"metric","params":{"field":"rule.level","customLabel":"Max rule level detected"}}]}', + visState: JSON.stringify({ + title: 'Stats', + type: 'metric', + params: { + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ type: 'range', from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + dimensions: { + metrics: [ + { type: 'vis_dimension', accessor: 0, format: { id: 'number', params: {} } }, + { type: 'vis_dimension', accessor: 1, format: { id: 'number', params: {} } }, + ], + }, + addTooltip: true, + addLegend: false, + type: 'metric', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total alerts' }, + }, + { + id: '2', + enabled: true, + type: 'max', + schema: 'metric', + params: { field: 'rule.level', customLabel: 'Max rule level detected' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Alerts-summary', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum","dimensions":{"metrics":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + dimensions: { + metrics: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Heatmap', _source: { title: 'Alerts volume by agent', - visState: - '{"title":"Alerts volume by agent","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Greens","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"black"}}],"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.id","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent ID"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 260":"rgb(247,252,245)","260 - 520":"rgb(233,247,228)","520 - 780":"rgb(211,238,205)","780 - 1,040":"rgb(184,227,177)","1,040 - 1,300":"rgb(152,213,148)","1,300 - 1,560":"rgb(116,196,118)","1,560 - 1,820":"rgb(75,176,98)","1,820 - 2,080":"rgb(47,152,79)","2,080 - 2,340":"rgb(21,127,59)","2,340 - 2,600":"rgb(0,100,40)"},"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Alerts volume by agent', + type: 'heatmap', + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 10, + colorSchema: 'Greens', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { type: 'linear', defaultYExtents: false }, + labels: { show: false, rotate: 0, overwriteColor: false, color: 'black' }, + }, + ], + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 260': 'rgb(247,252,245)', + '260 - 520': 'rgb(233,247,228)', + '520 - 780': 'rgb(211,238,205)', + '780 - 1,040': 'rgb(184,227,177)', + '1,040 - 1,300': 'rgb(152,213,148)', + '1,300 - 1,560': 'rgb(116,196,118)', + '1,560 - 1,820': 'rgb(75,176,98)', + '1,820 - 2,080': 'rgb(47,152,79)', + '2,080 - 2,340': 'rgb(21,127,59)', + '2,340 - 2,600': 'rgb(0,100,40)', + }, + legendOpen: true, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Top-10-requirements-over-time-by-agent', _source: { title: 'Requirements distribution by agent', - visState: - '{"title":"Requirements distribution by agent","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"auto","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-08-15T12:25:44.851Z","max":"2019-08-22T12:25:44.851Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Requirements distribution by agent', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'auto', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-08-15T12:25:44.851Z', max: '2019-08-22T12:25:44.851Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-HIPAA-Top-requirements-over-time', _source: { title: 'Requirements evolution over time', - visState: - '{"title":"Requirements evolution over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"auto","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-08-15T12:25:29.501Z","max":"2019-08-22T12:25:29.501Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-7d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.hipaa","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Requirements evolution over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'auto', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-08-15T12:25:29.501Z', max: '2019-08-22T12:25:29.501Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.hipaa', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' - } + _type: 'visualization', + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-mitre.ts b/server/integration-files/visualizations/overview/overview-mitre.ts index 539d1ae6a5..8ba3adada7 100644 --- a/server/integration-files/visualizations/overview/overview-mitre.ts +++ b/server/integration-files/visualizations/overview/overview-mitre.ts @@ -14,113 +14,658 @@ export default [ _id: 'Wazuh-App-Overview-MITRE', _source: { title: 'Mitre attack count', - visState: - '{"aggs":[{"enabled":true,"id":"1","params":{},"schema":"metric","type":"count"},{"enabled":true,"id":"2","params":{"field":"rule.mitre.id","customLabel":"Attack ID","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":244},"schema":"bucket","type":"terms"}],"params":{"dimensions":{"buckets":[],"metrics":[{"accessor":0,"aggType":"count","format":{"id":"number"},"params":{}}]},"perPage":10,"percentageCol":"","showMetricsAtAllLevels":false,"showPartialRows":false,"showTotal":false,"sort":{"columnIndex":null,"direction":null},"totalFunc":"sum"},"title":"mitre","type":"table"}', + visState: JSON.stringify({ + aggs: [ + { enabled: true, id: '1', params: {}, schema: 'metric', type: 'count' }, + { + enabled: true, + id: '2', + params: { + field: 'rule.mitre.id', + customLabel: 'Attack ID', + missingBucket: false, + missingBucketLabel: 'Missing', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + size: 244, + }, + schema: 'bucket', + type: 'terms', + }, + ], + params: { + dimensions: { + buckets: [], + metrics: [{ accessor: 0, aggType: 'count', format: { id: 'number' }, params: {} }], + }, + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + showToolbar: true, + sort: { columnIndex: null, direction: null }, + totalFunc: 'sum', + }, + title: 'mitre', + type: 'table', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Alerts-Evolution', _source: { title: 'Mitre alerts evolution', - visState: - '{"title":"Alert Evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true,"lineWidth":2}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT3H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-11-07T15:45:45.770Z","max":"2019-11-14T15:45:45.770Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-7d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.mitre.technique","customLabel":"Attack ID","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":""}}]}', + visState: JSON.stringify({ + title: 'Alert Evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + lineWidth: 2, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-11-07T15:45:45.770Z', max: '2019-11-14T15:45:45.770Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + customLabel: 'Attack ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Agent', _source: { title: 'Mitre techniques by agent', - visState: - '{"title":"attack by agent","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":0,"format":{"id":"number"},"params":{},"aggType":"count"}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.technique","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'attack by agent', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 0, format: { id: 'number' }, params: {}, aggType: 'count' }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Technique', _source: { title: 'Attacks by technique', - visState: - '{"title":"Attacks by tactic","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#34130C"},"dimensions":{"x":null,"y":[{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.mitre.technique","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Attacks by tactic', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: null, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Top-Tactics-By-Agent', _source: { title: 'Top tactics by agent', - visState: - '{"title":"Top tactics by agent - vertical","type":"area","params":{"addLegend":true,"addTimeMarker":false,"addTooltip":true,"categoryAxes":[{"id":"CategoryAxis-1","labels":{"filter":true,"show":true,"truncate":10},"position":"bottom","scale":{"type":"linear"},"show":true,"style":{},"title":{},"type":"category"}],"dimensions":{"x":{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"grid":{"categoryLines":false,"valueAxis":"ValueAxis-1"},"labels":{},"legendPosition":"right","seriesParams":[{"data":{"id":"1","label":"Count"},"drawLinesBetweenPoints":true,"interpolate":"linear","mode":"normal","show":"true","showCircles":true,"type":"histogram","valueAxis":"ValueAxis-1"}],"thresholdLine":{"color":"#34130C","show":false,"style":"full","value":10,"width":1},"times":[],"type":"area","valueAxes":[{"id":"ValueAxis-1","labels":{"filter":false,"rotate":0,"show":true,"truncate":100},"name":"LeftAxis-1","position":"left","scale":{"mode":"normal","type":"linear"},"show":true,"style":{},"title":{"text":"Count"},"type":"value"}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"4","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top tactics by agent - vertical', + type: 'area', + params: { + addLegend: true, + addTimeMarker: false, + addTooltip: true, + categoryAxes: [ + { + id: 'CategoryAxis-1', + labels: { filter: true, show: true, truncate: 10 }, + position: 'bottom', + scale: { type: 'linear' }, + show: true, + style: {}, + title: {}, + type: 'category', + }, + ], + dimensions: { + x: { + accessor: 1, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, + labels: {}, + legendPosition: 'right', + seriesParams: [ + { + data: { id: '1', label: 'Count' }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + mode: 'normal', + show: 'true', + showCircles: true, + type: 'histogram', + valueAxis: 'ValueAxis-1', + }, + ], + thresholdLine: { color: '#34130C', show: false, style: 'full', value: 10, width: 1 }, + times: [], + type: 'area', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { filter: false, rotate: 0, show: true, truncate: 100 }, + name: 'LeftAxis-1', + position: 'left', + scale: { mode: 'normal', type: 'linear' }, + show: true, + style: {}, + title: { text: 'Count' }, + type: 'value', + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Top-Tactics', _source: { title: 'Top tactics', - visState: - '{"title":"Top tactics PIE2","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.mitre.tactic","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top tactics PIE2', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-MITRE-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/overview/overview-nist.ts b/server/integration-files/visualizations/overview/overview-nist.ts index 573e4156ff..6c4b420d1f 100644 --- a/server/integration-files/visualizations/overview/overview-nist.ts +++ b/server/integration-files/visualizations/overview/overview-nist.ts @@ -14,114 +14,708 @@ export default [ _id: 'Wazuh-App-Overview-NIST-Requirements-over-time', _source: { title: 'Requirements over time', - visState: - '{"title":"NIST-Requirements-over-time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"labels":{"show":false},"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD HH:mm"}},"params":{"date":true,"interval":"PT1H","format":"YYYY-MM-DD HH:mm","bounds":{"min":"2019-08-20T12:33:23.360Z","max":"2019-08-22T12:33:23.360Z"}},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-2d","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"4","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'NIST-Requirements-over-time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT1H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-08-20T12:33:23.360Z', max: '2019-08-22T12:33:23.360Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-2d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-NIST-Requirements-Agents-heatmap', _type: 'visualization', _source: { title: 'Alerts volume by agent', - visState: - '{"aggs":[{"enabled":true,"id":"1","params":{},"schema":"metric","type":"count"},{"enabled":true,"id":"3","params":{"customLabel":"Requirement","field":"rule.nist_800_53","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":10},"schema":"group","type":"terms"},{"enabled":true,"id":"2","params":{"customLabel":"Agent","field":"agent.id","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":5},"schema":"segment","type":"terms"}],"params":{"addLegend":true,"addTooltip":true,"colorSchema":"Blues","colorsNumber":10,"colorsRange":[],"dimensions":{"series":[{"accessor":0,"aggType":"terms","format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"params":{}}],"x":{"accessor":1,"aggType":"terms","format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"params":{}},"y":[{"accessor":2,"aggType":"count","format":{"id":"number"},"params":{}}]},"enableHover":false,"invertColors":false,"legendPosition":"right","percentageMode":false,"setColorRange":false,"times":[],"type":"heatmap","valueAxes":[{"id":"ValueAxis-1","labels":{"color":"black","overwriteColor":false,"rotate":0,"show":false},"scale":{"defaultYExtents":false,"type":"linear"},"show":false,"type":"value"}]},"title":"NIST-Last-alerts","type":"heatmap"}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 160":"rgb(247,251,255)","160 - 320":"rgb(227,238,249)","320 - 480":"rgb(208,225,242)","480 - 640":"rgb(182,212,233)","640 - 800":"rgb(148,196,223)","800 - 960":"rgb(107,174,214)","960 - 1,120":"rgb(74,152,201)","1,120 - 1,280":"rgb(46,126,188)","1,280 - 1,440":"rgb(23,100,171)","1,440 - 1,600":"rgb(8,74,145)"}}}', + visState: JSON.stringify({ + aggs: [ + { enabled: true, id: '1', params: {}, schema: 'metric', type: 'count' }, + { + enabled: true, + id: '3', + params: { + customLabel: 'Requirement', + field: 'rule.nist_800_53', + missingBucket: false, + missingBucketLabel: 'Missing', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + size: 10, + }, + schema: 'group', + type: 'terms', + }, + { + enabled: true, + id: '2', + params: { + customLabel: 'Agent', + field: 'agent.id', + missingBucket: false, + missingBucketLabel: 'Missing', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + size: 5, + }, + schema: 'segment', + type: 'terms', + }, + ], + params: { + addLegend: true, + addTooltip: true, + colorSchema: 'Blues', + colorsNumber: 10, + colorsRange: [], + dimensions: { + series: [ + { + accessor: 0, + aggType: 'terms', + format: { + id: 'terms', + params: { + id: 'string', + missingBucketLabel: 'Missing', + otherBucketLabel: 'Other', + }, + }, + params: {}, + }, + ], + x: { + accessor: 1, + aggType: 'terms', + format: { + id: 'terms', + params: { id: 'string', missingBucketLabel: 'Missing', otherBucketLabel: 'Other' }, + }, + params: {}, + }, + y: [{ accessor: 2, aggType: 'count', format: { id: 'number' }, params: {} }], + }, + enableHover: false, + invertColors: false, + legendPosition: 'right', + percentageMode: false, + setColorRange: false, + times: [], + type: 'heatmap', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { color: 'black', overwriteColor: false, rotate: 0, show: false }, + scale: { defaultYExtents: false, type: 'linear' }, + show: false, + type: 'value', + }, + ], + }, + title: 'NIST-Last-alerts', + type: 'heatmap', + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 160': 'rgb(247,251,255)', + '160 - 320': 'rgb(227,238,249)', + '320 - 480': 'rgb(208,225,242)', + '480 - 640': 'rgb(182,212,233)', + '640 - 800': 'rgb(148,196,223)', + '800 - 960': 'rgb(107,174,214)', + '960 - 1,120': 'rgb(74,152,201)', + '1,120 - 1,280': 'rgb(46,126,188)', + '1,280 - 1,440': 'rgb(23,100,171)', + '1,440 - 1,600': 'rgb(8,74,145)', + }, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-NIST-requirements-by-agents', _source: { title: 'Requiments distribution by agent', - visState: - '{"title":"NIST-Top-requirements-by-agent","type":"area","params":{"type":"area","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.id","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', - uiStateJSON: '{"vis":{"legendOpen":false}}', + visState: JSON.stringify({ + title: 'NIST-Top-requirements-by-agent', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.id', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: false } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-NIST-Metrics', _source: { title: 'Stats', - visState: - '{"title":"nist-metrics","type":"metric","params":{"metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"type":"range","from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}},"dimensions":{"metrics":[{"type":"vis_dimension","accessor":0,"format":{"id":"number","params":{}}},{"type":"vis_dimension","accessor":1,"format":{"id":"number","params":{}}}]},"addTooltip":true,"addLegend":false,"type":"metric"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total alerts"}},{"id":"2","enabled":true,"type":"max","schema":"metric","params":{"field":"rule.level","customLabel":"Max rule level detected"}}]}', + visState: JSON.stringify({ + title: 'nist-metrics', + type: 'metric', + params: { + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ type: 'range', from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + dimensions: { + metrics: [ + { type: 'vis_dimension', accessor: 0, format: { id: 'number', params: {} } }, + { type: 'vis_dimension', accessor: 1, format: { id: 'number', params: {} } }, + ], + }, + addTooltip: true, + addLegend: false, + type: 'metric', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total alerts' }, + }, + { + id: '2', + enabled: true, + type: 'max', + schema: 'metric', + params: { field: 'rule.level', customLabel: 'Max rule level detected' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-NIST-Top-10-requirements', _source: { title: 'Top 10 requirements', - visState: - '{"title":"NIST-Top-10-requirements","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}}]}', + visState: JSON.stringify({ + title: 'NIST-Top-10-requirements', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-NIST-Agents', _source: { title: 'Most active agents', - visState: - '{"title":"NIST-Top-10-agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100},"dimensions":{"metric":{"accessor":1,"format":{"id":"number"},"params":{},"aggType":"count"},"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}}]}', + visState: JSON.stringify({ + title: 'NIST-Top-10-agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-NIST-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"NIST-Alerts-summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum","dimensions":{"metrics":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"buckets":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"},{"accessor":2,"format":{"id":"terms","params":{"id":"number","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.nist_800_53","orderBy":"1","order":"desc","size":20,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'NIST-Alerts-summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + dimensions: { + metrics: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + { + accessor: 2, + format: { + id: 'terms', + params: { + id: 'number', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.nist_800_53', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-oscap.ts b/server/integration-files/visualizations/overview/overview-oscap.ts index 5e9131107f..412ebcdbbd 100644 --- a/server/integration-files/visualizations/overview/overview-oscap.ts +++ b/server/integration-files/visualizations/overview/overview-oscap.ts @@ -14,553 +14,953 @@ export default [ _id: 'Wazuh-App-Overview-OSCAP-Last-score', _source: { title: 'Last score', - visState: - '{"title":"Last score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.score","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.scan.score', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Last-agent-scanned', _source: { title: 'Last agent scanned', - visState: - '{"title":"Last agent scanned","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last agent scanned', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'agent.name', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Last-scan-profile', _source: { title: 'Last scan profile', - visState: - '{"title":"Last scan profile","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.profile.title","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Last scan profile', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.scan.profile.title', + size: 1, + order: 'desc', + orderBy: '1', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Agents', _source: { title: 'Agents', - visState: - '{"params": {"isDonut": false, "shareYAxis": true, "addTooltip": true, "addLegend": true}, "listeners": {}, "type": "pie", "aggs": [{"type": "count", "enabled": true, "id": "1", "params": {}, "schema": "metric"}, {"type": "terms", "enabled": true, "id": "2", "params": {"orderBy": "1", "field": "agent.name", "order": "desc", "size": 5}, "schema": "segment"}], "title": "Agents"}', + visState: JSON.stringify({ + params: { isDonut: false, shareYAxis: true, addTooltip: true, addLegend: true }, + listeners: {}, + type: 'pie', + aggs: [ + { type: 'count', enabled: true, id: '1', params: {}, schema: 'metric' }, + { + type: 'terms', + enabled: true, + id: '2', + params: { orderBy: '1', field: 'agent.name', order: 'desc', size: 5 }, + schema: 'segment', + }, + ], + title: 'Agents', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Profiles', _source: { title: 'Profiles', - visState: - '{"params": {"isDonut": false, "legendPosition": "right", "shareYAxis": true, "addTooltip": true, "addLegend": true}, "listeners": {}, "type": "pie", "aggs": [{"type": "count", "enabled": true, "id": "1", "params": {}, "schema": "metric"}, {"type": "terms", "enabled": true, "id": "3", "params": {"orderBy": "1", "field": "data.oscap.scan.profile.title", "order": "desc", "size": 5}, "schema": "segment"}], "title": "Profiles"}', + visState: JSON.stringify({ + params: { + isDonut: false, + legendPosition: 'right', + shareYAxis: true, + addTooltip: true, + addLegend: true, + }, + listeners: {}, + type: 'pie', + aggs: [ + { type: 'count', enabled: true, id: '1', params: {}, schema: 'metric' }, + { + type: 'terms', + enabled: true, + id: '3', + params: { + orderBy: '1', + field: 'data.oscap.scan.profile.title', + order: 'desc', + size: 5, + }, + schema: 'segment', + }, + ], + title: 'Profiles', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Content', _source: { title: 'Content', - visState: - '{"params": {"isDonut": false, "legendPosition": "right", "shareYAxis": true, "addTooltip": true, "addLegend": true}, "listeners": {}, "type": "pie", "aggs": [{"type": "count", "enabled": true, "id": "1", "params": {}, "schema": "metric"}, {"type": "terms", "enabled": true, "id": "2", "params": {"orderBy": "1", "field": "data.oscap.scan.content", "order": "desc", "size": 5}, "schema": "segment"}], "title": "Content"}', + visState: JSON.stringify({ + params: { + isDonut: false, + legendPosition: 'right', + shareYAxis: true, + addTooltip: true, + addLegend: true, + }, + listeners: {}, + type: 'pie', + aggs: [ + { type: 'count', enabled: true, id: '1', params: {}, schema: 'metric' }, + { + type: 'terms', + enabled: true, + id: '2', + params: { orderBy: '1', field: 'data.oscap.scan.content', order: 'desc', size: 5 }, + schema: 'segment', + }, + ], + title: 'Content', + }), uiStateJSON: '{}', version: 1, description: '', kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Severity', _source: { title: 'Severity', - visState: - '{"title":"Severity","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.severity","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Severity', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.severity', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "rule.groups", - "value": "syslog", - "params": { - "query": "syslog", - "type": "phrase" - } - }, - "query": { - "match": { - "rule.groups": { - "query": "syslog", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'syslog', + params: { + query: 'syslog', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'syslog', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Top-5-agents-Severity-high', _source: { title: 'Top 5 agents - Severity high', - visState: - '{"title":"Top 5 Agents - Severity high","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":25,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 Agents - Severity high', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 25, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.severity", - "value": "high", - "params": { - "query": "high", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.severity": { - "query": "high", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.severity', + value: 'high', + params: { + query: 'high', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.severity': { + query: 'high', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Top-10-alerts', _source: { title: 'Top 10 alerts', - visState: - '{"title":"Wazuh App OSCAP Top 10 alerts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.title","size":10,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Wazuh App OSCAP Top 10 alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.title', size: 10, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Top-10-high-risk-alerts', _source: { title: 'Top 10 high risk alerts', - visState: - '{"title":"Wazuh App OSCAP Top 10 high risk alerts","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.oscap.check.title","size":10,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Wazuh App OSCAP Top 10 high risk alerts', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.oscap.check.title', size: 10, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.severity", - "value": "high", - "params": { - "query": "high", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.severity": { - "query": "high", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.severity', + value: 'high', + params: { + query: 'high', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.severity': { + query: 'high', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Highest-score', _source: { title: 'Highest score', - visState: - '{"title":"Highest score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"data.oscap.scan.score"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.score","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":0,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Highest score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'max', + schema: 'metric', + params: { field: 'data.oscap.scan.score' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.scan.score', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 0, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Lowest-score', _source: { title: 'Lowest score', - visState: - '{"title":"Lowest score","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"min","schema":"metric","params":{"field":"data.oscap.scan.score"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.score","size":1,"order":"asc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Lowest score', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'min', + schema: 'metric', + params: { field: 'data.oscap.scan.score' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.scan.score', size: 1, order: 'asc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Latest-alert', _source: { title: 'Latest alert', - visState: - '{"title":"Latest alert","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"max","schema":"metric","params":{"field":"timestamp"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.check.title","size":1,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":null,"direction":null}}}}', + visState: JSON.stringify({ + title: 'Latest alert', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'max', schema: 'metric', params: { field: 'timestamp' } }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { field: 'data.oscap.check.title', size: 1, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: null, direction: null } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.oscap.check.result", - "value": "fail", - "params": { - "query": "fail", - "type": "phrase" - } - }, - "query": { - "match": { - "data.oscap.check.result": { - "query": "fail", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.oscap.check.result', + value: 'fail', + params: { + query: 'fail', + type: 'phrase', + }, + }, + query: { + match: { + 'data.oscap.check.result': { + query: 'fail', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-OSCAP-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":40,"order":"desc","orderBy":"1","customLabel":"Agent"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.check.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Title"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.oscap.scan.profile.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1","customLabel":"Profile"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 40, + order: 'desc', + orderBy: '1', + customLabel: 'Agent', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.check.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Title', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.oscap.scan.profile.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Profile', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-osquery.ts b/server/integration-files/visualizations/overview/overview-osquery.ts index 7f6c17418a..8320dcf418 100644 --- a/server/integration-files/visualizations/overview/overview-osquery.ts +++ b/server/integration-files/visualizations/overview/overview-osquery.ts @@ -15,145 +15,655 @@ export default [ _type: 'visualization', _source: { title: 'Alerts over time', - visState: - '{"title":"Alerts over time","type":"area","params":{"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Alerts over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-added', _type: 'visualization', _source: { title: 'Top 5 added', - visState: - '{"title":"Top 5 added","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 added', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"data.osquery.action","value":"added","params":{"query":"added","type":"phrase"}},"query":{"match":{"data.osquery.action":{"query":"added","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.osquery.action', + value: 'added', + params: { query: 'added', type: 'phrase' }, + }, + query: { match: { 'data.osquery.action': { query: 'added', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-removed', _type: 'visualization', _source: { title: 'Top 5 removed', - visState: - '{"title":"Top 5 removed","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 removed', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"data.osquery.action","value":"removed","params":{"query":"removed","type":"phrase"}},"query":{"match":{"data.osquery.action":{"query":"removed","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.osquery.action', + value: 'removed', + params: { query: 'removed', type: 'phrase' }, + }, + query: { match: { 'data.osquery.action': { query: 'removed', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Agents-Osquery-Evolution', _type: 'visualization', _source: { title: 'Evolution over time', - visState: - '{"title":"Evolution over time","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.osquery.name","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Evolution over time', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.osquery.name', + size: 10, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Most-common-packs', _type: 'visualization', _source: { title: 'Most common packs', - visState: - '{"title":"Most common packs","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.osquery.pack","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Most common packs', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.osquery.pack', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"language":"lucene","query":""},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { language: 'lucene', query: '' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-rules', _type: 'visualization', _source: { title: 'Top 5 rules', - visState: - '{"title":"Top 5 rules","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","size":1,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Top 5 rules', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Top-5-Agents', _type: 'visualization', _source: { title: 'Top 5 Agents', - visState: - '{"title":"Top 5 Agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 Agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[{"meta":{"index":"wazuh-alerts","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"rule.groups","value":"osquery","params":{"query":"osquery","type":"phrase"}},"query":{"match":{"rule.groups":{"query":"osquery","type":"phrase"}}},"$state":{"store":"appState"}}]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'osquery', + params: { query: 'osquery', type: 'phrase' }, + }, + query: { match: { 'rule.groups': { query: 'osquery', type: 'phrase' } } }, + $state: { store: 'appState' }, + }, + ], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Agents-reporting', _type: 'visualization', _source: { title: 'Agents reporting', - visState: - '{"title":"Agents reporting","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":60}}},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"agent.id"}}]}', + visState: JSON.stringify({ + title: 'Agents reporting', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 60 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { field: 'agent.id' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Osquery-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"table","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":5,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.name","size":20,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Name"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.action","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Action"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agent"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.pack","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Pack"}},{"id":"6","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.osquery.calendarTime","size":2,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Date"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":5,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'table', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 5, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.name', + size: 20, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Name', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.action', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Action', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.pack', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Pack', + }, + }, + { + id: '6', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.osquery.calendarTime', + size: 2, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Date', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 5, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-pci.ts b/server/integration-files/visualizations/overview/overview-pci.ts index 7191486920..d73c49b97b 100644 --- a/server/integration-files/visualizations/overview/overview-pci.ts +++ b/server/integration-files/visualizations/overview/overview-pci.ts @@ -14,115 +14,626 @@ export default [ _id: 'Wazuh-App-Overview-PCI-DSS-Requirements-over-time', _source: { title: 'Requirements over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.pci_dss","size":"5","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.pci_dss', + size: '5', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-PCI-DSS-Requirements-Agents-heatmap', _type: 'visualization', _source: { title: 'PCI requirements heatmap', - visState: - '{"title":"PCI requirements heatmap","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Greens","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"#555"}}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agents"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 13":"rgb(247,252,245)","13 - 26":"rgb(233,247,228)","26 - 39":"rgb(211,238,205)","39 - 52":"rgb(184,227,177)","52 - 65":"rgb(152,213,148)","65 - 78":"rgb(116,196,118)","78 - 91":"rgb(75,176,98)","91 - 104":"rgb(47,152,79)","104 - 117":"rgb(21,127,59)","117 - 130":"rgb(0,100,40)"}}}', + visState: JSON.stringify({ + title: 'PCI requirements heatmap', + type: 'heatmap', + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 10, + colorSchema: 'Greens', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { type: 'linear', defaultYExtents: false }, + labels: { show: false, rotate: 0, overwriteColor: false, color: '#555' }, + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agents', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 13': 'rgb(247,252,245)', + '13 - 26': 'rgb(233,247,228)', + '26 - 39': 'rgb(211,238,205)', + '39 - 52': 'rgb(184,227,177)', + '52 - 65': 'rgb(152,213,148)', + '65 - 78': 'rgb(116,196,118)', + '78 - 91': 'rgb(75,176,98)', + '91 - 104': 'rgb(47,152,79)', + '104 - 117': 'rgb(21,127,59)', + '117 - 130': 'rgb(0,100,40)', + }, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-PCI-DSS-requirements', _source: { title: 'PCI DSS requirements', - visState: - '{"title":"PCI DSS requirements","type":"line","params":{"type":"line","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":false,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","format":"YYYY-MM-DD"},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"z":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"radiusRatio":50},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.pci_dss","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + visState: JSON.stringify({ + title: 'PCI DSS requirements', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: false, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + params: { date: true, interval: 'P1D', format: 'YYYY-MM-DD' }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + z: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + radiusRatio: 50, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.pci_dss', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { id: '4', enabled: true, type: 'count', schema: 'radius', params: {} }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-PCI-DSS-Agents', _source: { title: 'Agents', - visState: - '{"title":"Agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":10,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 10, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-PCI-DSS-Requirements-by-agent', _source: { title: 'Requirements by agent', - visState: - '{"title":"Requirements by agent","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":51},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.pci_dss","size":5,"order":"desc","orderBy":"1","customLabel":"Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Requirements by agent', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + radiusRatio: 51, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.pci_dss', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-PCI-DSS-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.pci_dss","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.pci_dss', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-PCI-DSS-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.pci_dss","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.pci_dss', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-pm.ts b/server/integration-files/visualizations/overview/overview-pm.ts index 97019fb5be..b3f1951c7e 100644 --- a/server/integration-files/visualizations/overview/overview-pm.ts +++ b/server/integration-files/visualizations/overview/overview-pm.ts @@ -15,80 +15,364 @@ export default [ _type: 'visualization', _source: { title: 'Events over time', - visState: - '{"title":"Events over time","type":"area","params":{"scale":"linear","yAxis":{},"smoothLines":true,"addTimeMarker":false,"interpolate":"linear","addLegend":true,"shareYAxis":true,"mode":"overlap","defaultYExtents":false,"setYExtents":false,"addTooltip":true,"times":[],"type":"area","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal","setYExtents":false,"defaultYExtents":false},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"normal","data":{"label":"Count","id":"1"},"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"legendPosition":"right"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":5,"order":"desc","orderBy":"1"}},{"id":"3","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Events over time', + type: 'area', + params: { + scale: 'linear', + yAxis: {}, + smoothLines: true, + addTimeMarker: false, + interpolate: 'linear', + addLegend: true, + shareYAxis: true, + mode: 'overlap', + defaultYExtents: false, + setYExtents: false, + addTooltip: true, + times: [], + type: 'area', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal', setYExtents: false, defaultYExtents: false }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'normal', + data: { label: 'Count', id: '1' }, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + legendPosition: 'right', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + { + id: '3', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-PM-Top-5-rules', _type: 'visualization', _source: { title: 'Top 5 rules', - visState: - '{"title":"Export rule distr","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"sum","schema":"metric","params":{"field":"rule.level"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.description","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Export rule distr', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'sum', + schema: 'metric', + params: { field: 'rule.level' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.description', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-PM-Top-5-agents-pie', _type: 'visualization', _source: { title: 'Top 5 agents pie', - visState: - '{"title":"Top 5 agents pie","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Top 5 agents pie', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"language":"lucene","query":""},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { language: 'lucene', query: '' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-PM-Events-per-agent-evolution', _source: { title: 'Events per control type evolution', - visState: - '{"title":"Events per control type evolution","type":"line","params":{"type":"line","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.title","size":5,"order":"desc","orderBy":"1"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Events per control type evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'data.title', size: 5, order: 'desc', orderBy: '1' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-PM-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.title","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Control"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":1,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.title', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Control', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 1, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-tsc.ts b/server/integration-files/visualizations/overview/overview-tsc.ts index 6e8b37c4f3..e51f7b5f9d 100644 --- a/server/integration-files/visualizations/overview/overview-tsc.ts +++ b/server/integration-files/visualizations/overview/overview-tsc.ts @@ -14,115 +14,626 @@ export default [ _id: 'Wazuh-App-Overview-TSC-Requirements-over-time', _source: { title: 'Requirements over time', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.tsc","size":"5","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.tsc', + size: '5', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"language":"lucene","query":""}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-TSC-Requirements-Agents-heatmap', _type: 'visualization', _source: { title: 'TSC requirements heatmap', - visState: - '{"title":"TSC requirements heatmap","type":"heatmap","params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":10,"colorSchema":"Greens","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"#555"}}]},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.tsc","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Agents"}}]}', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 13":"rgb(247,252,245)","13 - 26":"rgb(233,247,228)","26 - 39":"rgb(211,238,205)","39 - 52":"rgb(184,227,177)","52 - 65":"rgb(152,213,148)","65 - 78":"rgb(116,196,118)","78 - 91":"rgb(75,176,98)","91 - 104":"rgb(47,152,79)","104 - 117":"rgb(21,127,59)","117 - 130":"rgb(0,100,40)"}}}', + visState: JSON.stringify({ + title: 'TSC requirements heatmap', + type: 'heatmap', + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 10, + colorSchema: 'Greens', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { type: 'linear', defaultYExtents: false }, + labels: { show: false, rotate: 0, overwriteColor: false, color: '#555' }, + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.tsc', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agents', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 13': 'rgb(247,252,245)', + '13 - 26': 'rgb(233,247,228)', + '26 - 39': 'rgb(211,238,205)', + '39 - 52': 'rgb(184,227,177)', + '52 - 65': 'rgb(152,213,148)', + '65 - 78': 'rgb(116,196,118)', + '78 - 91': 'rgb(75,176,98)', + '91 - 104': 'rgb(47,152,79)', + '104 - 117': 'rgb(21,127,59)', + '117 - 130': 'rgb(0,100,40)', + }, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","query":{"query":"","language":"lucene"},"filter":[]}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, }, { _id: 'Wazuh-App-Overview-TSC-requirements', _source: { title: 'TSC requirements', - visState: - '{"title":"TSC requirements","type":"line","params":{"type":"line","grid":{"categoryLines":true,"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"line","mode":"normal","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":false,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"dimensions":{"x":{"accessor":0,"format":{"id":"date","params":{"pattern":"YYYY-MM-DD"}},"params":{"date":true,"interval":"P1D","format":"YYYY-MM-DD"},"aggType":"date_histogram"},"y":[{"accessor":2,"format":{"id":"number"},"params":{},"aggType":"count"}],"z":[{"accessor":3,"format":{"id":"number"},"params":{},"aggType":"count"}],"series":[{"accessor":1,"format":{"id":"terms","params":{"id":"string","otherBucketLabel":"Other","missingBucketLabel":"Missing"}},"params":{},"aggType":"terms"}]},"radiusRatio":50},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-1h","to":"now"},"useNormalizedEsInterval":true,"interval":"auto","drop_partials":false,"min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"rule.tsc","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"4","enabled":true,"type":"count","schema":"radius","params":{}}]}', + visState: JSON.stringify({ + title: 'TSC requirements', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: true, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: false, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } }, + params: { date: true, interval: 'P1D', format: 'YYYY-MM-DD' }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + z: [{ accessor: 3, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + radiusRatio: 50, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-1h', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.tsc', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { id: '4', enabled: true, type: 'count', schema: 'radius', params: {} }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-TSC-Agents', _source: { title: 'Agents', - visState: - '{"title":"Agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":10,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 10, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-TSC-Requirements-by-agent', _source: { title: 'Requirements by agent', - visState: - '{"title":"Requirements by agent","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100,"rotate":0},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"radiusRatio":51},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"rule.tsc","size":5,"order":"desc","orderBy":"1","customLabel":"Requirements"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Requirements by agent', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100, rotate: 0 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + radiusRatio: 51, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.tsc', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Requirements', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, }, - _type: 'visualization' + _type: 'visualization', }, { _id: 'Wazuh-App-Overview-TSC-Last-alerts', _type: 'visualization', _source: { title: 'Last alerts', - visState: - '{"title":"Last alerts","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.tsc","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Last alerts', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.tsc', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-TSC-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Agent name"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.tsc","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Requirement"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1","customLabel":"Rule description"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Agent name', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.tsc', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Requirement', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'Rule description', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/integration-files/visualizations/overview/overview-virustotal.ts b/server/integration-files/visualizations/overview/overview-virustotal.ts index c4e81e2e53..a7be2b4fbe 100644 --- a/server/integration-files/visualizations/overview/overview-virustotal.ts +++ b/server/integration-files/visualizations/overview/overview-virustotal.ts @@ -15,413 +15,911 @@ export default [ _type: 'visualization', _source: { title: 'Last files', - visState: - '{"title":"Last files","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Files"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.virustotal.source.file","size":5,"order":"desc","orderBy":"1"}}]}', - uiStateJSON: '{"vis":{"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Last files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Files' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'data.virustotal.source.file', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Files-Table', _type: 'visualization', _source: { title: 'Files', - visState: - '{"title":"Files","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Count"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.virustotal.source.file","size":10,"order":"desc","orderBy":"1","customLabel":"File"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.virustotal.permalink","size":1,"order":"desc","orderBy":"1","customLabel":"Link"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Files', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Count' }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.source.file', + size: 10, + order: 'desc', + orderBy: '1', + customLabel: 'File', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.virustotal.permalink', + size: 1, + order: 'desc', + orderBy: '1', + customLabel: 'Link', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Total-Malicious', _type: 'visualization', _source: { title: 'Total Malicious', - visState: - '{"title":"Total Malicious","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total malicious files"}}]}', + visState: JSON.stringify({ + title: 'Total Malicious', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total malicious files' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.malicious", - "value": "1", - "params": { - "query": "1", - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.malicious": { - "query": "1", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '1', + params: { + query: '1', + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: '1', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Total-Positives', _type: 'visualization', _source: { title: 'Total Positives', - visState: - '{"title":"Total Positives","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total positive files"}}]}', + visState: JSON.stringify({ + title: 'Total Positives', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total positive files' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal.positives", - "value": "exists" - }, - "exists": { - "field": "data.virustotal.positives" - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.positives", - "value": "0", - "params": { - "query": 0, - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.positives": { - "query": 0, - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Malicious-Evolution', _type: 'visualization', _source: { title: 'Malicious Evolution', - visState: - '{"title":"Malicious Evolution","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Malicious"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Malicious","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":false,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Malicious"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'Malicious Evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Malicious' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Malicious', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: false, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Malicious' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal.malicious", - "value": "exists" - }, - "exists": { - "field": "data.virustotal.malicious" - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.malicious", - "value": "0", - "params": { - "query": 0, - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.malicious": { - "query": 0, - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.malicious', + value: 'exists', + }, + exists: { + field: 'data.virustotal.malicious', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Total', _type: 'visualization', _source: { title: 'Total', - visState: - '{"title":"Total","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Total scans"}}]}', + visState: JSON.stringify({ + title: 'Total', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Total scans' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[{ - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal", - "value": "exists" - }, - "exists": { - "field": "data.virustotal" - }, - "$state": { - "store": "appState" - } - }], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal', + value: 'exists', + }, + exists: { + field: 'data.virustotal', + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Malicious-Per-Agent-Table', _type: 'visualization', _source: { title: 'Malicious Per Agent Table', - visState: - '{"title":"Malicious Per Agent Table","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":2,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"data.virustotal.source.md5","customLabel":"Malicious detected files"}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"agent.name","size":16,"order":"desc","orderBy":"1","customLabel":"Agent"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":2,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Malicious Per Agent Table', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 2, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { + field: 'data.virustotal.source.md5', + customLabel: 'Malicious detected files', + }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'agent.name', + size: 16, + order: 'desc', + orderBy: '1', + customLabel: 'Agent', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.malicious", - "value": "0", - "params": { - "query": "0", - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.malicious": { - "query": "0", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '0', + params: { + query: '0', + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: '0', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Malicious-Per-Agent', _type: 'visualization', _source: { title: 'Top 5 agents with unique malicious files', - visState: - '{"title":"Top 5 agents with unique malicious files","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"data.virustotal.source.md5"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1"}}]}', + visState: JSON.stringify({ + title: 'Top 5 agents with unique malicious files', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { field: 'data.virustotal.source.md5' }, + }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.malicious", - "value": "0", - "params": { - "query": "0", - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.malicious": { - "query": "0", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.malicious', + value: '0', + params: { + query: '0', + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.malicious': { + query: '0', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Alerts-Evolution', _type: 'visualization', _source: { title: 'Positives Heatmap', - visState: - '{ "title": "Alerts evolution by agents", "type": "histogram", "params": { "type": "histogram", "grid": { "categoryLines": false }, "categoryAxes": [ { "id": "CategoryAxis-1", "type": "category", "position": "bottom", "show": true, "style": {}, "scale": { "type": "linear" }, "labels": { "show": true, "filter": true, "truncate": 100 }, "title": {} } ], "valueAxes": [ { "id": "ValueAxis-1", "name": "LeftAxis-1", "type": "value", "position": "left", "show": true, "style": {}, "scale": { "type": "linear", "mode": "normal" }, "labels": { "show": true, "rotate": 0, "filter": false, "truncate": 100 }, "title": { "text": "Count" } } ], "seriesParams": [ { "show": true, "type": "histogram", "mode": "stacked", "data": { "label": "Count", "id": "1" }, "valueAxis": "ValueAxis-1", "drawLinesBetweenPoints": true, "lineWidth": 2, "showCircles": true } ], "addTooltip": true, "addLegend": true, "legendPosition": "right", "times": [], "addTimeMarker": false, "labels": { "show": false }, "thresholdLine": { "show": false, "value": 10, "width": 1, "style": "full", "color": "#E7664C" }, "dimensions": { "x": { "accessor": 0, "format": { "id": "date", "params": { "pattern": "YYYY-MM-DD HH:mm" } }, "params": { "date": true, "interval": "PT3H", "intervalESValue": 3, "intervalESUnit": "h", "format": "YYYY-MM-DD HH:mm", "bounds": { "min": "2020-04-17T12:11:35.943Z", "max": "2020-04-24T12:11:35.944Z" } }, "label": "timestamp per 3 hours", "aggType": "date_histogram" }, "y": [ { "accessor": 2, "format": { "id": "number" }, "params": {}, "label": "Count", "aggType": "count" } ], "series": [ { "accessor": 1, "format": { "id": "string", "params": { "parsedUrl": { "origin": "http://localhost:5601", "pathname": "/app/kibana", "basePath": "" } } }, "params": {}, "label": "Top 5 unusual terms in agent.name", "aggType": "significant_terms" } ] }, "radiusRatio": 50 }, "aggs": [ { "id": "1", "enabled": true, "type": "count", "schema": "metric", "params": {} }, { "id": "2", "enabled": true, "type": "date_histogram", "schema": "segment", "params": { "field": "timestamp", "timeRange": { "from": "now-7d", "to": "now" }, "useNormalizedEsInterval": true, "scaleMetricValues": false, "interval": "auto", "drop_partials": false, "min_doc_count": 1, "extended_bounds": {} } }, { "id": "3", "enabled": true, "type": "terms", "schema": "group", "params": { "field": "agent.name", "orderBy": "1", "order": "desc", "size": 5, "otherBucket": false, "otherBucketLabel": "Other", "missingBucket": false, "missingBucketLabel": "Missing" } } ] }', - uiStateJSON: - '{"vis":{"defaultColors":{"0 - 7":"rgb(247,251,255)","7 - 13":"rgb(219,233,246)","13 - 20":"rgb(187,214,235)","20 - 26":"rgb(137,190,220)","26 - 33":"rgb(83,158,205)","33 - 39":"rgb(42,123,186)","39 - 45":"rgb(11,85,159)"},"legendOpen":true}}', + visState: JSON.stringify({ + title: 'Alerts evolution by agents', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + intervalESValue: 3, + intervalESUnit: 'h', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2020-04-17T12:11:35.943Z', max: '2020-04-24T12:11:35.944Z' }, + }, + label: 'timestamp per 3 hours', + aggType: 'date_histogram', + }, + y: [ + { + accessor: 2, + format: { id: 'number' }, + params: {}, + label: 'Count', + aggType: 'count', + }, + ], + series: [ + { + accessor: 1, + format: { + id: 'string', + params: { + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/app/kibana', + basePath: '', + }, + }, + }, + params: {}, + label: 'Top 5 unusual terms in agent.name', + aggType: 'significant_terms', + }, + ], + }, + radiusRatio: 50, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { + defaultColors: { + '0 - 7': 'rgb(247,251,255)', + '7 - 13': 'rgb(219,233,246)', + '13 - 20': 'rgb(187,214,235)', + '20 - 26': 'rgb(137,190,220)', + '26 - 33': 'rgb(83,158,205)', + '33 - 39': 'rgb(42,123,186)', + '39 - 45': 'rgb(11,85,159)', + }, + legendOpen: true, + }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "exists", - "key": "data.virustotal.positives", - "value": "exists" - }, - "exists": { - "field": "data.virustotal.positives" - }, - "$state": { - "store": "appState" - } - }, - { - "meta": { - "index": "wazuh-alerts", - "negate": true, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.virustotal.positives", - "value": "0", - "params": { - "query": 0, - "type": "phrase" - } - }, - "query": { - "match": { - "data.virustotal.positives": { - "query": 0, - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'exists', + key: 'data.virustotal.positives', + value: 'exists', + }, + exists: { + field: 'data.virustotal.positives', + }, + $state: { + store: 'appState', + }, + }, + { + meta: { + index: 'wazuh-alerts', + negate: true, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.virustotal.positives', + value: '0', + params: { + query: 0, + type: 'phrase', + }, + }, + query: { + match: { + 'data.virustotal.positives': { + query: 0, + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-Virustotal-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', - visState: - '{"title":"Alerts summary","type":"table","params":{"perPage":10,"showPartialRows":false,"showMeticsAtAllLevels":false,"sort":{"columnIndex":3,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.id","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":50,"order":"desc","orderBy":"1","customLabel":"Rule ID"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.description","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":20,"order":"desc","orderBy":"1","customLabel":"Description"}},{"id":"4","enabled":true,"type":"terms","schema":"bucket","params":{"field":"rule.level","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":12,"order":"desc","orderBy":"1","customLabel":"Level"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, ]; diff --git a/server/integration-files/visualizations/overview/overview-vuls.ts b/server/integration-files/visualizations/overview/overview-vuls.ts index 8b7eef5044..023792da10 100644 --- a/server/integration-files/visualizations/overview/overview-vuls.ts +++ b/server/integration-files/visualizations/overview/overview-vuls.ts @@ -15,376 +15,1096 @@ export default [ _type: 'visualization', _source: { title: 'Severity count', - visState: - '{"title":"Alerts by action over time","type":"area","params":{"type":"area","grid":{"categoryLines":true,"style":{"color":"#eee"},"valueAxis":"ValueAxis-1"},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"area","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"showCircles":true,"interpolate":"cardinal","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","timeRange":{"from":"now-24h","to":"now","mode":"quick"},"useNormalizedEsInterval":true,"interval":"auto","time_zone":"Europe/Berlin","drop_partials":false,"customInterval":"2h","min_doc_count":1,"extended_bounds":{}}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.severity","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: JSON.stringify({ + title: 'Alerts by action over time', + type: 'area', + params: { + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'area', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Alert-summary', _type: 'visualization', _source: { title: 'Alert summary', - visState: - '{"title":"vulnerability","type":"table","params":{"perPage":10,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":4,"direction":"desc"},"showTotal":false,"totalFunc":"sum"},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.severity","size":5,"order":"asc","orderBy":"_key","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Severity"}},{"id":"3","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.title","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Title"}},{"id":"6","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.published","size":2,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"Published"}},{"id":"5","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.cve","size":1,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"CVE"}}]}', - uiStateJSON: - '{"vis":{"params":{"sort":{"columnIndex":4,"direction":"desc"}}}}', + visState: JSON.stringify({ + title: 'vulnerability', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: 4, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'asc', + orderBy: '_key', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severity', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.title', + size: 1000, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Title', + }, + }, + { + id: '6', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.published', + size: 2, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Published', + }, + }, + { + id: '5', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.cve', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'CVE', + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 4, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Commonly-affected-packages', _type: 'visualization', _source: { title: 'Commonly affected packages', - visState: - '{"title":"Commonly affected packages","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.package.name","size":5,"order":"desc","orderBy":"1","customLabel":"Affected package"}}]}', + visState: JSON.stringify({ + title: 'Commonly affected packages', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.package.name', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Affected package', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-top-CVEs', _type: 'visualization', _source: { title: 'Top CVEs', - visState: - '{"type":"table","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.cve","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"CVE"}}],"params":{"perPage":5,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum","percentageCol":"","row":true},"title":"CVE table"}', + visState: JSON.stringify({ + type: 'table', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.cve', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'CVE', + }, + }, + ], + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + percentageCol: '', + row: true, + }, + title: 'CVE table', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Most-common-CVEs', _type: 'visualization', _source: { title: 'Most common CVEs', - visState: - '{"title":"Most common CVEs","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cve","size":5,"order":"desc","orderBy":"1","customLabel":"CVE"}}]}', + visState: JSON.stringify({ + title: 'Most common CVEs', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cve', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'CVE', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-packages-CVEs', _type: 'visualization', _source: { title: 'Top affected packages by CVEs', - visState: - '{"type":"histogram","mode":"stacked","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cve","orderBy":"1","order":"desc","size":10,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.package.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}],"params":{"type":"area","grid":{"categoryLines":false},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"filter":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":true,"type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"drawLinesBetweenPoints":true,"lineWidth":2,"showCircles":true,"interpolate":"linear","valueAxis":"ValueAxis-1"}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false,"thresholdLine":{"show":false,"value":10,"width":1,"style":"full","color":"#E7664C"},"labels":{}},"title":"top packages by CVE"}', + visState: JSON.stringify({ + type: 'histogram', + mode: 'stacked', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cve', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.package.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + labels: {}, + }, + title: 'top packages by CVE', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-agents-severities', _type: 'visualization', _source: { title: 'Agents by severity', - visState: - '{"type":"heatmap","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing", "customLabel": " "}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.severity","orderBy":"1","order":"desc","size":5,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}],"params":{"type":"heatmap","addTooltip":true,"addLegend":true,"enableHover":false,"legendPosition":"right","times":[],"colorsNumber":4,"colorSchema":"Greens","setColorRange":false,"colorsRange":[],"invertColors":false,"percentageMode":false,"valueAxes":[{"show":false,"id":"ValueAxis-1","type":"value","scale":{"type":"linear","defaultYExtents":false},"labels":{"show":false,"rotate":0,"overwriteColor":false,"color":"black"}}]},"title":"Agents by severity"}', + visState: JSON.stringify({ + type: 'heatmap', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: ' ', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.severity', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { type: 'linear', defaultYExtents: false }, + labels: { show: false, rotate: 0, overwriteColor: false, color: 'black' }, + }, + ], + }, + title: 'Agents by severity', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-top-CWEs', _type: 'visualization', _source: { title: 'Top CWEs', - visState: - '{"type":"table","aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"bucket","params":{"field":"data.vulnerability.cwe_reference","orderBy":"1","order":"desc","size":50,"otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","customLabel":"CWE"}}],"params":{"perPage":5,"showPartialRows":false,"showMetricsAtAllLevels":false,"sort":{"columnIndex":null,"direction":null},"showTotal":false,"totalFunc":"sum","percentageCol":"","row":true},"title":"CWE table"}', + visState: JSON.stringify({ + type: 'table', + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.cwe_reference', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'CWE', + }, + }, + ], + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { columnIndex: null, direction: null }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + percentageCol: '', + row: true, + }, + title: 'CWE table', + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Most-common-CWEs', _type: 'visualization', _source: { title: 'Most common CWEs', - visState: - '{"title":"Most common CWEs","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.cwe_reference","size":5,"order":"desc","orderBy":"1","customLabel":"Severity"}}]}', + visState: JSON.stringify({ + title: 'Most common CWEs', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.cwe_reference', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Severity', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Metric-Critical-severity', _type: 'visualization', _source: { title: 'Metric Critical severity', - visState: - '{"title":"Metric Critical severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Critical severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Critical severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Critical severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Critical", - "params": { - "query": "Critical", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Critical", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Critical', + params: { + query: 'Critical', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Critical', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Metric-High-severity', _type: 'visualization', _source: { title: 'Metric High severity', - visState: - '{"title":"Metric High severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"High severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric High severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'High severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "High", - "params": { - "query": "High", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "High", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'High', + params: { + query: 'High', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'High', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Metric-Medium-severity', _type: 'visualization', _source: { title: 'Metric Medium severity', - visState: - '{"title":"Metric Medium severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Medium severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Medium severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Medium severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Medium", - "params": { - "query": "Medium", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Medium", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Medium', + params: { + query: 'Medium', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Medium', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Metric-Low-severity', _type: 'visualization', _source: { title: 'Metric Low severity', - visState: - '{"title":"Metric Low severity","type":"metric","params":{"addTooltip":true,"addLegend":false,"type":"metric","metric":{"percentageMode":false,"useRanges":false,"colorSchema":"Green to Red","metricColorMode":"None","colorsRange":[{"from":0,"to":10000}],"labels":{"show":true},"invertColors":false,"style":{"bgFill":"#000","bgColor":false,"labelColor":false,"subText":"","fontSize":20}}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{"customLabel":"Low severity alerts"}}]}', + visState: JSON.stringify({ + title: 'Metric Low severity', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [{ from: 0, to: 10000 }], + labels: { show: true }, + invertColors: false, + style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Low severity alerts' }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: `{ - "index":"wazuh-alerts", - "filter":[ - { - "meta": { - "index": "wazuh-alerts", - "negate": false, - "disabled": false, - "alias": null, - "type": "phrase", - "key": "data.vulnerability.severity", - "value": "Low", - "params": { - "query": "Low", - "type": "phrase" - } - }, - "query": { - "match": { - "data.vulnerability.severity": { - "query": "Low", - "type": "phrase" - } - } - }, - "$state": { - "store": "appState" - } - } - ], - "query":{"query":"","language":"lucene"} - }` - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'data.vulnerability.severity', + value: 'Low', + params: { + query: 'Low', + type: 'phrase', + }, + }, + query: { + match: { + 'data.vulnerability.severity': { + query: 'Low', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Most-affected-agents', _type: 'visualization', _source: { title: 'Most affected agents', - visState: - '{"title":"Most affected agents","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"agent.name","size":5,"order":"desc","orderBy":"1","customLabel":"Affected agent"}}]}', + visState: JSON.stringify({ + title: 'Most affected agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Affected agent', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Vulnerability-severity-distribution', _type: 'visualization', _source: { title: 'Severity distribution', - visState: - '{"title":"Severity distribution","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":false,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"data.vulnerability.severity","size":5,"order":"desc","orderBy":"1","customLabel":"Severity"}}]}', + visState: JSON.stringify({ + title: 'Severity distribution', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'data.vulnerability.severity', + size: 5, + order: 'desc', + orderBy: '1', + customLabel: 'Severity', + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, }, { _id: 'Wazuh-App-Overview-vuls-Vulnerability-evolution-affected-packages', _type: 'visualization', _source: { title: 'TOP affected packages alerts Evolution', - visState: - '{"title":"TOP affected packages alerts Evolution","type":"histogram","params":{"type":"histogram","grid":{"categoryLines":false,"style":{"color":"#eee"}},"categoryAxes":[{"id":"CategoryAxis-1","type":"category","position":"bottom","show":true,"style":{},"scale":{"type":"linear"},"labels":{"show":true,"truncate":100},"title":{}}],"valueAxes":[{"id":"ValueAxis-1","name":"LeftAxis-1","type":"value","position":"left","show":true,"style":{},"scale":{"type":"linear","mode":"normal"},"labels":{"show":true,"rotate":0,"filter":false,"truncate":100},"title":{"text":"Count"}}],"seriesParams":[{"show":"true","type":"histogram","mode":"stacked","data":{"label":"Count","id":"1"},"valueAxis":"ValueAxis-1","drawLinesBetweenPoints":true,"showCircles":true}],"addTooltip":true,"addLegend":true,"legendPosition":"right","times":[],"addTimeMarker":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"3","enabled":true,"type":"terms","schema":"group","params":{"field":"data.vulnerability.package.name","size":5,"order":"desc","orderBy":"1"}},{"id":"2","enabled":true,"type":"date_histogram","schema":"segment","params":{"field":"timestamp","interval":"auto","customInterval":"2h","min_doc_count":1,"extended_bounds":{}}}]}', + visState: JSON.stringify({ + title: 'TOP affected packages alerts Evolution', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'data.vulnerability.package.name', + size: 5, + order: 'desc', + orderBy: '1', + }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}' - } - } - } + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; diff --git a/server/lib/api-interceptor.ts b/server/lib/api-interceptor.ts index bc2b8efdc3..c7cf7c3a62 100644 --- a/server/lib/api-interceptor.ts +++ b/server/lib/api-interceptor.ts @@ -12,8 +12,13 @@ import axios, { AxiosResponse } from 'axios'; import { ManageHosts } from './manage-hosts'; +import https from 'https'; -process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +const _axios = axios.create({ httpsAgent }); interface APIHost{ url: string @@ -54,7 +59,7 @@ export const authenticate = async (apiHostID: string, authContext?: any): Promis ...(!!authContext ? { data: authContext } : {}) }; - const response: AxiosResponse = await axios(optionsRequest); + const response: AxiosResponse = await _axios(optionsRequest); const token: string = (((response || {}).data || {}).data || {}).token; if (!authContext) { CacheInternalUserAPIHostToken.set(apiHostID, token); @@ -107,7 +112,7 @@ export const requestAsCurrentUser = async (method: string, path: string, data: a const request = async (method: string, path: string, data: any, options: any): Promise => { try{ const optionsRequest = await buildRequestOptions(method, path, data, options); - const response: AxiosResponse = await axios(optionsRequest); + const response: AxiosResponse = await _axios(optionsRequest); return response; }catch(error){ throw error; diff --git a/server/lib/initial-wazuh-config.ts b/server/lib/initial-wazuh-config.ts index ec7e05f86a..e2e6ae1671 100644 --- a/server/lib/initial-wazuh-config.ts +++ b/server/lib/initial-wazuh-config.ts @@ -30,6 +30,12 @@ export const initialWazuhConfig: string = `--- # Also, you can check our repository: # https://github.com/wazuh/wazuh-kibana-app # +# ------------------------------- Disable roles ------------------------------- +# +# Defines which Elasticsearch roles disable Wazuh +# disabled_roles: +# - wazuh_disabled +# # ------------------------------- Index patterns ------------------------------- # # Default index pattern to use. @@ -111,13 +117,13 @@ export const initialWazuhConfig: string = `--- #wazuh.monitoring.frequency: 900 # # Configure wazuh-monitoring-* indices shards and replicas. -#wazuh.monitoring.shards: 2 +#wazuh.monitoring.shards: 1 #wazuh.monitoring.replicas: 0 # # Configure wazuh-monitoring-* indices custom creation interval. # Values: h (hourly), d (daily), w (weekly), m (monthly) -# Default: d -#wazuh.monitoring.creation: d +# Default: w +#wazuh.monitoring.creation: w # # Default index pattern to use for Wazuh monitoring #wazuh.monitoring.pattern: wazuh-monitoring-* @@ -126,7 +132,7 @@ export const initialWazuhConfig: string = `--- # # Customize the index prefix of predefined jobs # This change is not retroactive, if you change it new indexes will be created -# cron.prefix: test +# cron.prefix: wazuh # # --------------------------------- wazuh-sample-alerts ------------------------- # diff --git a/server/lib/reporting/agent-configuration.ts b/server/lib/reporting/agent-configuration.ts index 35181f8144..98878ec6f9 100644 --- a/server/lib/reporting/agent-configuration.ts +++ b/server/lib/reporting/agent-configuration.ts @@ -285,7 +285,7 @@ export const AgentConfiguration = { config: [ { component: 'syscheck', configuration: 'syscheck', matrix: true } ], - tabs: ['General', 'Who data'], + tabs: ['General','Who data'], labels: [ { disabled: 'Integrity monitoring disabled', diff --git a/server/lib/reporting/printer.ts b/server/lib/reporting/printer.ts index b890ecf591..ab0026ac8b 100644 --- a/server/lib/reporting/printer.ts +++ b/server/lib/reporting/printer.ts @@ -490,9 +490,29 @@ export class ReportPrinter{ style: 'standard' } }) - }); - - const widths = new Array(columns.length - 1).fill('auto'); + }); + + // 385 is the max initial width per column + let totalLength = columns.length - 1; + const widthColumn = 385/totalLength; + let totalWidth = totalLength * widthColumn; + + const widths:(number)[] = []; + + for (let step = 0; step < columns.length - 1; step++) { + + let columnLength = this.getColumnWidth(columns[step], tableRows, step); + + if (columnLength <= Math.round(totalWidth / totalLength)) { + widths.push(columnLength); + totalWidth -= columnLength; + } + else { + widths.push(Math.round(totalWidth / totalLength)); + totalWidth -= Math.round((totalWidth / totalLength)); + } + totalLength--; + } widths.push('*'); this.addContent({ @@ -597,4 +617,28 @@ export class ReportPrinter{ document.end(); } -} \ No newline at end of file + /** + * Returns the width of a given column + * + * @param column + * @param tableRows + * @param step + * @returns {number} + */ + getColumnWidth(column, tableRows, index){ + const widthCharacter = 5; //min width per character + + //Get the longest row value + const maxRowLength = tableRows.reduce((maxLength, row)=>{ + return (row[index].text.length > maxLength ? row[index].text.length : maxLength); + },0); + + //Get column name length + const headerLength = column.label.length; + + //Use the longest to get the column width + const maxLength = maxRowLength > headerLength ? maxRowLength : headerLength; + + return maxLength * widthCharacter; + } +} diff --git a/server/lib/update-registry.ts b/server/lib/update-registry.ts index eb489634d3..8550c0a763 100644 --- a/server/lib/update-registry.ts +++ b/server/lib/update-registry.ts @@ -148,7 +148,7 @@ export class UpdateRegistry { async updateAPIExtensions(id, extensions) { try { const content = await this.readContent(); - content.hosts[id].extensions = extensions; + if(content.hosts[id]) content.hosts[id].extensions = extensions; await this.writeContent(content); log( 'update-registry:updateAPIExtensions', diff --git a/server/routes/wazuh-api.ts b/server/routes/wazuh-api.ts index 8b17280eb8..3b41dd9f6f 100644 --- a/server/routes/wazuh-api.ts +++ b/server/routes/wazuh-api.ts @@ -145,4 +145,12 @@ export function WazuhApiRoutes(router: IRouter) { }, async (context, request, response) => ctrl.getSyscollector(context, request, response) ); + + // Return logged in user has wazuh disabled by role + router.get({ + path: '/api/check-wazuh', + validate: false + }, + async (context, request, response) => ctrl.isWazuhDisabled(context, request, response) + ); } diff --git a/server/start/cron-scheduler/error-handler.ts b/server/start/cron-scheduler/error-handler.ts index 515ed99412..a4772577f8 100644 --- a/server/start/cron-scheduler/error-handler.ts +++ b/server/start/cron-scheduler/error-handler.ts @@ -18,7 +18,7 @@ export function ErrorHandler(error, serverLogger) { if (errorLevel === DEBUG && logsLevel !== DEBUG) return; serverLogger[logLevel(errorLevel)](`${JSON.stringify(error)}`); } catch (error) { - serverLogger[logLevel(errorLevel)](`Message to long to show in console output, check the log file`) + serverLogger[logLevel(errorLevel)](`Message too long to show in console output, check the log file`) } } diff --git a/server/start/cron-scheduler/save-document.ts b/server/start/cron-scheduler/save-document.ts index 825893f8dc..7dbe5ff775 100644 --- a/server/start/cron-scheduler/save-document.ts +++ b/server/start/cron-scheduler/save-document.ts @@ -3,6 +3,7 @@ import { getConfiguration } from '../../lib/get-configuration'; import { log } from '../../lib/logger'; import { indexDate } from '../../lib/index-date'; import { WAZUH_INDEX_SHARDS, WAZUH_INDEX_REPLICAS } from '../../../common/constants' +import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError' export interface IIndexConfiguration { name: string @@ -43,7 +44,7 @@ export class SaveDocument { private async checkIndexAndCreateIfNotExists(index, shards, replicas) { try { - try { + await tryCatchForIndexPermissionError(index) (async() => { const exists = await this.esClientInternalUser.indices.exists({ index }); log(this.logPath, `Index '${index}' exists? ${exists.body}`, 'debug'); if (!exists.body) { @@ -60,10 +61,9 @@ export class SaveDocument { }); log(this.logPath, `Status of create a new index: ${JSON.stringify(response)}`, 'debug'); } - } catch (error) { - log(this.logPath ,`Error searching or creating '${index}' due to '${error.message || error}'`); - } + })(); } catch (error) { + log(this.logPath, error.message || error); this.checkDuplicateIndexError(error); } } diff --git a/server/start/index.ts b/server/start/index.ts index a1ee2e5a7d..d21f7d3109 100644 --- a/server/start/index.ts +++ b/server/start/index.ts @@ -1,4 +1,5 @@ export * from './cron-scheduler'; export * from './initialize'; export * from './monitoring'; -export * from './queue'; \ No newline at end of file +export * from './queue'; +export * from './tryCatchForIndexPermissionError'; \ No newline at end of file diff --git a/server/start/initialize/index.ts b/server/start/initialize/index.ts index 02db0bca5e..e40444d868 100644 --- a/server/start/initialize/index.ts +++ b/server/start/initialize/index.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import { ManageHosts } from '../../lib/manage-hosts'; import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_KIBANA_TEMPLATE_NAME, WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH } from '../../../common/constants'; import { createDataDirectoryIfNotExists } from '../../lib/filesystem'; +import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; const manageHosts = new ManageHosts(); @@ -98,39 +99,30 @@ export function jobInitializeRun(context) { /** * Checks if the .wazuh index exist in order to migrate to wazuh.yml */ - const checkWazuhIndex = async () => { - try { - log('initialize:checkWazuhIndex', `Checking ${WAZUH_INDEX} index.`, 'debug'); - - const result = await context.core.elasticsearch.client.asInternalUser.indices.exists({ + const checkWazuhIndex = tryCatchForIndexPermissionError(WAZUH_INDEX)( async () => { + log('initialize:checkWazuhIndex', `Checking ${WAZUH_INDEX} index.`, 'debug'); + const result = await context.core.elasticsearch.client.asInternalUser.indices.exists({ + index: WAZUH_INDEX + }); + if (result.body) { + const data = await context.core.elasticsearch.client.asInternalUser.search({ + index: WAZUH_INDEX, + size: 100 + }); + const apiEntries = (((data || {}).body || {}).hits || {}).hits || []; + await manageHosts.migrateFromIndex(apiEntries); + log( + 'initialize:checkWazuhIndex', + `Index ${WAZUH_INDEX} will be removed and its content will be migrated to wazuh.yml`, + 'debug' + ); + // Check if all APIs entries were migrated properly and delete it from the .wazuh index + await checkProperlyMigrate(); + await context.core.elasticsearch.client.asInternalUser.indices.delete({ index: WAZUH_INDEX }); - if (result.body) { - try { - const data = await context.core.elasticsearch.client.asInternalUser.search({ - index: WAZUH_INDEX, - size: 100 - }); - const apiEntries = (((data || {}).body || {}).hits || {}).hits || []; - await manageHosts.migrateFromIndex(apiEntries); - log( - 'initialize:checkWazuhIndex', - `Index ${WAZUH_INDEX} will be removed and its content will be migrated to wazuh.yml`, - 'debug' - ); - // Check if all APIs entries were migrated properly and delete it from the .wazuh index - await checkProperlyMigrate(); - await context.core.elasticsearch.client.asInternalUser.indices.delete({ - index: WAZUH_INDEX - }); - } catch (error) { - throw new Error(error); - } - } - } catch (error) { - return Promise.reject(error); } - }; + }); /** * Checks if the API entries were properly migrated @@ -243,16 +235,10 @@ export function jobInitializeRun(context) { // Init function. Check for "wazuh-version" document existance. const init = async () => { - try { - await Promise.all([ - checkWazuhIndex(), - checkWazuhRegistry() - ]); - } catch (error) { - log('initialize:init', error.message || error); - context.wazuh.logger.error(error.message || error); - return Promise.reject(error); - } + await Promise.all([ + checkWazuhIndex(), + checkWazuhRegistry() + ]); }; const createKibanaTemplate = () => { diff --git a/server/start/monitoring/index.ts b/server/start/monitoring/index.ts index 45b84d8878..8534db1c7d 100644 --- a/server/start/monitoring/index.ts +++ b/server/start/monitoring/index.ts @@ -19,7 +19,6 @@ import { buildIndexSettings } from '../../lib/build-index-settings'; import { WazuhHostsCtrl } from '../../controllers/wazuh-hosts'; import { WAZUH_MONITORING_PATTERN, - WAZUH_INDEX_SHARDS, WAZUH_INDEX_REPLICAS, WAZUH_MONITORING_TEMPLATE_NAME, WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, @@ -27,6 +26,7 @@ import { WAZUH_MONITORING_DEFAULT_ENABLED, WAZUH_MONITORING_DEFAULT_FREQUENCY, } from '../../../common/constants'; +import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; const blueWazuh = '\u001b[34mwazuh\u001b[39m'; const monitoringErrorLogColors = [blueWazuh, 'monitoring', 'error']; @@ -177,51 +177,39 @@ async function checkTemplate(context) { */ async function insertMonitoringDataElasticsearch(context, data) { const monitoringIndexName = MONITORING_INDEX_PREFIX + indexDate(MONITORING_CREATION); - try { if (!MONITORING_ENABLED){ return; }; - try{ - const exists = await context.core.elasticsearch.client.asInternalUser.indices.exists({index: monitoringIndexName}); - if(!exists.body){ - await createIndex(context, monitoringIndexName); - } - }catch(error){ - log('monitoring:insertMonitoringDataElasticsearch', error.message || error); - } - try{ - // Update the index configuration - const appConfig = getConfiguration(); - const indexConfiguration = buildIndexSettings( - appConfig, - 'wazuh.monitoring', - WAZUH_MONITORING_DEFAULT_INDICES_SHARDS - ); - - // To update the index settings with this client is required close the index, update the settings and open it - // Number of shards is not dynamic so delete that setting if it's given - delete indexConfiguration.settings.index.number_of_shards; - await context.core.elasticsearch.client.asInternalUser.indices.putSettings({ - index: monitoringIndexName, - body: indexConfiguration - }); + try { + await tryCatchForIndexPermissionError(monitoringIndexName) (async() => { + const exists = await context.core.elasticsearch.client.asInternalUser.indices.exists({index: monitoringIndexName}); + if(!exists.body){ + await createIndex(context, monitoringIndexName); + }; + + // Update the index configuration + const appConfig = getConfiguration(); + const indexConfiguration = buildIndexSettings( + appConfig, + 'wazuh.monitoring', + WAZUH_MONITORING_DEFAULT_INDICES_SHARDS + ); + // To update the index settings with this client is required close the index, update the settings and open it + // Number of shards is not dynamic so delete that setting if it's given + delete indexConfiguration.settings.index.number_of_shards; + await context.core.elasticsearch.client.asInternalUser.indices.putSettings({ + index: monitoringIndexName, + body: indexConfiguration + }); + + // Insert data to the monitoring index + await insertDataToIndex(context, monitoringIndexName, data); + })(); }catch(error){ log('monitoring:insertMonitoringDataElasticsearch', error.message || error); + context.wazuh.logger.error(error.message); } - - // Insert data to the monitoring index - await insertDataToIndex(context, monitoringIndexName, data); - } catch (error) { - const errorMessage = `Could not check if the index ${ - monitoringIndexName - } exists due to ${error.message || error}`; - log( - 'monitoring:insertMonitoringDataElasticsearch', - errorMessage - ); - context.wazuh.logger.error(errorMessage); - } } /** @@ -280,7 +268,7 @@ async function createIndex(context, indexName: string) { const IndexConfiguration = { settings: { index: { - number_of_shards: getAppConfigurationSetting('wazuh.monitoring.shards', appConfig, WAZUH_INDEX_SHARDS), + number_of_shards: getAppConfigurationSetting('wazuh.monitoring.shards', appConfig, WAZUH_MONITORING_DEFAULT_INDICES_SHARDS), number_of_replicas: getAppConfigurationSetting('wazuh.monitoring.replicas', appConfig, WAZUH_INDEX_REPLICAS) } } diff --git a/server/start/tryCatchForIndexPermissionError.ts b/server/start/tryCatchForIndexPermissionError.ts new file mode 100644 index 0000000000..607cdf5e72 --- /dev/null +++ b/server/start/tryCatchForIndexPermissionError.ts @@ -0,0 +1,35 @@ +/* + * Wazuh app - HOF to manage the message when elastic show a Response error / security_exception + * Copyright (C) 2015-2021 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 { log } from '../lib/logger'; + +export const tryCatchForIndexPermissionError = (wazuhIndex: string) => (functionToTryCatch) => async () => { + try { + await functionToTryCatch(); + } + catch (error) { + enum errorTypes{ + SECURITY_EXCEPTION = 'security_exception', + RESPONSE_ERROR = 'Response Error', + } + switch(error.message){ + case errorTypes.SECURITY_EXCEPTION: + error.message = (((((error.meta || error.message).body || error.message).error || error.message).root_cause[0] || error.message).reason || error.message); + break; + case errorTypes.RESPONSE_ERROR: + error.message = `Could not check if the index ${ + wazuhIndex + } exists due to no permissions for create, delete or check`; + break; + } + return Promise.reject(error); + } +} \ No newline at end of file From 802ad3bea81a3b8071fc234c6cef92db323ea326 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Wed, 23 Jun 2021 09:04:33 +0200 Subject: [PATCH 015/493] Changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81edad6db8..a02f180d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +### Fixed + +- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added From e402ef36dde384a8bb9008fcc80c8956ddd1a37f Mon Sep 17 00:00:00 2001 From: pablomarga Date: Wed, 23 Jun 2021 10:21:44 +0200 Subject: [PATCH 016/493] Requested changes --- common/constants.ts | 1 + server/lib/base-logger.ts | 6 +++--- server/lib/filesystem.ts | 10 +--------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index 73fec7669f..149eefdec6 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -149,6 +149,7 @@ export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( ); // Wazuh data path - logs +export const MAX_MB_LOG_FILES = 100; export const WAZUH_DATA_LOGS_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'logs'); export const WAZUH_DATA_LOGS_PLAIN_FILENAME = 'wazuhapp-plain.log'; export const WAZUH_DATA_LOGS_PLAIN_PATH = path.join( diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index ae3afb5933..b6043670bf 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -16,7 +16,7 @@ import path from 'path'; import { getConfiguration } from './get-configuration'; import { createDataDirectoryIfNotExists, createLogFileIfNotExists } from './filesystem'; -import { WAZUH_DATA_LOGS_DIRECTORY_PATH } from '../../common/constants'; +import { WAZUH_DATA_LOGS_DIRECTORY_PATH, MAX_MB_LOG_FILES } from '../../common/constants'; export interface IUIPlainLoggerSettings { level: string; @@ -135,7 +135,7 @@ export class BaseLogger { createLogFileIfNotExists(this.PLAIN_LOGS_PATH); if (this.allowed) { // check raw log file - if (this.getFilesizeInMegaBytes(this.RAW_LOGS_PATH) >= 100) { + if (this.getFilesizeInMegaBytes(this.RAW_LOGS_PATH) >= MAX_MB_LOG_FILES) { fs.renameSync( this.RAW_LOGS_PATH, `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.RAW_LOGS_FILE_NAME}` @@ -152,7 +152,7 @@ export class BaseLogger { } // check log file - if (this.getFilesizeInMegaBytes(this.PLAIN_LOGS_PATH) >= 100) { + if (this.getFilesizeInMegaBytes(this.PLAIN_LOGS_PATH) >= MAX_MB_LOG_FILES) { fs.renameSync( this.PLAIN_LOGS_PATH, `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.PLAIN_LOGS_FILE_NAME}` diff --git a/server/lib/filesystem.ts b/server/lib/filesystem.ts index 3dde0e1f74..908fb367ba 100644 --- a/server/lib/filesystem.ts +++ b/server/lib/filesystem.ts @@ -10,15 +10,7 @@ export const createDirectoryIfNotExists = (directory: string): void => { export const createLogFileIfNotExists = (filePath : string): void => { if (!fs.existsSync(filePath)) { - fs.writeFileSync( - filePath, - JSON.stringify({ - date: new Date(), - level: 'info', - location: 'logger', - message: 'Log file creation', - }) + '\n' - ); + fs.closeSync(fs.openSync(filePath, 'w')) }; }; From b93f4dcf456420cec4830ea517fd29f95885ba94 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Wed, 23 Jun 2021 10:25:11 +0200 Subject: [PATCH 017/493] Prettier applied --- server/lib/base-logger.ts | 2 +- server/lib/filesystem.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index b6043670bf..0c32c243e4 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -40,7 +40,7 @@ export class BaseLogger { constructor(plainLogsFile: string, rawLogsFile: string) { this.PLAIN_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, plainLogsFile); this.RAW_LOGS_PATH = path.join(WAZUH_DATA_LOGS_DIRECTORY_PATH, rawLogsFile); - this.PLAIN_LOGS_FILE_NAME= plainLogsFile; + this.PLAIN_LOGS_FILE_NAME = plainLogsFile; this.RAW_LOGS_FILE_NAME = rawLogsFile; } diff --git a/server/lib/filesystem.ts b/server/lib/filesystem.ts index 908fb367ba..d4aeab1bd5 100644 --- a/server/lib/filesystem.ts +++ b/server/lib/filesystem.ts @@ -2,25 +2,27 @@ import fs from 'fs'; import path from 'path'; import { WAZUH_DATA_ABSOLUTE_PATH } from '../../common/constants'; -export const createDirectoryIfNotExists = (directory: string): void => { +export const createDirectoryIfNotExists = (directory: string): void => { if (!fs.existsSync(directory)) { fs.mkdirSync(directory); - }; + } }; -export const createLogFileIfNotExists = (filePath : string): void => { +export const createLogFileIfNotExists = (filePath: string): void => { if (!fs.existsSync(filePath)) { - fs.closeSync(fs.openSync(filePath, 'w')) - }; + fs.closeSync(fs.openSync(filePath, 'w')); + } }; export const createDataDirectoryIfNotExists = (directory?: string) => { - const absoluteRoute = directory ? path.join(WAZUH_DATA_ABSOLUTE_PATH, directory) : WAZUH_DATA_ABSOLUTE_PATH; + const absoluteRoute = directory + ? path.join(WAZUH_DATA_ABSOLUTE_PATH, directory) + : WAZUH_DATA_ABSOLUTE_PATH; if (!fs.existsSync(absoluteRoute)) { fs.mkdirSync(absoluteRoute); - }; -} + } +}; export const getDataDirectoryRelative = (directory?: string) => { return path.join(WAZUH_DATA_ABSOLUTE_PATH, directory); -} +}; From 43e7d2f63626c49239eb79f35b04a2548fbb6ca8 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Wed, 23 Jun 2021 11:31:25 +0200 Subject: [PATCH 018/493] Refactor rotate function --- server/lib/base-logger.ts | 47 +++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index 0c32c243e4..8db9faf30a 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -127,6 +127,20 @@ export class BaseLogger { return fs.existsSync(filename); }; + rotateFiles = (file: string, pathFile: string, log?: string) => { + if (this.getFilesizeInMegaBytes(pathFile) >= MAX_MB_LOG_FILES) { + const fileExtension = path.extname(file); + const fileName = path.basename(file, fileExtension); + fs.renameSync( + pathFile, + `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${fileName}-${new Date().getTime()}${fileExtension}` + ); + if (log) { + fs.writeFileSync(pathFile, log + '\n'); + } + } + }; + /** * Checks if the wazuhapp.log file size is greater than 100MB, if so it rotates the file. */ @@ -135,29 +149,18 @@ export class BaseLogger { createLogFileIfNotExists(this.PLAIN_LOGS_PATH); if (this.allowed) { // check raw log file - if (this.getFilesizeInMegaBytes(this.RAW_LOGS_PATH) >= MAX_MB_LOG_FILES) { - fs.renameSync( - this.RAW_LOGS_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.RAW_LOGS_FILE_NAME}` - ); - fs.writeFileSync( - this.RAW_LOGS_PATH, - JSON.stringify({ - date: new Date(), - level: 'info', - location: 'logger', - message: 'Rotated log file', - }) + '\n' - ); - } - + this.rotateFiles( + this.RAW_LOGS_FILE_NAME, + this.RAW_LOGS_PATH, + JSON.stringify({ + date: new Date(), + level: 'info', + location: 'logger', + message: 'Rotated log file', + }) + ); // check log file - if (this.getFilesizeInMegaBytes(this.PLAIN_LOGS_PATH) >= MAX_MB_LOG_FILES) { - fs.renameSync( - this.PLAIN_LOGS_PATH, - `${WAZUH_DATA_LOGS_DIRECTORY_PATH}/${new Date().getTime()}${this.PLAIN_LOGS_FILE_NAME}` - ); - } + this.rotateFiles(this.PLAIN_LOGS_FILE_NAME, this.PLAIN_LOGS_PATH); } }; From eca61d4c57187522837088ad24959250c2a72214 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Wed, 23 Jun 2021 14:48:37 +0200 Subject: [PATCH 019/493] Identity components with hocs (#3387) * refactor(error-boundary-hoc): Improvement of hoc error-boundary to set displayName of all components. * refactor(error-boundary-hoc): Added Copyright --- .../hocs/error-boundary/with-error-boundary.tsx | 14 +++++++++----- public/components/common/hocs/utils/utils.tsx | 15 +++++++++++++++ public/controllers/management/index.js | 4 ++++ 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 public/components/common/hocs/utils/utils.tsx diff --git a/public/components/common/hocs/error-boundary/with-error-boundary.tsx b/public/components/common/hocs/error-boundary/with-error-boundary.tsx index 5ddab9f13f..c2fddbc464 100644 --- a/public/components/common/hocs/error-boundary/with-error-boundary.tsx +++ b/public/components/common/hocs/error-boundary/with-error-boundary.tsx @@ -12,9 +12,13 @@ import React from 'react'; import ErrorBoundary from '../../error-boundary/error-boundary'; +import { getDisplayName } from '../utils/utils'; -export const withErrorBoundary = (WrappedComponent) => (props) => ( - - - -); +export const withErrorBoundary = (WrappedComponent) => (props) => { + WrappedComponent.displayName = `withErrorBoundary(${getDisplayName(WrappedComponent)})`; + return ( + + + + ); +}; diff --git a/public/components/common/hocs/utils/utils.tsx b/public/components/common/hocs/utils/utils.tsx new file mode 100644 index 0000000000..5206f09ba8 --- /dev/null +++ b/public/components/common/hocs/utils/utils.tsx @@ -0,0 +1,15 @@ +/* + * Wazuh app - React Higher Order Components Utils + * Copyright (C) 2015-2021 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 getDisplayName = (WrappedComponent) => { + return WrappedComponent.displayName || WrappedComponent.name || 'UnknownComponent'; +}; diff --git a/public/controllers/management/index.js b/public/controllers/management/index.js index 20f98a1079..71e7061a6e 100644 --- a/public/controllers/management/index.js +++ b/public/controllers/management/index.js @@ -20,6 +20,10 @@ import { getAngularModule } from '../../kibana-services'; const app = getAngularModule(); +WzManagement.displayName = 'WzManagement'; +ManagementWelcomeWrapper.displayName = 'ManagementWelcomeWrapper'; +WzManagementConfiguration.displayName = 'WzManagementConfiguration'; + app .controller('managementController', ManagementController) .controller('groupsPreviewController', GroupsController) From 2bc0721a634e6ef06a355cfd2b36b1639ef5c697 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Wed, 23 Jun 2021 14:50:05 +0200 Subject: [PATCH 020/493] Implement try catch strategy on WzLog (#3373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(error-orchestrator): Implement try catch strategy on WzLog + Prettier * feat(error-orchestrator): Fixed contexts * feat(error-orchestrator): Improved use of ErrorOrchestratorService * bugfix(error-orchestrator): Fixed error message. * Added lowercase levels in storeError (#3377) * Added lowercase levels in storeError * Updated CHANGELOG Co-authored-by: Ibarra Maximiliano * [Feature] Mitre Att&ck Intelligence + adapt Framework (#3368) * refactor in vulnerabilities table component * refactor code in vuls inventory and add new table component with export csv * adapat table * finished refactor table component * delete console logs and fix wrong version * add new fields in suggestions * add changelog * changes in component table and remove status and type columns * fix columns position * feat(mitre): Add Mitre Att&ck intelligence section - Created Intelligence tab in Mitre Att&ck Module - Created left and right panel - Created resource button for the left panel - Created search bar for searhin in all resources - Created list of each resource * feat(mitre_intelligence): Modify the search results view and another improvements - Modify the Search results view - Improve useAsyncAction hook - Add Mitre Att&ck Intelligence to Agent modules component - Improve TableWithSearchBar component to accept filters as props - Refactor Mitre Atta&ck resources - Refator PanelSplit component - Fix filtersToObject helper - Update test * feat(mitre_att&ck_intelligence): Render description as markdown - Create Markdown component - Apply the Markdown component to the resource description in the resouce table * feat(mitre_atta&ck_intelligence): Add like operator to search resource by description * change endpoint and adapt component in mitre * fix flyout * feat(mirte_att&ck_integillence): Add the References resource - Added to left panel - Added resource view * add redirect to intelligence * fix merge * fix merge flyout * fix package version * fix(mitre_att&ck_intelligence): Organize resource suggestions and remove Reference resource * fix PR comments and add intelligence section redirect * Created new Mitre flyout * Changelog * fix comments PR * add redirect values in query params * apply prettier * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout * clear comments and imports * fix error handler techniques * delete session storage * delete files and fix get techniques data * Created new Mitre flyout (#3344) * Created new Mitre flyout * Changelog * Erasing comments * Erasing console.log * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout Co-authored-by: Antonio David Gutiérrez * fix redirect flyout to rules * feat(frontend/mitre_att&ck_intelligence): Removed welcome intelligence - Removed welcome intelligence view and adjustments when doing a general search - Set a resource type as selected - Update test * fix comments PR * fix(mitre_att&ck_intelligence): Change how to open the resource details flyout - Change how to open the resource details flyout - Refactor some componentes properties - Removed not used code * changelog: Added PR to chengelog * Update CHANGELOG.md * fix(mitre_att&ck_intelligence): Fix error in table-default.tsx * fix(mitre_att&ck_intelligence): PR request changes: - Add tests for: - Components: Markdown, PanelSplit - React Hooks: useAsyncAction - Renamed files - Add justification for using dangerouslySetInnerHTML property - Refactor requests to get mitre techniques - Fix tooltips to open tactic/technique details in Framework - Added some missing semicolon - Fix CSS class wz-markdown-wrapper name * fix get mitre Techniques from api Co-authored-by: eze9252 Co-authored-by: CPAlejandro Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: Franco Charriol * feat(error-orchestrator): Improved on createGetterSetter (#3376) * feat(error-orchestrator): Improved on createGetterSetter * bugfix(error-orchestrator): Added default value of disaply and store, remove location of types and fixed toastMessage of addError * feat(error-orchestrator): Added creatorGetterSetter on wazuh-app to avoid dependence on Kibana. * feat(error-orchestrator): Rebase 4.3 and apply new implementation getErrorOrchestrator. PR comments. * feat(error-orchestrator): Removed unnecessary parameter. * Fix creation of json file after a ui log (#3378) * feat(error-orchestrator): Removed unnecessary parameter. * fix(error-orchestrator): Fixed imports. * doc(error-orchestrator): Added README.md * doc(error-orchestrator): Added README.md Co-authored-by: Maximiliano Ibarra Co-authored-by: Ibarra Maximiliano Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: eze9252 Co-authored-by: CPAlejandro Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: Franco Charriol Co-authored-by: Pablo Martínez --- .../components/management/mg-logs/logs.js | 965 +++++++++--------- .../error-orchestrator/README.md | 56 + .../error-orchestrator.service.ts | 4 + 3 files changed, 558 insertions(+), 467 deletions(-) create mode 100644 public/react-services/error-orchestrator/README.md diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index a8b61e9612..9a8b62bb39 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -11,30 +11,38 @@ */ import React, { Component, Fragment } from 'react'; import { + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, EuiFlexGroup, EuiFlexItem, - EuiSelect, - EuiSwitch, - EuiSpacer, EuiPage, EuiPageBody, - EuiCodeBlock, EuiPanel, - EuiTitle, - EuiTextColor, EuiProgress, - EuiCallOut, - EuiButtonEmpty + EuiSelect, + EuiSpacer, + EuiSwitch, + EuiTextColor, + EuiTitle, } from '@elastic/eui'; -import 'brace/mode/less'; -import 'brace/theme/github'; +import 'brace/mode/less'; +import 'brace/theme/github'; import exportCsv from '../../../../../react-services/wz-csv'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { WzRequest } from '../../../../../react-services/wz-request'; import { formatUIDate } from '../../../../../react-services/time-service'; -import { withUserAuthorizationPrompt, withGlobalBreadcrumb } from '../../../../../components/common/hocs'; +import { + withGlobalBreadcrumb, + withUserAuthorizationPrompt, +} from '../../../../../components/common/hocs'; import { compose } from 'redux'; import { WzFieldSearch } from '../../../../../components/wz-field-search-bar/wz-field-search-bar'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + +const errorContext = 'WzLogs'; export default compose( withGlobalBreadcrumb([ @@ -42,505 +50,528 @@ export default compose( { text: 'Management', href: '#/manager' }, { text: 'Logs' } ]), - withUserAuthorizationPrompt([{action: 'cluster:status', resource: '*:*:*'}, {action: 'cluster:read', resource: 'node:id:*'}]) -) -(class WzLogs extends Component { - constructor(props) { - super(props); - this.offset = 350; - this.state = { - isCluster: false, - selectedDaemon: '', - logLevelSelect: '', - descendingSort: false, - searchBarValue: '', - appliedSearch: '', - logsList: '', - isLoading: false, - offset: 0, - selectedNode: '', - logsPath: '', - nodeList: [], - totalItems: 0, - daemonsList: [], - loadingLogs: false, - realTime: false + withUserAuthorizationPrompt([ + { action: 'cluster:status', resource: '*:*:*' }, + { action: 'cluster:read', resource: 'node:id:*' }, + ]) +)( + class WzLogs extends Component { + constructor(props) { + super(props); + this.offset = 350; + this.state = { + isCluster: false, + selectedDaemon: '', + logLevelSelect: '', + descendingSort: false, + searchBarValue: '', + appliedSearch: '', + logsList: '', + isLoading: false, + offset: 0, + selectedNode: '', + logsPath: '', + nodeList: [], + totalItems: 0, + daemonsList: [], + loadingLogs: false, + realTime: false, + }; + this.ITEM_STYLE = { width: '300px' }; + } + + updateHeight = () => { + this.height = window.innerHeight - this.offset; + this.forceUpdate(); }; - this.ITEM_STYLE = { width: '300px' }; - } - updateHeight = () => { - this.height = window.innerHeight - this.offset; - this.forceUpdate(); - }; - - async componentDidMount() { - this.height = window.innerHeight - this.offset; - window.addEventListener('resize', this.updateHeight); - this.setState({ isLoading: true }); - - const { nodeList, logsPath, selectedNode } = await this.getLogsPath(); - await this.initDaemonsList(logsPath); - - this.setState({ - selectedNode, - selectedDaemon: 'all', - logLevelSelect: 'all', - realTime: false, - descendingSort: false, - offset: 0, - totalItems: 0, - logsPath, - loadingLogs: false, - nodeList - }, async () => { - await this.setFullLogs(); - this.setState({ - isLoading: false - }); - }); + async componentDidMount() { + try { + this.height = window.innerHeight - this.offset; + window.addEventListener('resize', this.updateHeight); + this.setState({ isLoading: true }); + + const { nodeList, logsPath, selectedNode } = await this.getLogsPath(); + await this.initDaemonsList(logsPath); + + this.setState( + { + selectedNode, + selectedDaemon: 'all', + logLevelSelect: 'all', + realTime: false, + descendingSort: false, + offset: 0, + totalItems: 0, + logsPath, + loadingLogs: false, + nodeList, + }, + async () => { + await this.setFullLogs(); + this.setState({ + isLoading: false, + }); + } + ); + } catch (error) { + this.setState({ + isLoading: false, + }); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; - } + getErrorOrchestrator().handleError(options); + } + } - componentWillUnmount() { - window.removeEventListener('resize', this.updateHeight); - clearInterval(this.realTimeInterval); - } + componentWillUnmount() { + window.removeEventListener('resize', this.updateHeight); + clearInterval(this.realTimeInterval); + } - async initDaemonsList(logsPath) { - try { - const path = logsPath + '/summary'; - const data = await WzRequest.apiReq('GET', path, {}); - const formattedData = (((data || {}).data || {}).data || {}).affected_items; - const daemonsList = [...['all']]; - for (const daemon of formattedData) { - daemonsList.push(Object.keys(daemon)[0]); + async initDaemonsList(logsPath) { + try { + const path = logsPath + '/summary'; + const data = await WzRequest.apiReq('GET', path, {}); + const formattedData = (((data || {}).data || {}).data || {}).affected_items; + const daemonsList = [...['all']]; + for (const daemon of formattedData) { + daemonsList.push(Object.keys(daemon)[0]); + } + this.setState({ daemonsList }); + } catch (error) { + throw new Error(error); } - this.setState({ daemonsList }); - } catch (err) { - return Promise.reject('Error obtaining daemons list.'); - } // eslint-disable-line - } - - parseLogsToText(logs) { - let result = ''; - logs.forEach(item => { - result += - formatUIDate(item.timestamp) + - ' ' + - item.tag + - ' ' + - item.level.toUpperCase() + - ' ' + - item.description + - '\n'; - }); - return result; - } + } - buildFilters(customOffset = 0) { - let result = { limit: 100 }; - if (customOffset) { - result['offset'] = customOffset; + parseLogsToText(logs) { + let result = ''; + logs.forEach((item) => { + result += + formatUIDate(item.timestamp) + + ' ' + + item.tag + + ' ' + + item.level.toUpperCase() + + ' ' + + item.description + + '\n'; + }); + return result; } - if (this.state.logLevelSelect !== 'all') - result['level'] = this.state.logLevelSelect; - if (this.state.selectedDaemon !== 'all') - result['tag'] = this.state.selectedDaemon; - if (this.state.appliedSearch) result['search'] = this.state.appliedSearch; - if (this.state.descendingSort) result['sort'] = '+timestamp'; - else result['sort'] = '-timestamp'; - - return result; - } - async getFullLogs(customOffset = 0) { - const { logsPath } = this.state; - let result = ''; - let totalItems = 0; - if (this.state.selectedNode) { - try { - const tmpResult = await WzRequest.apiReq( - 'GET', - logsPath, - { params: this.buildFilters(customOffset) } - ); - const resultItems = ((tmpResult || {}).data.data || {}).affected_items; - totalItems = ((tmpResult || {}).data.data || {}).total_affected_items; - result = this.parseLogsToText(resultItems) || ''; - } catch (err) { - result = ''; - return Promise.reject('Error obtaining logs.'); + buildFilters(customOffset = 0) { + let result = { limit: 100 }; + if (customOffset) { + result['offset'] = customOffset; } - } else { + if (this.state.logLevelSelect !== 'all') result['level'] = this.state.logLevelSelect; + if (this.state.selectedDaemon !== 'all') result['tag'] = this.state.selectedDaemon; + if (this.state.appliedSearch) result['search'] = this.state.appliedSearch; + if (this.state.descendingSort) result['sort'] = '+timestamp'; + else result['sort'] = '-timestamp'; + + return result; + } + + async getFullLogs(customOffset = 0) { + const { logsPath } = this.state; + let result = ''; + let totalItems = 0; + try { - const tmpResult = await WzRequest.apiReq( - 'GET', - logsPath, - { params: this.buildFilters(customOffset) } - ); + const tmpResult = await WzRequest.apiReq('GET', logsPath, { + params: this.buildFilters(customOffset), + }); const resultItems = ((tmpResult || {}).data.data || {}).affected_items; totalItems = ((tmpResult || {}).data.data || {}).total_affected_items; result = this.parseLogsToText(resultItems) || ''; - } catch (err) { - result = ''; - return Promise.reject('Error obtaining logs'); + } catch (error) { + throw new Error(error); } - } - this.setState({ totalItems }); - return result; - } - async setFullLogs() { - try{ - const result = await this.getFullLogs(); - this.setState({ logsList: result, offset: 0 }); - }catch(error){ - + this.setState({ totalItems }); + return result; } - } - /** - * Returns an object with the path to request Wazuh logs, the list of nodes and the current selected node. - */ - async getLogsPath() { - try { - const clusterStatus = await WzRequest.apiReq( - 'GET', - '/cluster/status', - {} - ); - const clusterEnabled = - (((clusterStatus || {}).data || {}).data || {}).running === 'yes' && - (((clusterStatus || {}).data || {}).data || {}).enabled === 'yes'; - - if (clusterEnabled) { - let nodeList = ''; - let selectedNode = ''; - const nodeListTmp = await WzRequest.apiReq( - 'GET', - '/cluster/nodes', - {} - ); - if ( - Array.isArray((((nodeListTmp || {}).data || {}).data || {}).affected_items) - ) { - nodeList = nodeListTmp.data.data.affected_items; - selectedNode = nodeListTmp.data.data.affected_items.filter( - item => item.type === 'master' - )[0].name; - } - return { - nodeList, - logsPath: `/cluster/${selectedNode}/logs`, - selectedNode: selectedNode + async setFullLogs() { + try { + const result = await this.getFullLogs(); + this.setState({ logsList: result, offset: 0 }); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, }; + + getErrorOrchestrator().handleError(options); } - } catch (error) { - return Promise.reject('Error obtaining logs path.'); } - return { nodeList: '', logsPath: '/manager/logs', selectedNode: '' }; - } + /** + * Returns an object with the path to request Wazuh logs, the list of nodes and the current selected node. + */ + async getLogsPath() { + try { + const clusterStatus = await WzRequest.apiReq('GET', '/cluster/status', {}); + const clusterEnabled = + (((clusterStatus || {}).data || {}).data || {}).running === 'yes' && + (((clusterStatus || {}).data || {}).data || {}).enabled === 'yes'; + + if (clusterEnabled) { + let nodeList = ''; + let selectedNode = ''; + const nodeListTmp = await WzRequest.apiReq('GET', '/cluster/nodes', {}); + if (Array.isArray((((nodeListTmp || {}).data || {}).data || {}).affected_items)) { + nodeList = nodeListTmp.data.data.affected_items; + selectedNode = nodeListTmp.data.data.affected_items.filter( + (item) => item.type === 'master' + )[0].name; + } + return { + nodeList, + logsPath: `/cluster/${selectedNode}/logs`, + selectedNode: selectedNode, + }; + } + + return { nodeList: '', logsPath: '/manager/logs', selectedNode: '' }; + } catch (error) { + throw new Error(error); + } + } - getDaemonsOptions() { - const daemonsList = - this.state.daemonsList.length > 0 - ? this.state.daemonsList.map(item => { + getDaemonsOptions() { + return this.state.daemonsList.length > 0 + ? this.state.daemonsList.map((item) => { return { value: item, text: item === 'all' ? 'All daemons' : item }; }) : [{ value: 'all', text: 'All daemons' }]; + } - return daemonsList; - } + getLogLevelOptions() { + return [ + { value: 'all', text: 'All log levels' }, + { value: 'info', text: 'Info' }, + { value: 'error', text: 'Error' }, + { value: 'warning', text: 'Warning' }, + { value: 'critical', text: 'Critical' }, + { value: 'debug', text: 'Debug' }, + { value: 'debug2', text: 'Debug2' }, + ]; + } - getLogLevelOptions() { - return [ - { value: 'all', text: 'All log levels' }, - { value: 'info', text: 'Info' }, - { value: 'error', text: 'Error' }, - { value: 'warning', text: 'Warning' }, - { value: 'critical', text: 'Critical' }, - { value: 'debug', text: 'Debug' }, - { value: 'debug2', text: 'Debug2'} - ]; - } + getNodeList() { + try { + if (this.state.nodeList && Array.isArray(this.state.nodeList)) { + return this.state.nodeList.map((item) => { + return { value: item.name, text: `${item.name} (${item.type})` }; + }); + } else { + return false; + } + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: 'Error obtaining list of nodes.', + title: error.name, + }, + }; - getnodeList() { - try { - if (this.state.nodeList && Array.isArray(this.state.nodeList)) { - const nodeList = this.state.nodeList.map(item => { - return { value: item.name, text: `${item.name} (${item.type})` }; - }); - return nodeList; - } else { - return false; + getErrorOrchestrator().handleError(options); } - } catch (err) { - return Promise.reject('Error obtaining list of nodes.'); } - } - onDaemonChange = e => { - this.setState( - { - selectedDaemon: e.target.value - }, - this.setFullLogs - ); - }; - - onLogLevelChange = e => { - this.setState( - { - logLevelSelect: e.target.value - }, - this.setFullLogs - ); - }; - - onSortSwitchChange = e => { - this.setState( - { - descendingSort: e.target.checked - }, - this.setFullLogs - ); - }; - - onSelectNode = e => { - this.setState( - { - selectedNode: e.target.value, - logsPath: `/cluster/${e.target.value}/logs` - }, - this.setFullLogs - ); - }; - - onSearchBarChange = e => { - this.setState({ - searchBarValue: e - }); - }; - - onSearchBarSearch = e => { - this.setState( - { - appliedSearch: e - }, - this.setFullLogs - ); - }; - - makeSearch() { - this.setState( - { appliedSearch: this.state.searchBarValue }, - this.setFullLogs - ); - } + onDaemonChange = (e) => { + this.setState( + { + selectedDaemon: e.target.value, + }, + this.setFullLogs + ); + }; - setRealTimeInterval() { - if (this.state.realTime) - this.realTimeInterval = setInterval(() => this.setFullLogs(), 5000); - else clearInterval(this.realTimeInterval); - } + onLogLevelChange = (e) => { + this.setState( + { + logLevelSelect: e.target.value, + }, + this.setFullLogs + ); + }; - switchRealTime() { - this.setState({ realTime: !this.state.realTime }, this.setRealTimeInterval); - } + onSortSwitchChange = (e) => { + this.setState( + { + descendingSort: e.target.checked, + }, + this.setFullLogs + ); + }; - showToast = (color, title, time) => { - getToasts().add({ - color: color, - title: title, - toastLifeTimeMs: time, - }); - } + onSelectNode = (e) => { + this.setState( + { + selectedNode: e.target.value, + logsPath: `/cluster/${e.target.value}/logs`, + }, + this.setFullLogs + ); + }; - exportFormatted = async () => { - try { - this.showToast('success', 'Your download should begin automatically...', 3000); - const filters = this.buildFilters(); - await exportCsv( - this.state.selectedNode ? `/cluster/${this.state.selectedNode}/logs` : '/manager/logs', - Object.keys(filters).map(filter => ({name: filter, value: filters[filter]})), - `wazuh-${this.state.selectedNode ? `${this.state.selectedNode}-` : ''}ossec-log` - ); - return; - } catch (error) { - this.showToast('danger', error, 3000); + onSearchBarChange = (e) => { + this.setState({ + searchBarValue: e, + }); + }; + + onSearchBarSearch = (e) => { + this.setState( + { + appliedSearch: e, + }, + this.setFullLogs + ); + }; + + makeSearch() { + this.setState({ appliedSearch: this.state.searchBarValue }, this.setFullLogs); } - } - header() { - const daemonsOptions = this.getDaemonsOptions(); - const logLevelOptions = this.getLogLevelOptions(); - const nodeList = this.getnodeList(); - - return ( -
- - - -

Logs

-
-
- - - - - Export formatted - - - - -
- - - -

List and filter Wazuh logs.

-
-
-
- - - - - - - - - - {this.state.selectedNode && ( - - - - )} - - - - - this.switchRealTime()} - /> - - - - - - - - - - -
- ); - } + setRealTimeInterval() { + if (this.state.realTime) this.realTimeInterval = setInterval(() => this.setFullLogs(), 5000); + else clearInterval(this.realTimeInterval); + } - async loadExtraLogs() { - this.setState({ loadingLogs: true }); - const customOffset = this.state.offset + 100; - const result = await this.getFullLogs(customOffset); - this.setState({ - offset: customOffset, - logsList: this.state.logsList.concat(result), - loadingLogs: false - }); - } + switchRealTime() { + this.setState({ realTime: !this.state.realTime }, this.setRealTimeInterval); + } - logsTable() { - return ( -
- {(this.state.logsList && ( - -
- - {this.state.logsList} - -
- - {(this.state.offset + 100 < this.state.totalItems && ( - - - this.loadExtraLogs() : undefined} - > - Load more logs + showToast = (color, title, time) => { + getToasts().add({ + color: color, + title: title, + toastLifeTimeMs: time, + }); + }; + + exportFormatted = async () => { + try { + this.showToast('success', 'Your download should begin automatically...', 3000); + const filters = this.buildFilters(); + await exportCsv( + this.state.selectedNode ? `/cluster/${this.state.selectedNode}/logs` : '/manager/logs', + Object.keys(filters).map((filter) => ({ name: filter, value: filters[filter] })), + `wazuh-${this.state.selectedNode ? `${this.state.selectedNode}-` : ''}ossec-log` + ); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message, + title: error.name, + }, + }; + + getErrorOrchestrator().handleError(options); + } + }; + + header() { + const daemonsOptions = this.getDaemonsOptions(); + const logLevelOptions = this.getLogLevelOptions(); + const nodeList = this.getNodeList(); + + return ( +
+ + + +

Logs

+
+
+ + + + + Export formatted - ))} - - )) || ( - - )} -
- ); - } - - render() { - return ( - - - - {this.header()} - - {(!this.state.isLoading && this.logsTable()) || ( - - - - + + + + + +

List and filter Wazuh logs.

+
+
+
+ + + + + + + + + + {this.state.selectedNode && ( + + + + )} + + + + + this.switchRealTime()} + /> - )} -
-
-
- ); +
+
+ + + + + + +
+ ); + } + + async loadExtraLogs() { + this.setState({ loadingLogs: true }); + const customOffset = this.state.offset + 100; + const result = await this.getFullLogs(customOffset); + this.setState({ + offset: customOffset, + logsList: this.state.logsList.concat(result), + loadingLogs: false, + }); + } + + logsTable() { + return ( +
+ {(this.state.logsList && ( + +
+ + {this.state.logsList} + +
+ + {this.state.offset + 100 < this.state.totalItems && ( + + + this.loadExtraLogs() : undefined} + > + Load more logs + + + + )} +
+ )) || ( + + )} +
+ ); + } + + render() { + return ( + + + + {this.header()} + + {(!this.state.isLoading && this.logsTable()) || ( + + + + + + + )} + + + + ); + } } -}) +); diff --git a/public/react-services/error-orchestrator/README.md b/public/react-services/error-orchestrator/README.md new file mode 100644 index 0000000000..0ac26c7aa6 --- /dev/null +++ b/public/react-services/error-orchestrator/README.md @@ -0,0 +1,56 @@ +# `error-orchestrator.service` + +This is a client side orchestrator error service. + +## Example import + +```tsx +import { createGetterSetter } from '../utils/create-getter-setter'; +``` + +## Example usage + +### Using service + +```tsx +try { + // Our code here... + throw new Error('Some error here...'); +} catch (error) { + const options: UIErrorLog = { + context: 'contextError', + level: UI_LOGGER_LEVELS.WARNING, + severity: UI_ERROR_SEVERITIES.BUSINESS, + display: true, + store: true, + error: { + error: error, + message: error.message, + title: error.name, + }, + }; + + getErrorOrchestrator().handleError(options); +} +``` + +The `options` object may contain: + +- `context: string` (defaults to "" and not required) - to give more precision about location of the error. +- `level: UILogLevel` ([UILogLevel](./types.ts) - WARNING | INFO | ERROR) to determine the error type. +- `severity: UIErrorSeverity` ([UIErrorSeverity](./types.ts) UI | BUSINESS | CRITICAL) - to indicate that the class must use factory. +- `display: boolean` (default true) - to show the error to the user in UI. +- `store: boolean` (default false) - Used to stored by request [POST] on our logs (wazuh-ui-plain.log). +- `error: Error` - error caught, message and title to show on UI o store on logs. + +## Software and libraries used + +- [loglevel](https://github.com/pimterry/loglevel) + +## Copyright & License + +Copyright © 2021 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](LICENSE) file. diff --git a/public/react-services/error-orchestrator/error-orchestrator.service.ts b/public/react-services/error-orchestrator/error-orchestrator.service.ts index 50f651201d..3faf31a763 100644 --- a/public/react-services/error-orchestrator/error-orchestrator.service.ts +++ b/public/react-services/error-orchestrator/error-orchestrator.service.ts @@ -16,6 +16,10 @@ import { ErrorOrchestrator, UIErrorLog } from './types'; export class ErrorOrchestratorService { public constructor() {} + /** + * HandlerError static method + * @param {UIErrorLog} uiErrorLog + */ static handleError({ context, level, From 8be467405cdeea97d768606baa1555e2bf5b703b Mon Sep 17 00:00:00 2001 From: gabiwassan Date: Wed, 23 Jun 2021 09:57:17 -0300 Subject: [PATCH 021/493] doc(error-orchestrator): Updated and fixed CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 841ec2c7d5..c67a6fb4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) -- Added fields status and type in vulnerabilities table [#3196] (https://github.com/wazuh/wazuh-kibana-app/pull/3196) +- Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) ### Changed From 6aa05213090d2e724c2884a613c4a2185d949000 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Wed, 23 Jun 2021 18:31:56 +0200 Subject: [PATCH 022/493] Fix changelog --- CHANGELOG.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02f180d7b..8212f5ac89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) -- Added fields status and type in vulnerabilities table [#3196] (https://github.com/wazuh/wazuh-kibana-app/pull/3196) +- Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) ### Changed @@ -23,6 +24,57 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 + +### Added + +- Wazuh help links in the Kibana help menu [#3170](https://github.com/wazuh/wazuh-kibana-app/pull/3170) +- Redirect to group details using the `group` query param in the URL [#3184](https://github.com/wazuh/wazuh-kibana-app/pull/3184) +- Configuration to disable Wazuh App access from X-Pack/ODFE role [#3222](https://github.com/wazuh/wazuh-kibana-app/pull/3222) [#3292](https://github.com/wazuh/wazuh-kibana-app/pull/3292) +- Added confirmation message when closing a form [#3221](https://github.com/wazuh/wazuh-kibana-app/pull/3221) +- Improvement to hide navbar Wazuh label. [#3240](https://github.com/wazuh/wazuh-kibana-app/pull/3240) +- Add modal creating new rule/decoder [#3274](https://github.com/wazuh/wazuh-kibana-app/pull/3274) + +### Changed + +- Removed module titles [#3160](https://github.com/wazuh/wazuh-kibana-app/pull/3160) +- Changed default `wazuh.monitoring.creation` app setting from `d` to `w` [#3174](https://github.com/wazuh/wazuh-kibana-app/pull/3174) +- Changed default `wazuh.monitoring.shards` app setting from `2` to `1` [#3174](https://github.com/wazuh/wazuh-kibana-app/pull/3174) +- Removed Sha1 field from registry key detail [#3189](https://github.com/wazuh/wazuh-kibana-app/pull/3189) +- Removed tooltip in last breadcrumb in header breadcrumb [3250](https://github.com/wazuh/wazuh-kibana-app/pull/3250) +- Refactored the Health check component [#3197](https://github.com/wazuh/wazuh-kibana-app/pull/3197) +- Added version in package downloaded name in agent deploy command [#3210](https://github.com/wazuh/wazuh-kibana-app/issues/3210) +- Removed restriction to allow only current active agents from vulnerability inventory [#3243](https://github.com/wazuh/wazuh-kibana-app/pull/3243) +- Health check actions notifications refactored and added debug mode [#3258](https://github.com/wazuh/wazuh-kibana-app/pull/3258) +- Improved visualizations object configuration readability [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) +- Changed the way kibana-vis hides the visualization while loading, this should prevent errors caused by having a 0 height visualization [#3349](https://github.com/wazuh/wazuh-kibana-app/pull/3349) + +### Fixed + +- Fixed screen flickers in Cluster visualization [#3159](https://github.com/wazuh/wazuh-kibana-app/pull/3159) +- Fixed the broken links when using `server.basePath` Kibana setting [#3161](https://github.com/wazuh/wazuh-kibana-app/pull/3161) +- Fixed filter in reports [#3173](https://github.com/wazuh/wazuh-kibana-app/pull/3173) +- Fixed typo error in Settings/Configuration [#3234](https://github.com/wazuh/wazuh-kibana-app/pull/3234) +- Fixed fields overlap in the agent summary screen [#3217](https://github.com/wazuh/wazuh-kibana-app/pull/3217) +- Fixed Ruleset Test, each request is made in a different session instead of all in the same session [#3257](https://github.com/wazuh/wazuh-kibana-app/pull/3257) +- Fixed the `Visualize` button is not displaying when expanding a field in the Events sidebar [#3237](https://github.com/wazuh/wazuh-kibana-app/pull/3237) +- Fix modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) +- Fix improving and removing WUI error logs [#3260](https://github.com/wazuh/wazuh-kibana-app/pull/3260) +- Fix some errors of PDF reports [#3272](https://github.com/wazuh/wazuh-kibana-app/pull/3272) +- Fix TypeError when selecting macOS agent deployment in a Safari Browser [#3289](https://github.com/wazuh/wazuh-kibana-app/pull/3289) +- Fix error in how the SCA check's checks are displayed [#](https://github.com/wazuh/wazuh-kibana-app/pull/3297) +- Fixed message of error when add sample data fails [#3241](https://github.com/wazuh/wazuh-kibana-app/pull/3241) +- Fixed modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) +- Fixed dark mode visualization background in pdf reports [#3315](https://github.com/wazuh/wazuh-kibana-app/pull/3315) +- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) +- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) +- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) +- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) +- Fixed error that caused the labels in certain visualizations to overlap [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) +- Fixed export to csv button in dashboards tables [#3358](https://github.com/wazuh/wazuh-kibana-app/pull/3358) +- Fixed Elastic UI breaking changes in 7.12 [#3345](https://github.com/wazuh/wazuh-kibana-app/pull/3345) +- Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) + ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 ### Added @@ -35,8 +87,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Added vulnerabilities inventory that affect to an agent [#3069](https://github.com/wazuh/wazuh-kibana-app/pull/3069) - Added retry button to check api again in health check [#3109](https://github.com/wazuh/wazuh-kibana-app/pull/3109) - Added `wazuh-statistics` template and a new mapping for these indices [#3111](https://github.com/wazuh/wazuh-kibana-app/pull/3111) -- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126 -- Added lowercase levels in storeError [#3377](https://github.com/wazuh/wazuh-kibana-app/pull/3377) +- Added link to documentation "Checking connection with Manager" in deploy new agent [#3126](https://github.com/wazuh/wazuh-kibana-app/pull/3126) +- Fixed Agent Evolution graph showing agents from multiple APIs [#3256](https://github.com/wazuh/wazuh-kibana-app/pull/3256) +- Added Disabled index pattern checks in Health Check [#3311](https://github.com/wazuh/wazuh-kibana-app/pull/3311) ### Changed @@ -62,6 +115,25 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed check for TCP protocol in deploy new agent [#3163](https://github.com/wazuh/wazuh-kibana-app/pull/3163) - Fixed RBAC issue with agent group permissions [#3181](https://github.com/wazuh/wazuh-kibana-app/pull/3181) - Fixed change index pattern from menu doesn't work [#3187](https://github.com/wazuh/wazuh-kibana-app/pull/3187) +- Fixed Alerts Summary of modules for reports [#3303](https://github.com/wazuh/wazuh-kibana-app/pull/3303) + +## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2, 7.11.2 - Revision 4108 + +### Fixed + +- Unable to change selected index pattern from the Wazuh menu [#3330](https://github.com/wazuh/wazuh-kibana-app/pull/3330) + +## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2, 7.11.2 - Revision 4107 + +### Added + +- Support for Kibana 7.11.2 +- Added a warning message for the `Install and enroll the agent` step of `Deploy new agent` guide [#3238](https://github.com/wazuh/wazuh-kibana-app/pull/3238) + +### Fixed + +- Conflict with the creation of the index pattern when performing the Health Check [#3223](https://github.com/wazuh/wazuh-kibana-app/pull/3223) +- Fixing mac os agents add command [#3207](https://github.com/wazuh/wazuh-kibana-app/pull/3207) ## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2 - Revision 4106 @@ -2024,4 +2096,4 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Search bar across panels now support parenthesis grouping -- Several CSS fixes for IE browser +- Several CSS fixes for IE browser \ No newline at end of file From 21e91531a62d77953d29a07e839c6da723c9bd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= Date: Wed, 23 Jun 2021 21:28:59 +0200 Subject: [PATCH 023/493] Agregate errorBoundary (#3389) --- .../health-check/container/health-check.container.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/components/health-check/container/health-check.container.tsx b/public/components/health-check/container/health-check.container.tsx index 442e0f0ac7..36c6a5f317 100644 --- a/public/components/health-check/container/health-check.container.tsx +++ b/public/components/health-check/container/health-check.container.tsx @@ -33,7 +33,7 @@ import { checkSetupService, } from '../services'; import { CheckResult } from '../components/check-result'; -import { withReduxProvider } from '../../common/hocs'; +import { withErrorBoundary, withReduxProvider } from '../../common/hocs'; import { getHttp } from '../../../kibana-services'; import { HEALTH_CHECK_REDIRECTION_TIME, @@ -49,6 +49,7 @@ import { import { getDataPlugin } from '../../../kibana-services'; import { CheckLogger } from '../types/check_logger'; +import { compose } from 'redux'; const checks = { api: { @@ -251,7 +252,7 @@ function HealthCheckComponent() { ); } -export const HealthCheck = withReduxProvider(HealthCheckComponent); +export const HealthCheck = compose (withErrorBoundary,withReduxProvider) (HealthCheckComponent); export const HealthCheckTest = HealthCheckComponent; From 708099184ebd362c713d16ea478f8c3425c64ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 24 Jun 2021 11:01:46 +0200 Subject: [PATCH 024/493] feat(module_github): Add sample data for the GitHub module - Add logic to generate alerts for the module - Add GitHub to the Secutiry events description in Settings > Sample data - Enable GitHub sample data for the Security events category --- common/constants.ts | 1 + .../add-modules-data/sample-data.tsx | 2 +- .../generate-alerts/generate-alerts-script.js | 24 +++++ .../lib/generate-alerts/sample-data/github.js | 97 +++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 server/lib/generate-alerts/sample-data/github.js diff --git a/common/constants.ts b/common/constants.ts index 6bcfb2121f..c7d4f8fd9e 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -70,6 +70,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }, + { github: true} ], [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ { rootcheck: true }, diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index f4457d7be0..9625104662 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -47,7 +47,7 @@ export default class WzSampleData extends Component { this.categories = [ { title: 'Sample security information', - description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, authorization, ssh, web).', + description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, GitHub, authorization, ssh, web).', image: '', categorySampleAlertsIndex: 'security' }, diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index 61ed911ec1..2fed378a4c 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -40,6 +40,7 @@ import * as Vulnerability from './sample-data/vulnerabilities'; import * as SSH from './sample-data/ssh'; import * as Apache from './sample-data/apache'; import * as Web from './sample-data/web'; +import * as GitHub from './sample-data/github'; //Alert const alertIDMax = 6000; @@ -911,6 +912,29 @@ function generateAlert(params) { alert.previous_output = previousOutput.join('\n'); } } + + if (params.github){ + alert.location = GitHub.LOCATION; + alert.decoder = GitHub.DECODER; + const alertType = randomArrayItem(GitHub.ALERT_TYPES); + const actor = randomArrayItem(GitHub.ACTORS); + alert.data = { + github : { ...alertType.data.github } + }; + alert.data.github.org = randomArrayItem(GitHub.ORGANIZATION_NAMES); + alert.data.github.repo && (alert.data.github.repo = `${alert.data.github.org}/${randomArrayItem(GitHub.REPOSITORY_NAMES)}`); + alert.data.github.repository && (alert.data.github.repository = `${alert.data.github.org}/${randomArrayItem(GitHub.REPOSITORY_NAMES)}`); + alert.data.github.actor = actor.name; + alert.data.github.actor_location && alert.data.github.actor_location.country_code && (alert.data.github.actor_location.country_code = actor.country_code); + alert.data.github.user && (alert.data.github.user = randomArrayItem(GitHub.USER_NAMES)); + alert.data.github.config && alert.data.github.config.url && (alert.data.github.config.url = randomArrayItem(GitHub.SERVER_ADDRESS_WEBHOOK)); + alert.data.github['@timestamp'] = alert.timestamp; + alert.data.github.created_at && (alert.data.github.created_at = alert.timestamp); + alert.rule = { + ...alertType.rule + }; + } + return alert; } diff --git a/server/lib/generate-alerts/sample-data/github.js b/server/lib/generate-alerts/sample-data/github.js new file mode 100644 index 0000000000..70e4ba791c --- /dev/null +++ b/server/lib/generate-alerts/sample-data/github.js @@ -0,0 +1,97 @@ +/* + * Wazuh app - GitHub sample data + * Copyright (C) 2015-2021 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 LOCATION = 'github'; + +export const DECODER = { "name": "json" }; + +export const COUNTRY_CODES = [ + 'AR', + 'CA', + 'DE', + 'ES', + 'FR', + 'GR', + 'IN', + 'MX', + 'SE', + 'US' +]; + +const baseElements = Array(10).fill(); + +export const ORGANIZATION_NAMES = baseElements.map((_, index) => `Organization${index + 1}`); + +export const USER_NAMES = baseElements.map((_, index) => `User${index + 1}`); + +export const REPOSITORY_NAMES = baseElements.map((_, index) => `Repo${index + 1}`); + +export const ACTORS = baseElements.map((_, index) => ({ name: USER_NAMES[index], country_code: COUNTRY_CODES[index] })); + +export const SERVER_ADDRESS_WEBHOOK = [ + 'https://server/webhook', + 'https://cool_server/integrations/webhook', + 'https://another_server/github_notifications', + 'https://my_web/notifications/webhook', +]; + +export const ALERT_TYPES = [ + { "rule": { "level": 5, "description": "GitHub Organization audit log export.", "id": "91193", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624444988681.000000", "org": "_ORGANIZATION_", "created_at": "1624444988681.000000", "action": "org.audit_log_export", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "ElEQJvOCnhWZ2mVpjzYOMw" } } }, + { "rule": { "level": 5, "description": "GitHub Team create.", "id": "91397", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624445678369.000000", "org": "_ORGANIZATION_", "created_at": "1624445678369.000000", "action": "team.create", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "_document_id": "cC4uIXPNDz1O1G21Vjs8Vw" } } }, + { "rule": { "level": 5, "description": "GitHub Team add member.", "id": "91393", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624445678470.000000", "org": "_ORGANIZATION_", "created_at": "1624445678470.000000", "action": "team.add_member", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "user": "_USER_", "_document_id": "0Z4NBBhHM2T4gEuWziZfvQ" } } }, + { "rule": { "level": 5, "description": "GitHub Team add member.", "id": "91393", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624445927571.000000", "org": "_ORGANIZATION_", "created_at": "1624445927571.000000", "action": "team.add_member", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "user": "_USER_", "_document_id": "Hi6dpYdi9G5PrEqqTkEYnA" } } }, + { "rule": { "level": 5, "description": "GitHub Repo create.", "id": "91318", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_repo"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624445965569.000000", "visibility": "private", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "created_at": "1624445965569.000000", "action": "repo.create", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "fXwGe7IW-BX8Ze64V_AORg" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624445969188.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624446009635.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 5, "description": "GitHub Organization audit log export.", "id": "91193", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446236415.000000", "org": "_ORGANIZATION_", "created_at": "1624446236415.000000", "action": "org.audit_log_git_event_export", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "vkV52PbNTZPJRRNLuOZcuw" } } }, + { "rule": { "level": 5, "description": "GitHub Organization audit log export.", "id": "91193", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446254661.000000", "org": "_ORGANIZATION_", "created_at": "1624446254661.000000", "action": "org.audit_log_export", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "gwkccTbAcX2WujhEXS3r0Q" } } }, + { "rule": { "level": 5, "description": "GitHub Team create.", "id": "91397", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446278480.000000", "org": "_ORGANIZATION_", "created_at": "1624446278480.000000", "action": "team.create", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "_document_id": "Qf6RhFYhb7ysdV8K8ukYFw" } } }, + { "rule": { "level": 5, "description": "GitHub Team add member.", "id": "91393", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446278606.000000", "org": "_ORGANIZATION_", "created_at": "1624446278606.000000", "action": "team.add_member", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "user": "_USER_", "_document_id": "T6DZ-t0-a9yQShoBbUxc_g" } } }, + { "rule": { "level": 7, "description": "GitHub Team destroy.", "id": "91399", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446293390.000000", "org": "_ORGANIZATION_", "created_at": "1624446293390.000000", "action": "team.destroy", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/_REPOSITORY_", "_document_id": "ZLC0q4Ka_R4gGw3gWgxc3w" } } }, + { "rule": { "level": 7, "description": "GitHub Team remove member.", "id": "91401", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446387691.000000", "org": "_ORGANIZATION_", "created_at": "1624446387691.000000", "action": "team.remove_member", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/backend", "user": "_USER_", "_document_id": "PYn3TOghg5FYze673svhgw" } } }, + { "rule": { "level": 5, "description": "GitHub Team add member.", "id": "91393", "firedtimes": 3, "mail": false, "groups": ["github", "git", "git_team"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446397464.000000", "org": "_ORGANIZATION_", "created_at": "1624446397464.000000", "action": "team.add_member", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "team": "_ORGANIZATION_/backend", "user": "_USER_", "_document_id": "z4qIP_kjzjnilIhL8ak0mg" } } }, + { "rule": { "level": 3, "description": "GitHub Dependency graph new repos enable.", "id": "91131", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_dependency_graph_new_repos"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446915154.000000", "org": "_ORGANIZATION_", "created_at": "1624446915154.000000", "action": "dependency_graph_new_repos.enable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "2Az9XCqb-Fe8k0KkLQlk0A" } } }, + { "rule": { "level": 12, "description": "GitHub Dependency graph new repos disable.", "id": "91130", "firedtimes": 1, "mail": true, "groups": ["github", "git", "git_dependency_graph_new_repos"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446916718.000000", "org": "_ORGANIZATION_", "created_at": "1624446916718.000000", "action": "dependency_graph_new_repos.disable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "TzBGANy3SmrnxI8GW9bpQA" } } }, + { "rule": { "level": 5, "description": "GitHub Hook create.", "id": "91162", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_hook"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624446982688.000000", "org": "_ORGANIZATION_", "hook_id": "303999727", "name": "webhook", "created_at": "1624446982688.000000", "action": "hook.create", "active": "true", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "config": { "content_type": "json", "insecure_ssl": "0", "secret": "********", "url": "_SERVER_ADDRESS_WEBHOOK_" }, "events": ["push"], "_document_id": "SSlObiXNNtzQzxFooK4-fw" } } }, + { "rule": { "level": 5, "description": "GitHub Hook events changed.", "id": "91165", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_hook"] }, "data": { "integration": "github", "github": { "org": "_ORGANIZATION_", "created_at": "1624447042505.000000", "active": "true", "actor": "_USER_", "@timestamp": "1624447042505.000000", "hook_id": "303999727", "name": "webhook", "action": "hook.events_changed", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "config": { "content_type": "json", "insecure_ssl": "0", "secret": "********", "url": "_SERVER_ADDRESS_WEBHOOK_" }, "events": ["push", "create", "deployment", "fork", "issues"], "_document_id": "Ba9NJbFnSfJB1zGEn29asw", "events_were": ["push"] } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447139607.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 3, "description": "GitHub Git push.", "id": "91160", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447520462.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.push", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 3, "description": "GitHub Git push.", "id": "91160", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447522682.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.push", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624447527007.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 5, "description": "GitHub Repo create.", "id": "91318", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_repo"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447568303.000000", "visibility": "private", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "created_at": "1624447568303.000000", "action": "repo.create", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "AcrdSmMW0PpEEmuGWiTcoQ" } } }, + { "rule": { "level": 9, "description": "GitHub Repo destroy.", "id": "91320", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_repo"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447588615.000000", "visibility": "private", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "created_at": "1624447588615.000000", "action": "repo.destroy", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "H-bRCuWh_FAoZxzW8BV9JA" } } }, + { "rule": { "level": 3, "description": "GitHub Git fetch.", "id": "91159", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624447744877.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.fetch", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "false" } } }, + { "rule": { "level": 7, "description": "GitHub Organization update default repository permission.", "id": "91231", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448015027.000000", "org": "_ORGANIZATION_", "created_at": "1624448015027.000000", "action": "org.update_default_repository_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "BHpvG7xc2bTNW3ME3nAgDw" } } }, + { "rule": { "level": 7, "description": "GitHub Organization update default repository permission.", "id": "91231", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448020670.000000", "org": "_ORGANIZATION_", "created_at": "1624448020670.000000", "action": "org.update_default_repository_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "t5ZumMJeWBs2CqZT-n4JNA" } } }, + { "rule": { "level": 7, "description": "GitHub Organization update member repository creation permission.", "id": "91233", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448034735.000000", "visibility": "private_internal", "org": "_ORGANIZATION_", "created_at": "1624448034735.000000", "action": "org.update_member_repository_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "CAwbh8KpE75aa0ajCpRISw" } } }, + { "rule": { "level": 7, "description": "GitHub Organization update member repository creation permission.", "id": "91233", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448038247.000000", "visibility": "internal", "org": "_ORGANIZATION_", "created_at": "1624448038247.000000", "action": "org.update_member_repository_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "s96ibVD5sEyRDxYgQ8gKhQ" } } }, + { "rule": { "level": 9, "description": "GitHub Private repository forking enable.", "id": "91273", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_private_repository_forking"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448046546.000000", "org": "_ORGANIZATION_", "created_at": "1624448046546.000000", "action": "private_repository_forking.enable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "NZWBrO2Ac02LnG3TFeEykA" } } }, + { "rule": { "level": 5, "description": "GitHub Private repository forking disable.", "id": "91274", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_private_repository_forking"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448051193.000000", "org": "_ORGANIZATION_", "created_at": "1624448051193.000000", "action": "private_repository_forking.disable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "5EkgWPa8Du6ZJ_5oOfU_rg" } } }, + { "rule": { "level": 3, "description": "GitHub Generic rule.", "id": "91449", "firedtimes": 1, "mail": false, "groups": ["github", "git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448069427.000000", "org": "_ORGANIZATION_", "created_at": "1624448069427.000000", "action": "members_can_create_private_pages.disable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "0rtyFg2GD2-oJyJsOtRZ_A" } } }, + { "rule": { "level": 3, "description": "GitHub Generic rule.", "id": "91449", "firedtimes": 2, "mail": false, "groups": ["github", "git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448073290.000000", "org": "_ORGANIZATION_", "created_at": "1624448073290.000000", "action": "members_can_create_private_pages.enable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "sSbImF40N-hLe0mfDHkfMg" } } }, + { "rule": { "level": 3, "description": "GitHub Generic rule.", "id": "91449", "firedtimes": 3, "mail": false, "groups": ["github", "git"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448089991.000000", "org": "_ORGANIZATION_", "created_at": "1624448089991.000000", "action": "repository_visibility_change.enable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "dWJ-7ZR6DdumQeu01PAGig" } } }, + { "rule": { "level": 3, "description": "GitHub Issues.", "id": "91169", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_issues"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448109958.000000", "org": "_ORGANIZATION_", "created_at": "1624448109958.000000", "action": "issues.deletes_enabled", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "gWT0UNMVFaI8ZPB3tGGsew" } } }, + { "rule": { "level": 3, "description": "GitHub Issues.", "id": "91169", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_issues"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448114493.000000", "org": "_ORGANIZATION_", "created_at": "1624448114493.000000", "action": "issues.deletes_disabled", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "T2hgq3r3yVD23Np6CAD-zQ" } } }, + { "rule": { "level": 5, "description": "GitHub Organization display commenter full name enabled.", "id": "91202", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448121171.000000", "org": "_ORGANIZATION_", "created_at": "1624448121171.000000", "action": "org.display_commenter_full_name_enabled", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "o-Edi8owvz1iPv78RPPSJw" } } }, + { "rule": { "level": 3, "description": "GitHub Organization.", "id": "91188", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448125116.000000", "org": "_ORGANIZATION_", "created_at": "1624448125116.000000", "action": "org.display_commenter_full_name_disabled", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "OxJjqpug2FM8RJuzE1CZpA" } } }, + { "rule": { "level": 3, "description": "GitHub Organization.", "id": "91188", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448133245.000000", "org": "_ORGANIZATION_", "created_at": "1624448133245.000000", "action": "org.enable_reader_discussion_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "5KmA_VkQPn3I6gY4L8qFPA" } } }, + { "rule": { "level": 3, "description": "GitHub Organization.", "id": "91188", "firedtimes": 3, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448138392.000000", "org": "_ORGANIZATION_", "created_at": "1624448138392.000000", "action": "org.disable_reader_discussion_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "JQ3JAd3zHmpRpGZYJsJIQw" } } }, + { "rule": { "level": 5, "description": "GitHub Organization enable member team creation permission.", "id": "91203", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448148271.000000", "org": "_ORGANIZATION_", "created_at": "1624448148271.000000", "action": "org.enable_member_team_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "sd2fnKW-Jc_OZI9xm2pyyQ" } } }, + { "rule": { "level": 9, "description": "GitHub Organization disable member team creation permission.", "id": "91198", "firedtimes": 1, "mail": false, "groups": ["github", "git", "git_org"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448154972.000000", "org": "_ORGANIZATION_", "created_at": "1624448154972.000000", "action": "org.disable_member_team_creation_permission", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "ppjVxGQBAQts82at9Az3XQ" } } }, + { "rule": { "level": 12, "description": "GitHub Repository vulnerability alerts disable.", "id": "91367", "firedtimes": 1, "mail": true, "groups": ["github", "git", "git_repository_vulnerability_alerts"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448419210.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "created_at": "1624448419210.000000", "action": "repository_vulnerability_alerts.disable", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "user": "_USER_", "_document_id": "wgf0uCen5LG4su6jQ2xKDA" } } }, + { "rule": { "level": 5, "description": "GitHub Repo create.", "id": "91318", "firedtimes": 2, "mail": false, "groups": ["github", "git", "git_repo"] }, "data": { "integration": "github", "github": { "actor": "_USER_", "@timestamp": "1624448419470.000000", "visibility": "public", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "created_at": "1624448419470.000000", "action": "repo.create", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "_document_id": "oLAjZ_DbHvzZlPmRCXr4MA" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 3, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624448422207.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "true" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 4, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624448423987.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "true" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 5, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624448432101.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "true" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 6, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624448487893.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "true" } } }, + { "rule": { "level": 3, "description": "GitHub Git clone.", "id": "91158", "firedtimes": 7, "mail": false, "groups": ["github", "git", "git_git"] }, "data": { "integration": "github", "github": { "@timestamp": "1624448736294.000000", "org": "_ORGANIZATION_", "repo": "_ORGANIZATION_/_REPOSITORY_", "actor_location": { "country_code": "_COUNTRY_CODE_" }, "action": "git.clone", "transport_protocol_name": "http", "transport_protocol": "1", "repository": "_ORGANIZATION_/_REPOSITORY_", "repository_public": "true" } } }, +]; From bcd3f8fea16a49fdbce12f42b2073572a4b7c0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 24 Jun 2021 16:59:17 +0200 Subject: [PATCH 025/493] feat(module_github): Add access and visibility of GitHub module to the UI - Add GitHub module to the main menu under Modules > Security information management - Add GitHub module to the agent menu under Modules > Security information menu - Add Github module to configure visibility in Settings > Modules - Add Github module to the Modules directory - Add basic structure of GitHub module with tabs Audit logs, Dashboard and Events --- common/constants.ts | 11 ++++++----- common/wazu-menu/wz-menu-overview.cy.ts | 1 + common/wazuh-modules.ts | 5 +++++ public/components/common/modules/modules-defaults.js | 5 +++++ .../common/welcome/components/menu-agent.js | 6 ++++-- public/components/common/welcome/overview-welcome.js | 2 ++ public/components/settings/modules/modules.js | 3 ++- public/components/wz-menu/wz-menu-agent.js | 5 +++-- public/components/wz-menu/wz-menu-overview.js | 10 ++++++++-- public/services/common-data.js | 4 ++-- 10 files changed, 38 insertions(+), 14 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index c7d4f8fd9e..e8d03e7436 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -256,7 +256,8 @@ export enum WAZUH_MODULES_ID { CIS_CAT = 'ciscat', VIRUSTOTAL = 'virustotal', GDPR = 'gdpr', -} + GITHUB = 'github' +}; export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { MANAGEMENT = 'management', @@ -273,19 +274,19 @@ export enum WAZUH_MENU_MANAGEMENT_SECTIONS_ID { LOGS = 'logs', REPORTING = 'reporting', STATISTICS = 'statistics', -} +}; export enum WAZUH_MENU_TOOLS_SECTIONS_ID { API_CONSOLE = 'devTools', RULESET_TEST = 'logtest', -} +}; export enum WAZUH_MENU_SECURITY_SECTIONS_ID { USERS = 'users', ROLES = 'roles', POLICIES = 'policies', ROLES_MAPPING = 'roleMapping', -} +}; export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { SETTINGS = 'settings', @@ -296,7 +297,7 @@ export enum WAZUH_MENU_SETTINGS_SECTIONS_ID { LOGS = 'logs', MISCELLANEOUS = 'miscellaneous', ABOUT = 'about', -} +}; export const AUTHORIZED_AGENTS = 'authorized-agents'; diff --git a/common/wazu-menu/wz-menu-overview.cy.ts b/common/wazu-menu/wz-menu-overview.cy.ts index 32622ac474..41a4501fe0 100644 --- a/common/wazu-menu/wz-menu-overview.cy.ts +++ b/common/wazu-menu/wz-menu-overview.cy.ts @@ -30,4 +30,5 @@ export enum WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID { CIS_CAT = 'menuModulesCiscatLink', VIRUSTOTAL = 'menuModulesVirustotalLink', GDPR = 'menuModulesGdprLink', + GITHUB = 'menuModulesGitHubLink' } diff --git a/common/wazuh-modules.ts b/common/wazuh-modules.ts index 69b372a604..9913464f3a 100644 --- a/common/wazuh-modules.ts +++ b/common/wazuh-modules.ts @@ -118,6 +118,11 @@ export const WAZUH_MODULES = { description: 'Monitor and collect the activity from Docker containers such as creation, running, starting, stopping or pausing events.' }, + github: { + title: 'GitHub', + description: + 'Monitoring events from audit logs that helps detect threats targeting your GitHub organizations.' + }, devTools: { title: 'API console', description: 'Test the Wazuh API endpoints.' diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 0a7232f2c2..5b489d5807 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -70,6 +70,11 @@ export const ModulesDefaults = { tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], buttons: ['reporting'] }, + github: { + init: 'dashboard', + tabs: [{ id: 'inventory', name: 'Audit logs' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], + buttons: ['reporting'] + }, syscollector: { notModule: true }, diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index 76868ed98f..c12e42b469 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -67,14 +67,16 @@ class WzMenuAgent extends Component { gdpr: { id: WAZUH_MODULES_ID.GDPR, text: 'GDPR', isPin: this.menuAgent.gdpr ? this.menuAgent.gdpr : false }, hipaa: { id: WAZUH_MODULES_ID.HIPAA, text: 'HIPAA', isPin: this.menuAgent.hipaa ? this.menuAgent.hipaa : false }, nist: { id: WAZUH_MODULES_ID.NIST_800_53, text: 'NIST 800-53', isPin: this.menuAgent.nist ? this.menuAgent.nist : false }, - tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC', isPin: this.menuAgent.tsc ? this.menuAgent.tsc : false } + tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC', isPin: this.menuAgent.tsc ? this.menuAgent.tsc : false }, + github: { id: WAZUH_MODULES_ID.GITHUB, text: 'GitHub', isPin: this.menuAgent.github ? this.menuAgent.github : false }, }; this.securityInformationItems = [ this.agentSections.general, this.agentSections.fim, this.agentSections.aws, - this.agentSections.gcp + this.agentSections.gcp, + this.agentSections.github ]; this.auditingItems = [ this.agentSections.pm, diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 0f94fd6c4c..3d3df6ff10 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -98,6 +98,8 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends this.buildTabCard('aws', 'logoAWSMono')} {this.props.extensions.gcp && this.buildTabCard('gcp', 'logoGCPMono')} + {this.props.extensions.github && + this.buildTabCard('github', 'logoGithub')} diff --git a/public/components/settings/modules/modules.js b/public/components/settings/modules/modules.js index c0c07613c1..1f4bf720e4 100644 --- a/public/components/settings/modules/modules.js +++ b/public/components/settings/modules/modules.js @@ -33,7 +33,8 @@ export class EnableModulesWrapper extends Component { { name: 'general', default: true, agent: false }, { name: 'fim', default: true, agent: false }, { name: 'aws', default: false, agent: false }, - { name: 'gcp', default: false, agent: false } + { name: 'gcp', default: false, agent: false }, + { name: 'github', default: false, agent: false } ] }, { diff --git a/public/components/wz-menu/wz-menu-agent.js b/public/components/wz-menu/wz-menu-agent.js index b318767d79..8e11154d62 100644 --- a/public/components/wz-menu/wz-menu-agent.js +++ b/public/components/wz-menu/wz-menu-agent.js @@ -62,13 +62,14 @@ class WzMenuAgent extends Component { gdpr: { id: WAZUH_MODULES_ID.GDPR, text: 'GDPR' }, hipaa: { id: WAZUH_MODULES_ID.HIPAA, text: 'HIPAA' }, nist: { id: WAZUH_MODULES_ID.NIST_800_53, text: 'NIST 800-53' }, - tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC' } + tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC' }, + github: {id: WAZUH_MODULES_ID.GITHUB, text: 'GitHub'} }; this.securityInformationItems = [ this.agentSections.general, this.agentSections.fim, - this.agentSections.gcp + this.agentSections.gcp, ]; this.auditingItems = [ this.agentSections.pm, diff --git a/public/components/wz-menu/wz-menu-overview.js b/public/components/wz-menu/wz-menu-overview.js index fd5c046ec6..b5626abc1e 100644 --- a/public/components/wz-menu/wz-menu-overview.js +++ b/public/components/wz-menu/wz-menu-overview.js @@ -13,7 +13,7 @@ import React, { Component } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFlexGrid, EuiButtonEmpty, EuiSideNav, EuiIcon } from '@elastic/eui'; import { connect } from 'react-redux'; import store from '../../redux/store'; -import { updateCurrentTab, updateCurrentAgentData } from '../../redux/actions/appStateActions'; +import { updateCurrentAgentData } from '../../redux/actions/appStateActions'; import { AppState } from '../../react-services/app-state'; import { AppNavigate } from '../../react-services/app-navigate'; import { getAngularModule } from '../../kibana-services'; @@ -138,13 +138,19 @@ class WzMenuOverview extends Component { cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.TSC, text: 'TSC', }, + github: { + id: WAZUH_MODULES_ID.GITHUB, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.GITHUB, + text: 'GitHub', + }, }; this.securityInformationItems = [ this.overviewSections.general, this.overviewSections.fim, this.overviewSections.aws, - this.overviewSections.gcp + this.overviewSections.gcp, + this.overviewSections.github ]; this.auditingItems = [ this.overviewSections.pm, diff --git a/public/services/common-data.js b/public/services/common-data.js index f7e3f31523..ec482d159c 100644 --- a/public/services/common-data.js +++ b/public/services/common-data.js @@ -146,7 +146,8 @@ export class CommonData { virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, - docker: { group: 'docker' } + docker: { group: 'docker' }, + github: { group: 'github' } }; const filters = []; @@ -159,7 +160,6 @@ export class CommonData { isCluster ) ); - if (tab !== 'general' && tab !== 'welcome') { if (tab === 'pci') { this.removeDuplicateExists('rule.pci_dss'); From 769da1c8c7eff338ef5de113089d6f0b732e47e8 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Mon, 28 Jun 2021 12:09:30 +0200 Subject: [PATCH 026/493] Imolement try catch controller/agent --- public/controllers/agent/agents-preview.js | 129 +- public/controllers/agent/agents.js | 544 ++--- .../agent/components/agents-preview.js | 605 +++--- .../agent/components/agents-table.js | 1820 +++++++++-------- .../agent/components/checkUpgrade.tsx | 140 +- .../agent/components/register-agent.js | 1172 ++++++----- 6 files changed, 2316 insertions(+), 2094 deletions(-) diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index 7983a31c50..415ac5f011 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -21,8 +21,12 @@ import { ShareAgent } from '../../factories/share-agent'; import { formatUIDate } from '../../react-services/time-service'; import { ErrorHandler } from '../../react-services/error-handler'; import { getDataPlugin, getToasts } from '../../kibana-services'; -import { connect } from 'react-redux'; import store from '../../redux/store'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services'; + +const errorContext = 'AgentsPreviewController'; export class AgentsPreviewController { /** @@ -32,15 +36,7 @@ export class AgentsPreviewController { * @param {Object} errorHandler * @param {Object} csvReq */ - constructor( - $scope, - $location, - $route, - errorHandler, - csvReq, - commonData, - $window - ) { + constructor($scope, $location, $route, errorHandler, csvReq, commonData, $window) { this.$scope = $scope; this.genericReq = GenericRequest; this.$location = $location; @@ -62,14 +58,11 @@ export class AgentsPreviewController { this.api = JSON.parse(AppState.getCurrentAPI()).id; const loc = this.$location.search(); if ((loc || {}).agent && (loc || {}).agent !== '000') { - this.commonData.setTimefilter( getDataPlugin().timefilter.timefilter.getTime()); + this.commonData.setTimefilter(getDataPlugin().timefilter.timefilter.getTime()); return this.showAgent({ id: loc.agent }); } - this.isClusterEnabled = - AppState.getClusterInfo() && - AppState.getClusterInfo().status === 'enabled'; - + this.isClusterEnabled = AppState.getClusterInfo() && AppState.getClusterInfo().status === 'enabled'; this.loading = true; this.osPlatforms = []; this.versions = []; @@ -77,7 +70,7 @@ export class AgentsPreviewController { this.nodes = []; this.mostActiveAgent = { name: '', - id: '' + id: '', }; this.prevSearch = false; @@ -95,16 +88,14 @@ export class AgentsPreviewController { } else { this.hasAgents = true; } - // Watcher for URL params this.$scope.$watch('submenuNavItem', () => { this.$location.search('tab', this.submenuNavItem); }); - this.$scope.$on('wazuhFetched', evt => { + this.$scope.$on('wazuhFetched', (evt) => { evt.stopPropagation(); }); - this.registerAgentsProps = { addNewAgent: flag => this.addNewAgent(flag), hasAgents: this.hasAgents, @@ -126,7 +117,7 @@ export class AgentsPreviewController { this.downloadCsv(filters); this.$scope.$applyAsync(); }, - showAgent: agent => { + showAgent: (agent) => { this.showAgent(agent); this.$scope.$applyAsync(); }, @@ -134,18 +125,11 @@ export class AgentsPreviewController { return await this.getMostActive(); }, clickAction: (item, openAction = false) => { - clickAction( - item, - openAction, - instance, - this.shareAgent, - this.$location, - this.$scope - ); + clickAction(item, openAction, instance, this.shareAgent, this.$location, this.$scope); this.$scope.$applyAsync(); }, - formatUIDate: date => formatUIDate(date), - summary: this.summary + formatUIDate: (date) => formatUIDate(date), + summary: this.summary, }; //Load this.load(); @@ -175,10 +159,7 @@ export class AgentsPreviewController { */ async downloadCsv(filters) { try { - ErrorHandler.info( - 'Your download should begin automatically...', - 'CSV' - ); + ErrorHandler.info('Your download should begin automatically...', 'CSV'); const output = await this.csvReq.fetch('/agents', this.api, filters); const blob = new Blob([output], { type: 'text/csv' }); // eslint-disable-line @@ -186,7 +167,17 @@ export class AgentsPreviewController { return; } catch (error) { - ErrorHandler.handle(error, 'Download CSV'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error exporting CSV: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } return; } @@ -195,12 +186,16 @@ export class AgentsPreviewController { try { const data = await this.genericReq.request( 'GET', - `/elastic/top/${this.firstUrlParam}/${this.secondUrlParam}/agent.name/${this.pattern}?agentsList=${store.getState().appStateReducers.allowedAgents.toString()}` + `/elastic/top/${this.firstUrlParam}/${this.secondUrlParam}/agent.name/${ + this.pattern + }?agentsList=${store.getState().appStateReducers.allowedAgents.toString()}` ); this.mostActiveAgent.name = data.data.data; const info = await this.genericReq.request( 'GET', - `/elastic/top/${this.firstUrlParam}/${this.secondUrlParam}/agent.id/${this.pattern}?agentsList=${store.getState().appStateReducers.allowedAgents.toString()}` + `/elastic/top/${this.firstUrlParam}/${this.secondUrlParam}/agent.id/${ + this.pattern + }?agentsList=${store.getState().appStateReducers.allowedAgents.toString()}` ); if (info.data.data === '' && this.mostActiveAgent.name !== '') { this.mostActiveAgent.id = '000'; @@ -208,8 +203,21 @@ export class AgentsPreviewController { this.mostActiveAgent.id = info.data.data; } return this.mostActiveAgent; - } catch (error) { - getToasts().addDanger({title: 'An error occurred while trying to get the most active agent', text: error.message || error }); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `An error occurred while trying to get the most active agent: ${ + error.message || error + }`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -219,13 +227,23 @@ export class AgentsPreviewController { async load() { try { this.errorInit = false; - const clusterInfo = AppState.getClusterInfo(); - this.firstUrlParam = - clusterInfo.status === 'enabled' ? 'cluster' : 'manager'; + this.firstUrlParam = clusterInfo.status === 'enabled' ? 'cluster' : 'manager'; this.secondUrlParam = clusterInfo[this.firstUrlParam]; this.pattern = (await getDataPlugin().indexPatterns.get(AppState.getCurrentPattern())).title; } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); this.errorInit = ErrorHandler.handle(error, '', { silent: true }); } this.loading = false; @@ -250,14 +268,24 @@ export class AgentsPreviewController { try { const result = await this.genericReq.request('GET', '/hosts/apis'); const entries = result.data || []; - const host = entries.filter(e => { + const host = entries.filter((e) => { return e.id == this.api; }); const url = host[0].url; const numToClean = url.startsWith('https://') ? 8 : 7; return url.substr(numToClean); } catch (error) { - return false; + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `Could not get the Wazuh API address: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -268,8 +296,19 @@ export class AgentsPreviewController { try { const data = await WzRequest.apiReq('GET', '//', {}); const result = ((data || {}).data || {}).data || {}; - return result.api_version + return result.api_version; } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Could not get the Wazuh version: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); return version; } } diff --git a/public/controllers/agent/agents.js b/public/controllers/agent/agents.js index 18de8d6b00..c44e32e3df 100644 --- a/public/controllers/agent/agents.js +++ b/public/controllers/agent/agents.js @@ -20,7 +20,7 @@ import { AppState } from '../../react-services/app-state'; import { WazuhConfig } from '../../react-services/wazuh-config'; import { GenericRequest } from '../../react-services/generic-request'; import { WzRequest } from '../../react-services/wz-request'; -import { getToasts } from '../../kibana-services'; +import { getToasts } from '../../kibana-services'; import { ShareAgent } from '../../factories/share-agent'; import { TabVisualizations } from '../../factories/tab-visualizations'; import { formatUIDate } from '../../react-services/time-service'; @@ -31,6 +31,11 @@ import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActi import { WAZUH_ALERTS_PATTERN } from '../../../common/constants'; import { getDataPlugin } from '../../kibana-services'; import { hasAgentSupportModule } from '../../react-services/wz-agents'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services'; + +const errorContext = 'AgentsController'; export class AgentsController { /** * Class constructor @@ -52,7 +57,6 @@ export class AgentsController { reportingService, visFactoryService, csvReq - ) { this.$scope = $scope; this.$location = $location; @@ -71,9 +75,7 @@ export class AgentsController { // Config on-demand this.$scope.isArray = Array.isArray; - this.configurationHandler = new ConfigurationHandler( - errorHandler - ); + this.configurationHandler = new ConfigurationHandler(errorHandler); this.$scope.currentConfig = null; this.$scope.configurationTab = ''; this.$scope.configurationSubTab = ''; @@ -104,7 +106,7 @@ export class AgentsController { false, false, false, - false + false, ]; } @@ -141,8 +143,7 @@ export class AgentsController { this.$scope.tab = this.commonData.checkTabLocation(); } this.tabHistory = []; - if (!this.ignoredTabs.includes(this.$scope.tab)) - this.tabHistory.push(this.$scope.tab); + if (!this.ignoredTabs.includes(this.$scope.tab)) this.tabHistory.push(this.$scope.tab); // Tab names this.$scope.tabNames = TabNames; @@ -154,28 +155,21 @@ export class AgentsController { * @param {Object} item * @param {Array} array */ - this.$scope.inArray = (item, array) => - item && Array.isArray(array) && array.includes(item); + this.$scope.inArray = (item, array) => item && Array.isArray(array) && array.includes(item); - this.$scope.switchSubtab = async ( - subtab, - force = false, - onlyAgent = false - ) => this.switchSubtab(subtab, force, onlyAgent); + this.$scope.switchSubtab = async (subtab, force = false, onlyAgent = false) => + this.switchSubtab(subtab, force, onlyAgent); this.changeAgent = false; this.$scope.switchTab = (tab, force = false) => this.switchTab(tab, force); - this.$scope.getAgentStatusClass = agentStatus => - agentStatus === 'active' ? 'teal' : 'red'; + this.$scope.getAgentStatusClass = (agentStatus) => (agentStatus === 'active' ? 'teal' : 'red'); - this.$scope.formatAgentStatus = agentStatus => { - return ['active', 'disconnected'].includes(agentStatus) ? - agentStatus : - 'never connected'; + this.$scope.formatAgentStatus = (agentStatus) => { + return ['active', 'disconnected'].includes(agentStatus) ? agentStatus : 'never connected'; }; - this.$scope.getAgent = async newAgentId => this.getAgent(newAgentId); + this.$scope.getAgent = async (newAgentId) => this.getAgent(newAgentId); this.$scope.goGroups = (agent, group) => this.goGroups(agent, group); this.$scope.downloadCsv = async (path, fileName, filters = []) => this.downloadCsv(path, fileName, filters); @@ -183,19 +177,19 @@ export class AgentsController { this.$scope.search = (term, specificPath) => this.$scope.$broadcast('wazuhSearch', { term, - specificPath + specificPath, }); this.$scope.searchSyscheckFile = (term, specificFilter) => this.$scope.$broadcast('wazuhSearch', { term, - specificFilter + specificFilter, }); this.$scope.searchRootcheck = (term, specificFilter) => this.$scope.$broadcast('wazuhSearch', { term, - specificFilter + specificFilter, }); this.$scope.launchRootcheckScan = () => this.launchRootcheckScan(); @@ -203,8 +197,7 @@ export class AgentsController { this.$scope.startVis2Png = () => this.startVis2Png(); - this.$scope.shouldShowComponent = component => - this.shouldShowComponent(component); + this.$scope.shouldShowComponent = (component) => this.shouldShowComponent(component); this.$scope.$on('$destroy', () => { this.visFactoryService.clearAll(); @@ -217,27 +210,16 @@ export class AgentsController { this.$location.path('/manager/groups'); }; - this.$scope.exportConfiguration = enabledComponents => { - this.reportingService.startConfigReport( - this.$scope.agent, - 'agentConfig', - enabledComponents - ); + this.$scope.exportConfiguration = (enabledComponents) => { + this.reportingService.startConfigReport(this.$scope.agent, 'agentConfig', enabledComponents); }; - this.$scope.restartAgent = async agent => { + this.$scope.restartAgent = async (agent) => { this.$scope.restartingAgent = true; try { - const data = await WzRequest.apiReq( - 'PUT', - `/agents/${agent.id}/restart`, - {} - ); + const data = await WzRequest.apiReq('PUT', `/agents/${agent.id}/restart`, {}); const result = ((data || {}).data || {}).data || false; - const failed = - result && - Array.isArray(result.failed_items) && - result.failed_items.length; + const failed = result && Array.isArray(result.failed_items) && result.failed_items.length; if (failed) { throw new Error(result.failed_items[0].detail); } else if (result) { @@ -247,7 +229,18 @@ export class AgentsController { } this.$scope.restartingAgent = false; } catch (error) { - ErrorHandler.handle(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error restarting the agent: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); this.$scope.restartingAgent = false; } this.$scope.$applyAsync(); @@ -256,41 +249,51 @@ export class AgentsController { //Load try { this.$scope.getAgent(); - } catch (e) { - ErrorHandler.handle( - 'Unexpected exception loading controller', - 'Agents' - ); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error getting the agent: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } // Config on demand this.$scope.getXML = () => this.configurationHandler.getXML(this.$scope); this.$scope.getJSON = () => this.configurationHandler.getJSON(this.$scope); - this.$scope.isString = item => typeof item === 'string'; - this.$scope.hasSize = obj => - obj && typeof obj === 'object' && Object.keys(obj).length; - this.$scope.offsetTimestamp = (text, time) => - this.offsetTimestamp(text, time); - this.$scope.switchConfigTab = ( - configurationTab, - sections, - navigate = true - ) => { + this.$scope.isString = (item) => typeof item === 'string'; + this.$scope.hasSize = (obj) => obj && typeof obj === 'object' && Object.keys(obj).length; + this.$scope.offsetTimestamp = (text, time) => this.offsetTimestamp(text, time); + this.$scope.switchConfigTab = (configurationTab, sections, navigate = true) => { this.$scope.navigate = navigate; try { this.$scope.configSubTab = JSON.stringify({ configurationTab: configurationTab, - sections: sections + sections: sections, }); if (!this.$location.search().configSubTab) { - AppState.setSessionStorageItem( - 'configSubTab', - this.$scope.configSubTab - ); + AppState.setSessionStorageItem('configSubTab', this.$scope.configSubTab); this.$location.search('configSubTab', true); } } catch (error) { - ErrorHandler.handle(error, 'Set configuration path'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `${error.message || error} Set configuration path`, + }, + }; + getErrorOrchestrator().handleError(options); } this.configurationHandler.switchConfigTab( configurationTab, @@ -306,21 +309,14 @@ export class AgentsController { if (!this.$location.search().configWodle) { this.$location.search('configWodle', this.$scope.configWodle); } - this.configurationHandler.switchWodle( - wodleName, - this.$scope, - this.$scope.agent.id - ); + this.configurationHandler.switchWodle(wodleName, this.$scope, this.$scope.agent.id); }; this.$scope.switchConfigurationTab = (configurationTab, navigate) => { // Check if configuration is synced this.checkSync(); this.$scope.navigate = navigate; - this.configurationHandler.switchConfigurationTab( - configurationTab, - this.$scope - ); + this.configurationHandler.switchConfigurationTab(configurationTab, this.$scope); if (!this.$scope.navigate) { const configSubTab = this.$location.search().configSubTab; if (configSubTab) { @@ -333,7 +329,7 @@ export class AgentsController { false ); } catch (error) { - ErrorHandler.handle(error, 'Get configuration path'); + throw new Error(error); } } else { const configWodle = this.$location.search().configWodle; @@ -347,11 +343,8 @@ export class AgentsController { this.$location.search('configWodle', null); } }; - this.$scope.switchConfigurationSubTab = configurationSubTab => { - this.configurationHandler.switchConfigurationSubTab( - configurationSubTab, - this.$scope - ); + this.$scope.switchConfigurationSubTab = (configurationSubTab) => { + this.configurationHandler.switchConfigurationSubTab(configurationSubTab, this.$scope); if (configurationSubTab === 'pm-sca') { this.$scope.currentConfig.sca = this.configurationHandler.parseWodle( this.$scope.currentConfig, @@ -359,8 +352,8 @@ export class AgentsController { ); } }; - this.$scope.updateSelectedItem = i => (this.$scope.selectedItem = i); - this.$scope.getIntegration = list => + this.$scope.updateSelectedItem = (i) => (this.$scope.selectedItem = i); + this.$scope.getIntegration = (list) => this.configurationHandler.getIntegration(list, this.$scope); this.$scope.switchScaScan = () => { @@ -368,7 +361,7 @@ export class AgentsController { if (!this.$scope.showScaScan) { this.$scope.$emit('changeTabView', { tabView: this.$scope.tabView, - tab: this.$scope.tab + tab: this.$scope.tab, }); } this.$scope.$applyAsync(); @@ -387,51 +380,55 @@ export class AgentsController { this.switchGroupEdit(); }; - this.$scope.showConfirmAddGroup = group => { - this.$scope.addingGroupToAgent = this.$scope.addingGroupToAgent - ? false - : group; + this.$scope.showConfirmAddGroup = (group) => { + this.$scope.addingGroupToAgent = this.$scope.addingGroupToAgent ? false : group; }; this.$scope.cancelAddGroup = () => (this.$scope.addingGroupToAgent = false); - this.$scope.loadScaChecks = policy => + this.$scope.loadScaChecks = (policy) => (this.$scope.lookingSca = { ...policy, - id: policy.policy_id + id: policy.policy_id, }); this.$scope.closeScaChecks = () => (this.$scope.lookingSca = false); - this.$scope.confirmAddGroup = group => { - this.groupHandler - .addAgentToGroup(group, this.$scope.agent.id) - .then(() => WzRequest.apiReq('GET', `/agents`, { + this.$scope.confirmAddGroup = async (group) => { + try { + await this.groupHandler.addAgentToGroup(group, this.$scope.agent.id); + const response = await WzRequest.apiReq('GET', `/agents`, { params: { agents_list: this.$scope.agent.id, - } - })) - .then(agent => { - this.$scope.agent.group = agent.data.data.affected_items[0].group; - this.$scope.groups = this.$scope.groups.filter( - item => !agent.data.data.affected_items[0].group.includes(item) - ); - this.$scope.addingGroupToAgent = false; - this.$scope.editGroup = false; - ErrorHandler.info(`Group ${group} has been added.`); - this.$scope.$applyAsync(); - }) - .catch(error => { - this.$scope.editGroup = false; - this.$scope.addingGroupToAgent = false; - ErrorHandler.handle( - error.message || error, - 'Error adding group to agent' - ); + }, }); + this.$scope.agent.group = response.data.data.affected_items[0].group; + this.$scope.groups = this.$scope.groups.filter( + (item) => !responseP.data.data.affected_items[0].group.includes(item) + ); + this.$scope.addingGroupToAgent = false; + this.$scope.editGroup = false; + ErrorHandler.info(`Group ${group} has been added.`); + this.$scope.$applyAsync(); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error adding group: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); + this.$scope.editGroup = false; + this.$scope.addingGroupToAgent = false; + } }; - this.$scope.expand = i => this.expand(i); + this.$scope.expand = (i) => this.expand(i); this.setTabs(); } @@ -461,14 +458,13 @@ export class AgentsController { } else { this.$scope.$emit('changeTabView', { tabView: subtab, - tab: this.$scope.tab + tab: this.$scope.tab, }); } this.$scope.tabView = subtab; return; } catch (error) { - ErrorHandler.handle(error, 'Agents'); - return; + throw new Error(error); } } @@ -487,46 +483,46 @@ export class AgentsController { this.commonData.setRefreshInterval(timefilter.getRefreshInterval()); timefilter.setRefreshInterval({ pause: true, - value: 0 + value: 0, }); } else if (this.ignoredTabs.includes(this.$scope.tab)) { timefilter.setRefreshInterval(this.commonData.getRefreshInterval()); } - if(tab === 'syscollector'){ // TODO: Migrate syscollector to React + if (tab === 'syscollector') { + // TODO: Migrate syscollector to React let breadcrumb = [ - { - text: '', - }, - { - text: 'Agents', - href: "#/agents-preview" - }, - { agent: this.$scope.agent }, - { - text: 'Inventory Data', - className: 'wz-global-breadcrumb-popover' - }, - ]; + { + text: '', + }, + { + text: 'Agents', + href: '#/agents-preview', + }, + { agent: this.$scope.agent }, + { + text: 'Inventory Data', + className: 'wz-global-breadcrumb-popover', + }, + ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); - $('#breadcrumbNoTitle').attr("title",""); + $('#breadcrumbNoTitle').attr('title', ''); } // Update agent status if (!force && ((this.$scope || {}).agent || false)) { try { - const agentInfo = await WzRequest.apiReq( - 'GET', - `/agents`, { - params: { - agents_list: this.$scope.agent.id, - select: 'status' - } - } - ); + const agentInfo = await WzRequest.apiReq('GET', `/agents`, { + params: { + agents_list: this.$scope.agent.id, + select: 'status', + }, + }); this.$scope.agent.status = (((((agentInfo || {}).data || {}).data || {}).affected_items || [])[0] || {}).status || this.$scope.agent.status; - } catch (error) { } // eslint-disable-line + } catch (error) { + throw new Error(error); + } } try { @@ -535,15 +531,17 @@ export class AgentsController { //remove to component this.$scope.scaProps = { agent: this.$scope.agent, - loadScaChecks: policy => this.$scope.loadScaChecks(policy), - downloadCsv: (path, name) => this.downloadCsv(path, name) + loadScaChecks: (policy) => this.$scope.loadScaChecks(policy), + downloadCsv: (path, name) => this.downloadCsv(path, name), }; } if (tab === 'syscollector') try { await this.loadSyscollector(this.$scope.agent.id); - } catch (error) { } // eslint-disable-line + } catch (error) { + throw new Error(error); + } if (tab === 'configuration') { this.$scope.switchConfigurationTab('welcome'); } else { @@ -551,8 +549,7 @@ export class AgentsController { } if (!this.ignoredTabs.includes(tab)) this.tabHistory.push(tab); - if (this.tabHistory.length > 2) - this.tabHistory = this.tabHistory.slice(-2); + if (this.tabHistory.length > 2) this.tabHistory = this.tabHistory.slice(-2); if (this.$scope.tab === tab && !force) { this.$scope.$applyAsync(); @@ -563,37 +560,40 @@ export class AgentsController { const sameTab = this.$scope.tab === tab; this.$location.search('tab', tab); const preserveDiscover = - this.tabHistory.length === 2 && - this.tabHistory[0] === this.tabHistory[1] && - !force; + this.tabHistory.length === 2 && this.tabHistory[0] === this.tabHistory[1] && !force; this.$scope.tab = tab; const targetSubTab = - this.targetLocation && typeof this.targetLocation === 'object' ? - this.targetLocation.subTab : - 'panels'; + this.targetLocation && typeof this.targetLocation === 'object' + ? this.targetLocation.subTab + : 'panels'; if (!this.ignoredTabs.includes(this.$scope.tab)) { - this.$scope.switchSubtab( - targetSubTab, - true, - onlyAgent, - sameTab, - preserveDiscover - ); + this.$scope.switchSubtab(targetSubTab, true, onlyAgent, sameTab, preserveDiscover); } this.shareAgent.deleteTargetLocation(); this.targetLocation = null; this.$scope.$applyAsync(); } catch (error) { - return Promise.reject(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); } this.$scope.configurationTabsProps = {}; - this.$scope.buildProps = tabs => { + this.$scope.buildProps = (tabs) => { const cleanTabs = []; - tabs.forEach(x => { + tabs.forEach((x) => { if ( this.$scope.configurationTab === 'integrity-monitoring' && x.id === 'fim-whodata' && @@ -604,18 +604,15 @@ export class AgentsController { cleanTabs.push({ id: x.id, - name: x.name + name: x.name, }); }); this.$scope.configurationTabsProps = { - clickAction: tab => { + clickAction: (tab) => { this.$scope.switchConfigurationSubTab(tab); }, - selectedTab: - this.$scope.configurationSubTab || (tabs && tabs.length) - ? tabs[0].id - : '', - tabs: cleanTabs + selectedTab: this.$scope.configurationSubTab || (tabs && tabs.length) ? tabs[0].id : '', + tabs: cleanTabs, }; }; @@ -627,9 +624,11 @@ export class AgentsController { * @param {*} id */ addMitrefilter(id) { - const filter = `{"meta":{"index": ${AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN}},"query":{"match":{"rule.mitre.id":{"query":"${id}","type":"phrase"}}}}`; + const filter = `{"meta":{"index": ${ + AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN + }},"query":{"match":{"rule.mitre.id":{"query":"${id}","type":"phrase"}}}}`; this.$rootScope.$emit('addNewKibanaFilter', { - filter: JSON.parse(filter) + filter: JSON.parse(filter), }); } @@ -639,10 +638,7 @@ export class AgentsController { setTabs() { this.$scope.agentsTabsProps = false; if (this.$scope.agent) { - this.currentPanel = this.commonData.getCurrentPanel( - this.$scope.tab, - true - ); + this.currentPanel = this.commonData.getCurrentPanel(this.$scope.tab, true); if (!this.currentPanel) return; @@ -653,27 +649,23 @@ export class AgentsController { ); const cleanTabs = []; - tabs.forEach(x => { - if (!hasAgentSupportModule(this.$scope.agent, x.id) - ) - return; + tabs.forEach((x) => { + if (!hasAgentSupportModule(this.$scope.agent, x.id)) return; cleanTabs.push({ id: x.id, - name: x.name + name: x.name, }); }); this.$scope.agentsTabsProps = { - clickAction: tab => { + clickAction: (tab) => { this.switchTab(tab, true); }, selectedTab: this.$scope.tab || - (this.currentPanel && this.currentPanel.length - ? this.currentPanel[0] - : ''), - tabs: cleanTabs + (this.currentPanel && this.currentPanel.length ? this.currentPanel[0] : ''), + tabs: cleanTabs, }; this.$scope.$applyAsync(); } @@ -684,43 +676,55 @@ export class AgentsController { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; goDiscover() { this.targetLocation = { tab: 'general', - subTab: 'discover' + subTab: 'discover', }; return this.switchTab('general'); } - onClickUpgrade() { + async onClickUpgrade() { try { - WzRequest.apiReq('PUT', `/agents/${this.$scope.agent.id}/upgrade`, {}) - .then(() => { - this.showToast('success', 'The agent is being upgrade.', '', 5000); - }) - .catch(() => { - this.showToast('warning', 'This agent is already upgrade.', '', 5000); - }); + await WzRequest.apiReq('PUT', `/agents/${this.$scope.agent.id}/upgrade`, {}); + this.showToast('success', 'The agent is being upgrade.', '', 5000); } catch (error) { - console.log(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error upgrading the agent: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } - onClickRestart() { + async onClickRestart() { try { - WzRequest.apiReq('PUT', `/agents/${this.$scope.agent.id}/restart`, {}) - .then(() => { - this.showToast('success', 'Agent restarted.', '', 5000); - }) - .catch(() => { - this.showToast('warning', 'Error restarting agent.', '', 5000); - }); + await WzRequest.apiReq('PUT', `/agents/${this.$scope.agent.id}/restart`, {}); + this.showToast('success', 'Agent restarted.', '', 5000); } catch (error) { - console.log(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error restarting the agent: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -756,7 +760,8 @@ export class AgentsController { async checkSync() { const isSync = await WzRequest.apiReq( 'GET', - `/agents/${this.$scope.agent.id}/group/is_sync`, {} + `/agents/${this.$scope.agent.id}/group/is_sync`, + {} ); this.$scope.isSynchronized = (((((isSync || {}).data || {}).data || {}).affected_items || [])[0] || {}).synced || false; @@ -769,15 +774,11 @@ export class AgentsController { */ async loadSyscollector(id) { try { - const syscollectorData = await this.genericReq.request( - 'GET', - `/api/syscollector/${id}` - ); + const syscollectorData = await this.genericReq.request('GET', `/api/syscollector/${id}`); this.$scope.syscollector = (syscollectorData || {}).data || {}; - return; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } @@ -799,7 +800,7 @@ export class AgentsController { const data = await WzRequest.apiReq('GET', `/agents`, { params: { agents_list: id, - } + }, }); const agentInfo = ((((data || {}).data || {}).data || {}).affected_items || [])[0] || false; @@ -808,12 +809,9 @@ export class AgentsController { if (!this.$scope.agent) return; if (agentInfo && this.$scope.agent.os) { - this.$scope.agentOS = - this.$scope.agent.os.name + ' ' + this.$scope.agent.os.version; + this.$scope.agentOS = this.$scope.agent.os.name + ' ' + this.$scope.agent.os.version; const isLinux = this.$scope.agent.os.uname.includes('Linux'); - this.$scope.agent.agentPlatform = isLinux - ? 'linux' - : this.$scope.agent.os.platform; + this.$scope.agent.agentPlatform = isLinux ? 'linux' : this.$scope.agent.os.platform; } else { this.$scope.agentOS = '-'; this.$scope.agent.agentPlatform = false; @@ -822,16 +820,13 @@ export class AgentsController { await this.$scope.switchTab(this.$scope.tab, true); const groups = await WzRequest.apiReq('GET', '/groups', {}); this.$scope.groups = groups.data.data.affected_items - .map(item => item.name) - .filter( - item => - this.$scope.agent.group && !this.$scope.agent.group.includes(item) - ); + .map((item) => item.name) + .filter((item) => this.$scope.agent.group && !this.$scope.agent.group.includes(item)); this.loadWelcomeCardsProps(); this.$scope.getWelcomeCardsProps = (resultState) => { - return {...this.$scope.welcomeCardsProps, resultState } - } + return { ...this.$scope.welcomeCardsProps, resultState }; + }; this.$scope.load = false; this.$scope.$applyAsync(); return; @@ -841,31 +836,24 @@ export class AgentsController { this.$scope.emptyAgent = 'Wazuh API timeout.'; } } - if ( - error && - typeof error === 'string' && - error.includes('Agent does not exist') - ) { + if (error && typeof error === 'string' && error.includes('Agent does not exist')) { this.$location.search('agent', null); this.$location.path('/agents-preview'); } + this.$scope.load = false; + this.$scope.$applyAsync(); + throw new Error(error); } - - this.$scope.load = false; - this.$scope.$applyAsync(); - return; } shouldShowComponent(component) { - return hasAgentSupportModule(this.$scope.agent, component) + return hasAgentSupportModule(this.$scope.agent, component); } cleanExtensions(extensions) { const result = {}; for (const extension in extensions) { - if ( - hasAgentSupportModule(this.$scope.agent, extension) - ) { + if (hasAgentSupportModule(this.$scope.agent, extension)) { result[extension] = extensions[extension]; } } @@ -877,7 +865,7 @@ export class AgentsController { */ loadWelcomeCardsProps() { this.$scope.welcomeCardsProps = { - switchTab: tab => this.switchTab(tab), + switchTab: (tab) => this.switchTab(tab), extensions: this.cleanExtensions(this.$scope.extensions), agent: this.$scope.agent, api: AppState.getCurrentAPI(), @@ -885,7 +873,7 @@ export class AgentsController { setExtensions: (api, extensions) => { AppState.setExtensions(api, extensions); this.$scope.extensions = extensions; - } + }, }; } @@ -903,6 +891,18 @@ export class AgentsController { try { return text + formatUIDate(time); } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: false, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); return time !== '-' ? `${text}${time} (UTC)` : time; } } @@ -914,7 +914,7 @@ export class AgentsController { */ goGroups(agent, group) { AppState.setNavigation({ - status: true + status: true, }); this.visFactoryService.clearAll(); this.shareAgent.setAgent(agent, group); @@ -931,19 +931,15 @@ export class AgentsController { */ async downloadCsv(path, fileName, filters = []) { try { - ErrorHandler.info( - 'Your download should begin automatically...', - 'CSV' - ); + ErrorHandler.info('Your download should begin automatically...', 'CSV'); const currentApi = JSON.parse(AppState.getCurrentAPI()).id; const output = await this.csvReq.fetch(path, currentApi, filters); const blob = new Blob([output], { - type: 'text/csv' + type: 'text/csv', }); // eslint-disable-line - FileSaver.saveAs(blob, fileName); } catch (error) { - ErrorHandler.handle(error, 'Download CSV'); + throw new Error(error); } return; } @@ -957,9 +953,7 @@ export class AgentsController { syscollectorFilters.push( this.filterHandler.managerQuery(AppState.getClusterInfo().cluster, true) ); - syscollectorFilters.push( - this.filterHandler.agentQuery(this.$scope.agent.id) - ); + syscollectorFilters.push(this.filterHandler.agentQuery(this.$scope.agent.id)); } this.reportingService.startVis2Png( this.$scope.tab, @@ -974,15 +968,23 @@ export class AgentsController { if (!isActive) { throw new Error('Agent is not active'); } - await WzRequest.apiReq( - 'PUT', - `/rootcheck/${this.$scope.agent.id}`, - {} - ); + await WzRequest.apiReq('PUT', `/rootcheck/${this.$scope.agent.id}`, {}); ErrorHandler.info( - `Policy monitoring scan launched successfully on agent ${this.$scope.agent.id}`); + `Policy monitoring scan launched successfully on agent ${this.$scope.agent.id}` + ); } catch (error) { - ErrorHandler.handle(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: ` Policy monitoring scan failed on agent ${this.$scope.agent.id} due to: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } return; } @@ -995,15 +997,23 @@ export class AgentsController { } await WzRequest.apiReq('PUT', `/syscheck`, { params: { - agents_list: this.$scope.agent.id - } + agents_list: this.$scope.agent.id, + }, }); - ErrorHandler.info( - `FIM scan launched successfully on agent ${this.$scope.agent.id}`, - '' - ); + ErrorHandler.info(`FIM scan launched successfully on agent ${this.$scope.agent.id}`, ''); } catch (error) { - ErrorHandler.handle(error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: ` FIM scan failed on agent ${this.$scope.agent.id} due to: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } return; } @@ -1025,7 +1035,7 @@ export class AgentsController { false, false, false, - false + false, ]; } diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index a64590abbd..9257ac2764 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -22,11 +22,10 @@ import { EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, - EuiToolTip + EuiToolTip, } from '@elastic/eui'; -import { Pie } from "../../../components/d3/pie"; -import { ProgressChart } from "../../../components/d3/progress"; -import { AgentsTable } from './agents-table' +import { Pie } from '../../../components/d3/pie'; +import { AgentsTable } from './agents-table'; import { WzRequest } from '../../../react-services/wz-request'; import KibanaVis from '../../../kibana-integrations/kibana-vis'; import WzReduxProvider from '../../../redux/wz-redux-provider'; @@ -36,11 +35,20 @@ import { FilterHandler } from '../../../utils/filter-handler'; import { TabVisualizations } from '../../../factories/tab-visualizations'; import { WazuhConfig } from './../../../react-services/wazuh-config.js'; import { WzDatePicker } from '../../../components/wz-date-picker/wz-date-picker'; -import { withReduxProvider, withGlobalBreadcrumb, withUserAuthorizationPrompt } from '../../../components/common/hocs'; +import { + withReduxProvider, + withGlobalBreadcrumb, + withUserAuthorizationPrompt, +} from '../../../components/common/hocs'; import { formatUIDate } from '../../../../public/react-services/time-service'; import { compose } from 'redux'; -import { withErrorBoundary } from '../../../components/common/hocs' -import './agents-preview.scss' +import { withErrorBoundary } from '../../../components/common/hocs'; +import './agents-preview.scss'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'AgentsPreview'; const FILTER_ACTIVE = 'active'; const FILTER_DISCONNECTED = 'disconnected'; @@ -50,282 +58,340 @@ export const AgentsPreview = compose( withErrorBoundary, withReduxProvider, withGlobalBreadcrumb([{ text: '' }, { text: 'Agents' }]), - withUserAuthorizationPrompt([[{action: 'agent:read', resource: 'agent:id:*'},{action: 'agent:read', resource: 'agent:group:*'}]]) -)(class AgentsPreview extends Component { - _isMount = false; - constructor(props) { - super(props); - this.state = { data: [], loading: false, showAgentsEvolutionVisualization: false, agentTableFilters: [] }; - this.wazuhConfig = new WazuhConfig(); - this.agentStatusLabelToIDMap = { - 'Active': 'active', - 'Disconnected': 'disconnected', - 'Never connected': 'never_connected' + withUserAuthorizationPrompt([ + [ + { action: 'agent:read', resource: 'agent:id:*' }, + { action: 'agent:read', resource: 'agent:group:*' }, + ], + ]) +)( + class AgentsPreview extends Component { + _isMount = false; + constructor(props) { + super(props); + this.state = { + data: [], + loading: false, + showAgentsEvolutionVisualization: false, + agentTableFilters: [], + }; + this.wazuhConfig = new WazuhConfig(); + this.agentStatusLabelToIDMap = { + Active: 'active', + Disconnected: 'disconnected', + 'Never connected': 'never_connected', + }; } - } - async componentDidMount() { - this._isMount = true; - this.getSummary(); - if( this.wazuhConfig.getConfig()['wazuh.monitoring.enabled'] ){ - this._isMount && this.setState({ showAgentsEvolutionVisualization: true }); - const tabVisualizations = new TabVisualizations(); - tabVisualizations.removeAll(); - tabVisualizations.setTab('general'); - tabVisualizations.assign({ - general: 1 - }); - const filterHandler = new FilterHandler(AppState.getCurrentPattern()); - await VisFactoryHandler.buildOverviewVisualizations( - filterHandler, - 'general', - null - ); + async componentDidMount() { + this._isMount = true; + this.getSummary(); + if (this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']) { + this._isMount && this.setState({ showAgentsEvolutionVisualization: true }); + const tabVisualizations = new TabVisualizations(); + tabVisualizations.removeAll(); + tabVisualizations.setTab('general'); + tabVisualizations.assign({ + general: 1, + }); + const filterHandler = new FilterHandler(AppState.getCurrentPattern()); + await VisFactoryHandler.buildOverviewVisualizations(filterHandler, 'general', null); + } } - } - componentWillUnmount() { - this._isMount = false; - } + componentWillUnmount() { + this._isMount = false; + } - agentStatusLabelToID(label){ - return this.agentStatusLabelToIDMap[label]; - } + agentStatusLabelToID(label) { + return this.agentStatusLabelToIDMap[label]; + } - groupBy = function(arr) { - return arr.reduce(function(prev, item) { - if (item in prev) prev[item]++; - else prev[item] = 1; - return prev; - }, {}); - }; + groupBy = function (arr) { + return arr.reduce(function (prev, item) { + if (item in prev) prev[item]++; + else prev[item] = 1; + return prev; + }, {}); + }; - async getSummary() { - try { - this.setState({ loading: true }); - const summaryData = await WzRequest.apiReq('GET', '/agents/summary/status', {}); - this.summary = summaryData.data.data; - this.totalAgents = this.summary.total; - const model = [ - { id: 'active', label: "Active", value: this.summary['active'] || 0 }, - { id: 'disconnected', label: "Disconnected", value: this.summary['disconnected'] || 0 }, - { id: 'neverConnected', label: "Never connected", value: this.summary['never_connected'] || 0 } - ]; - this.setState({ data: model }); - this.agentsCoverity = this.totalAgents ? ((this.summary['active'] || 0) / this.totalAgents) * 100 : 0; - const lastAgent = await WzRequest.apiReq('GET', '/agents', {params: { limit: 1, sort: '-dateAdd', q: 'id!=000' }}); - this.lastAgent = lastAgent.data.data.affected_items[0]; - this.mostActiveAgent = await this.props.tableProps.getMostActive(); - const osresult = await WzRequest.apiReq('GET', '/agents/summary/os', {}); - this.platforms = this.groupBy(osresult.data.data.affected_items); - const platformsModel = []; - for (let [key, value] of Object.entries(this.platforms)) { - platformsModel.push({ id: key, label: key, value: value }); + async getSummary() { + try { + this.setState({ loading: true }); + const summaryData = await WzRequest.apiReq('GET', '/agents/summary/status', {}); + this.summary = summaryData.data.data; + this.totalAgents = this.summary.total; + const model = [ + { id: 'active', label: 'Active', value: this.summary['active'] || 0 }, + { id: 'disconnected', label: 'Disconnected', value: this.summary['disconnected'] || 0 }, + { + id: 'neverConnected', + label: 'Never connected', + value: this.summary['never_connected'] || 0, + }, + ]; + this.setState({ data: model }); + this.agentsCoverity = this.totalAgents + ? ((this.summary['active'] || 0) / this.totalAgents) * 100 + : 0; + const lastAgent = await WzRequest.apiReq('GET', '/agents', { + params: { limit: 1, sort: '-dateAdd', q: 'id!=000' }, + }); + this.lastAgent = lastAgent.data.data.affected_items[0]; + this.mostActiveAgent = await this.props.tableProps.getMostActive(); + const osresult = await WzRequest.apiReq('GET', '/agents/summary/os', {}); + this.platforms = this.groupBy(osresult.data.data.affected_items); + const platformsModel = []; + for (let [key, value] of Object.entries(this.platforms)) { + platformsModel.push({ id: key, label: key, value: value }); + } + this._isMount && this.setState({ platforms: platformsModel, loading: false }); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Could not get the agents summary`, + }, + }; + getErrorOrchestrator().handleError(options); } - this._isMount && - this.setState({ platforms: platformsModel, loading: false }); - } catch (error) {} - } + } - removeFilters(){ - this._isMount && this.setState({agentTableFilters: []}) - } + removeFilters() { + this._isMount && this.setState({ agentTableFilters: [] }); + } - showLastAgent() { - this.props.tableProps.showAgent(this.lastAgent); - } + showLastAgent() { + this.props.tableProps.showAgent(this.lastAgent); + } - showMostActiveAgent() { - this.mostActiveAgent.name ? this.props.tableProps.showAgent(this.mostActiveAgent) : ''; - } + showMostActiveAgent() { + this.mostActiveAgent.name ? this.props.tableProps.showAgent(this.mostActiveAgent) : ''; + } - showAgentsWithFilters(filter) { - this._isMount && - this.setState({ - agentTableFilters: [{ field: 'q', value: `status=${filter}` }], - }); - } + showAgentsWithFilters(filter) { + this._isMount && + this.setState({ + agentTableFilters: [{ field: 'q', value: `status=${filter}` }], + }); + } - render() { - const colors = ['#017D73', '#bd271e', '#69707D']; - return ( - - - - {this.state.loading && ( - - - - ) || ( - - - - - {this.totalAgents > 0 && ( - - this._isMount && this.setState({ - agentTableFilters: [ {field: 'q', value: `status=${this.agentStatusLabelToID(status)}`}] - })} - width={300} - height={150} - data={this.state.data} - colors={colors} - /> - - )} - - - - {this.totalAgents > 0 && ( - - - - - {this.summary && ( - - - - this.showAgentsWithFilters(FILTER_ACTIVE)} >{this.state.data[0].value} - )} - titleSize={'s'} - description="Active" - titleColor="secondary" - className="white-space-nowrap" - /> - - - - this.showAgentsWithFilters(FILTER_DISCONNECTED)} >{this.state.data[1].value} - )} - titleSize={'s'} - description="Disconnected" - titleColor="danger" - className="white-space-nowrap" - /> - - - - this.showAgentsWithFilters(FILTER_NEVER_CONNECTED)} >{this.state.data[2].value} - )} - titleSize={'s'} - description="Never connected" - titleColor="subdued" - className="white-space-nowrap" - /> - - - - - - )} - - {this.lastAgent && ( - - - this.showLastAgent()}>{this.lastAgent.name} - } - titleSize="s" - description="Last registered agent" - titleColor="primary" - className="pb-12 white-space-nowrap" - /> - - )} - {this.mostActiveAgent && ( - - + + + {(this.state.loading && ( + + + + )) || ( + + + + + {this.totalAgents > 0 && ( + + + this._isMount && + this.setState({ + agentTableFilters: [ + { + field: 'q', + value: `status=${this.agentStatusLabelToID(status)}`, + }, + ], + }) } - title={ - - this.showMostActiveAgent()}>{this.mostActiveAgent.name || '-'} - } - className="white-space-nowrap" - titleSize="s" - description="Most active agent" - titleColor="primary" + width={300} + height={150} + data={this.state.data} + colors={colors} /> )} - - - - - )} - - )} - {this.state.showAgentsEvolutionVisualization && ( - - - + + + {this.totalAgents > 0 && ( -
- - - -
- {this.props.resultState === 'loading' && - ( -
- -
- ) } - + + + + {this.summary && ( + + + + this.showAgentsWithFilters(FILTER_ACTIVE)} + > + {this.state.data[0].value} + + + } + titleSize={'s'} + description="Active" + titleColor="secondary" + className="white-space-nowrap" + /> + + + + + this.showAgentsWithFilters(FILTER_DISCONNECTED) + } + > + {this.state.data[1].value} + + + } + titleSize={'s'} + description="Disconnected" + titleColor="danger" + className="white-space-nowrap" + /> + + + + + this.showAgentsWithFilters(FILTER_NEVER_CONNECTED) + } + > + {this.state.data[2].value} + + + } + titleSize={'s'} + description="Never connected" + titleColor="subdued" + className="white-space-nowrap" + /> + + + + + + )} + + {this.lastAgent && ( + + + this.showLastAgent()}> + {this.lastAgent.name} + + + } + titleSize="s" + description="Last registered agent" + titleColor="primary" + className="pb-12 white-space-nowrap" + /> + + )} + {this.mostActiveAgent && ( + + + this.showMostActiveAgent()}> + {this.mostActiveAgent.name || '-'} + + + } + className="white-space-nowrap" + titleSize="s" + description="Most active agent" + titleColor="primary" + /> + + )} + + + +
-
- - - No results found in the selected time range} - actions={ - { }} /> - } - /> - -
- - )} - - + )} + + )} + {this.state.showAgentsEvolutionVisualization && ( + + + + +
+ + + +
+ {this.props.resultState === 'loading' && ( +
+ +
+ )} +
+
+
+ + No results found in the selected time range} + actions={ {}} />} + /> + +
+ )} + + formatUIDate(date)} + formatUIDate={(date) => formatUIDate(date)} reload={() => this.getSummary()} /> - -
- ); + + + ); + } } -}); +); AgentsTable.propTypes = { tableProps: PropTypes.object, - showAgent: PropTypes.func + showAgent: PropTypes.func, }; diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 1e1432b246..85ce6be84d 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -28,7 +28,7 @@ import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, - EuiLoadingSpinner + EuiLoadingSpinner, } from '@elastic/eui'; import { CheckUpgrade } from './checkUpgrade'; import { getToasts } from '../../../kibana-services'; @@ -40,992 +40,1044 @@ import { WzSearchBar, filtersToObject } from '../../../components/wz-search-bar' import { getAgentFilterValues } from '../../../controllers/management/components/management/groups/get-agents-filters-values'; import { WzButtonPermissions } from '../../../components/common/permissions/button'; import { formatUIDate } from '../../../react-services/time-service'; -import { withErrorBoundary } from '../../../components/common/hocs' - -export const AgentsTable = withErrorBoundary (class AgentsTable extends Component { - _isMount = false; - constructor(props) { - super(props); - this.state = { - agents: [], - isLoading: false, - pageIndex: 0, - pageSize: 15, - sortDirection: 'asc', - sortField: 'id', - totalItems: 0, - selectedItems: [], - allSelected: false, - purgeModal: false, - filters: sessionStorage.getItem('agents_preview_selected_options') ? JSON.parse(sessionStorage.getItem('agents_preview_selected_options')) : [] - }; - this.suggestions = [ - { type: 'q', label: 'status', description: 'Filter by agent connection status', operators: ['=', '!=',], values: ['active', 'disconnected', 'never_connected', 'pending'] }, - { type: 'q', label: 'os.platform', description: 'Filter by OS platform', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('os.platform', value, { q: 'id!=000' }) }, - { type: 'q', label: 'ip', description: 'Filter by agent IP', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('ip', value, { q: 'id!=000' }) }, - { type: 'q', label: 'name', description: 'Filter by agent name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('name', value, { q: 'id!=000' }) }, - { type: 'q', label: 'id', description: 'Filter by agent id', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('id', value, { q: 'id!=000' }) }, - { type: 'q', label: 'group', description: 'Filter by agent group', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('group', value, { q: 'id!=000' }) }, - { type: 'q', label: 'node_name', description: 'Filter by node name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('node_name', value, { q: 'id!=000' }) }, - { type: 'q', label: 'manager', description: 'Filter by manager', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('manager', value, { q: 'id!=000' }) }, - { type: 'q', label: 'version', description: 'Filter by agent version', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('version', value, { q: 'id!=000' }) }, - { type: 'q', label: 'configSum', description: 'Filter by agent config sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('configSum', value, { q: 'id!=000' }) }, - { type: 'q', label: 'mergedSum', description: 'Filter by agent merged sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('mergedSum', value, { q: 'id!=000' }) }, - { type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, { q: 'id!=000' }) }, - { type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, { q: 'id!=000' }) }, - ]; - this.downloadCsv.bind(this); - } +import { withErrorBoundary } from '../../../components/common/hocs'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'AgentsTable'; + +export const AgentsTable = withErrorBoundary( + class AgentsTable extends Component { + _isMount = false; + constructor(props) { + super(props); + this.state = { + agents: [], + isLoading: false, + pageIndex: 0, + pageSize: 15, + sortDirection: 'asc', + sortField: 'id', + totalItems: 0, + selectedItems: [], + allSelected: false, + purgeModal: false, + filters: sessionStorage.getItem('agents_preview_selected_options') + ? JSON.parse(sessionStorage.getItem('agents_preview_selected_options')) + : [], + }; + this.suggestions = [ + { + type: 'q', + label: 'status', + description: 'Filter by agent connection status', + operators: ['=', '!='], + values: ['active', 'disconnected', 'never_connected', 'pending'], + }, + { + type: 'q', + label: 'os.platform', + description: 'Filter by OS platform', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('os.platform', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'ip', + description: 'Filter by agent IP', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('ip', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'name', + description: 'Filter by agent name', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('name', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'id', + description: 'Filter by agent id', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('id', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'group', + description: 'Filter by agent group', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('group', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'node_name', + description: 'Filter by node name', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('node_name', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'manager', + description: 'Filter by manager', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('manager', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'version', + description: 'Filter by agent version', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('version', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'configSum', + description: 'Filter by agent config sum', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('configSum', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'mergedSum', + description: 'Filter by agent merged sum', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('mergedSum', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'dateAdd', + description: 'Filter by add date', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('dateAdd', value, { q: 'id!=000' }), + }, + { + type: 'q', + label: 'lastKeepAlive', + description: 'Filter by last keep alive', + operators: ['=', '!='], + values: async (value) => getAgentFilterValues('lastKeepAlive', value, { q: 'id!=000' }), + }, + ]; + this.downloadCsv.bind(this); + } - async UNSAFE_componentWillMount() { - const managerVersion = await WzRequest.apiReq('GET', '//', {}); - const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); - const agentActive = await WzRequest.apiReq('GET', '/agents', { - params: { - q: 'status=active' - } - }); + async UNSAFE_componentWillMount() { + const managerVersion = await WzRequest.apiReq('GET', '//', {}); + const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); + const agentActive = await WzRequest.apiReq('GET', '/agents', { + params: { + q: 'status=active', + }, + }); - this.setState({ - managerVersion: managerVersion.data.data.api_version, - agentActive: agentActive.data.data.totalItems, - avaibleAgents: totalAgent.data.data.affected_items - }); - } + this.setState({ + managerVersion: managerVersion.data.data.api_version, + agentActive: agentActive.data.data.totalItems, + avaibleAgents: totalAgent.data.data.affected_items, + }); + } - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - this._isMount && this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection - }); - }; - - async componentDidMount() { - this._isMount = true; - await this.getItems(); - } + onTableChange = ({ page = {}, sort = {} }) => { + const { index: pageIndex, size: pageSize } = page; + const { field: sortField, direction: sortDirection } = sort; + this._isMount && + this.setState({ + pageIndex, + pageSize, + sortField, + sortDirection, + }); + }; - componentWillUnmount() { - this._isMount = false; - if (sessionStorage.getItem('agents_preview_selected_options')) { - sessionStorage.removeItem('agents_preview_selected_options'); + async componentDidMount() { + this._isMount = true; + await this.getItems(); } - } - async reloadAgents() { - // const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); - // this._isMount && this.setState({ - // isLoading: true, - // avaibleAgents: totalAgent.data.data.items - // }); - await this.getItems(); - await this.props.reload(); - } + componentWillUnmount() { + this._isMount = false; + if (sessionStorage.getItem('agents_preview_selected_options')) { + sessionStorage.removeItem('agents_preview_selected_options'); + } + } - async componentDidUpdate(prevProps, prevState) { - if (!(_.isEqual(prevState.filters, this.state.filters)) - || prevState.pageIndex !== this.state.pageIndex - || prevState.pageSize !== this.state.pageSize - || prevState.sortField !== this.state.sortField - || prevState.sortDirection !== this.state.sortDirection) { + async reloadAgents() { await this.getItems(); - } else if (!(_.isEqual(prevProps.filters, this.props.filters)) && this.props.filters && this.props.filters.length) { - this.setState({ filters: this.props.filters, pageIndex: 0 }); - this.props.removeFilters(); + await this.props.reload(); } - // if (prevState.allSelected === false && this.state.allSelected === true) { - // this._isMount && this.setState({ loadingAllItem: true }); - // this.getAllItems(); - // } - } - async getItems() { - try { - this._isMount && this.setState({ isLoading: true }); - const rawAgents = await this.props.wzReq( - 'GET', - '/agents', - { params: this.buildFilter() } - ); + async componentDidUpdate(prevProps, prevState) { + if ( + !_.isEqual(prevState.filters, this.state.filters) || + prevState.pageIndex !== this.state.pageIndex || + prevState.pageSize !== this.state.pageSize || + prevState.sortField !== this.state.sortField || + prevState.sortDirection !== this.state.sortDirection + ) { + await this.getItems(); + } else if ( + !_.isEqual(prevProps.filters, this.props.filters) && + this.props.filters && + this.props.filters.length + ) { + this.setState({ filters: this.props.filters, pageIndex: 0 }); + this.props.removeFilters(); + } + } - const formatedAgents = ( - ((rawAgents || {}).data || {}).data || {} - ).affected_items.map(this.formatAgent.bind(this)); + async getItems() { + try { + this._isMount && this.setState({ isLoading: true }); + const rawAgents = await this.props.wzReq('GET', '/agents', { params: this.buildFilter() }); - this._isMount && - this.setState({ - agents: formatedAgents, - totalItems: (((rawAgents || {}).data || {}).data || {}).total_affected_items, - isLoading: false - }); - } catch (error) { - this.setState({ isLoading: false }); + const formatedAgents = (((rawAgents || {}).data || {}).data || {}).affected_items.map( + this.formatAgent.bind(this) + ); + + this._isMount && + this.setState({ + agents: formatedAgents, + totalItems: (((rawAgents || {}).data || {}).data || {}).total_affected_items, + isLoading: false, + }); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Could not get the agents list`, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ isLoading: false }); + } } - } - async getAllItems() { - const { pageIndex, pageSize } = this.state; - const filterTable = { - offset: pageIndex * pageSize, - limit: pageSize, - q: this.buildQFilter(), - sort: this.buildSortFilter() - }; + async getAllItems() { + const { pageIndex, pageSize } = this.state; + const filterTable = { + offset: pageIndex * pageSize, + limit: pageSize, + q: this.buildQFilter(), + sort: this.buildSortFilter(), + }; - const filterAll = { - q: this.buildQFilter(), - sort: this.buildSortFilter() - }; + const filterAll = { + q: this.buildQFilter(), + sort: this.buildSortFilter(), + }; - const rawAgents = await this.props.wzReq('GET', '/agents', filterTable); + const rawAgents = await this.props.wzReq('GET', '/agents', filterTable); - const agentsFiltered = await this.props - .wzReq('GET', '/agents', filterAll) - .then(() => { + const agentsFiltered = await this.props.wzReq('GET', '/agents', filterAll).then(() => { this._isMount && this.setState({ loadingAllItem: false }); }); - const formatedAgents = ( - ((rawAgents || {}).data || {}).data || {} - ).items.map(this.formatAgent.bind(this)); - this._isMount && - this.setState({ - agents: formatedAgents, - avaibleAgents: agentsFiltered.data.data.items, - totalItems: (((rawAgents || {}).data || {}).data || {}).totalItems, - isLoading: false - }); - } + const formatedAgents = (((rawAgents || {}).data || {}).data || {}).items.map( + this.formatAgent.bind(this) + ); + this._isMount && + this.setState({ + agents: formatedAgents, + avaibleAgents: agentsFiltered.data.data.items, + totalItems: (((rawAgents || {}).data || {}).data || {}).totalItems, + isLoading: false, + }); + } - buildFilter() { - const { pageIndex, pageSize, filters } = this.state; + buildFilter() { + const { pageIndex, pageSize, filters } = this.state; - const filter = { - ...filtersToObject(filters), - offset: (pageIndex * pageSize) || 0, - limit: pageSize, - sort: this.buildSortFilter() - }; - filter.q = !filter.q ? `id!=000` : `id!=000;${filter.q}`; + const filter = { + ...filtersToObject(filters), + offset: pageIndex * pageSize || 0, + limit: pageSize, + sort: this.buildSortFilter(), + }; + filter.q = !filter.q ? `id!=000` : `id!=000;${filter.q}`; - return filter; - } + return filter; + } - buildSortFilter() { - const { sortField, sortDirection } = this.state; + buildSortFilter() { + const { sortField, sortDirection } = this.state; - const field = sortField === 'os_name' ? 'os.name,os.version' : sortField; - const direction = sortDirection === 'asc' ? '+' : '-'; + const field = sortField === 'os_name' ? 'os.name,os.version' : sortField; + const direction = sortDirection === 'asc' ? '+' : '-'; - return direction + field; - } + return direction + field; + } - buildQFilter() { - const { q } = this.state; - return q === '' ? `id!=000` : `id!=000;${q}`; - } + buildQFilter() { + const { q } = this.state; + return q === '' ? `id!=000` : `id!=000;${q}`; + } - formatAgent(agent) { - const checkField = field => { - return field !== undefined ? field : '-'; - }; - const agentVersion = - agent.version !== undefined ? agent.version.split(' ')[1] : '-'; - const node_name = agent.node_name && agent.node_name !== 'unknown' ? agent.node_name : '-'; - - return { - id: agent.id, - name: agent.name, - ip: agent.ip, - status: agent.status, - group: checkField(agent.group), - os_name: agent, - version: agentVersion, - node_name: node_name, - dateAdd: agent.dateAdd ? formatUIDate(agent.dateAdd) : '-', - lastKeepAlive: agent.lastKeepAlive ? formatUIDate(agent.lastKeepAlive) : '-', - actions: agent, - upgrading: false - }; - } + formatAgent(agent) { + const checkField = (field) => { + return field !== undefined ? field : '-'; + }; + const agentVersion = agent.version !== undefined ? agent.version.split(' ')[1] : '-'; + const node_name = agent.node_name && agent.node_name !== 'unknown' ? agent.node_name : '-'; - actionButtonsRender(agent) { - return ( -
- - { - ev.stopPropagation(); - this.props.clickAction(agent, 'default'); - }} - iconType="eye" - color={'primary'} - aria-label="Open summary panel for this agent" - /> - -   - {agent.status !== 'never_connected' && - + return { + id: agent.id, + name: agent.name, + ip: agent.ip, + status: agent.status, + group: checkField(agent.group), + os_name: agent, + version: agentVersion, + node_name: node_name, + dateAdd: agent.dateAdd ? formatUIDate(agent.dateAdd) : '-', + lastKeepAlive: agent.lastKeepAlive ? formatUIDate(agent.lastKeepAlive) : '-', + actions: agent, + upgrading: false, + }; + } + + actionButtonsRender(agent) { + return ( +
+ { + onClick={(ev) => { ev.stopPropagation(); - this.props.clickAction(agent, 'configuration'); + this.props.clickAction(agent, 'default'); }} + iconType="eye" color={'primary'} - iconType="wrench" - aria-label="Open configuration for this agent" + aria-label="Open summary panel for this agent" /> +   + {agent.status !== 'never_connected' && ( + + { + ev.stopPropagation(); + this.props.clickAction(agent, 'configuration'); + }} + color={'primary'} + iconType="wrench" + aria-label="Open configuration for this agent" + /> + + )} +
+ ); + } + + addIconPlatformRender(agent) { + let icon = false; + const checkField = (field) => { + return field !== undefined ? field : '-'; + }; + const os = (agent || {}).os; + + if (((os || {}).uname || '').includes('Linux')) { + icon = 'linux'; + } else if ((os || {}).platform === 'windows') { + icon = 'windows'; + } else if ((os || {}).platform === 'darwin') { + icon = 'apple'; + } + const os_name = + checkField(((agent || {}).os || {}).name) + + ' ' + + checkField(((agent || {}).os || {}).version); + + return ( + + {' '} + {os_name === '- -' ? '-' : os_name} + + ); + } + + addHealthStatusRender(status) { + const color = (status) => { + if (status.toLowerCase() === 'active') { + return 'success'; + } else if (status.toLowerCase() === 'disconnected') { + return 'danger'; + } else if (status.toLowerCase() === 'never_connected') { + return 'subdued'; } -
- ); - } + }; + + return ( + + + {status === 'never_connected' ? 'never connected' : status} + + + ); + } - addIconPlatformRender(agent) { - let icon = false; - const checkField = field => { - return field !== undefined ? field : '-'; + reloadAgent = () => { + this._isMount && + this.setState({ + isLoading: true, + }); + this.props.reload(); }; - const os = (agent || {}).os; - - if (((os || {}).uname || '').includes('Linux')) { - icon = 'linux'; - } else if ((os || {}).platform === 'windows') { - icon = 'windows'; - } else if ((os || {}).platform === 'darwin') { - icon = 'apple'; + + addUpgradeStatus(version, agent) { + const { managerVersion } = this.state; + return ( + + ); } - const os_name = - checkField(((agent || {}).os || {}).name) + - ' ' + - checkField(((agent || {}).os || {}).version); - - return ( - - {' '} - {os_name === '- -' ? '-' : os_name} - - ); - } + downloadCsv = () => { + const filters = this.buildFilter(); + const formatedFilters = Object.keys(filters) + .filter((field) => !['limit', 'offset', 'sort'].includes(field)) + .map((field) => ({ name: field, value: filters[field] })); + this.props.downloadCsv(formatedFilters); + }; + formattedButton() { + return ( + + + Export formatted + + + ); + } - addHealthStatusRender(status) { - const color = status => { - if (status.toLowerCase() === 'active') { - return 'success'; - } else if (status.toLowerCase() === 'disconnected') { - return 'danger'; - } else if (status.toLowerCase() === 'never_connected') { - return 'subdued'; - } + showToast = (color, title, text, time) => { + getToasts().add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); }; - return {status === 'never_connected' ? 'never connected' : status}; - } + /* MULTISELECT TABLE */ + onSelectionChange = (selectedItems) => { + const { managerVersion, pageSize } = this.state; - reloadAgent = () => { - this._isMount && this.setState({ - isLoading: true - }); - this.props.reload(); - }; - - addUpgradeStatus(version, agent) { - const { managerVersion } = this.state; - return ( - - ); - } + selectedItems.forEach((item) => { + if (managerVersion > item.version && item.version !== '.') { + item.outdated = true; + } + }); - downloadCsv = () => { - const filters = this.buildFilter(); - const formatedFilters = Object.keys(filters) - .filter(field => !['limit', 'offset', 'sort'].includes(field)) - .map(field => ({ name: field, value: filters[field] })) - this.props.downloadCsv(formatedFilters); - }; - formattedButton() { - return ( - - - Export formatted - - - ); - } + selectedItems.length !== pageSize + ? this._isMount && this.setState({ allSelected: false }) + : false; - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time - }); - }; - - /* MULTISELECT TABLE */ - onSelectionChange = selectedItems => { - const { managerVersion, pageSize } = this.state; - - selectedItems.forEach(item => { - if (managerVersion > item.version && item.version !== '.') { - item.outdated = true; - } - }); - - selectedItems.length !== pageSize - ? this._isMount && this.setState({ allSelected: false }) - : false; - - this._isMount && this.setState({ selectedItems }); - }; - - renderUpgradeButton() { - const { selectedItems } = this.state; - - if ( - selectedItems.length === 0 || - (selectedItems.length > 0 && - selectedItems.filter(item => item.outdated).length === 0) || - (selectedItems.length > 0 && - selectedItems.filter(item => item.upgrading).length > 0) || - (selectedItems.length > 0 && - selectedItems.filter(item => item.status === 'Active').length === 0) || - (selectedItems.length > 0 && - selectedItems.filter(item => item.status === 'Active').length === 0 && - selectedItems.filter(item => item.status === 'Disconnected').length > - 0) || - selectedItems.filter(item => item.outdated && item.status === 'Active') - .length === 0 - ) { - return; - } + this._isMount && this.setState({ selectedItems }); + }; - return ( - - - Upgrade{' '} - { - selectedItems.filter( - item => item.outdated && item.status === 'Active' - ).length - }{' '} - agents - - - ); - } + renderUpgradeButton() { + const { selectedItems } = this.state; + + if ( + selectedItems.length === 0 || + (selectedItems.length > 0 && selectedItems.filter((item) => item.outdated).length === 0) || + (selectedItems.length > 0 && selectedItems.filter((item) => item.upgrading).length > 0) || + (selectedItems.length > 0 && + selectedItems.filter((item) => item.status === 'Active').length === 0) || + (selectedItems.length > 0 && + selectedItems.filter((item) => item.status === 'Active').length === 0 && + selectedItems.filter((item) => item.status === 'Disconnected').length > 0) || + selectedItems.filter((item) => item.outdated && item.status === 'Active').length === 0 + ) { + return; + } - renderUpgradeButtonAll() { - const { selectedItems, avaibleAgents, managerVersion } = this.state; - - if ( - selectedItems.length > 0 && - avaibleAgents.filter( - agent => - agent.version !== 'Wazuh ' + managerVersion && - agent.status === 'Active' - ).length === 0 - ) { - return; + return ( + + + Upgrade{' '} + {selectedItems.filter((item) => item.outdated && item.status === 'Active').length}{' '} + agents + + + ); } - return ( - - - Upgrade all agents - - - ); - } + renderUpgradeButtonAll() { + const { selectedItems, avaibleAgents, managerVersion } = this.state; - renderRestartButton() { - const { selectedItems } = this.state; + if ( + selectedItems.length > 0 && + avaibleAgents.filter( + (agent) => agent.version !== 'Wazuh ' + managerVersion && agent.status === 'Active' + ).length === 0 + ) { + return; + } - if ( - selectedItems.length === 0 || - selectedItems.filter(item => item.status === 'Active').length === 0 - ) { - return; + return ( + + + Upgrade all agents + + + ); } - return ( - - - Restart{' '} - {selectedItems.filter(item => item.status === 'Active').length} agents - - - ); - } + renderRestartButton() { + const { selectedItems } = this.state; - renderRestartButtonAll() { - const { selectedItems, agentActive, avaibleAgents } = this.state; + if ( + selectedItems.length === 0 || + selectedItems.filter((item) => item.status === 'Active').length === 0 + ) { + return; + } - if ( - (selectedItems.length > 0 && - avaibleAgents.filter(item => item.status === 'Active').length === 0 && - selectedItems.length === 0) || - agentActive === 0 - ) { - return; + return ( + + + Restart {selectedItems.filter((item) => item.status === 'Active').length} agents + + + ); } - return ( - - - Restart all agents - - - ); - } + renderRestartButtonAll() { + const { selectedItems, agentActive, avaibleAgents } = this.state; - renderPurgeButton() { - const { selectedItems } = this.state; + if ( + (selectedItems.length > 0 && + avaibleAgents.filter((item) => item.status === 'Active').length === 0 && + selectedItems.length === 0) || + agentActive === 0 + ) { + return; + } - if (selectedItems.length === 0) { - return; + return ( + + + Restart all agents + + + ); } - return ( - - { - this.setState({ purgeModal: true }); - }} - > - Delete {selectedItems.length} agents - - - ); - } + renderPurgeButton() { + const { selectedItems } = this.state; - renderPurgeButtonAll() { - const { selectedItems, allSelected } = this.state; + if (selectedItems.length === 0) { + return; + } - if (selectedItems.length === 0 && !allSelected) { - return; + return ( + + { + this.setState({ purgeModal: true }); + }} + > + Delete {selectedItems.length} agents + + + ); } - return ( - - { - this._isMount && this.setState({ purgeModal: true }); - }} - > - Delete all agents - - - ); - } + renderPurgeButtonAll() { + const { selectedItems, allSelected } = this.state; - callOutRender() { - const { selectedItems, pageSize, allSelected, totalItems } = this.state; + if (selectedItems.length === 0 && !allSelected) { + return; + } - if (selectedItems.length === 0) { - return; - } else if (selectedItems.length === pageSize) { return ( -
- - + { + this._isMount && this.setState({ purgeModal: true }); + }} > - - - { - this._isMount && this.setState(prevState => ({ - allSelected: !prevState.allSelected - })); - }} - > - {allSelected - ? `Clear all agents selection (${totalItems})` - : `Select all agents (${totalItems})`} - - - - - -
+ Delete all agents + + ); } - } - - setUpgradingState(agentID) { - const { agents } = this.state; - agents.forEach(element => { - element.id === agentID ? (element.upgrading = true) : false; - }); - this._isMount && this.setState({ agents }); - } - changeUpgradingState = agentID => { - const { agents } = this.state; - agents.forEach(element => { - element.id === agentID && element.upgrading === true - ? (element.upgrading = false) - : false; - }); - this._isMount && this.setState(() => ({ agents })); - }; - - onClickUpgrade = () => { - const { selectedItems } = this.state; - ActionAgents.upgradeAgents(selectedItems); - }; - - onClickUpgradeAll = () => { - const { avaibleAgents, managerVersion } = this.state; - ActionAgents.upgradeAllAgents(avaibleAgents, managerVersion); - }; - - onClickRestart = () => { - const { selectedItems } = this.state; - ActionAgents.restartAgents(selectedItems); - this.reloadAgents(); - }; - - onClickRestartAll = () => { - const { avaibleAgents } = this.state; - ActionAgents.restartAllAgents(avaibleAgents); - this.reloadAgents(); - }; - - onClickPurge = () => { - const { selectedItems } = this.state; - const auxAgents = selectedItems - .map(agent => { - return agent.id !== '000' ? agent.id : null; - }) - .filter(agent => agent !== null); - - WzRequest.apiReq('DELETE', `/agents`, { - purge: true, - ids: auxAgents, - older_than: '1s' - }) - .then(value => { - value.status === 200 - ? this.showToast( - 'success', - `Selected agents were successfully deleted`, - '', - 5000 - ) - : this.showToast( - 'warning', - `Failed to delete selected agents`, - '', - 5000 - ); - }) - .catch(error => { - this.showToast( - 'danger', - `Failed to delete selected agents`, - error, - 5000 + callOutRender() { + const { selectedItems, pageSize, allSelected, totalItems } = this.state; + + if (selectedItems.length === 0) { + return; + } else if (selectedItems.length === pageSize) { + return ( +
+ + + + + { + this._isMount && + this.setState((prevState) => ({ + allSelected: !prevState.allSelected, + })); + }} + > + {allSelected + ? `Clear all agents selection (${totalItems})` + : `Select all agents (${totalItems})`} + + + + + +
); - }) - .finally(() => { - this.getAllItems(); - this.reloadAgents(); + } + } + + setUpgradingState(agentID) { + const { agents } = this.state; + agents.forEach((element) => { + element.id === agentID ? (element.upgrading = true) : false; }); - this._isMount && this.setState({ purgeModal: false }); - }; - - onClickPurgeAll = () => { - const { avaibleAgents } = this.state; - const auxAgents = avaibleAgents - .map(agent => { - return agent.id !== '000' ? agent.id : null; - }) - .filter(agent => agent !== null); - - WzRequest.apiReq('DELETE', `/agents`, { - purge: true, - ids: auxAgents, - older_than: '1s' - }) - .then(value => { - value.status === 200 - ? this.showToast( - 'success', - `All agents have been successfully deleted`, - '', - 5000 - ) - : this.showToast('warning', `Failed to delete all agents`, '', 5000); - }) - .catch(error => { - this.showToast('danger', `Failed to delete all agents`, error, 5000); - }) - .finally(() => { - this.getAllItems(); - this.reloadAgents(); + this._isMount && this.setState({ agents }); + } + + changeUpgradingState = (agentID) => { + const { agents } = this.state; + agents.forEach((element) => { + element.id === agentID && element.upgrading === true ? (element.upgrading = false) : false; }); + this._isMount && this.setState(() => ({ agents })); + }; - this._isMount && this.setState({ purgeModal: false }); - }; - - columns() { - return [ - { - field: 'id', - name: 'ID', - sortable: true, - width: '6%' - }, - { - field: 'name', - name: 'Name', - sortable: true, - width: '15%', - truncateText: true - }, - { - field: 'ip', - name: 'IP', - width: '10%', - truncateText: true, - sortable: true - }, - { - field: 'group', - name: 'Group(s)', - width: '20%', - truncateText: true, - sortable: true, - render: groups => groups !== '-' ? this.renderGroups(groups) : '-' - }, - { - field: 'os_name', - name: 'OS', - sortable: true, - width: '15%', - truncateText: true, - render: this.addIconPlatformRender - }, - { - field: 'node_name', - name: 'Cluster node', - width: '10%', - truncateText: true, - sortable: true - }, - { - field: 'version', - name: 'Version', - width: '5%', - truncateText: true, - sortable: true - /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ - }, - { - field: 'dateAdd', - name: 'Registration date', - width: '10%', - truncateText: true, - sortable: true - }, - { - field: 'lastKeepAlive', - name: 'Last keep alive', - width: '10%', - truncateText: true, - sortable: true - }, - { - field: 'status', - name: 'Status', - truncateText: true, - sortable: true, - width: '15%', - render: this.addHealthStatusRender - }, - { - align: 'right', - width: '5%', - field: 'actions', - name: 'Actions', - render: agent => this.actionButtonsRender(agent) - } - ]; - } + onClickUpgrade = () => { + const { selectedItems } = this.state; + ActionAgents.upgradeAgents(selectedItems); + }; - headRender() { - const formattedButton = this.formattedButton(); - return ( -
- - - - - {!!this.state.totalItems && ( - -

Agents ({this.state.totalItems})

-
- )} -
-
-
- - this.props.addingNewAgent()} - > - Deploy new agent - - - {formattedButton} -
- -
- ); - } + onClickUpgradeAll = () => { + const { avaibleAgents, managerVersion } = this.state; + ActionAgents.upgradeAllAgents(avaibleAgents, managerVersion); + }; - filterBarRender() { - return ( - - - this.setState({ filters, pageIndex: 0 })} - placeholder="Filter or search agent" - /> - - - this.reloadAgents()} - > - Refresh - - - - ); - } + onClickRestart = () => { + const { selectedItems } = this.state; + ActionAgents.restartAgents(selectedItems); + this.reloadAgents(); + }; - tableRender() { - const getRowProps = item => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - className: 'customRowClass', - onClick: () => { } - }; + onClickRestartAll = () => { + const { avaibleAgents } = this.state; + ActionAgents.restartAllAgents(avaibleAgents); + this.reloadAgents(); }; - const getCellProps = (item, column) => { - if (column.field == "actions") { - return - } - return { - onMouseDown: (ev) => { - AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": item.id, }); ev.stopPropagation() - } + onClickPurge = async () => { + const { selectedItems } = this.state; + const auxAgents = selectedItems + .map((agent) => { + return agent.id !== '000' ? agent.id : null; + }) + .filter((agent) => agent !== null); + try { + const response = await WzRequest.apiReq('DELETE', `/agents`, { + purge: true, + ids: auxAgents, + older_than: '1s', + }); + response.status === 200 + ? this.showToast('success', `Selected agents were successfully deleted`, '', 5000) + : this.showToast('warning', `Failed to delete selected agents`, '', 5000); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Failed to delete selected agents`, + }, + }; + getErrorOrchestrator().handleError(options); } + this.getAllItems(); + this.reloadAgents(); + this._isMount && this.setState({ purgeModal: false }); }; - const { - pageIndex, - pageSize, - totalItems, - agents, - sortField, - sortDirection, - isLoading - } = this.state; - const columns = this.columns(); - const pagination = - totalItems > 15 - ? { - pageIndex: pageIndex, - pageSize: pageSize, - totalItemCount: totalItems, - pageSizeOptions: [15, 25, 50, 100] - } - : false; - const sorting = { - sort: { - field: sortField, - direction: sortDirection + onClickPurgeAll = () => { + const { avaibleAgents } = this.state; + const auxAgents = avaibleAgents + .map((agent) => { + return agent.id !== '000' ? agent.id : null; + }) + .filter((agent) => agent !== null); + try { + const response = WzRequest.apiReq('DELETE', `/agents`, { + purge: true, + ids: auxAgents, + older_than: '1s', + }); + response.status === 200 + ? this.showToast('success', `All agents have been successfully deleted`, '', 5000) + : this.showToast('warning', `Failed to delete all agents`, '', 5000); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Failed to delete all agents`, + }, + }; + getErrorOrchestrator().handleError(options); } + this.getAllItems(); + this.reloadAgents(); + this._isMount && this.setState({ purgeModal: false }); }; - const selection = { - selectable: agent => agent.id, - /* onSelectionChange: this.onSelectionChange */ - }; + columns() { + return [ + { + field: 'id', + name: 'ID', + sortable: true, + width: '6%', + }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '15%', + truncateText: true, + }, + { + field: 'ip', + name: 'IP', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'group', + name: 'Group(s)', + width: '20%', + truncateText: true, + sortable: true, + render: (groups) => (groups !== '-' ? this.renderGroups(groups) : '-'), + }, + { + field: 'os_name', + name: 'OS', + sortable: true, + width: '15%', + truncateText: true, + render: this.addIconPlatformRender, + }, + { + field: 'node_name', + name: 'Cluster node', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'version', + name: 'Version', + width: '5%', + truncateText: true, + sortable: true, + /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ + }, + { + field: 'dateAdd', + name: 'Registration date', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'lastKeepAlive', + name: 'Last keep alive', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'status', + name: 'Status', + truncateText: true, + sortable: true, + width: '15%', + render: this.addHealthStatusRender, + }, + { + align: 'right', + width: '5%', + field: 'actions', + name: 'Actions', + render: (agent) => this.actionButtonsRender(agent), + }, + ]; + } - return ( - - - - - - ); - } + headRender() { + const formattedButton = this.formattedButton(); + return ( +
+ + + + + {!!this.state.totalItems && ( + +

Agents ({this.state.totalItems})

+
+ )} +
+
+
+ + this.props.addingNewAgent()} + > + Deploy new agent + + + {formattedButton} +
+ +
+ ); + } - filterGroupBadge = (group) => { - const { filters } = this.state; - let auxFilters = filters.map(filter => filter.value.match(/group=(.*S?)/)[1]); - if (filters.length > 0) { - !auxFilters.includes(group) ? - this.setState({ - filters: [...filters, { field: "q", value: `group=${group}` }], - }) : false; - } else { - this.setState({ - filters: [...filters, { field: "q", value: `group=${group}` }], - }) + filterBarRender() { + return ( + + + this.setState({ filters, pageIndex: 0 })} + placeholder="Filter or search agent" + /> + + + this.reloadAgents()}> + Refresh + + + + ); } - } - renderGroups(groups) { - return ( - - ) - } + tableRender() { + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + className: 'customRowClass', + onClick: () => {}, + }; + }; + + const getCellProps = (item, column) => { + if (column.field == 'actions') { + return; + } + return { + onMouseDown: (ev) => { + AppNavigate.navigateToModule(ev, 'agents', { tab: 'welcome', agent: item.id }); + ev.stopPropagation(); + }, + }; + }; - render() { - const { - allSelected, - purgeModal, - selectedItems, - loadingAllItem - } = this.state; - const title = this.headRender(); - const filter = this.filterBarRender(); - const upgradeButton = this.renderUpgradeButton(); - const restartButton = this.renderRestartButton(); - const purgeButton = this.renderPurgeButton(); - const upgradeButtonAll = this.renderUpgradeButtonAll(); - const restartButtonAll = this.renderRestartButtonAll(); - const purgeButtonAll = this.renderPurgeButtonAll(); - const table = this.tableRender(); - const callOut = this.callOutRender(); - let renderPurgeModal, loadItems, barButtons; - - if (purgeModal) { - renderPurgeModal = ( - - 15 + ? { + pageIndex: pageIndex, + pageSize: pageSize, + totalItemCount: totalItems, + pageSizeOptions: [15, 25, 50, 100], } - onCancel={() => { - this.setState({ purgeModal: false }); - }} - onConfirm={allSelected ? this.onClickPurgeAll : this.onClickPurge} - cancelButtonText="No, don't do it" - confirmButtonText="Yes, delete agents" - defaultFocusedButton="confirm" - buttonColor="danger" - > -

Are you sure you want to do this?

-
-
- ); - } + : false; + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + const selection = { + selectable: (agent) => agent.id, + /* onSelectionChange: this.onSelectionChange */ + }; - if (loadingAllItem) { - barButtons = ( + return ( - + ); - } else { - barButtons = ( - - {allSelected ? upgradeButtonAll : upgradeButton} - {allSelected ? restartButtonAll : restartButton} - {allSelected ? purgeButtonAll : purgeButton} - + } + + filterGroupBadge = (group) => { + const { filters } = this.state; + let auxFilters = filters.map((filter) => filter.value.match(/group=(.*S?)/)[1]); + if (filters.length > 0) { + !auxFilters.includes(group) + ? this.setState({ + filters: [...filters, { field: 'q', value: `group=${group}` }], + }) + : false; + } else { + this.setState({ + filters: [...filters, { field: 'q', value: `group=${group}` }], + }); + } + }; + + renderGroups(groups) { + return ( + ); } - return ( -
- {filter} - - - {title} - {loadItems} - {selectedItems.length > 0 && barButtons} - {callOut} - {table} - {renderPurgeModal} - -
- ); + render() { + const { allSelected, purgeModal, selectedItems, loadingAllItem } = this.state; + const title = this.headRender(); + const filter = this.filterBarRender(); + const upgradeButton = this.renderUpgradeButton(); + const restartButton = this.renderRestartButton(); + const purgeButton = this.renderPurgeButton(); + const upgradeButtonAll = this.renderUpgradeButtonAll(); + const restartButtonAll = this.renderRestartButtonAll(); + const purgeButtonAll = this.renderPurgeButtonAll(); + const table = this.tableRender(); + const callOut = this.callOutRender(); + let renderPurgeModal, loadItems, barButtons; + + if (purgeModal) { + renderPurgeModal = ( + + { + this.setState({ purgeModal: false }); + }} + onConfirm={allSelected ? this.onClickPurgeAll : this.onClickPurge} + cancelButtonText="No, don't do it" + confirmButtonText="Yes, delete agents" + defaultFocusedButton="confirm" + buttonColor="danger" + > +

Are you sure you want to do this?

+
+
+ ); + } + + if (loadingAllItem) { + barButtons = ( + + + + + + ); + } else { + barButtons = ( + + {allSelected ? upgradeButtonAll : upgradeButton} + {allSelected ? restartButtonAll : restartButton} + {allSelected ? purgeButtonAll : purgeButton} + + ); + } + + return ( +
+ {filter} + + + {title} + {loadItems} + {selectedItems.length > 0 && barButtons} + {callOut} + {table} + {renderPurgeModal} + +
+ ); + } } -}); +); AgentsTable.propTypes = { wzReq: PropTypes.func, @@ -1033,5 +1085,5 @@ AgentsTable.propTypes = { downloadCsv: PropTypes.func, clickAction: PropTypes.func, timeService: PropTypes.func, - reload: PropTypes.func + reload: PropTypes.func, }; diff --git a/public/controllers/agent/components/checkUpgrade.tsx b/public/controllers/agent/components/checkUpgrade.tsx index 66988a5efc..cff82809f9 100644 --- a/public/controllers/agent/components/checkUpgrade.tsx +++ b/public/controllers/agent/components/checkUpgrade.tsx @@ -11,78 +11,90 @@ */ import React, { Component } from 'react'; -import { - EuiLoadingSpinner, - EuiToolTip -} from '@elastic/eui'; +import { EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'CheckUpgrade'; export class CheckUpgrade extends Component { - props!: { - id: String, - version: String - agent: Object, - upgrading: Boolean, - managerVersion: String, - changeStatusUpdate: Function, - reloadAgent: Function, - }; - interval: any; + props!: { + id: String; + version: String; + agent: Object; + upgrading: Boolean; + managerVersion: String; + changeStatusUpdate: Function; + reloadAgent: Function; + }; + interval: any; - constructor(props) { - super(props); - } + constructor(props) { + super(props); + } - componentWillUnmount() { - clearInterval(this.interval); - } + componentWillUnmount() { + clearInterval(this.interval); + } - componentDidUpdate(prevProps) { - if (prevProps.upgrading !== this.props.upgrading) { - if (this.props.upgrading === true) - this.interval = setInterval(() => this.checkUpgrade(this.props.id), 3000); - } - } + componentDidUpdate(prevProps) { + if (prevProps.upgrading !== this.props.upgrading) { + if (this.props.upgrading === true) + this.interval = setInterval(() => this.checkUpgrade(this.props.id), 3000); + } + } - checkUpgrade(agentId) { - WzRequest.apiReq('GET', `/agents/${agentId}/upgrade_result`, {}).then(value => { - if (value.status === 200) { - this.props.changeStatusUpdate(agentId); - this.props.reloadAgent(); - clearInterval(this.interval); - console.log(`${this.props.id} agente termina intervalo`); - } - }) - .catch((error) => { - console.log(error); - }); - }; + async checkUpgrade(agentId) { + try { + const response = await WzRequest.apiReq('GET', `/agents/${agentId}/upgrade_result`, {}); + if (response.data === 200) { + this.props.changeStatusUpdate(agentId); + this.props.reloadAgent(); + clearInterval(this.interval); + console.log(`${this.props.id} agent ends interval`); + } + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } - addUpgraingProgress() { - const { id, version, upgrading, managerVersion } = this.props; + addUpgraingProgress() { + const { id, version, upgrading, managerVersion } = this.props; - if (version === '.' || version === managerVersion) { - return; - } else if (upgrading === true) { - /* this.interval = setInterval(() => this.checkUpgrade(id), 30000); */ - return ( - - - - ) - } - }; + if (version === '.' || version === managerVersion) { + return; + } else if (upgrading === true) { + /* this.interval = setInterval(() => this.checkUpgrade(id), 30000); */ + return ( + + + + ); + } + } - render() { - const { version } = this.props; - let upgrading = this.addUpgraingProgress(); + render() { + const { version } = this.props; + let upgrading = this.addUpgraingProgress(); - return ( -
- {version} -   - {upgrading} -
- ) - } -} \ No newline at end of file + return ( +
+ {version} +   + {upgrading} +
+ ); + } +} diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 1abd76464c..f3156e712a 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -33,680 +33,722 @@ import { EuiSpacer, EuiProgress, EuiCode, - EuiLink + EuiLink, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; -import { withErrorBoundary } from '../../../components/common/hocs' +import { withErrorBoundary } from '../../../components/common/hocs'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'RegisterAgent'; const architectureButtons = [ { id: 'i386', - label: 'i386' + label: 'i386', }, { id: 'x86_64', - label: 'x86_64' + label: 'x86_64', }, { id: 'armhf', - label: 'armhf' + label: 'armhf', }, { id: 'aarch64', - label: 'aarch64' - } + label: 'aarch64', + }, ]; const architectureCentos5 = [ { id: 'i386', - label: 'i386' + label: 'i386', }, { id: 'x86_64', - label: 'x86_64' - } + label: 'x86_64', + }, ]; const versionButtonsCentos = [ { id: 'centos5', - label: 'CentOS5' + label: 'CentOS5', }, { id: 'centos6', - label: 'CentOS6 or higher' - } + label: 'CentOS6 or higher', + }, ]; const osButtons = [ { id: 'rpm', - label: 'Red Hat / CentOS' + label: 'Red Hat / CentOS', }, { id: 'deb', - label: 'Debian / Ubuntu' + label: 'Debian / Ubuntu', }, { id: 'win', - label: 'Windows' + label: 'Windows', }, { id: 'macos', - label: 'MacOS' - } + label: 'MacOS', + }, ]; const sysButtons = [ { id: 'systemd', - label: 'Systemd' + label: 'Systemd', }, { id: 'sysV', - label: 'SysV Init' - } + label: 'SysV Init', + }, ]; const pTextCheckConnectionStyle = { marginTop: '3em', }; -export const RegisterAgent = withErrorBoundary (class RegisterAgent extends Component { - constructor(props) { - super(props); - this.wazuhConfig = new WazuhConfig(); - this.configuration = this.wazuhConfig.getConfig(); - this.state = { - status: 'incomplete', - selectedOS: '', - selectedSYS: '', - neededSYS: false, - selectedArchitecture: '', - selectedVersion: '', - version: '', - wazuhVersion: '', - serverAddress: '', - wazuhPassword: '', - groups: [], - selectedGroup: [], - udpProtocol: false, - }; - this.restartAgentCommand = { - rpm: this.systemSelector(), - deb: this.systemSelector(), - macos: 'sudo /Library/Ossec/bin/wazuh-control start', - }; - } +export const RegisterAgent = withErrorBoundary( + class RegisterAgent extends Component { + constructor(props) { + super(props); + this.wazuhConfig = new WazuhConfig(); + this.configuration = this.wazuhConfig.getConfig(); + this.state = { + status: 'incomplete', + selectedOS: '', + selectedSYS: '', + neededSYS: false, + selectedArchitecture: '', + selectedVersion: '', + version: '', + wazuhVersion: '', + serverAddress: '', + wazuhPassword: '', + groups: [], + selectedGroup: [], + udpProtocol: false, + }; + this.restartAgentCommand = { + rpm: this.systemSelector(), + deb: this.systemSelector(), + macos: 'sudo /Library/Ossec/bin/wazuh-control start', + }; + } - async componentDidMount() { - try { - this.setState({ loading: true }); - const wazuhVersion = await this.props.getWazuhVersion(); - let serverAddress = false; - let wazuhPassword = ''; - let hidePasswordInput = false; - serverAddress = this.configuration['enrollment.dns'] || false; - if (!serverAddress) { - serverAddress = await this.props.getCurrentApiAddress(); - } - let authInfo = await this.getAuthInfo(); - const needsPassword = (authInfo.auth || {}).use_password === 'yes'; - if (needsPassword) { - wazuhPassword = this.configuration['enrollment.password'] || authInfo['authd.pass'] || ''; - if (wazuhPassword) { - hidePasswordInput = true; + async componentDidMount() { + try { + this.setState({ loading: true }); + const wazuhVersion = await this.props.getWazuhVersion(); + let serverAddress = false; + let wazuhPassword = ''; + let hidePasswordInput = false; + serverAddress = this.configuration['enrollment.dns'] || false; + if (!serverAddress) { + serverAddress = await this.props.getCurrentApiAddress(); + } + let authInfo = await this.getAuthInfo(); + const needsPassword = (authInfo.auth || {}).use_password === 'yes'; + if (needsPassword) { + wazuhPassword = this.configuration['enrollment.password'] || authInfo['authd.pass'] || ''; + if (wazuhPassword) { + hidePasswordInput = true; + } } - } - const udpProtocol = await this.getRemoteInfo(); - const groups = await this.getGroups(); - this.setState({ - serverAddress, - needsPassword, - hidePasswordInput, - versionButtonsCentos, - architectureButtons, - architectureCentos5, - wazuhPassword, - udpProtocol, - wazuhVersion, - groups, - loading: false, - }); - } catch (error) { - this.setState({ - wazuhVersion: version, - loading: false, - }); + const udpProtocol = await this.getRemoteInfo(); + const groups = await this.getGroups(); + this.setState({ + serverAddress, + needsPassword, + hidePasswordInput, + versionButtonsCentos, + architectureButtons, + architectureCentos5, + wazuhPassword, + udpProtocol, + wazuhVersion, + groups, + loading: false, + }); + } catch (error) { + this.setState({ + wazuhVersion: version, + loading: false, + }); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + display: false, + store: false, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } - } - async getAuthInfo() { - try { - const result = await WzRequest.apiReq('GET', '/agents/000/config/auth/auth', {}); - return (result.data || {}).data || {}; - } catch (error) { - return false; + async getAuthInfo() { + try { + const result = await WzRequest.apiReq('GET', '/agents/000/config/auth/auth', {}); + return (result.data || {}).data || {}; + } catch (error) { + throw new Error(error); + } } - } - async getRemoteInfo() { - try { - const result = await WzRequest.apiReq('GET', '/agents/000/config/request/remote', {}); - const remote = ((result.data || {}).data || {}).remote || {}; - return (remote[0] || {}).protocol !== 'tcp' && (remote[0] || {}).protocol[0] !== 'TCP'; - } catch (error) { - return false; + async getRemoteInfo() { + try { + const result = await WzRequest.apiReq('GET', '/agents/000/config/request/remote', {}); + const remote = ((result.data || {}).data || {}).remote || {}; + return (remote[0] || {}).protocol !== 'tcp' && (remote[0] || {}).protocol[0] !== 'TCP'; + } catch (error) { + throw new Error(error); + } } - } - - selectOS(os) { - this.setState({ - selectedOS: os, - selectedVersion: '', - selectedArchitecture: '', - selectedSYS: 'systemd', - }); - } - - systemSelector() { - if (this.state.selectedOS === 'rpm') { - if (this.state.selectedSYS === 'systemd') { - return 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; - } else return 'sudo chkconfig --add wazuh-agent\nsudo service wazuh-agent start'; - } else if (this.state.selectedOS === 'deb') { - if (this.state.selectedSYS === 'systemd') { - return 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; - } else return 'sudo update-rc.d wazuh-agent defaults 95 10\nsudo service wazuh-agent start'; - } else return ''; - } - - selectSYS(sys) { - this.setState({ selectedSYS: sys }); - } - - setServerAddress(event) { - this.setState({ serverAddress: event.target.value }); - } - setGroupName(selectedGroup) { - this.setState({ selectedGroup }); - } - - setArchitecture(selectedArchitecture) { - this.setState({ selectedArchitecture }); - } - - setVersion(selectedVersion) { - this.setState({ selectedVersion, selectedArchitecture: '' }); - } + selectOS(os) { + this.setState({ + selectedOS: os, + selectedVersion: '', + selectedArchitecture: '', + selectedSYS: 'systemd', + }); + } - setWazuhPassword(event) { - this.setState({ wazuhPassword: event.target.value }); - } + systemSelector() { + if (this.state.selectedOS === 'rpm') { + if (this.state.selectedSYS === 'systemd') { + return 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; + } else return 'sudo chkconfig --add wazuh-agent\nsudo service wazuh-agent start'; + } else if (this.state.selectedOS === 'deb') { + if (this.state.selectedSYS === 'systemd') { + return 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; + } else return 'sudo update-rc.d wazuh-agent defaults 95 10\nsudo service wazuh-agent start'; + } else return ''; + } - obfuscatePassword(text) { - let obfuscate = ''; - const regex = /WAZUH_REGISTRATION_PASSWORD=?\040?\'(.*?)\'/gm; - const match = regex.exec(text); - const password = match[1]; - if (password) { - [...password].forEach(() => (obfuscate += '*')); - text = text.replace(password, obfuscate); + selectSYS(sys) { + this.setState({ selectedSYS: sys }); } - return text; - } - async getGroups() { - try { - const result = await WzRequest.apiReq('GET', '/groups', {}); - return result.data.data.affected_items.map((item) => ({ label: item.name, id: item.name })); - } catch (error) { - return []; + setServerAddress(event) { + this.setState({ serverAddress: event.target.value }); } - } - optionalDeploymentVariables() { - let deployment = `WAZUH_MANAGER='${this.state.serverAddress}' `; + setGroupName(selectedGroup) { + this.setState({ selectedGroup }); + } - if (this.state.selectedOS == 'win') { - deployment += `WAZUH_REGISTRATION_SERVER='${this.state.serverAddress}' `; + setArchitecture(selectedArchitecture) { + this.setState({ selectedArchitecture }); } - if (this.state.needsPassword) { - deployment += `WAZUH_REGISTRATION_PASSWORD='${this.state.wazuhPassword}' `; + setVersion(selectedVersion) { + this.setState({ selectedVersion, selectedArchitecture: '' }); } - if (this.state.udpProtocol) { - deployment += `WAZUH_PROTOCOL='UDP' `; + setWazuhPassword(event) { + this.setState({ wazuhPassword: event.target.value }); } - if (this.state.selectedGroup.length) { - deployment += `WAZUH_AGENT_GROUP='${this.state.selectedGroup.map((item) => item.label).join(',')}' `; + obfuscatePassword(text) { + let obfuscate = ''; + const regex = /WAZUH_REGISTRATION_PASSWORD=?\040?\'(.*?)\'/gm; + const match = regex.exec(text); + const password = match[1]; + if (password) { + [...password].forEach(() => (obfuscate += '*')); + text = text.replace(password, obfuscate); + } + return text; } - // macos doesnt need = param - if (this.state.selectedOS === 'macos') { - return deployment.replace(/=/g, ' '); + async getGroups() { + try { + const result = await WzRequest.apiReq('GET', '/groups', {}); + return result.data.data.affected_items.map((item) => ({ label: item.name, id: item.name })); + } catch (error) { + throw new Error(error); + } } - return deployment; - } + optionalDeploymentVariables() { + let deployment = `WAZUH_MANAGER='${this.state.serverAddress}' `; + + if (this.state.selectedOS == 'win') { + deployment += `WAZUH_REGISTRATION_SERVER='${this.state.serverAddress}' `; + } + + if (this.state.needsPassword) { + deployment += `WAZUH_REGISTRATION_PASSWORD='${this.state.wazuhPassword}' `; + } + if (this.state.udpProtocol) { + deployment += `WAZUH_PROTOCOL='UDP' `; + } + + if (this.state.selectedGroup.length) { + deployment += `WAZUH_AGENT_GROUP='${this.state.selectedGroup + .map((item) => item.label) + .join(',')}' `; + } - resolveRPMPackage() { - switch (`${this.state.selectedVersion}-${this.state.selectedArchitecture}`) { - case 'centos5-i386': - return `https://packages.wazuh.com/4.x/yum5/i386/wazuh-agent-${this.state.wazuhVersion}-1.el5.i386.rpm`; - case 'centos5-x86_64': - return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.el5.x86_64.rpm`; - case 'centos6-i386': - return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.i386.rpm`; - case 'centos6-aarch64': - return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.aarch64.rpm`; - case 'centos6-x86_64': - return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; - case 'centos6-armhf': - return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.armv7hl.rpm`; - default: - return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + // macos doesnt need = param + if (this.state.selectedOS === 'macos') { + return deployment.replace(/=/g, ' '); + } + + return deployment; } - } - resolveDEBPackage() { - switch (`${this.state.selectedArchitecture}`) { - case 'i386': - return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_i386.deb`; - case 'aarch64': - return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_arm64.deb`; - case 'armhf': - return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_armhf.deb`; - case 'x86_64': - return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_amd64.deb`; - default: - return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_amd64.deb`; + resolveRPMPackage() { + switch (`${this.state.selectedVersion}-${this.state.selectedArchitecture}`) { + case 'centos5-i386': + return `https://packages.wazuh.com/4.x/yum5/i386/wazuh-agent-${this.state.wazuhVersion}-1.el5.i386.rpm`; + case 'centos5-x86_64': + return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.el5.x86_64.rpm`; + case 'centos6-i386': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.i386.rpm`; + case 'centos6-aarch64': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.aarch64.rpm`; + case 'centos6-x86_64': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + case 'centos6-armhf': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.armv7hl.rpm`; + default: + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + } } - } - optionalPackages() { - switch (this.state.selectedOS) { - case 'rpm': - return this.resolveRPMPackage(); - case 'deb': - return this.resolveDEBPackage(); - default: - return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + resolveDEBPackage() { + switch (`${this.state.selectedArchitecture}`) { + case 'i386': + return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_i386.deb`; + case 'aarch64': + return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_arm64.deb`; + case 'armhf': + return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_armhf.deb`; + case 'x86_64': + return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_amd64.deb`; + default: + return `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${this.state.wazuhVersion}-1_amd64.deb`; + } } - } - checkMissingOSSelection() { - if (!this.state.selectedOS) { - return ['Operating system']; + optionalPackages() { + switch (this.state.selectedOS) { + case 'rpm': + return this.resolveRPMPackage(); + case 'deb': + return this.resolveDEBPackage(); + default: + return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + } } - switch (this.state.selectedOS) { - case 'rpm': - return [ - ...(!this.state.selectedVersion ? ['OS version'] : []), - ...(this.state.selectedVersion && !this.state.selectedArchitecture - ? ['OS architecture'] - : []), - ]; - case 'deb': - return [...(!this.state.selectedArchitecture ? ['OS architecture'] : [])]; - default: - return []; + + checkMissingOSSelection() { + if (!this.state.selectedOS) { + return ['Operating system']; + } + switch (this.state.selectedOS) { + case 'rpm': + return [ + ...(!this.state.selectedVersion ? ['OS version'] : []), + ...(this.state.selectedVersion && !this.state.selectedArchitecture + ? ['OS architecture'] + : []), + ]; + case 'deb': + return [...(!this.state.selectedArchitecture ? ['OS architecture'] : [])]; + default: + return []; + } } - } - render() { - const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); - const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; - const textAndLinkToCheckConnectionDocumentation = ( -

- To verify the connection with the Manager, please follow this{' '} - - document. - -

- ); - const missingOSSelection = this.checkMissingOSSelection(); - const ipInput = ( - -

- You can predefine the Wazuh server address with the enrollment.dns{' '} - Wazuh app setting. + render() { + const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); + const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; + const textAndLinkToCheckConnectionDocumentation = ( +

+ To verify the connection with the Manager, please follow this{' '} + + document. +

+ ); + const missingOSSelection = this.checkMissingOSSelection(); + const ipInput = ( + +

+ You can predefine the Wazuh server address with the enrollment.dns{' '} + Wazuh app setting. +

+ this.setServerAddress(event)} + /> +
+ ); + + const groupInput = ( + +

Select one or more existing groups

+ { + this.setGroupName(group); + }} + isDisabled={!this.state.groups.length} + isClearable={true} + data-test-subj="demoComboBox" + /> +
+ ); + + const passwordInput = ( this.setServerAddress(event)} + placeholder="Wazuh password" + value={this.state.wazuhPassword} + onChange={(event) => this.setWazuhPassword(event)} /> -
- ); - - const groupInput = ( - -

Select one or more existing groups

- { - this.setGroupName(group); - }} - isDisabled={!this.state.groups.length} - isClearable={true} - data-test-subj="demoComboBox" - /> -
- ); - - const passwordInput = ( - this.setWazuhPassword(event)} - /> - ); - - const codeBlock = { - zIndex: '100', - }; - const customTexts = { - rpmText: `sudo ${this.optionalDeploymentVariables()}yum install ${this.optionalPackages()}`, - debText: `curl -so wazuh-agent-${this.state.wazuhVersion}.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent-${this.state.wazuhVersion}.deb`, - macosText: `curl -so wazuh-agent-${this.state.wazuhVersion}.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${ - this.state.wazuhVersion - }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${this.state.wazuhVersion}.pkg -target /`, - winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${ - this.state.wazuhVersion - }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${this.state.wazuhVersion}.msi /q ${this.optionalDeploymentVariables()}`, - }; - - const field = `${this.state.selectedOS}Text`; - const text = customTexts[field]; - const language = this.state.selectedOS === 'win' ? 'ps' : 'bash'; - const windowsAdvice = this.state.selectedOS === 'win' && ( - <> - - - - ); - const restartAgentCommand = this.restartAgentCommand[this.state.selectedOS]; - const onTabClick = (selectedTab) => { - this.selectSYS(selectedTab.id); - }; - - const guide = ( -
- {this.state.selectedOS && ( - -

You can use this command to install and enroll the Wazuh agent in one or more hosts.

- Running this command on a host with an agent already installed upgrades the agent package without enrolling the agent. To enroll it, see the Wazuh documentation.} - iconType="iInCircle" - /> - - - {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} - - {windowsAdvice} - - {(copy) => ( - - Copy command - - )} - -
- )} -
- ); - - const tabs = [ - { - id: 'systemd', - name: 'Systemd', - content: ( - - - - - {this.systemSelector()} - - - {(copy) => ( - - Copy command - - )} - - {textAndLinkToCheckConnectionDocumentation} - - - ), - }, - { - id: 'sysV', - name: 'SysV Init', - content: ( - - + ); + + const codeBlock = { + zIndex: '100', + }; + const customTexts = { + rpmText: `sudo ${this.optionalDeploymentVariables()}yum install ${this.optionalPackages()}`, + debText: `curl -so wazuh-agent-${ + this.state.wazuhVersion + }.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent-${ + this.state.wazuhVersion + }.deb`, + macosText: `curl -so wazuh-agent-${ + this.state.wazuhVersion + }.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${ + this.state.wazuhVersion + }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${ + this.state.wazuhVersion + }.pkg -target /`, + winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${ + this.state.wazuhVersion + }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${ + this.state.wazuhVersion + }.msi /q ${this.optionalDeploymentVariables()}`, + }; + + const field = `${this.state.selectedOS}Text`; + const text = customTexts[field]; + const language = this.state.selectedOS === 'win' ? 'ps' : 'bash'; + const windowsAdvice = this.state.selectedOS === 'win' && ( + <> + + + + ); + const restartAgentCommand = this.restartAgentCommand[this.state.selectedOS]; + const onTabClick = (selectedTab) => { + this.selectSYS(selectedTab.id); + }; + + const guide = ( +
+ {this.state.selectedOS && ( +

+ You can use this command to install and enroll the Wazuh agent in one or more hosts. +

+ + Running this command on a host with an agent already installed upgrades the + agent package without enrolling the agent. To enroll it, see the{' '} + + Wazuh documentation + + . + + } + iconType="iInCircle" + /> + - {this.systemSelector()} + {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} - + {windowsAdvice} + {(copy) => ( Copy command )} - {textAndLinkToCheckConnectionDocumentation}
- - ), - }, - ]; - - const steps = [ - { - title: 'Choose the Operating system', - children: ( - this.selectOS(os)} - /> - ), - }, - ...(this.state.selectedOS == 'rpm' - ? [ - { - title: 'Choose the version', - children: ( - this.setVersion(version)} - /> - ), - }, - ] - : []), - ...(this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos5' - ? [ - { - title: 'Choose the architecture', - children: ( - this.setArchitecture(architecture)} - /> - ), - }, - ] - : []), - ...(this.state.selectedOS == 'deb' || - (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6') - ? [ - { - title: 'Choose the architecture', - children: ( - this.setArchitecture(architecture)} - /> - ), - }, - ] - : []), - { - title: 'Wazuh server address', - children: {ipInput}, - }, - ...(!(!this.state.needsPassword || this.state.hidePasswordInput) - ? [ - { - title: 'Wazuh password', - children: {passwordInput}, - }, - ] - : []), - { - title: 'Assign the agent to a group', - children: {groupInput}, - }, - { - title: 'Install and enroll the agent', - children: missingOSSelection.length ? ( - - ) : ( -
{guide}
- ), - }, - ...(this.state.selectedOS == 'rpm' || this.state.selectedOS == 'deb' - ? [ - { - title: 'Start the agent', - children: missingOSSelection.length ? ( - - ) : ( - - ), - }, - ] - : []), - - ...(!missingOSSelection.length && - this.state.selectedOS !== 'rpm' && - this.state.selectedOS !== 'deb' && - restartAgentCommand - ? [ - { - title: 'Start the agent', - children: ( - - - - {restartAgentCommand} - - - {(copy) => ( - - Copy command - - )} - - - - ), - }, - ] - : []), - ]; - return ( -
- - - - - - - - -

Deploy a new agent

-
-
- - {this.props.hasAgents && ( - this.props.addNewAgent(false)} - iconType="cross" - > - Close - - )} - {!this.props.hasAgents && ( - this.props.reload()} - iconType="refresh" - > - Refresh - - )} - + )} +
+ ); + + const tabs = [ + { + id: 'systemd', + name: 'Systemd', + content: ( + + + + + {this.systemSelector()} + + + {(copy) => ( + + Copy command + + )} + + {textAndLinkToCheckConnectionDocumentation} + + + ), + }, + { + id: 'sysV', + name: 'SysV Init', + content: ( + + + + + {this.systemSelector()} + + + {(copy) => ( + + Copy command + + )} + + {textAndLinkToCheckConnectionDocumentation} + + + ), + }, + ]; + + const steps = [ + { + title: 'Choose the Operating system', + children: ( + this.selectOS(os)} + /> + ), + }, + ...(this.state.selectedOS == 'rpm' + ? [ + { + title: 'Choose the version', + children: ( + this.setVersion(version)} + /> + ), + }, + ] + : []), + ...(this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos5' + ? [ + { + title: 'Choose the architecture', + children: ( + this.setArchitecture(architecture)} + /> + ), + }, + ] + : []), + ...(this.state.selectedOS == 'deb' || + (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6') + ? [ + { + title: 'Choose the architecture', + children: ( + this.setArchitecture(architecture)} + /> + ), + }, + ] + : []), + { + title: 'Wazuh server address', + children: {ipInput}, + }, + ...(!(!this.state.needsPassword || this.state.hidePasswordInput) + ? [ + { + title: 'Wazuh password', + children: {passwordInput}, + }, + ] + : []), + { + title: 'Assign the agent to a group', + children: {groupInput}, + }, + { + title: 'Install and enroll the agent', + children: missingOSSelection.length ? ( + + ) : ( +
{guide}
+ ), + }, + ...(this.state.selectedOS == 'rpm' || this.state.selectedOS == 'deb' + ? [ + { + title: 'Start the agent', + children: missingOSSelection.length ? ( + + ) : ( + + ), + }, + ] + : []), + + ...(!missingOSSelection.length && + this.state.selectedOS !== 'rpm' && + this.state.selectedOS !== 'deb' && + restartAgentCommand + ? [ + { + title: 'Start the agent', + children: ( + + + + {restartAgentCommand} + + + {(copy) => ( + + Copy command + + )} + + - - {this.state.loading && ( - <> + ), + }, + ] + : []), + ]; + return ( +
+ + + + + + - + +

Deploy a new agent

+
- - - )} - {!this.state.loading && ( - - - - )} -
-
-
-
-
-
- ); + + {this.props.hasAgents && ( + this.props.addNewAgent(false)} + iconType="cross" + > + Close + + )} + {!this.props.hasAgents && ( + this.props.reload()} + iconType="refresh" + > + Refresh + + )} + + + + {this.state.loading && ( + <> + + + + + + )} + {!this.state.loading && ( + + + + )} + + + + + +
+ ); + } } -}) +); From 5cbd0c1e4953725d47c2cffcb978404667b0c4f2 Mon Sep 17 00:00:00 2001 From: pablomarga Date: Mon, 28 Jun 2021 13:02:42 +0200 Subject: [PATCH 027/493] Changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf247c5f9a..9a4b0e3320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +- Refactored all try catch strategy on Controller/Agent section [#3398](https://github.com/wazuh/wazuh-kibana-app/issues/3398) ### Fixed From 511f1b40b8aa290203592b9d95215b6b0785e76e Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Mon, 28 Jun 2021 15:57:14 +0200 Subject: [PATCH 028/493] Now for level.Error always displayError (#3395) * feat(error-orchestrator): Now for level.Error always displayError + prettier. * feat(error-orchestrator): PR comments. * feat(error-orchestrator): PR comments --- .../error-orchestrator/error-orchestrator-base.ts | 12 ++++++++---- .../error-orchestrator/error-orchestrator-ui.ts | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.ts b/public/react-services/error-orchestrator/error-orchestrator-base.ts index 7b3bace617..653bd2db08 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-base.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-base.ts @@ -13,6 +13,13 @@ import loglevel from 'loglevel'; import { GenericRequest } from '../../react-services/generic-request'; import { ErrorOrchestrator, UIErrorLog } from './types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; + +const winstonLevelDict = { + [UI_LOGGER_LEVELS.ERROR]: 'error', + [UI_LOGGER_LEVELS.WARNING]: 'warn', + [UI_LOGGER_LEVELS.INFO]: 'info', +}; export class ErrorOrchestratorBase implements ErrorOrchestrator { public loadErrorLog(errorLog: UIErrorLog) { @@ -26,10 +33,7 @@ export class ErrorOrchestratorBase implements ErrorOrchestrator { private async storeError(errorLog: UIErrorLog) { try { - let winstonLevel = errorLog.level.toLowerCase(); - if(errorLog.level === 'WARNING'){ - winstonLevel = 'warn'; - } + const winstonLevel = winstonLevelDict[errorLog.level.toLowerCase()] || 'error'; await GenericRequest.request('POST', `/utils/logs/ui`, { message: errorLog.error.message, diff --git a/public/react-services/error-orchestrator/error-orchestrator-ui.ts b/public/react-services/error-orchestrator/error-orchestrator-ui.ts index 03d992b110..5c61247419 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-ui.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-ui.ts @@ -16,6 +16,13 @@ import { UI_LOGGER_LEVELS } from '../../../common/constants'; import loglevel from 'loglevel'; export class ErrorOrchestratorUI extends ErrorOrchestratorBase { + public loadErrorLog(errorLog: UIErrorLog) { + super.loadErrorLog({ + ...errorLog, + display: errorLog.level === UI_LOGGER_LEVELS.ERROR || errorLog.display, + }); + } + public displayError(errorLog: UIErrorLog) { switch (errorLog.level) { case UI_LOGGER_LEVELS.INFO: From 21d3b9c1075da5fc90e089529500b1b648266a4a Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Mon, 28 Jun 2021 20:01:06 +0200 Subject: [PATCH 029/493] Refactor try catch in Settings section (#3393) * feat(settings): Applied try catch strategy and errorOrchestrator service. * docs(settings): Updated changelog * test(fieldform): Added simple snapshot test * feat(settings): PR comments. --- CHANGELOG.md | 1 + public/components/settings/api/add-api.js | 18 ++ public/components/settings/api/api-is-down.js | 20 +++ public/components/settings/api/api-table.js | 45 +++-- .../configuration/components/bottom-bar.tsx | 33 ++-- .../__snapshots__/field-form.test.tsx.snap | 67 +++++++ .../category/components/field-form.test.tsx | 39 ++++ .../category/components/field-form.tsx | 21 ++- public/components/settings/modules/modules.js | 170 +++++++++++------- .../components/settings/settings-logs/logs.js | 2 +- public/controllers/settings/index.js | 10 ++ public/react-services/app-state.js | 18 +- .../error-orchestrator-base.ts | 8 +- 13 files changed, 356 insertions(+), 96 deletions(-) create mode 100644 public/components/settings/configuration/components/categories/components/category/components/__snapshots__/field-form.test.tsx.snap create mode 100644 public/components/settings/configuration/components/categories/components/category/components/field-form.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index cf247c5f9a..f35ec8ff36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +- Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) ### Fixed diff --git a/public/components/settings/api/add-api.js b/public/components/settings/api/add-api.js index e10e8bf5d0..37cc373c07 100644 --- a/public/components/settings/api/add-api.js +++ b/public/components/settings/api/add-api.js @@ -26,7 +26,11 @@ import { EuiPanel } from '@elastic/eui'; import { withErrorBoundary } from '../../common/hocs'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +const errorContext = 'AddApi'; export const AddApi = withErrorBoundary (class AddApi extends Component { constructor(props) { super(props); @@ -93,6 +97,20 @@ export const AddApi = withErrorBoundary (class AddApi extends Component { 'Wazuh API not reachable, please review your configuration', fetchingData: false }); + + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + store: true, + error: { + error: error, + message: error.message || error, + title: `Wazuh API not reachable, please review your configuration: ${error.message || error}`, + }, + }; + + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/settings/api/api-is-down.js b/public/components/settings/api/api-is-down.js index c81f9f87e5..0bb2f3401e 100644 --- a/public/components/settings/api/api-is-down.js +++ b/public/components/settings/api/api-is-down.js @@ -31,7 +31,13 @@ import { EuiPanel } from '@elastic/eui'; import { withErrorBoundary } from '../../common/hocs'; +import { + UI_ERROR_SEVERITIES, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +const errorContext = 'ApiIsDown' export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { constructor(props) { super(props); @@ -102,6 +108,20 @@ export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { ) { this.setState({ error: error.data.message, status: 'danger' }); } + + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/settings/api/api-table.js b/public/components/settings/api/api-table.js index 3bdab8d65e..9b5704b541 100644 --- a/public/components/settings/api/api-table.js +++ b/public/components/settings/api/api-table.js @@ -34,8 +34,13 @@ import { AppState } from '../../../react-services/app-state'; import { API_USER_STATUS_RUN_AS } from '../../../../server/lib/cache-api-user-has-run-as'; import { withErrorBoundary, withReduxProvider } from '../../common/hocs'; import { compose } from 'redux' +import { + UI_ERROR_SEVERITIES, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; - +const errorContext = 'ApiTable'; export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiTable extends Component { constructor(props) { super(props); @@ -122,16 +127,34 @@ export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiT entries[idx].status = 'online'; } catch (error) { const code = ((error || {}).data || {}).code; - const downReason = typeof error === 'string' ? error : - (error || {}).message || ((error || {}).data || {}).message || 'Wazuh is not reachable'; + const downReason = + typeof error === 'string' + ? error + : (error || {}).message || + ((error || {}).data || {}).message || + 'Wazuh is not reachable'; const status = code === 3099 ? 'down' : 'unknown'; - entries[idx].status = { status, downReason }; + entries[idx].status = { status, downReason }; + throw error; + } finally { + this.setState({ + apiEntries: entries, + }); } - this.setState({ - apiEntries: entries - }); } catch (error) { - console.error('Error checking manager connection ', error); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error checking manager connection: ${error.message || error}`, + }, + }; + + getErrorOrchestrator().handleError(options); } } @@ -236,7 +259,7 @@ export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiT type='check' /> - + ) : value === API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED ? ( Set as default

}} iconType={ @@ -310,7 +333,7 @@ export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiT
this.props.showAddApi()} diff --git a/public/components/settings/configuration/components/bottom-bar.tsx b/public/components/settings/configuration/components/bottom-bar.tsx index 72c8fd7f83..bb2b404fc8 100644 --- a/public/components/settings/configuration/components/bottom-bar.tsx +++ b/public/components/settings/configuration/components/bottom-bar.tsx @@ -25,8 +25,16 @@ import { EuiButton } from '@elastic/eui'; import { WazuhConfig } from '../../../../react-services/wazuh-config'; - - +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; + +const errorContext = 'BottomBar' interface IBottomBarProps { updatedConfig: { [setting: string]: string | number | boolean | object } setUpdateConfig(setting: {}): void @@ -96,7 +104,19 @@ const saveSettings = async (updatedConfig: {}, setUpdateConfig: Function, setLoa successToast(); setUpdateConfig({}); } catch (error) { - errorToast(error); + const options: UIErrorLog = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error saving the configuration: ${error.message || error}`, + }, + }; + + getErrorOrchestrator().handleError(options); } finally { setLoading(false); } @@ -164,13 +184,6 @@ const successToast = () => { }); } -const errorToast = (error) => { - getToasts().add({ - color: 'danger', - title: `Error saving the configuration: ${error.message || error}`, - }); -} - const formatValueCachedConfiguration = (value) => typeof value === 'string' ? isNaN(Number(value)) ? value : Number(value) : value; diff --git a/public/components/settings/configuration/components/categories/components/category/components/__snapshots__/field-form.test.tsx.snap b/public/components/settings/configuration/components/categories/components/category/components/__snapshots__/field-form.test.tsx.snap new file mode 100644 index 0000000000..55482f7924 --- /dev/null +++ b/public/components/settings/configuration/components/categories/components/category/components/__snapshots__/field-form.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldForm component renders correctly to match the snapshot 1`] = ` + + + + +
+
+ + + + +
+
+
+
+
+
+`; diff --git a/public/components/settings/configuration/components/categories/components/category/components/field-form.test.tsx b/public/components/settings/configuration/components/categories/components/category/components/field-form.test.tsx new file mode 100644 index 0000000000..8324b3bbd7 --- /dev/null +++ b/public/components/settings/configuration/components/categories/components/category/components/field-form.test.tsx @@ -0,0 +1,39 @@ +/* + * Wazuh app - React test for FieldForm component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { FieldForm } from './field-form'; +import { ISetting } from '../../../../../configuration'; +import { mount } from 'enzyme'; + +describe('FieldForm component', () => { + it('renders correctly to match the snapshot', () => { + const item: ISetting = { + setting: 'string', + value: 'boolean', + description: 'string', + category: 'string', + name: 'string', + form: { type: 'text', params: {} } + }; + const updatedConfig = {}; + const setUpdatedConfig = jest.fn(); + + const wrapper = mount( + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx index fec7a208ca..5f055a4592 100644 --- a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx +++ b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx @@ -25,7 +25,15 @@ import 'brace/mode/javascript'; import 'brace/snippets/javascript'; import 'brace/ext/language_tools'; import "brace/ext/searchbox"; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, UIErrorSeverity, + UILogLevel, +} from '../../../../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../../../../react-services/common-services'; +const errorContext = 'FieldForm'; interface IFieldForm { item: ISetting updatedConfig: { [field: string]: string | number | boolean | [] } @@ -110,7 +118,18 @@ const ArrayForm: React.FunctionComponent = (props) => { const parsed = JSON.parse(list); onChange(parsed, props); } catch (error) { - console.log(error); + const options: UIErrorLog = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + + getErrorOrchestrator().handleError(options); } } return ( diff --git a/public/components/settings/modules/modules.js b/public/components/settings/modules/modules.js index c0c07613c1..5dd8d65105 100644 --- a/public/components/settings/modules/modules.js +++ b/public/components/settings/modules/modules.js @@ -1,79 +1,117 @@ import React, { Component } from 'react'; import { + EuiBadge, + EuiBetaBadge, + EuiDescriptionList, EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiPage, - EuiDescriptionList, - EuiTitle, - EuiBadge, - EuiBetaBadge, + EuiPanel, + EuiSpacer, EuiSwitch, - EuiSpacer + EuiTitle, } from '@elastic/eui'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { AppState } from '../../../react-services/app-state'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; import store from '../../../redux/store'; import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; -import { withUserAuthorizationPrompt, withErrorBoundary, withReduxProvider } from '../../common/hocs'; -import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../../common/constants'; -import { compose } from 'redux' +import { + withErrorBoundary, + withReduxProvider, + withUserAuthorizationPrompt, +} from '../../common/hocs'; +import { UI_LOGGER_LEVELS, WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../../common/constants'; +import { compose } from 'redux'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'EnableModulesWrapper'; export class EnableModulesWrapper extends Component { constructor(props) { - super(props); - this.currentApi = JSON.parse(AppState.getCurrentAPI()).id; - this.state = { - extensions: [], - groups: [ - { - title: 'Security Information Management', - modules: [ - { name: 'general', default: true, agent: false }, - { name: 'fim', default: true, agent: false }, - { name: 'aws', default: false, agent: false }, - { name: 'gcp', default: false, agent: false } - ] - }, - { - title: 'Auditing and Policy Monitoring', - modules: [ - { name: 'pm', default: true, agent: false }, - { name: 'sca', default: true, agent: false }, - { name: 'audit', default: true, agent: false }, - { name: 'oscap', default: false, agent: false }, - { name: 'ciscat', default: false, agent: false } - ] - }, - { - title: 'Threat Detection and Response', - modules: [ - { name: 'vuls', default: true, agent: false }, - { name: 'mitre', default: true, agent: false }, - { name: 'virustotal', default: false, agent: false }, - { name: 'osquery', default: false, agent: false }, - { name: 'docker', default: false, agent: false }, - ] + try { + super(props); + this.currentApi = JSON.parse(AppState.getCurrentAPI(true)).id; + this.state = { + extensions: [], + groups: [ + { + title: 'Security Information Management', + modules: [ + { name: 'general', default: true, agent: false }, + { name: 'fim', default: true, agent: false }, + { name: 'aws', default: false, agent: false }, + { name: 'gcp', default: false, agent: false } + ] + }, + { + title: 'Auditing and Policy Monitoring', + modules: [ + { name: 'pm', default: true, agent: false }, + { name: 'sca', default: true, agent: false }, + { name: 'audit', default: true, agent: false }, + { name: 'oscap', default: false, agent: false }, + { name: 'ciscat', default: false, agent: false } + ] + }, + { + title: 'Threat Detection and Response', + modules: [ + { name: 'vuls', default: true, agent: false }, + { name: 'mitre', default: true, agent: false }, + { name: 'virustotal', default: false, agent: false }, + { name: 'osquery', default: false, agent: false }, + { name: 'docker', default: false, agent: false }, + ] + }, + { + title: 'Regulatory Compliance', + modules: [ + { name: 'pci', default: true, agent: false }, + { name: 'nist', default: true, agent: false }, + { name: 'gdpr', default: false, agent: false }, + { name: 'hipaa', default: false, agent: false }, + { name: 'tsc', default: false, agent: false } + ] + } + ] + }; + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, }, - { - title: 'Regulatory Compliance', - modules: [ - { name: 'pci', default: true, agent: false }, - { name: 'nist', default: true, agent: false }, - { name: 'gdpr', default: false, agent: false }, - { name: 'hipaa', default: false, agent: false }, - { name: 'tsc', default: false, agent: false } - ] - } - ] - }; + }; + + getErrorOrchestrator().handleError(options); + } } async componentDidMount() { - store.dispatch(updateSelectedSettingsSection('modules')); - const extensions = await AppState.getExtensions(this.currentApi); - this.setState({ extensions }); + try { + store.dispatch(updateSelectedSettingsSection('modules')); + const extensions = await AppState.getExtensions(this.currentApi); + this.setState({ extensions }); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); + } } toggleExtension(extension) { @@ -82,7 +120,19 @@ export class EnableModulesWrapper extends Component { this.setState({ extensions }); try { this.currentApi && AppState.setExtensions(this.currentApi, extensions); - } catch (error) {} //eslint-disable-line + } catch (error) { + const options = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); + } } buildModuleGroup(extensions) { @@ -170,4 +220,4 @@ export const EnableModules = compose ( withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]) -)(EnableModulesWrapper); \ No newline at end of file +)(EnableModulesWrapper); diff --git a/public/components/settings/settings-logs/logs.js b/public/components/settings/settings-logs/logs.js index 369f40efaf..b8d7841785 100644 --- a/public/components/settings/settings-logs/logs.js +++ b/public/components/settings/settings-logs/logs.js @@ -137,4 +137,4 @@ class SettingsLogs extends Component { export default withErrorBoundary( SettingsLogs -) \ No newline at end of file +) diff --git a/public/controllers/settings/index.js b/public/controllers/settings/index.js index 2188922d2e..98052092da 100644 --- a/public/controllers/settings/index.js +++ b/public/controllers/settings/index.js @@ -21,6 +21,16 @@ import {WzSampleDataWrapper} from '../../components/add-modules-data/WzSampleDat import { getAngularModule } from '../../kibana-services'; const app = getAngularModule(); + +EnableModules.displayName = 'EnableModules'; +WzSampleDataWrapper.displayName = 'WzSampleDataWrapper'; +WzConfigurationSettings.displayName = 'WzConfigurationSettings'; +SettingsLogs.displayName = 'SettingsLogs'; +SettingsMiscellaneous.displayName = 'SettingsMiscellaneous'; +ApiTable.displayName = 'ApiTable'; +AddApi.displayName = 'AddApi'; +ApiIsDown.displayName = 'ApiIsDown'; + app .controller('settingsController', SettingsController) .value('EnableModules', EnableModules) diff --git a/public/react-services/app-state.js b/public/react-services/app-state.js index 2909fd8ad3..1fa87b1761 100644 --- a/public/react-services/app-state.js +++ b/public/react-services/app-state.js @@ -73,10 +73,8 @@ export class AppState { return extensions; } } - } catch (err) { - console.log('Error get extensions'); - console.log(err); - throw err; + } catch (error) { + throw error; } }; @@ -181,10 +179,8 @@ export class AppState { try { const currentAPI = getCookies().get('currentApi'); return currentAPI ? decodeURI(currentAPI) : false; - } catch (err) { - console.log('Error get current Api'); - console.log(err); - throw err; + } catch (error) { + throw error; } } @@ -358,8 +354,8 @@ export class AppState { } if (navigate) { const encodedURI = encodeURI(JSON.stringify(navigate)); - getCookies().set('navigate', encodedURI, { - path: window.location.pathname + getCookies().set('navigate', encodedURI, { + path: window.location.pathname }); } } @@ -402,7 +398,7 @@ export class AppState { title: 'CSV', text: 'Error generating CSV', toastLifeTimeMs: 4000, - }); + }); } return; } diff --git a/public/react-services/error-orchestrator/error-orchestrator-base.ts b/public/react-services/error-orchestrator/error-orchestrator-base.ts index 653bd2db08..ba75723517 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-base.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-base.ts @@ -32,13 +32,17 @@ export class ErrorOrchestratorBase implements ErrorOrchestrator { } private async storeError(errorLog: UIErrorLog) { + const getLocation = () => { + return errorLog?.context || errorLog.error?.error?.stack instanceof String || 'No context'; + } + try { const winstonLevel = winstonLevelDict[errorLog.level.toLowerCase()] || 'error'; await GenericRequest.request('POST', `/utils/logs/ui`, { - message: errorLog.error.message, + message: errorLog?.error?.message || 'No message', level: winstonLevel, - location: errorLog.context || errorLog.error.error.stack, + location: getLocation(), }); } catch (error) { loglevel.error('Failed on request [POST /utils/logs/ui]', error); From dea23e073350ca7625687e88c18c1b431eb8ddba Mon Sep 17 00:00:00 2001 From: pablomarga Date: Tue, 29 Jun 2021 09:41:56 +0200 Subject: [PATCH 030/493] Remove old line --- public/controllers/agent/agents-preview.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index 415ac5f011..29fdaecb38 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -244,7 +244,6 @@ export class AgentsPreviewController { }, }; getErrorOrchestrator().handleError(options); - this.errorInit = ErrorHandler.handle(error, '', { silent: true }); } this.loading = false; this.$scope.$applyAsync(); From 3cfd7311b54e94093cbc604887455942552cb01c Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Tue, 29 Jun 2021 18:24:54 +0200 Subject: [PATCH 031/493] Added click outside detector component to euiFlyout --- .../wz-logtest/components/logtest.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index d8c7d62bae..9ebc5b26d5 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -25,6 +25,7 @@ import { EuiSpacer, EuiTextArea, EuiTitle, + EuiOutsideClickDetector, } from '@elastic/eui'; import { WzRequest } from '../../../react-services'; import { withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; @@ -229,26 +230,25 @@ export const Logtest = compose( )) || ( - { + + { props.openCloseFlyout(); - }} - > - props.openCloseFlyout()}> - - - {props.isRuleset.includes('rules') ?

Ruleset Test

:

Decoders Test

} -
-
- - - - - - {buildLogtest()} - -
+ }}> + props.openCloseFlyout()}> + + + {props.isRuleset.includes('rules') ?

Ruleset Test

:

Decoders Test

} +
+
+ + + + + + {buildLogtest()} + +
+
)}
From 44acc0fbfc45b070815977dfad2c3cc56eb55fd2 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 30 Jun 2021 10:54:48 +0200 Subject: [PATCH 032/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35ec8ff36..2e1f975c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From e1a2d9b6bbf5d98ca6dfcaabee151a366d073b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 30 Jun 2021 11:45:00 +0200 Subject: [PATCH 033/493] fix(frontent_wz_menu_agent): Fix problem in the Agent menu with a missing componente property - Fix a missing component property in the component to update the agent data --- public/components/common/welcome/components/menu-agent.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index c12e42b469..129b91d434 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -16,6 +16,7 @@ import { AppState } from '../../../../react-services/app-state'; import { hasAgentSupportModule } from '../../../../react-services/wz-agents'; import { getAngularModule, getToasts } from '../../../../kibana-services'; import { WAZUH_MODULES_ID } from '../../../../../common/constants'; +import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; class WzMenuAgent extends Component { constructor(props) { @@ -260,7 +261,11 @@ const mapStateToProps = state => { }; }; +const mapDispatchToProps = dispatch => ({ + updateCurrentAgentData: (agentData) => dispatch(updateCurrentAgentData(agentData)) +}); + export default connect( mapStateToProps, - null + mapDispatchToProps )(WzMenuAgent); From 9ebcc579dedf6e1b6eb76bce27812c6702031f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 30 Jun 2021 11:48:54 +0200 Subject: [PATCH 034/493] feat(frontend_module_github_dashboard): Add the Dashboard visualizations to the GitHub module - Add visualizations for overview and agents - Add metrics to the module for the count of Organizations, Repositories and Actors - Removed not necessary /server/integration-files/visualizations/agents/index.js file because there is a .ts file --- .../components/overview/metrics/metrics.tsx | 5 + .../visualize/agent-visualizations.js | 45 +- public/components/visualize/visualizations.js | 45 +- .../visualizations/agents/agents-github.ts | 502 ++++++++++++++++++ .../visualizations/agents/index.js | 52 -- .../visualizations/agents/index.ts | 4 +- .../visualizations/overview/index.ts | 4 +- .../overview/overview-github.ts | 501 +++++++++++++++++ 8 files changed, 1102 insertions(+), 56 deletions(-) create mode 100644 server/integration-files/visualizations/agents/agents-github.ts delete mode 100644 server/integration-files/visualizations/agents/index.js create mode 100644 server/integration-files/visualizations/overview/overview-github.ts diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index 939bc5da9b..002b7807b8 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -101,6 +101,11 @@ export const Metrics = withAllowedAgents(class Metrics extends Component { { name: "Last scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}}, { name: "Highest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "secondary"}, { name: "Lowest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "asc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "danger"}, + ], + github: [ + { name: "Organizations", type: "unique-count", field: "data.github.org"}, + { name: "Repositories", type: "unique-count", field: "data.github.repo", color: "secondary"}, + { name: "Actors", type: "unique-count", field: "data.github.actor", color: "danger"}, ] } } diff --git a/public/components/visualize/agent-visualizations.js b/public/components/visualize/agent-visualizations.js index 6eb6313433..224989304d 100644 --- a/public/components/visualize/agent-visualizations.js +++ b/public/components/visualize/agent-visualizations.js @@ -874,5 +874,48 @@ export const agentVisualizations = { ] } ] - } + }, + github: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alerts evolution by organization', + id: 'Wazuh-App-Overview-GitHub-Alerts-Evolution-By-Organization', + width: 60 + }, + { + title: 'Top 5 organizations by alerts', + id: 'Wazuh-App-Overview-GitHub-Top-5-Organizations-By-Alerts', + width: 40 + } + ] + }, + { + height: 360, + vis: [ + { + title: 'Top alerts by action type and organization', + id: 'Wazuh-App-Overview-GitHub-Alert-Action-Type-By-Organization', + width: 40 + }, + { + title: 'Users with more alerts', + id: 'Wazuh-App-Overview-GitHub-Users-With-More-Alerts', + width: 60 + } + ] + }, + { + hide: true, + vis: [ + { + title: 'Alerts summary', + id: 'Wazuh-App-Overview-GitHub-Alert-Summary', + } + ] + } + ] + }, }; diff --git a/public/components/visualize/visualizations.js b/public/components/visualize/visualizations.js index 7675656924..0f4a9ad6b1 100644 --- a/public/components/visualize/visualizations.js +++ b/public/components/visualize/visualizations.js @@ -954,5 +954,48 @@ export const visualizations = { ] } ] - } + }, + github: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alerts evolution by organization', + id: 'Wazuh-App-Overview-GitHub-Alerts-Evolution-By-Organization', + width: 60 + }, + { + title: 'Top 5 organizations by alerts', + id: 'Wazuh-App-Overview-GitHub-Top-5-Organizations-By-Alerts', + width: 40 + } + ] + }, + { + height: 360, + vis: [ + { + title: 'Top alerts by action type and organization', + id: 'Wazuh-App-Overview-GitHub-Alert-Action-Type-By-Organization', + width: 40 + }, + { + title: 'Users with more alerts', + id: 'Wazuh-App-Overview-GitHub-Users-With-More-Alerts', + width: 60 + } + ] + }, + { + hide: true, + vis: [ + { + title: 'Alerts summary', + id: 'Wazuh-App-Overview-GitHub-Alert-Summary', + } + ] + } + ] + }, }; diff --git a/server/integration-files/visualizations/agents/agents-github.ts b/server/integration-files/visualizations/agents/agents-github.ts new file mode 100644 index 0000000000..d89c92af39 --- /dev/null +++ b/server/integration-files/visualizations/agents/agents-github.ts @@ -0,0 +1,502 @@ +/* + * Wazuh app - Module for Agents/GitHub visualizations + * Copyright (C) 2015-2021 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 default [ + { + _id: 'Wazuh-App-Overview-GitHub-Alerts-Evolution-By-Organization', + _source: { + title: 'Alerts evolution by organization', + visState: JSON.stringify({ + "title": "Alerts evolution by organization", + "type": "area", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "date_histogram", + "params": { + "field": "timestamp", + "timeRange": { + "from": "now-7d", + "to": "now" + }, + "useNormalizedEsInterval": true, + "scaleMetricValues": false, + "interval": "auto", + "drop_partials": false, + "min_doc_count": 1, + "extended_bounds": {}, + "customLabel": "" + }, + "schema": "segment" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } + ], + "params": { + "type": "area", + "grid": { + "categoryLines": false + }, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" + }, + "labels": { + "show": true, + "filter": true, + "truncate": 100, + "rotate": 0 + }, + "title": {} + } + ], + "valueAxes": [ + { + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" + }, + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 + }, + "title": { + "text": "Count" + } + } + ], + "seriesParams": [ + { + "show": true, + "type": "line", + "mode": "normal", + "data": { + "label": "Count", + "id": "1" + }, + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "showCircles": true, + "interpolate": "linear", + "valueAxis": "ValueAxis-1" + } + ], + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + }, + "labels": {}, + "orderBucketsBySum": false + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-5-Organizations-By-Alerts', + _source: { + title: 'Top 5 organizations by alerts', + visState: JSON.stringify({ + "title": "Top 5 organizations by alerts", + "type": "pie", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + } + ], + "params": { + "type": "pie", + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "isDonut": false, + "labels": { + "show": false, + "values": true, + "last_level": true, + "truncate": 100 + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Users-With-More-Alerts', + _source: { + title: 'Users with more alerts', + visState: JSON.stringify({ + "title": "Users with more alerts", + "type": "line", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.actor", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } + ], + "params": { + "type": "line", + "grid": { + "categoryLines": false + }, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" + }, + "labels": { + "show": true, + "filter": true, + "truncate": 100 + }, + "title": {} + } + ], + "valueAxes": [ + { + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" + }, + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 + }, + "title": { + "text": "Count" + } + } + ], + "seriesParams": [ + { + "show": true, + "type": "histogram", + "mode": "stacked", + "data": { + "label": "Count", + "id": "1" + }, + "valueAxis": "ValueAxis-1", + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "interpolate": "linear", + "showCircles": true + } + ], + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "labels": {}, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Alert-Action-Type-By-Organization', + _source: { + title: 'Top alerts by alert action type and organization', + visState: JSON.stringify({ + "title": "Top alerts by alert action type and organization", + "type": "pie", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.action", + "orderBy": "1", + "order": "desc", + "size": 3, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + } + ], + "params": { + "type": "pie", + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "isDonut": true, + "labels": { + "show": false, + "values": true, + "last_level": true, + "truncate": 100 + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Alert-Summary', + _source: { + title: 'Alert summary', + visState: JSON.stringify({ + "title": "Alert summary", + "type": "table", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "agent.name", + "orderBy": "1", + "order": "desc", + "size": 50, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 10, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.description", + "orderBy": "1", + "order": "desc", + "size": 10, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + } + ], + "params": { + "perPage": 10, + "showPartialRows": false, + "showMetricsAtAllLevels": false, + "sort": { + "columnIndex": null, + "direction": null + }, + "showTotal": false, + "totalFunc": "sum", + "percentageCol": "" + } + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + } +]; diff --git a/server/integration-files/visualizations/agents/index.js b/server/integration-files/visualizations/agents/index.js deleted file mode 100644 index 4f33d7ca78..0000000000 --- a/server/integration-files/visualizations/agents/index.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Wazuh app - Module to export agents visualizations raw content - * Copyright (C) 2015-2021 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 audit from './agents-audit'; -import fim from './agents-fim'; -import general from './agents-general'; -import gcp from './agents-gcp'; -import oscap from './agents-oscap'; -import ciscat from './agents-ciscat'; -import pci from './agents-pci'; -import gdpr from './agents-gdpr'; -import hipaa from './agents-hipaa'; -import mitre from './agents-mitre'; -import nist from './agents-nist'; -import tsc from './agents-tsc'; -import pm from './agents-pm'; -import virustotal from './agents-virustotal'; -import vuls from './agents-vuls'; -import osquery from './agents-osquery'; -import docker from './agents-docker'; -import welcome from './agents-welcome'; -import aws from './agents-aws'; - -export { - audit, - fim, - general, - gcp, - oscap, - ciscat, - pci, - gdpr, - hipaa, - nist, - tsc, - pm, - virustotal, - vuls, - osquery, - mitre, - docker, - welcome, - aws -}; diff --git a/server/integration-files/visualizations/agents/index.ts b/server/integration-files/visualizations/agents/index.ts index 4f33d7ca78..092e97f23d 100644 --- a/server/integration-files/visualizations/agents/index.ts +++ b/server/integration-files/visualizations/agents/index.ts @@ -28,6 +28,7 @@ import osquery from './agents-osquery'; import docker from './agents-docker'; import welcome from './agents-welcome'; import aws from './agents-aws'; +import github from './agents-github'; export { audit, @@ -48,5 +49,6 @@ export { mitre, docker, welcome, - aws + aws, + github }; diff --git a/server/integration-files/visualizations/overview/index.ts b/server/integration-files/visualizations/overview/index.ts index 1a65fefbad..e1744c977f 100644 --- a/server/integration-files/visualizations/overview/index.ts +++ b/server/integration-files/visualizations/overview/index.ts @@ -27,6 +27,7 @@ import vuls from './overview-vuls'; import mitre from './overview-mitre'; import osquery from './overview-osquery'; import docker from './overview-docker'; +import github from './overview-github'; export { audit, @@ -46,5 +47,6 @@ export { vuls, mitre, osquery, - docker + docker, + github }; diff --git a/server/integration-files/visualizations/overview/overview-github.ts b/server/integration-files/visualizations/overview/overview-github.ts new file mode 100644 index 0000000000..82eb13c856 --- /dev/null +++ b/server/integration-files/visualizations/overview/overview-github.ts @@ -0,0 +1,501 @@ +/* + * Wazuh app - Module for Overview/GitHub visualizations + * Copyright (C) 2015-2021 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 default [ + { + _id: 'Wazuh-App-Overview-GitHub-Alerts-Evolution-By-Organization', + _source: { + title: 'Alerts evolution by organization', + visState: JSON.stringify({ + "title": "Alerts evolution by organization", + "type": "area", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "date_histogram", + "params": { + "field": "timestamp", + "timeRange": { + "from": "now-7d", + "to": "now" + }, + "useNormalizedEsInterval": true, + "scaleMetricValues": false, + "interval": "auto", + "drop_partials": false, + "min_doc_count": 1, + "extended_bounds": {}, + "customLabel": "" + }, + "schema": "segment" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } + ], + "params": { + "type": "area", + "grid": { + "categoryLines": false + }, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" + }, + "labels": { + "show": true, + "filter": true, + "truncate": 100, + "rotate": 0 + }, + "title": {} + } + ], + "valueAxes": [ + { + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" + }, + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 + }, + "title": { + "text": "Count" + } + } + ], + "seriesParams": [ + { + "show": true, + "type": "line", + "mode": "normal", + "data": { + "label": "Count", + "id": "1" + }, + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "showCircles": true, + "interpolate": "linear", + "valueAxis": "ValueAxis-1" + } + ], + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + }, + "labels": {}, + "orderBucketsBySum": false + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-5-Organizations-By-Alerts', + _source: { + title: 'Top 5 organizations by alerts', + visState: JSON.stringify({ + "title": "Top 5 organizations by alerts", + "type": "pie", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + } + ], + "params": { + "type": "pie", + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "isDonut": false, + "labels": { + "show": false, + "values": true, + "last_level": true, + "truncate": 100 + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Users-With-More-Alerts', + _source: { + title: 'Users with more alerts', + visState: JSON.stringify({ + "title": "Users with more alerts", + "type": "line", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.actor", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } + ], + "params": { + "type": "line", + "grid": { + "categoryLines": false + }, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" + }, + "labels": { + "show": true, + "filter": true, + "truncate": 100 + }, + "title": {} + } + ], + "valueAxes": [ + { + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" + }, + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 + }, + "title": { + "text": "Count" + } + } + ], + "seriesParams": [ + { + "show": true, + "type": "histogram", + "mode": "stacked", + "data": { + "label": "Count", + "id": "1" + }, + "valueAxis": "ValueAxis-1", + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "interpolate": "linear", + "showCircles": true + } + ], + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "labels": {}, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Alert-Action-Type-By-Organization', + _source: { + title: 'Top alerts by alert action type and organization', + visState: JSON.stringify({ + "title": "Top alerts by alert action type and organization", + "type": "pie", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.action", + "orderBy": "1", + "order": "desc", + "size": 3, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + } + ], + "params": { + "type": "pie", + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "isDonut": true, + "labels": { + "show": false, + "values": true, + "last_level": true, + "truncate": 100 + } + } + }), + uiStateJSON: '', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Alert-Summary', + _source: { + title: 'Alert summary', + visState: JSON.stringify({ + "title": "Alert summary", + "type": "table", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "agent.name", + "orderBy": "1", + "order": "desc", + "size": 50, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.github.org", + "orderBy": "1", + "order": "desc", + "size": 10, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.description", + "orderBy": "1", + "order": "desc", + "size": 10, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "bucket" + } + ], + "params": { + "perPage": 10, + "showPartialRows": false, + "showMetricsAtAllLevels": false, + "sort": { + "columnIndex": null, + "direction": null + }, + "showTotal": false, + "totalFunc": "sum", + "percentageCol": "" + } + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + } + }, + _type: 'visualization', + } +]; From 57c625907fe06985ac64087fe04a0b7411cd9222 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 30 Jun 2021 16:20:18 -0300 Subject: [PATCH 035/493] Implement try catch strategy ManagementController (#3374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Applied new strategy try-catch error handling in ManagementController * Added lowercase levels in storeError (#3377) * Added lowercase levels in storeError * Updated CHANGELOG Co-authored-by: Ibarra Maximiliano * [Feature] Mitre Att&ck Intelligence + adapt Framework (#3368) * refactor in vulnerabilities table component * refactor code in vuls inventory and add new table component with export csv * adapat table * finished refactor table component * delete console logs and fix wrong version * add new fields in suggestions * add changelog * changes in component table and remove status and type columns * fix columns position * feat(mitre): Add Mitre Att&ck intelligence section - Created Intelligence tab in Mitre Att&ck Module - Created left and right panel - Created resource button for the left panel - Created search bar for searhin in all resources - Created list of each resource * feat(mitre_intelligence): Modify the search results view and another improvements - Modify the Search results view - Improve useAsyncAction hook - Add Mitre Att&ck Intelligence to Agent modules component - Improve TableWithSearchBar component to accept filters as props - Refactor Mitre Atta&ck resources - Refator PanelSplit component - Fix filtersToObject helper - Update test * feat(mitre_att&ck_intelligence): Render description as markdown - Create Markdown component - Apply the Markdown component to the resource description in the resouce table * feat(mitre_atta&ck_intelligence): Add like operator to search resource by description * change endpoint and adapt component in mitre * fix flyout * feat(mirte_att&ck_integillence): Add the References resource - Added to left panel - Added resource view * add redirect to intelligence * fix merge * fix merge flyout * fix package version * fix(mitre_att&ck_intelligence): Organize resource suggestions and remove Reference resource * fix PR comments and add intelligence section redirect * Created new Mitre flyout * Changelog * fix comments PR * add redirect values in query params * apply prettier * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout * clear comments and imports * fix error handler techniques * delete session storage * delete files and fix get techniques data * Created new Mitre flyout (#3344) * Created new Mitre flyout * Changelog * Erasing comments * Erasing console.log * Resolving comments and upgrading code * Applying comments upgrades to references table * Applying more upgrade comments * fix(mitre_intelligence): Remove the Promise.reject in resource details flyout Co-authored-by: Antonio David Gutiérrez * fix redirect flyout to rules * feat(frontend/mitre_att&ck_intelligence): Removed welcome intelligence - Removed welcome intelligence view and adjustments when doing a general search - Set a resource type as selected - Update test * fix comments PR * fix(mitre_att&ck_intelligence): Change how to open the resource details flyout - Change how to open the resource details flyout - Refactor some componentes properties - Removed not used code * changelog: Added PR to chengelog * Update CHANGELOG.md * fix(mitre_att&ck_intelligence): Fix error in table-default.tsx * fix(mitre_att&ck_intelligence): PR request changes: - Add tests for: - Components: Markdown, PanelSplit - React Hooks: useAsyncAction - Renamed files - Add justification for using dangerouslySetInnerHTML property - Refactor requests to get mitre techniques - Fix tooltips to open tactic/technique details in Framework - Added some missing semicolon - Fix CSS class wz-markdown-wrapper name * fix get mitre Techniques from api Co-authored-by: eze9252 Co-authored-by: CPAlejandro Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: Franco Charriol * feat(error-orchestrator): Improved on createGetterSetter (#3376) * feat(error-orchestrator): Improved on createGetterSetter * bugfix(error-orchestrator): Added default value of disaply and store, remove location of types and fixed toastMessage of addError * feat(error-orchestrator): Added creatorGetterSetter on wazuh-app to avoid dependence on Kibana. * Fix creation of json file after a ui log (#3378) * Updated error orchestrator implemetation * Update import errorOchestratorService * Updated imported orchestrator getter * Added context ManagementeController * Updated CHANGELOG Co-authored-by: Ibarra Maximiliano Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> Co-authored-by: eze9252 Co-authored-by: CPAlejandro Co-authored-by: Alejandro Cuéllar Peinado Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Co-authored-by: Franco Charriol Co-authored-by: Gabriel Wassan Co-authored-by: Pablo Martínez --- CHANGELOG.md | 2 + public/components/common/hooks/index.ts | 2 +- public/controllers/management/management.js | 177 +++++++++++++------- public/plugin.ts | 1 + 4 files changed, 117 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01659167b5..fcb25d84c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) - Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) +- Implement try catch strategy ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) + ### Changed diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 87e5151c2e..270031ca7f 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -23,4 +23,4 @@ export * from './useAllowedAgents'; export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; -export * from './use_async_action'; \ No newline at end of file +export * from './use_async_action'; diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index 30d2670441..3ee255eed1 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -15,7 +15,14 @@ import { WazuhConfig } from '../../react-services/wazuh-config'; import { WzRequest } from '../../react-services/wz-request'; import { ErrorHandler } from '../../react-services/error-handler'; import { ShareAgent } from '../../factories/share-agent'; -import { RulesetHandler, RulesetResources } from './components/management/ruleset/utils/ruleset-handler'; +import { + RulesetHandler, + RulesetResources, +} from './components/management/ruleset/utils/ruleset-handler'; + +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { getErrorOrchestrator } from '../../react-services/common-services'; export class ManagementController { /** @@ -23,14 +30,7 @@ export class ManagementController { * @param {*} $scope * @param {*} $location */ - constructor( - $scope, - $rootScope, - $location, - configHandler, - errorHandler, - $interval - ) { + constructor($scope, $rootScope, $location, configHandler, errorHandler, $interval) { this.$scope = $scope; this.$rootScope = $rootScope; this.$location = $location; @@ -48,6 +48,8 @@ export class ManagementController { this.logtestOpened = false; this.uploadOpened = false; this.rulesetTab = RulesetResources.RULES; + this.context = 'ManagementController'; + this.$scope.$on('setCurrentGroup', (ev, params) => { this.currentGroup = (params || {}).currentGroup || false; @@ -105,7 +107,7 @@ export class ManagementController { this.$scope.$on('viewFileOnly', (ev, params) => { $scope.$broadcast('viewFileOnlyTable', { file: params.item, - path: params.path + path: params.path, }); }); @@ -141,11 +143,9 @@ export class ManagementController { this.$scope.$applyAsync(); }); - this.$rootScope.$on('performRestart', ev => { + this.$rootScope.$on('performRestart', (ev) => { ev.stopPropagation(); - this.clusterInfo.status === 'enabled' - ? this.restartCluster() - : this.restartManager(); + this.clusterInfo.status === 'enabled' ? this.restartCluster() : this.restartManager(); }); this.$rootScope.timeoutIsReady; @@ -163,18 +163,18 @@ export class ManagementController { }) this.welcomeCardsProps = { - switchTab: (tab, setNav) => this.switchTab(tab, setNav) + switchTab: (tab, setNav) => this.switchTab(tab, setNav), }; this.managementTabsProps = { - clickAction: tab => this.switchTab(tab, true), + clickAction: (tab) => this.switchTab(tab, true), selectedTab: this.tab, tabs: [ { id: 'status', name: 'Status' }, { id: 'logs', name: 'Logs' }, { id: 'monitoring', name: 'Cluster' }, - { id: 'reporting', name: 'Reporting' } - ] + { id: 'reporting', name: 'Reporting' }, + ], }; this.logtestProps = { @@ -185,18 +185,18 @@ export class ManagementController { }; this.managementProps = { - switchTab: section => this.switchTab(section, true), + switchTab: (section) => this.switchTab(section, true), section: '', groupsProps: {}, configurationProps: { agent: { - id: '000' + id: '000', }, // TODO: get dynamically the agent? - updateWazuhNotReadyYet: status => { + updateWazuhNotReadyYet: (status) => { this.$rootScope.wazuhNotReadyYet = status; this.$scope.$applyAsync(); }, - wazuhNotReadyYet: () => this.$rootScope.wazuhNotReadyYet + wazuhNotReadyYet: () => this.$rootScope.wazuhNotReadyYet, }, logtestProps: this.logtestProps, }; @@ -207,26 +207,41 @@ export class ManagementController { * When controller loads */ $onInit() { - this.clusterInfo = AppState.getClusterInfo(); + try { + this.clusterInfo = AppState.getClusterInfo(); - if (this.shareAgent.getAgent() && this.shareAgent.getSelectedGroup()) { - this.tab = 'groups'; - this.switchTab(this.tab); - return; - } + if (this.shareAgent.getAgent() && this.shareAgent.getSelectedGroup()) { + this.tab = 'groups'; + this.switchTab(this.tab); + return; + } - const location = this.$location.search(); + const location = this.$location.search(); - if (location && location.tab) { - this.tab = location.tab; - this.switchTab(this.tab); - } + if (location && location.tab) { + this.tab = location.tab; + this.switchTab(this.tab); + } - this.uploadFilesProps = { - msg: this.$scope.mctrl.rulesetTab, - path: `etc/${this.$scope.mctrl.rulesetTab}`, - upload: (files) => this.uploadFiles(files, this.$scope.mctrl.rulesetTab) - }; + this.uploadFilesProps = { + msg: this.$scope.mctrl.rulesetTab, + path: `etc/${this.$scope.mctrl.rulesetTab}`, + upload: (files) => this.uploadFiles(files, this.$scope.mctrl.rulesetTab), + }; + } catch (error) { + const errorOptions = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + context: this.context, + error: { + error: error, + message: error?.message || '', + title: 'Error restarting cluster', + }, + }; + + getErrorOrchestrator().handleError(errorOptions); + } } /** @@ -249,10 +264,19 @@ export class ManagementController { } catch (error) { this.isRestarting = false; this.$scope.$applyAsync(); - ErrorHandler.handle( - error.message || error, - 'Error restarting manager' - ); + + const errorOptions = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + context: this.context, + error: { + error: error, + message: error?.message || '', + title: 'Error restarting manager', + }, + }; + + getErrorOrchestrator().handleError(errorOptions); } } @@ -263,16 +287,22 @@ export class ManagementController { await this.configHandler.restartCluster(); this.isRestarting = false; this.$scope.$applyAsync(); - ErrorHandler.info( - 'Restarting cluster, it will take up to 30 seconds.' - ); + ErrorHandler.info('Restarting cluster, it will take up to 30 seconds.'); } catch (error) { this.isRestarting = false; this.$scope.$applyAsync(); - ErrorHandler.handle( - error.message || error, - 'Error restarting cluster' - ); + const errorOptions = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + context: this.context, + error: { + error: error, + message: error?.message || '', + title: 'Error restarting cluster', + }, + }; + + getErrorOrchestrator().handleError(errorOptions); } } @@ -287,7 +317,7 @@ export class ManagementController { this.$location.search('editSubTab', tab); this.$scope.$broadcast('configurationIsReloaded', { globalConfigTab: this.globalConfigTab, - reloadConfigSubTab: true + reloadConfigSubTab: true, }); } @@ -341,8 +371,7 @@ export class ManagementController { this.currentList = false; this.managementTabsProps.selectedTab = this.tab; } - this.managementProps.section = - this.tab === 'ruleset' ? this.rulesetTab : this.tab; + this.managementProps.section = this.tab === 'ruleset' ? this.rulesetTab : this.tab; this.$location.search('tab', this.tab); this.loadNodeList(); } @@ -407,17 +436,25 @@ export class ManagementController { const clusterEnabled = clusterInfo.status === 'enabled'; if (clusterEnabled) { const response = await WzRequest.apiReq('GET', '/cluster/nodes', {}); - const nodeList = - (((response || {}).data || {}).data || {}).items || false; + const nodeList = (((response || {}).data || {}).data || {}).items || false; if (Array.isArray(nodeList) && nodeList.length) { - this.nodeList = nodeList.map(item => item.name).reverse(); - this.selectedNode = nodeList.filter( - item => item.type === 'master' - )[0].name; + this.nodeList = nodeList.map((item) => item.name).reverse(); + this.selectedNode = nodeList.filter((item) => item.type === 'master')[0].name; } } } catch (error) { - console.log(error.message || error); // eslint-disable-line + const errorOptions = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + context: this.context, + error: { + error: error, + message: error?.message || '', + title: 'Error loading node list' + }, + }; + + getErrorOrchestrator().handleError(errorOptions); } this.loadingNodes = false; this.$scope.$applyAsync(); @@ -445,7 +482,7 @@ export class ManagementController { this.uploadFilesProps = { msg: this.rulesetTab, path: `etc/${this.rulesetTab}`, - upload: (files) => this.uploadFiles(files, this.rulesetTab) + upload: (files) => this.uploadFiles(files, this.rulesetTab), }; } @@ -468,7 +505,7 @@ export class ManagementController { index: idx, uploaded: true, file: file, - error: 0 + error: 0, }); } catch (error) { this.errors = true; @@ -476,7 +513,7 @@ export class ManagementController { index: idx, uploaded: false, file: file, - error: error + error: error, }); } } @@ -484,8 +521,20 @@ export class ManagementController { ErrorHandler.info('Upload successful'); return; } catch (error) { - if (Array.isArray(error) && error.length) return Promise.reject(error); - ErrorHandler.handle('Files cannot be uploaded'); + if (Array.isArray(error) && error.length) throw error; + + const errorOptions = { + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + context: this.context, + error: { + error: error, + message: error?.message || '', + title: 'Files cannot be loaded', + }, + }; + + getErrorOrchestrator().handleError(errorOptions); } } } diff --git a/public/plugin.ts b/public/plugin.ts index 71a02b84f1..90a809dc29 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -123,6 +123,7 @@ export class WazuhPlugin implements Plugin Date: Wed, 30 Jun 2021 21:22:21 +0200 Subject: [PATCH 036/493] Refactor try catch strategy on Overview section. (#3408) * feat(overview): Refactor try catch strategy on overview section. * docs(overview): Updated changelog * test(stats): Added snapshot test. * feat(wzVisualize): Refactor handler error. * feat(overview): Fixed errorContext. --- CHANGELOG.md | 6 +- public/components/visualize/wz-visualize.js | 46 ++- .../__snapshots__/stats.test.tsx.snap | 356 ++++++++++++++++++ .../overview/components/alerts-stats.js | 6 +- .../agents-selection-table.js | 105 +++--- .../overview-actions/agents-selector.scss | 57 ++- .../overview-actions/overview-actions.js | 17 +- .../overview/components/select-agent.js | 47 ++- .../overview/components/stats.test.tsx | 25 ++ public/controllers/overview/index.js | 8 + public/controllers/overview/overview.js | 51 ++- 11 files changed, 591 insertions(+), 133 deletions(-) create mode 100644 public/controllers/overview/components/__snapshots__/stats.test.tsx.snap create mode 100644 public/controllers/overview/components/stats.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index fcb25d84c4..cb68e9841e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,12 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) -- Implemented `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) +- Added implementation of `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) - Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) -- Implement try catch strategy ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - +- Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) +- Added try catch strategy ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) ### Changed diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 21ffca0f15..77a19de59a 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -38,10 +38,14 @@ import { SecurityAlerts } from './components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { withReduxProvider,withErrorBoundary } from '../common/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'; const visHandler = new VisHandlers(); +const errorContext = 'WzVisualize'; export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class WzVisualize extends Component { _isMount = false; constructor(props) { @@ -66,16 +70,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this.newFields={}; } - - showToast(color, title = '', text = '', time = 3000) { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - async componentDidMount() { this._isMount = true; visHandler.removeAll(); @@ -111,9 +105,23 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class // Check if there is sample alerts installed try { - const thereAreSampleAlerts = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})).data.sampleAlertsInstalled; + const thereAreSampleAlerts = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})) + .data.sampleAlertsInstalled; this._isMount && this.setState({ thereAreSampleAlerts }); - } catch (error) { } + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); + } } async componentDidUpdate(prevProps) { @@ -143,10 +151,20 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this.setState({ isRefreshing: false }); this.reloadToast(); this.newFields={}; - } catch (err) { + } catch (error) { this.setState({ isRefreshing: false }); - this.showToast('danger', 'The index pattern could not be refreshed'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: 'The index pattern could not be refreshed' || error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } else if (this.state.isRefreshing) { await new Promise(r => setTimeout(r, 150)); diff --git a/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap b/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap new file mode 100644 index 0000000000..ec89c47c2a --- /dev/null +++ b/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap @@ -0,0 +1,356 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Stats component renders correctly to match the snapshot 1`] = ` + + + + +
+ +
+ +
+ + +
+ + + + } + titleColor="primary" + > +
+ +
+

+ Total agents +

+
+
+ +

+ + + + + +

+
+
+
+
+
+ +
+ + + + } + titleColor="secondary" + > +
+ +
+

+ Active agents +

+
+
+ +

+ + + + + +

+
+
+
+
+
+ +
+ + + + } + titleColor="danger" + > +
+ +
+

+ Disconnected agents +

+
+
+ +

+ + + + + +

+
+
+
+
+
+ +
+ + + + } + titleColor="subdued" + > +
+ +
+

+ Never connected agents +

+
+
+ +

+ + + + + +

+
+
+
+
+
+ +
+ +
+ +
+ + + + +`; diff --git a/public/controllers/overview/components/alerts-stats.js b/public/controllers/overview/components/alerts-stats.js index d4174eb569..abcb12b676 100644 --- a/public/controllers/overview/components/alerts-stats.js +++ b/public/controllers/overview/components/alerts-stats.js @@ -13,12 +13,10 @@ import React, { Component } from 'react'; import { visualizations } from '../../../components/visualize/visualizations'; import PropTypes from 'prop-types'; -import { EuiStat, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; import { connect } from 'react-redux'; import { buildPhrasesFilter, buildRangeFilter } from '../../../../../../src/plugins/data/common'; -import { esFilters } from '../../../../../../src/plugins/data/common'; import { getIndexPattern } from '../../../../public/components/overview/mitre/lib'; -//import '../../../../public/less/loader'; import { WAZUH_ALERTS_PATTERN } from '../../../../common/constants'; import { AppState } from '../../../react-services/app-state'; import { getDataPlugin } from '../../../kibana-services'; @@ -81,7 +79,7 @@ class AlertsStats extends Component { return stats; } - addFilter(filter) { + addFilter(filter) { const { filterManager } = getDataPlugin().query; const matchPhrase = {}; matchPhrase[filter.key] = filter.value; diff --git a/public/controllers/overview/components/overview-actions/agents-selection-table.js b/public/controllers/overview/components/overview-actions/agents-selection-table.js index 5e1a2d0f72..9a7515aba0 100644 --- a/public/controllers/overview/components/overview-actions/agents-selection-table.js +++ b/public/controllers/overview/components/overview-actions/agents-selection-table.js @@ -1,13 +1,10 @@ import React, { Component, Fragment } from 'react'; - import { - EuiBadge, - EuiHealth, - EuiButton, + EuiButtonIcon, EuiCheckbox, - EuiFieldSearch, EuiFlexGroup, EuiFlexItem, + EuiHealth, EuiSpacer, EuiTable, EuiTableBody, @@ -15,29 +12,25 @@ import { EuiTableHeader, EuiTableHeaderCell, EuiTableHeaderCellCheckbox, + EuiTableHeaderMobile, EuiTablePagination, EuiTableRow, EuiTableRowCell, EuiTableRowCellCheckbox, EuiTableSortMobile, - EuiKeyPadMenu, - EuiKeyPadMenuItem, - EuiTableHeaderMobile, - EuiButtonIcon, - EuiIcon, - EuiPopover, - EuiText, - EuiToolTip + EuiToolTip, } from '@elastic/eui'; - import { WzRequest } from '../../../../react-services/wz-request'; -import { LEFT_ALIGNMENT, RIGHT_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; -import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; -import store from '../../../../redux/store'; -import { GroupTruncate } from '../../../../components/common/util/agent-group-truncate/' -import { WzSearchBar, filtersToObject } from '../../../../components/wz-search-bar'; +import { LEFT_ALIGNMENT } from '@elastic/eui/lib/services'; +import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; +import store from '../../../../redux/store'; +import { GroupTruncate } from '../../../../components/common/util/agent-group-truncate/'; +import { filtersToObject, WzSearchBar } from '../../../../components/wz-search-bar'; import { getAgentFilterValues } from '../../../../controllers/management/components/management/groups/get-agents-filters-values'; import _ from 'lodash'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; const checkField = field => { return field !== undefined ? field : '-'; @@ -46,10 +39,6 @@ const checkField = field => { export class AgentSelectionTable extends Component { constructor(props) { super(props); - - // const selectedOptions = JSON.parse( - // sessionStorage.getItem('agents_preview_selected_options') - // ); this.state = { itemIdToSelectedMap: {}, itemIdToOpenActionsPopoverMap: {}, @@ -159,9 +148,7 @@ export class AgentSelectionTable extends Component { tmpSelectedAgents[store.getState().appStateReducers.currentAgentData.id] = true; } this._isMounted && this.setState({itemIdToSelectedMap: this.props.selectedAgents}); - try{ - await this.getItems(); - }catch(error){} + await this.getItems(); } componentWillUnmount(){ @@ -178,16 +165,27 @@ export class AgentSelectionTable extends Component { try { const stringText = arrayText.toString(); const splitString = stringText.split(','); - const resultString = splitString.join(', '); - return resultString; - } catch (err) { + return splitString.join(', '); + } catch (error) { + const options = { + context: `${AgentSelectionTable.name}.getArrayFormatted`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); return arrayText; } } async getItems() { - try{ - this._isMounted && this.setState({isLoading: true}); + try { + this._isMounted && this.setState({ isLoading: true }); const rawData = await WzRequest.apiReq('GET', '/agents', { params: this.buildFilter() }); const data = (((rawData || {}).data || {}).data || {}).affected_items; const totalItems = (((rawData || {}).data || {}).data || {}).total_affected_items; @@ -202,8 +200,20 @@ export class AgentSelectionTable extends Component { }; }); this._isMounted && this.setState({ agents: formattedData, totalItems, isLoading: false }); - }catch(err){ + } catch (error) { this._isMounted && this.setState({ isLoading: false }); + const options = { + context: `${AgentSelectionTable.name}.getItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); } } @@ -506,18 +516,11 @@ export class AgentSelectionTable extends Component { return undefined; }; - async onQueryChange(result){ - // sessionStorage.setItem( - // 'agents_preview_selected_options', - // JSON.stringify(result.selectedOptions) - // ); - this._isMounted && this.setState({ isLoading: true, ...result}, async () => { - try{ - await this.getItems() - }catch(error){ - this._isMounted && this.setState({ isLoading: false}); - } - }); + async onQueryChange(result) { + this._isMounted && + this.setState({ isLoading: true, ...result }, async () => { + await this.getItems(); + }); } getSelectedItems(){ @@ -542,9 +545,21 @@ export class AgentSelectionTable extends Component { const formattedData = data.data.data.affected_items[0] //TODO: do it correctly store.dispatch(updateCurrentAgentData(formattedData)); this.props.updateAgentSearch([agentID]); - }catch(error){ + } catch(error) { store.dispatch(updateCurrentAgentData({})); this.props.removeAgentsFilter(true); + const options = { + context: `${AgentSelectionTable.name}.selectAgentAndApply`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); } } diff --git a/public/controllers/overview/components/overview-actions/agents-selector.scss b/public/controllers/overview/components/overview-actions/agents-selector.scss index b5d49e0234..00ee2dd91f 100644 --- a/public/controllers/overview/components/overview-actions/agents-selector.scss +++ b/public/controllers/overview/components/overview-actions/agents-selector.scss @@ -1,49 +1,48 @@ -.VisualizeTopMenu .euiKeyPadMenuItem{ - width: auto; - height: 65px; +.VisualizeTopMenu .euiKeyPadMenuItem { + width: auto; + height: 65px; } -.VisualizeTopMenu > .euiKeyPadMenuItem{ - width: 75px!important; - height: 30px!important; - margin-left: 8px; +.VisualizeTopMenu > .euiKeyPadMenuItem { + width: 75px !important; + height: 30px !important; + margin-left: 8px; } -.VisualizeTopMenu{ - width: auto!important +.VisualizeTopMenu { + width: auto !important; } -.TopMenuAgent{ - display: flex; - background: #f4f5f8; - border-radius: 5px; - padding: 5px; +.TopMenuAgent { + display: flex; + background: #f4f5f8; + border-radius: 5px; + padding: 5px; } -.TopMenuAgent .euiKeyPadMenuItem:first-child{ - margin-right: 8px; +.TopMenuAgent .euiKeyPadMenuItem:first-child { + margin-right: 8px; } -.TopMenuAgent .euiKeyPadMenuItem:nth-child(1) .euiBetaBadge{ - background: #006BB4; - color: white; +.TopMenuAgent .euiKeyPadMenuItem:nth-child(1) .euiBetaBadge { + background: #006bb4; + color: white; } .TopMenuAgent .euiKeyPadMenuItem:nth-child(2) .euiBetaBadge { - background: #BD271E; - color: white; + background: #bd271e; + color: white; } - -.wz-explore-agent .euiButtonEmpty:focus{ - background-color: transparent; +.wz-explore-agent .euiButtonEmpty:focus { + background-color: transparent; } .wz-unpin-agent { - background-color: rgba(0, 107, 180, 0.1); - border-radius: 0; - height: 40px; + background-color: rgba(0, 107, 180, 0.1); + border-radius: 0; + height: 40px; } .wz-unpin-agent:hover { - transform: translateY(0px)!important; -} \ No newline at end of file + transform: translateY(0px) !important; +} diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index 2046c6e98e..49034f2fba 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -13,18 +13,16 @@ import React, { Component } from 'react'; import store from '../../../../redux/store'; import { connect } from 'react-redux'; import { showExploreAgentModal, updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; - - import { - EuiFlexItem, + EuiButtonEmpty, EuiButtonIcon, + EuiFlexItem, EuiIcon, - EuiOverlayMask, EuiModal, + EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiModalBody, - EuiButtonEmpty, + EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; import './agents-selector.scss'; @@ -32,6 +30,7 @@ import { AgentSelectionTable } from './agents-selection-table'; import { WAZUH_ALERTS_PATTERN } from '../../../../../common/constants'; import { AppState } from '../../../../react-services/app-state'; import { getDataPlugin } from '../../../../kibana-services'; + class OverviewActions extends Component { constructor(props) { super(props); @@ -158,10 +157,10 @@ class OverviewActions extends Component { this.agentTableSearch(agentsIdList)} + updateAgentSearch={(agentsIdList) => this.agentTableSearch(agentsIdList)} removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} selectedAgents={this.getSelectedAgents()} - > + /> @@ -216,4 +215,4 @@ const mapStateToProps = state => { }; }; -export default connect(mapStateToProps, null)(OverviewActions); \ No newline at end of file +export default connect(mapStateToProps, null)(OverviewActions); diff --git a/public/controllers/overview/components/select-agent.js b/public/controllers/overview/components/select-agent.js index 3bda19e17d..a3589f3bba 100644 --- a/public/controllers/overview/components/select-agent.js +++ b/public/controllers/overview/components/select-agent.js @@ -12,7 +12,6 @@ */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; - import { EuiFlyout, EuiFlyoutBody, @@ -23,10 +22,11 @@ import { EuiFlexItem, EuiBasicTable } from '@elastic/eui'; - -// import { WzRequest } from '../../../../react-services/wz-request'; import { WzRequest } from '../../../react-services/wz-request'; import { withErrorBoundary } from '../../../components/common/hocs'; +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 SelectAgent = withErrorBoundary (class SelectAgent extends Component { constructor(props) { @@ -86,21 +86,34 @@ export const SelectAgent = withErrorBoundary (class SelectAgent extends Componen } async getItems() { - const rawAgents = await WzRequest.apiReq( - 'GET', - '/agents', - this.buildFilter() - ); + try { + const rawAgents = await WzRequest.apiReq('GET', '/agents', this.buildFilter()); + const formattedAgents = (((rawAgents || {}).data || {}).data || {}).items.map( + this.formatAgent.bind(this) + ); - const formatedAgents = ( - ((rawAgents || {}).data || {}).data || {} - ).items.map(this.formatAgent.bind(this)); - this.setState({ - agents: formatedAgents, - totalItems: (((rawAgents || {}).data || {}).data || {}).totalItems, - isProcessing: false, - isLoading: false - }); + this.setState({ + agents: formattedAgents, + totalItems: (((rawAgents || {}).data || {}).data || {}).totalItems, + }); + } catch (error) { + const options = { + context: `${SelectAgent.name}.getItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } finally { + this.setState({ + isProcessing: false, + isLoading: false, + }); + } } buildSortFilter() { diff --git a/public/controllers/overview/components/stats.test.tsx b/public/controllers/overview/components/stats.test.tsx new file mode 100644 index 0000000000..a4a7d7f218 --- /dev/null +++ b/public/controllers/overview/components/stats.test.tsx @@ -0,0 +1,25 @@ +/* + * Wazuh app - React test for Stats component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { Stats } from './stats'; + +describe('Stats component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/overview/index.js b/public/controllers/overview/index.js index 175d3f8a68..c4939d0a46 100644 --- a/public/controllers/overview/index.js +++ b/public/controllers/overview/index.js @@ -21,6 +21,14 @@ import { getAngularModule } from '../../kibana-services'; const app = getAngularModule(); +OverviewWelcome.displayName = 'OverviewWelcome'; +WzCurrentOverviewSectionWrapper.displayName = 'WzCurrentOverviewSectionWrapper'; +WzCurrentAgentsSectionWrapper.displayName = 'WzCurrentAgentsSectionWrapper'; +Stats.displayName = 'StatsOverview'; +Mitre.displayName = 'Mitre'; +SelectAgent.displayName = 'SelectAgent'; +RequirementCard.displayName = 'RequirementCard'; + app .controller('overviewController', OverviewController) .value('OverviewWelcome', OverviewWelcome) diff --git a/public/controllers/overview/overview.js b/public/controllers/overview/overview.js index f5bb1ca137..4dc42cc56e 100644 --- a/public/controllers/overview/overview.js +++ b/public/controllers/overview/overview.js @@ -22,8 +22,10 @@ import { updateCurrentTab, updateCurrentAgentData } from '../../redux/actions/ap import { VisFactoryHandler } from '../../react-services/vis-factory-handler'; import { RawVisualizations } from '../../factories/raw-visualizations'; import store from '../../redux/store'; -import { WAZUH_ALERTS_PATTERN } from '../../../common/constants'; +import { UI_LOGGER_LEVELS, WAZUH_ALERTS_PATTERN } from '../../../common/constants'; import { getDataPlugin } from '../../kibana-services'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services'; export class OverviewController { /** @@ -221,11 +223,21 @@ export class OverviewController { } this.tabView = subtab; } catch (error) { - ErrorHandler.handle(error.message || error); + const options = { + context: `${OverviewController.name}.switchSubtab`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); } this.agentsSelectionProps.subtab = subtab; this.$scope.$applyAsync(); - return; } /** @@ -260,8 +272,6 @@ export class OverviewController { await this.getSummary(); } - - if (this.tab === newTab && !force) return; // Restore force value if we come from md-nav action @@ -277,10 +287,20 @@ export class OverviewController { } this.overviewModuleReady = true; } catch (error) { - ErrorHandler.handle(error.message || error); + const options = { + context: `${OverviewController.name}.switchTab`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); } this.$scope.$applyAsync(); - return; } /** @@ -311,7 +331,6 @@ export class OverviewController { } else { throw new Error('Error fetching /agents/summary from Wazuh API'); } - return; } catch (error) { return Promise.reject(error); } @@ -326,7 +345,6 @@ export class OverviewController { this.wzMonitoringEnabled = !!configuration['wazuh.monitoring.enabled']; - return; } catch (error) { this.wzMonitoringEnabled = true; return Promise.reject(error); @@ -357,12 +375,21 @@ export class OverviewController { for (var i in rows) { this.$scope.attacksCount[rows[i]['col-0-2']] = rows[i]['col-1-1']; } - }); } catch (error) { - ErrorHandler.handle(error.message || error); + const options = { + context: `${OverviewController.name}.init`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + + getErrorOrchestrator().handleError(options); } this.$scope.$applyAsync(); - return; } } From 7374ae383aacfb9474490a464cb3ab7386e98448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= Date: Wed, 30 Jun 2021 21:33:54 +0200 Subject: [PATCH 037/493] Implement try catch strategy Security (#3390) * Apply new error handling Security * Requested changes * feat(users): Changed errorContext. * docs(users): Updated changelog Co-authored-by: gabiwassan --- CHANGELOG.md | 3 +- public/components/security/main.tsx | 101 +++--- .../security/policies/create-policy.tsx | 19 +- .../security/policies/edit-policy.tsx | 17 +- .../security/policies/policies-table.tsx | 260 ++++++++------- .../components/roles-mapping-create.tsx | 41 ++- .../components/roles-mapping-edit.tsx | 18 +- .../components/roles-mapping-table.tsx | 20 +- .../helpers/rule-editor.helper.ts | 53 ++- .../security/roles-mapping/roles-mapping.tsx | 31 +- .../components/security/roles/edit-role.tsx | 22 +- .../components/security/roles/roles-table.tsx | 307 ++++++++++-------- .../security/users/components/create-user.tsx | 18 +- .../security/users/components/edit-user.tsx | 18 +- .../security/users/components/users-table.tsx | 36 +- public/components/security/users/users.tsx | 25 +- 16 files changed, 622 insertions(+), 367 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb68e9841e..b323060c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,10 @@ All notable changes to the Wazuh app project will be documented in this file. - Added implementation of `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added try catch strategy with ErrorOrchestrator service on User section [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) - Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) -- Added try catch strategy ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) +- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) ### Changed diff --git a/public/components/security/main.tsx b/public/components/security/main.tsx index 22f6844bb3..b89ed22c82 100644 --- a/public/components/security/main.tsx +++ b/public/components/security/main.tsx @@ -19,10 +19,20 @@ import { API_USER_STATUS_RUN_AS } from '../../../server/lib/cache-api-user-has-r import { AppState } from '../../react-services/app-state'; import { ErrorHandler } from '../../react-services/error-handler'; import { RolesMapping } from './roles-mapping/roles-mapping'; -import { withReduxProvider, withGlobalBreadcrumb, withUserAuthorizationPrompt, withErrorBoundary } from '../common/hocs'; +import { + withReduxProvider, + withGlobalBreadcrumb, + withUserAuthorizationPrompt, + withErrorBoundary, +} from '../common/hocs'; import { compose } from 'redux'; import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; import { updateSecuritySection } from '../../redux/actions/securityActions'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services'; + +const errorContext = 'WzSecurity'; const tabs = [ { @@ -56,49 +66,60 @@ export const WzSecurity = compose( const dispatch = useDispatch(); // Get the initial tab when the component is initiated - const securityTabRegExp = new RegExp(`tab=(${tabs.map(tab => tab.id).join('|')})`); + const securityTabRegExp = new RegExp(`tab=(${tabs.map((tab) => tab.id).join('|')})`); const tab = window.location.href.match(securityTabRegExp); - const [selectedTabId, setSelectedTabId] = useState(tab && tab[1] || 'users'); - + const [selectedTabId, setSelectedTabId] = useState((tab && tab[1]) || 'users'); const listenerLocationChanged = () => { const tab = window.location.href.match(securityTabRegExp); - setSelectedTabId(tab && tab[1] || 'users') - } + setSelectedTabId((tab && tab[1]) || 'users'); + }; const checkRunAsUser = async () => { const currentApi = AppState.getCurrentAPI(); try { - const ApiCheck = await GenericRequest.request('POST', - '/api/check-api', - currentApi - ); + const ApiCheck = await GenericRequest.request('POST', '/api/check-api', currentApi); return ApiCheck.data.allow_run_as; - } catch (error) { - ErrorHandler.handle(error, 'Error checking the current API'); + throw new Error(error); } - } + }; const [allowRunAs, setAllowRunAs] = useState(); useEffect(() => { - checkRunAsUser() - .then(result => setAllowRunAs(result)) - .catch(error => console.log(error, 'Error checking if run_as user is enabled')) - }, []) + try { + const fetchAllowRunAs = async () => { + setAllowRunAs(await checkRunAsUser()); + }; + fetchAllowRunAs(); + } catch (error) { + const options = { + context: `${WzSecurity.name}.fetchAllowRunAs`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }, []); // This allows to redirect to a Security tab if you click a Security link in menu when you're already in a Security section useEffect(() => { - window.addEventListener('popstate', listenerLocationChanged) + window.addEventListener('popstate', listenerLocationChanged); return () => window.removeEventListener('popstate', listenerLocationChanged); }); useEffect(() => { dispatch(updateSecuritySection(selectedTabId)); - }) + }); - const onSelectedTabChanged = id => { + const onSelectedTabChanged = (id) => { window.location.href = window.location.href.replace(`tab=${selectedTabId}`, `tab=${id}`); setSelectedTabId(id); }; @@ -110,13 +131,13 @@ export const WzSecurity = compose( onClick={() => onSelectedTabChanged(tab.id)} isSelected={tab.id === selectedTabId} disabled={tab.disabled} - key={index}> + key={index} + > {tab.name} )); }; - const isNotRunAs = (allowRunAs) => { let runAsWarningTxt = ''; switch (allowRunAs) { @@ -137,40 +158,34 @@ export const WzSecurity = compose( 'The role mapping has no effect because the current Wazuh API user has run_as disabled.'; break; } - + return ( - - - - + + + - + ); - } - + }; return ( {renderTabs()} - - {selectedTabId === 'users' && - - } - {selectedTabId === 'roles' && - - } - {selectedTabId === 'policies' && - - } - {selectedTabId === 'roleMapping' && + + {selectedTabId === 'users' && } + {selectedTabId === 'roles' && } + {selectedTabId === 'policies' && } + {selectedTabId === 'roleMapping' && ( <> - {(allowRunAs !== undefined && allowRunAs !== API_USER_STATUS_RUN_AS.ENABLED) && isNotRunAs(allowRunAs)} + {allowRunAs !== undefined && + allowRunAs !== API_USER_STATUS_RUN_AS.ENABLED && + isNotRunAs(allowRunAs)} - } + )} diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 706ab88de3..c1aca52422 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -20,6 +20,11 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzOverlayMask } from '../../common/util'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'CreatePolicyFlyout'; export const CreatePolicyFlyout = ({ closeFlyout }) => { const [isModalVisible, setIsModalVisible] = useState(false); @@ -182,8 +187,18 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { setAddedResources([]); setEffectValue(null); } catch (error) { - ErrorHandler.handle(error, 'Error creating policy'); - return; + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } closeFlyout(); }; diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index c91a152c36..bfaade9ee8 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -22,8 +22,12 @@ import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; import { WzOverlayMask } from '../../common/util'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; import _ from 'lodash'; +const errorContext = 'EditPolicyFlyout'; export const EditPolicyFlyout = ({ policy, closeFlyout }) => { const isReserved = WzAPIUtils.isReservedID(policy.id); @@ -73,7 +77,18 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { ErrorHandler.info('Role was successfully updated with the selected policies'); closeFlyout(); } catch (error) { - ErrorHandler.handle(error, 'Unexpected error'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/components/security/policies/policies-table.tsx b/public/components/security/policies/policies-table.tsx index 8907b0f3cd..df87b798ab 100644 --- a/public/components/security/policies/policies-table.tsx +++ b/public/components/security/policies/policies-table.tsx @@ -1,140 +1,148 @@ - import React, { useState, useEffect } from 'react'; -import { - EuiInMemoryTable, - EuiBadge, - EuiToolTip, - EuiButtonIcon -} from '@elastic/eui'; +import { EuiInMemoryTable, EuiBadge, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; import { WzButtonModalConfirm } from '../../common/buttons'; -import { CreatePolicyFlyout } from './create-policy'; +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 PoliciesTable = ({policies, loading, editPolicy, createPolicy,updatePolicies}) => { +const errorContext = 'PoliciesTable'; - const getRowProps = (item) => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - onClick: () => { - editPolicy(item); - createPolicy(item) - }, - }; +export const PoliciesTable = ({ policies, loading, editPolicy, createPolicy, updatePolicies }) => { + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + onClick: () => { + editPolicy(item); + createPolicy(item); + }, }; + }; - - const columns = [ - { - field: 'id', - name: 'ID', - width: 75, - sortable: true, - truncateText: true, - }, - { - field: 'name', - name: 'Name', - sortable: true, - truncateText: true, - }, - { - field: 'policy.actions', - name: 'Actions', - sortable: true, - render: actions => { - return (actions || []).join(", ") - }, - truncateText: true, - }, - { - field: 'policy.resources', - name: 'Resources', - sortable: true, - truncateText: true, - }, - { - field: 'policy.effect', - name: 'Effect', - sortable: true, - truncateText: true, - }, - { - field: 'id', - name: 'Status', - render: (item) => { - return WzAPIUtils.isReservedID(item) && Reserved - }, - width: 150, - sortable: false, - }, - { - align: 'right', - width: '5%', - name: 'Actions', - render: item => ( -
ev.stopPropagation()}> - { - try{ - const response = await WzRequest.apiReq( - 'DELETE', - `/security/policies/`, - { - params: { - policy_ids: item.id - } - } - ); - const data = (response.data || {}).data; - if (data.failed_items && data.failed_items.length){ - return; - } - ErrorHandler.info('Policy was successfully deleted'); - await updatePolicies(); - }catch(error){ - ErrorHandler.handle(error, 'Error deleting policy'); + const columns = [ + { + field: 'id', + name: 'ID', + width: 75, + sortable: true, + truncateText: true, + }, + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + }, + { + field: 'policy.actions', + name: 'Actions', + sortable: true, + render: (actions) => { + return (actions || []).join(', '); + }, + truncateText: true, + }, + { + field: 'policy.resources', + name: 'Resources', + sortable: true, + truncateText: true, + }, + { + field: 'policy.effect', + name: 'Effect', + sortable: true, + truncateText: true, + }, + { + field: 'id', + name: 'Status', + render: (item) => { + return WzAPIUtils.isReservedID(item) && Reserved; + }, + width: 150, + sortable: false, + }, + { + align: 'right', + width: '5%', + name: 'Actions', + render: (item) => ( +
ev.stopPropagation()}> + { + try { + const response = await WzRequest.apiReq('DELETE', `/security/policies/`, { + params: { + policy_ids: item.id, + }, + }); + const data = (response.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; } - }} - modalProps={{buttonColor: 'danger'}} - iconType='trash' - color='danger' - aria-label='Delete policy' + ErrorHandler.info('Policy was successfully deleted'); + await updatePolicies(); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }} + modalProps={{ buttonColor: 'danger' }} + iconType="trash" + color="danger" + aria-label="Delete policy" /> -
- ) - } - ]; +
+ ), + }, + ]; - const sorting = { - sort: { - field: 'id', - direction: 'asc', - }, - }; + const sorting = { + sort: { + field: 'id', + direction: 'asc', + }, + }; - const search = { - box: { - incremental: false, - schema: true, - }, - }; + const search = { + box: { + incremental: false, + schema: true, + }, + }; - return ( - - ); -}; \ No newline at end of file + return ( + + ); +}; diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 5dd081c093..3b8041848a 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -18,7 +18,12 @@ import { ErrorHandler } from '../../../../react-services/error-handler'; import { RuleEditor } from './rule-editor'; import RulesServices from '../../rules/services'; import RolesServices from '../../roles/services'; -import { WzOverlayMask } from '../../../common/util' +import { WzOverlayMask } from '../../../common/util'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; + +const errorContext = 'RolesMappingCreate'; export const RolesMappingCreate = ({ closeFlyout, @@ -58,7 +63,18 @@ export const RolesMappingCreate = ({ ); ErrorHandler.info('Role mapping was successfully created'); } catch (error) { - ErrorHandler.handle(error, 'There was an error'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } onSave(); closeFlyout(false); @@ -88,10 +104,14 @@ export const RolesMappingCreate = ({ } useEffect(() => { - if(initialSelectedRoles.length != selectedRoles.length || initialRuleName != ruleName || hasChangeMappingRules){ + if ( + initialSelectedRoles.length != selectedRoles.length || + initialRuleName != ruleName || + hasChangeMappingRules + ) { setHasChanges(true); - }else{ - setHasChanges(false) + } else { + setHasChanges(false); } }, [selectedRoles, ruleName, hasChangeMappingRules]); @@ -103,11 +123,12 @@ export const RolesMappingCreate = ({ hasChanges ? setIsModalVisible(true) : closeFlyout(false); }} > - { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> + { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }} + >

Create new role mapping  

diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index cbc9f9acf0..a1c6f2986c 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -22,6 +22,11 @@ import RolesServices from '../../roles/services'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; import { WzOverlayMask } from '../../../common/util' import _ from 'lodash'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; + +const errorContext = 'RolesMappingEdit'; export const RolesMappingEdit = ({ rule, @@ -81,7 +86,18 @@ export const RolesMappingEdit = ({ ErrorHandler.info('Role mapping was successfully updated'); } catch (error) { - ErrorHandler.handle(error, 'There was an error'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } onSave(); setIsLoading(false); diff --git a/public/components/security/roles-mapping/components/roles-mapping-table.tsx b/public/components/security/roles-mapping/components/roles-mapping-table.tsx index 09a00f0288..c5c53a4d05 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-table.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-table.tsx @@ -13,6 +13,11 @@ import { ErrorHandler } from '../../../../react-services/error-handler'; import { WzButtonModalConfirm } from '../../../common/buttons'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; import RulesServices from '../../rules/services'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; + +const errorContext = 'RolesMappingTable'; export const RolesMappingTable = ({ rolesEquivalences, rules, loading, editRule, updateRules }) => { const getRowProps = item => { @@ -99,8 +104,19 @@ export const RolesMappingTable = ({ rolesEquivalences, rules, loading, editRule, await RulesServices.DeleteRules([item.id]); ErrorHandler.info('Role mapping was successfully deleted'); updateRules(); - } catch (err) { - ErrorHandler.handle(err, 'Error deleting the role mapping'); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }} modalProps={{ buttonColor: 'danger' }} diff --git a/public/components/security/roles-mapping/helpers/rule-editor.helper.ts b/public/components/security/roles-mapping/helpers/rule-editor.helper.ts index 8cbeb0490d..f8d69e4ac2 100644 --- a/public/components/security/roles-mapping/helpers/rule-editor.helper.ts +++ b/public/components/security/roles-mapping/helpers/rule-editor.helper.ts @@ -1,17 +1,21 @@ +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 getJsonFromRule = (internalUserRules, rules, logicalOperator) => { const ruleObject: any = {}; - const usersRulesArray = internalUserRules.map(item => { + const usersRulesArray = internalUserRules.map((item) => { const tmpRule = {}; tmpRule[item.searchOperation] = {}; tmpRule[item.searchOperation][item.user_field] = item.value; return tmpRule; }); - const rulesArray = rules.map(item => { + const rulesArray = rules.map((item) => { const tmpRule = {}; tmpRule[item.searchOperation] = {}; tmpRule[item.searchOperation][item.user_field] = item.value; return tmpRule; - }); + }); if (usersRulesArray.length && rulesArray.length) { ruleObject['OR'] = [ @@ -23,23 +27,23 @@ export const getJsonFromRule = (internalUserRules, rules, logicalOperator) => { }, ]; } else { - if(rulesArray.length == 1){ - return rulesArray[0] - }else if(usersRulesArray.length == 1){ - return usersRulesArray[0] - }else{ - if (usersRulesArray.length) { + if (rulesArray.length == 1) { + return rulesArray[0]; + } else if (usersRulesArray.length == 1) { + return usersRulesArray[0]; + } else { + if (usersRulesArray.length) { ruleObject['OR'] = usersRulesArray; } if (rulesArray.length) { - ruleObject[logicalOperator] = rulesArray; + ruleObject[logicalOperator] = rulesArray; } } - } + } return ruleObject; }; -const formatRules = rulesArray => { +const formatRules = (rulesArray) => { let wrongFormat = false; const tmpRules = rulesArray.map((item, idx) => { if (Object.keys(item).length !== 1 || Array.isArray(item[Object.keys(item)[0]])) { @@ -59,8 +63,8 @@ const formatRules = rulesArray => { }; const hasInternalUsers = (rules, internalUsers) => { - return rules.every(rule => { - return internalUsers.some(user => user.user === rule.value); + return rules.every((rule) => { + return internalUsers.some((user) => user.user === rule.value); }); }; @@ -71,7 +75,7 @@ const getFormatedRules = (rulesArray, internalUsers) => { let formatedRules; let logicalOperator; - const operatorsCount = rulesArray.filter(rule => Array.isArray(rule[Object.keys(rule)[0]])) + const operatorsCount = rulesArray.filter((rule) => Array.isArray(rule[Object.keys(rule)[0]])) .length; switch (operatorsCount) { case 0: // only custom rules or internal users @@ -119,10 +123,11 @@ const getFormatedRules = (rulesArray, internalUsers) => { }; export const decodeJsonRule = (jsonRule, internalUsers) => { + const errorContext = 'decodeJsonRule'; try { var wrongFormat = false; const ruleObject = JSON.parse(jsonRule); - + if (Object.keys(ruleObject).length !== 1) { wrongFormat = true; } @@ -144,10 +149,22 @@ export const decodeJsonRule = (jsonRule, internalUsers) => { logicalOperator: formatedRules.logicalOperator || logicalOperator, }; } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); return { customRules: [], internalUsersRules: [], wrongFormat: true }; } }; -export const getSelectedUsersFromRules = rules => { - return rules.map(rule => ({ label: rule.value, id: rule.value })); +export const getSelectedUsersFromRules = (rules) => { + return rules.map((rule) => ({ label: rule.value, id: rule.value })); }; diff --git a/public/components/security/roles-mapping/roles-mapping.tsx b/public/components/security/roles-mapping/roles-mapping.tsx index 526c54a326..1a687d2c6e 100644 --- a/public/components/security/roles-mapping/roles-mapping.tsx +++ b/public/components/security/roles-mapping/roles-mapping.tsx @@ -26,6 +26,11 @@ import { Role } from '../roles/types/role.type'; import RolesServices from '../roles/services'; import RulesServices from '../rules/services'; import { useSelector } from 'react-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'; + +const errorContext = 'RolesMapping'; export const RolesMapping = () => { const [isEditingRule, setIsEditingRule] = useState(false); @@ -70,7 +75,18 @@ export const RolesMapping = () => { }).sort((a, b) => (a.user > b.user) ? 1 : (a.user < b.user) ? -1 : 0); setInternalUsers(_users); } catch (error) { - ErrorHandler.handle('There was an error loading internal users'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; @@ -79,7 +95,18 @@ export const RolesMapping = () => { const _rules = await RulesServices.GetRules(); setRules(_rules); } catch (error) { - ErrorHandler.handle('There was an error loading rules'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 1e957ca736..9bfebb832a 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -20,7 +20,12 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { EditRolesTable } from './edit-role-table'; -import { WzOverlayMask } from '../../common/util' +import { WzOverlayMask } from '../../common/util'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'EditRole'; const reservedRoles = ['administrator', 'readonly', 'users_admin', 'agents_readonly', 'agents_admin', 'cluster_readonly', 'cluster_admin']; @@ -63,7 +68,7 @@ export const EditRole = ({ role, closeFlyout }) => { setAssignedPolicies(selectedPoliciesCopy); setPolicies(filteredPolicies); } catch (error) { - ErrorHandler.handle(error, 'Error'); + throw new Error(error); } setIsLoading(false); } @@ -100,7 +105,18 @@ export const EditRole = ({ role, closeFlyout }) => { setSelectedPolicies([]); await update(); } catch (error) { - ErrorHandler.handle(error, 'There was an error'); + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/components/security/roles/roles-table.tsx b/public/components/security/roles/roles-table.tsx index ca53f6c0da..4e903c6ecd 100644 --- a/public/components/security/roles/roles-table.tsx +++ b/public/components/security/roles/roles-table.tsx @@ -1,153 +1,180 @@ - import React, { useState, useEffect } from 'react'; import { - EuiInMemoryTable, - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiToolTip, - EuiButtonIcon, - EuiSpacer, - EuiLoadingSpinner + EuiInMemoryTable, + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiButtonIcon, + EuiSpacer, + EuiLoadingSpinner, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzButtonModalConfirm } from '../../common/buttons'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +const errorContext = 'RolesTable'; -export const RolesTable = ({roles, policiesData, loading, editRole, updateRoles}) => { - - const getRowProps = item => { - const { id } = item; - return { - 'data-test-subj': `row-${id}`, - onClick: () => editRole(item), - }; - }; +export const RolesTable = ({ roles, policiesData, loading, editRole, updateRoles }) => { + const getRowProps = (item) => { + const { id } = item; + return { + 'data-test-subj': `row-${id}`, + onClick: () => editRole(item), + }; + }; - const columns = [ - { - field: 'id', - name: 'ID', - width: 75, - sortable: true, - truncateText: true, - }, - { - field: 'name', - name: 'Name', - width: 200, - sortable: true, - truncateText: true, - }, - { - field: 'policies', - name: 'Policies', - render: policies => { - return policiesData && - {policies.map(policy => { - const data = ((policiesData || []).find(x => x.id === policy) || {}); - return data.name && - - Actions -

{((data.policy || {}).actions || []).join(", ")}

- - Resources -

{((data.policy || {}).resources || []).join(", ")}

- - Effect -

{(data.policy || {}).effect}

-
- }> - {}} onClickAriaLabel={`${data.name} policy`} title={null}>{ - data.name - } - - ; - })} -
|| - - }, - sortable: true, - }, - { - field: 'id', - name: 'Status', - render: (item) => { - return WzAPIUtils.isReservedID(item) && Reserved - }, - width: 150, - sortable: false, - }, - { - align: 'right', - width: '5%', - name: 'Actions', - render: item => ( -
ev.stopPropagation()}> - { - try{ - const response = await WzRequest.apiReq( - 'DELETE', - `/security/roles/`, - { - params: { - role_ids: item.id - } + const columns = [ + { + field: 'id', + name: 'ID', + width: 75, + sortable: true, + truncateText: true, + }, + { + field: 'name', + name: 'Name', + width: 200, + sortable: true, + truncateText: true, + }, + { + field: 'policies', + name: 'Policies', + render: (policies) => { + return ( + (policiesData && ( + + {policies.map((policy) => { + const data = (policiesData || []).find((x) => x.id === policy) || {}; + return ( + data.name && ( + + + Actions +

{((data.policy || {}).actions || []).join(', ')}

+ + Resources +

{((data.policy || {}).resources || []).join(', ')}

+ + Effect +

{(data.policy || {}).effect}

+
} - ); - const data = (response.data || {}).data; - if (data.failed_items && data.failed_items.length){ - return; - } - ErrorHandler.info('Role was successfully deleted'); - await updateRoles(); - }catch(error){} - }} - modalProps={{buttonColor: 'danger'}} - iconType='trash' - color='danger' - aria-label='Delete role' - /> -
- ) - } - ]; + > + {}} + onClickAriaLabel={`${data.name} policy`} + title={null} + > + {data.name} + + + + ) + ); + })} + + )) || + ); + }, + sortable: true, + }, + { + field: 'id', + name: 'Status', + render: (item) => { + return WzAPIUtils.isReservedID(item) && Reserved; + }, + width: 150, + sortable: false, + }, + { + align: 'right', + width: '5%', + name: 'Actions', + render: (item) => ( +
ev.stopPropagation()}> + { + try { + const response = await WzRequest.apiReq('DELETE', `/security/roles/`, { + params: { + role_ids: item.id, + }, + }); + const data = (response.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; + } + ErrorHandler.info('Role was successfully deleted'); + await updateRoles(); + } catch (error) { + const options = { + context: errorContext, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }} + modalProps={{ buttonColor: 'danger' }} + iconType="trash" + color="danger" + aria-label="Delete role" + /> +
+ ), + }, + ]; - const sorting = { - sort: { - field: 'id', - direction: 'asc', - }, - }; + const sorting = { + sort: { + field: 'id', + direction: 'asc', + }, + }; - const search = { - box: { - incremental: false, - schema: true, - }, - }; + const search = { + box: { + incremental: false, + schema: true, + }, + }; - return ( - - ); -}; \ No newline at end of file + return ( + + ); +}; diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index 75965f1e86..a7b80f099d 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -26,7 +26,10 @@ import RolesServices from '../../roles/services'; import { WzButtonPermissions } from '../../../common/permissions/button'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; -import { WzOverlayMask } from '../../../common/util' +import { WzOverlayMask } from '../../../common/util'; +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 CreateUser = ({ closeFlyout }) => { const [selectedRoles, setSelectedRole] = useState([]); @@ -158,7 +161,18 @@ export const CreateUser = ({ closeFlyout }) => { ErrorHandler.info('User was successfully created'); closeFlyout(true); } catch (error) { - ErrorHandler.handle(error, 'There was an error'); + const options = { + context: `${CreateUser.name}.editUser`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); setIsLoading(false); } }; diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 22c82d25cf..14181d6d48 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -29,6 +29,9 @@ import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; import { WzOverlayMask } from '../../../common/util' import _ from 'lodash'; +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 EditUser = ({ currentUser, closeFlyout, rolesObject }) => { const userRolesFormatted = @@ -148,7 +151,18 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { ErrorHandler.info('User was successfully updated'); closeFlyout(true); } catch (error) { - ErrorHandler.handle(error, 'There was an error'); + const options = { + context: `${EditUser.name}.editUser`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); setIsLoading(false); } }; @@ -208,7 +222,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { useEffect(() => { if ( - initialPassword != password || initialPassword != confirmPassword || + initialPassword != password || initialPassword != confirmPassword || !_.isEqual(userRolesFormatted, selectedRoles) || allowRunAs ) { setHasChanges(true); diff --git a/public/components/security/users/components/users-table.tsx b/public/components/security/users/components/users-table.tsx index 76a3dee26e..b0add823bb 100644 --- a/public/components/security/users/components/users-table.tsx +++ b/public/components/security/users/components/users-table.tsx @@ -12,6 +12,9 @@ import { WzButtonModalConfirm } from '../../../common/buttons'; import UsersServices from '../services'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; +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 UsersTable = ({ users, editUserFlyover, rolesLoading, roles, onSave }) => { const getRowProps = item => { @@ -22,6 +25,29 @@ export const UsersTable = ({ users, editUserFlyover, rolesLoading, roles, onSave }; }; + const onConfirmDeleteUser = (item) => { + return async () => { + try { + await UsersServices.DeleteUsers([item.id]); + ErrorHandler.info('User was successfully deleted'); + onSave(); + } catch (error) { + const options = { + context: `${UsersTable.name}.onConfirmDeleteUser`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + }; + const columns: EuiBasicTableColumn[] = [ { field: 'username', @@ -73,15 +99,7 @@ export const UsersTable = ({ users, editUserFlyover, rolesLoading, roles, onSave }} isDisabled={WzAPIUtils.isReservedID(item.id)} modalTitle={`Do you want to delete ${item.username} user?`} - onConfirm={async () => { - try { - await UsersServices.DeleteUsers([item.id]); - ErrorHandler.info('User was successfully deleted'); - onSave(); - } catch (err) { - ErrorHandler.handle(err, 'Error deleting the user'); - } - }} + onConfirm={onConfirmDeleteUser(item)} modalProps={{ buttonColor: 'danger' }} iconType="trash" color="danger" diff --git a/public/components/security/users/users.tsx b/public/components/security/users/users.tsx index d863e47638..106d04defb 100644 --- a/public/components/security/users/users.tsx +++ b/public/components/security/users/users.tsx @@ -10,7 +10,6 @@ import { EuiEmptyPrompt, } from '@elastic/eui'; import { UsersTable } from './components/users-table'; - import { CreateUser } from './components/create-user'; import { EditUser } from './components/edit-user'; import UsersServices from './services'; @@ -18,6 +17,9 @@ import RolesServices from '../roles/services'; import { User } from './types/user.type'; import { useApiService } from '../../common/hooks/useApiService'; import { Role } from '../roles/types/role.type'; +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 Users = () => { const [isEditFlyoutVisible, setIsEditFlyoutVisible] = useState(false); @@ -29,12 +31,25 @@ export const Users = () => { const [rolesObject, setRolesObject] = useState({}); const getUsers = async () => { - const _users = await UsersServices.GetUsers().catch(error => { + try { + const _users = await UsersServices.GetUsers(); + setUsers(_users as User[]); + } catch (error) { setUsers([]); setSecurityError(true); - }); - - setUsers(_users as User[]); + const options = { + context: `${Users.name}.getUsers`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } }; useEffect(() => { From e759dde0d3c80dbc60cbcd071cdf71a5e6b0e8da Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Thu, 1 Jul 2021 12:40:30 -0300 Subject: [PATCH 038/493] Implement try catch strategy in Ruleset (#3410) * Implemented try-catch strategy in Managemente Ruleset * Updated CHANGELOG * Requested changes * Removed throw condition * Removed comment * Requested changes * Added main-ruleset test and component snapshot * Changed wrong comment in test file * Updated context options in error handler Co-authored-by: Ibarra Maximiliano Co-authored-by: Gabriel Wassan --- CHANGELOG.md | 1 + .../__snapshots__/main-ruleset.test.tsx.snap | 122 +++++ .../management/ruleset/actions-buttons.js | 142 +++--- .../management/ruleset/decoder-info.js | 144 +++--- .../management/ruleset/list-editor.js | 226 +++++----- .../management/ruleset/main-ruleset.test.tsx | 35 ++ .../management/ruleset/rule-info.js | 423 ++++++++++-------- .../management/ruleset/ruleset-editor.js | 54 ++- .../management/ruleset/ruleset-filter-bar.js | 32 +- .../management/ruleset/ruleset-table.js | 264 ++++++----- .../management/ruleset/section-selector.js | 26 +- .../ruleset/utils/ruleset-handler.ts | 8 +- .../ruleset/utils/valid-configuration.js | 2 +- 13 files changed, 939 insertions(+), 540 deletions(-) create mode 100644 public/controllers/management/components/management/ruleset/__snapshots__/main-ruleset.test.tsx.snap create mode 100644 public/controllers/management/components/management/ruleset/main-ruleset.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index b323060c6a..638f395dad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) - Added try catch strategy with ErrorOrchestrator service on User section [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) - Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) +- Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) diff --git a/public/controllers/management/components/management/ruleset/__snapshots__/main-ruleset.test.tsx.snap b/public/controllers/management/components/management/ruleset/__snapshots__/main-ruleset.test.tsx.snap new file mode 100644 index 0000000000..9805e19804 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/__snapshots__/main-ruleset.test.tsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Ruleset component renders correctly to match the snapshot 1`] = ` + + + + + + +
+ + +
+ + +
+ Loading ... +
+
+ +
+ + +
+ +
+ + + + + + +`; diff --git a/public/controllers/management/components/management/ruleset/actions-buttons.js b/public/controllers/management/components/management/ruleset/actions-buttons.js index dd142b1694..486d412a48 100644 --- a/public/controllers/management/components/management/ruleset/actions-buttons.js +++ b/public/controllers/management/components/management/ruleset/actions-buttons.js @@ -12,9 +12,7 @@ import React, { Component, Fragment } from 'react'; // Eui components import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import { getToasts } from '../../../../../kibana-services'; - -import { connect } from 'react-redux'; +import { getToasts } from '../../../../../kibana-services'; import { toggleShowFiles, @@ -31,6 +29,12 @@ import columns from './utils/columns'; import { resourceDictionary, RulesetHandler, RulesetResources } from './utils/ruleset-handler'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; +import { connect } from 'react-redux'; + +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzRulesetActionButtons extends Component { constructor(props) { super(props); @@ -42,12 +46,12 @@ class WzRulesetActionButtons extends Component { this.refreshTimeoutId = null; } - showToast(title, text, color){ + showToast(title, text, color) { getToasts().add({ title, text, color, - toastLifeTimeMs: 3000 + toastLifeTimeMs: 3000, }); } /** @@ -57,13 +61,29 @@ class WzRulesetActionButtons extends Component { try { this.setState({ generatingCsv: true }); const { section, filters } = this.props.state; //TODO get filters from the search bar from the REDUX store - const mapFilters = filters.map(filter => ({ + const mapFilters = filters.map((filter) => ({ name: filter.field, - value: filter.value + value: filter.value, })); // adapt to shape used in /api/csv file: server/controllers/wazuh-api.js - await this.exportCsv(`/${section}${this.props.state.showingFiles ? '/files' : ''}`, mapFilters, section); + await this.exportCsv( + `/${section}${this.props.state.showingFiles ? '/files' : ''}`, + mapFilters, + section + ); } catch (error) { - this.showToast('Error exporting as CSV', error.message || error, 'danger'); + const options = { + context: `${WzRulesetActionButtons.name}.generateCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error generating CSV: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + + this.setState({ generatingCsv: false }); } this.setState({ generatingCsv: false }); } @@ -77,7 +97,7 @@ class WzRulesetActionButtons extends Component { try { let errors = false; let results = []; - const rulesetHandler = new RulesetHandler(resource); + const rulesetHandler = new RulesetHandler(resource); for (let idx in files) { const { file, content } = files[idx]; try { @@ -86,7 +106,7 @@ class WzRulesetActionButtons extends Component { index: idx, uploaded: true, file: file, - error: 0 + error: 0, }); } catch (error) { errors = true; @@ -94,17 +114,14 @@ class WzRulesetActionButtons extends Component { index: idx, uploaded: false, file: file, - error: error + error: error, }); } } if (errors) throw results; - //ErrorHandler.info('Upload successful'); return; } catch (error) { - if (Array.isArray(error) && error.length) return Promise.reject(error); - //TODO handle the erros - //ErrorHandler.handle('Files cannot be uploaded'); + throw error; } } @@ -119,7 +136,20 @@ class WzRulesetActionButtons extends Component { this.props.updateIsProcessing(true); this.props.updatePageIndex(0); this.props.updateLoadingStatus(false); - } catch (error) {}; + } catch (error) { + const options = { + context: `${WzRulesetActionButtons.name}.toggleFiles`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: false, + error: { + error: error, + message: `Error generating CSV: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** @@ -130,7 +160,7 @@ class WzRulesetActionButtons extends Component { this.props.updateIsProcessing(true); // this.onRefreshLoading(); } catch (error) { - return Promise.reject(error); + throw error; } } @@ -149,17 +179,17 @@ class WzRulesetActionButtons extends Component { const { section, showingFiles } = this.props.state; const getReadPermissionsFiles = () => { - const { permissionResource } = resourceDictionary[section]; - return [ + const { permissionResource } = resourceDictionary[section]; + return [ { action: `${section}:read`, resource: permissionResource('*'), - } - ]; + }, + ]; }; const getUpdatePermissionsFiles = () => { - const { permissionResource } = resourceDictionary[section]; + const { permissionResource } = resourceDictionary[section]; return [ { action: `${section}:update`, @@ -168,8 +198,8 @@ class WzRulesetActionButtons extends Component { { action: `${section}:read`, resource: permissionResource('*'), - } - ]; + }, + ]; }; // Export button @@ -201,7 +231,7 @@ class WzRulesetActionButtons extends Component { > {`Add new ${section} file`} - ); + ); //Add new CDB list button const addNewCdbListButton = ( @@ -235,30 +265,35 @@ class WzRulesetActionButtons extends Component { // Refresh const refresh = ( - await this.refresh()} - > + await this.refresh()}> Refresh ); const uploadFile = async (files, resource) => { - await this.uploadFiles(files, resource); - await this.refresh(); + try { + await this.uploadFiles(files, resource); + await this.refresh(); + } catch (error) { + const options = { + context: `${WzRulesetActionButtons.name}.uploadFile`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error files cannot be uploaded: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } }; return ( - {section !== 'lists' && ( - {manageFiles} - )} - {section !== 'lists' && ( - {addNewRuleButton} - )} - {section === 'lists' && ( - {addNewCdbListButton} - )} + {section !== 'lists' && {manageFiles}} + {section !== 'lists' && {addNewRuleButton}} + {section === 'lists' && {addNewCdbListButton}} {(section === 'lists' || showingFiles) && ( { +const mapStateToProps = (state) => { return { - state: state.rulesetReducers + state: state.rulesetReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - toggleShowFiles: status => dispatch(toggleShowFiles(status)), - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updteAddingRulesetFile: content => - dispatch(updteAddingRulesetFile(content)), - updateListContent: content => dispatch(updateListContent(content)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updatePageIndex: pageIndex => dispatch(updatePageIndex(pageIndex)) + toggleShowFiles: (status) => dispatch(toggleShowFiles(status)), + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updteAddingRulesetFile: (content) => dispatch(updteAddingRulesetFile(content)), + updateListContent: (content) => dispatch(updateListContent(content)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updatePageIndex: (pageIndex) => dispatch(updatePageIndex(pageIndex)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzRulesetActionButtons); +export default connect(mapStateToProps, mapDispatchToProps)(WzRulesetActionButtons); diff --git a/public/controllers/management/components/management/ruleset/decoder-info.js b/public/controllers/management/components/management/ruleset/decoder-info.js index d16dd498c5..68f12291bc 100644 --- a/public/controllers/management/components/management/ruleset/decoder-info.js +++ b/public/controllers/management/components/management/ruleset/decoder-info.js @@ -26,32 +26,57 @@ import { cleanFileContent, cleanInfo, updateFilters, - cleanFilters + cleanFilters, } from '../../../../../redux/actions/rulesetActions'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzDecoderInfo extends Component { constructor(props) { super(props); this.rulesetHandler = new RulesetHandler(RulesetResources.DECODERS); + + const handleFileClick = async () => { + try { + const result = await this.rulesetHandler.getFileContent(value); + const file = { name: value, content: result, path: item.relative_dirname }; + this.props.updateFileContent(file); + } catch (error) { + const options = { + context: `${WzDecoderInfo.name}.handleFileClick`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error updating file content: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + this.columns = [ { field: 'name', name: 'Name', align: 'left', - sortable: true + sortable: true, }, { field: 'details.program_name', name: 'Program name', align: 'left', - sortable: true + sortable: true, }, { field: 'details.order', name: 'Order', align: 'left', - sortable: true + sortable: true, }, { field: 'filename', @@ -61,22 +86,17 @@ class WzDecoderInfo extends Component { render: (value, item) => { return ( - { - const result = await this.rulesetHandler.getFileContent(value); - const file = { name: value, content: result, path: item.relative_dirname }; - this.props.updateFileContent(file); - } - }>{value} + handleFileClick()}>{value} ); - } + }, }, { field: 'relative_dirname', name: 'Path', align: 'left', - sortable: true - } + sortable: true, + }, ]; } @@ -99,13 +119,18 @@ class WzDecoderInfo extends Component { return ( - Position{position} + Position + {position} File - this.setNewFiltersAndBack([{field:'filename', value: file}])}> + + this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + } + >  {file} @@ -115,7 +140,11 @@ class WzDecoderInfo extends Component { Path - this.setNewFiltersAndBack([{field:'relative_dirname', value: path}])}> + + this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + } + >  {path} @@ -132,30 +161,35 @@ class WzDecoderInfo extends Component { */ renderDetails(details) { const detailsToRender = []; - const capitalize = str => str[0].toUpperCase() + str.slice(1); + const capitalize = (str) => str[0].toUpperCase() + str.slice(1); - Object.keys(details).forEach(key => { + Object.keys(details).forEach((key) => { let content = details[key]; if (key === 'order') { content = this.colorOrder(content); - } else if (typeof details[key] === 'object'){ + } else if (typeof details[key] === 'object') { content = (
    - {Object.keys(details[key]).map(k => ( -
  • + {Object.keys(details[key]).map((k) => ( +
  • {k}:  {details[key][k]}
  • ))}
- ) + ); } else { content = {details[key]}; } detailsToRender.push( - - {capitalize(key)}
{content}
+ + {capitalize(key)} +
{content}
); }); @@ -173,11 +207,8 @@ class WzDecoderInfo extends Component { const result = []; for (let i = 0, len = valuesArray.length; i < len; i++) { const coloredString = ( - - {valuesArray[i].startsWith(" ") ? valuesArray[i] : ` ${valuesArray[i]}`} + + {valuesArray[i].startsWith(' ') ? valuesArray[i] : ` ${valuesArray[i]}`} ); result.push(coloredString); @@ -200,10 +231,7 @@ class WzDecoderInfo extends Component { const result = [starts]; for (let i = 0, len = valuesArray.length; i < len; i++) { const coloredString = ( - + {valuesArray[i]} ); @@ -222,18 +250,21 @@ class WzDecoderInfo extends Component { render() { const { decoderInfo, isLoading } = this.props.state; - const currentDecoder = (this.state && this.state.currentDecoder) ? this.state.currentDecoder : decoderInfo.current; + const currentDecoder = + this.state && this.state.currentDecoder ? this.state.currentDecoder : decoderInfo.current; const decoders = decoderInfo.affected_items; - const currentDecoderArr = decoders.filter(r => { return r.name === currentDecoder }); + const currentDecoderArr = decoders.filter((r) => { + return r.name === currentDecoder; + }); const currentDecoderInfo = currentDecoderArr[0]; const { position, details, filename, name, relative_dirname } = currentDecoderInfo; const columns = this.columns; - const onClickRow = item => { + const onClickRow = (item) => { return { onClick: () => { this.changeBetweenDecoders(item.name); - } + }, }; }; @@ -253,7 +284,10 @@ class WzDecoderInfo extends Component { iconSize="l" iconType="arrowLeft" onClick={() => { - window.location.href = window.location.href.replace(new RegExp('redirectRule=' + '[^&]*'), ''); + window.location.href = window.location.href.replace( + new RegExp('redirectRule=' + '[^&]*'), + '' + ); this.props.cleanInfo(); }} /> @@ -276,8 +310,9 @@ class WzDecoderInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
+ initialIsOpen={true} + > +
{this.renderInfo(position, filename, relative_dirname)}
@@ -293,10 +328,9 @@ class WzDecoderInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
- {this.renderDetails(details)} -
+ initialIsOpen={true} + > +
{this.renderDetails(details)}
@@ -311,8 +345,9 @@ class WzDecoderInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
+ initialIsOpen={true} + > +
{ +const mapStateToProps = (state) => { return { - state: state.rulesetReducers + state: state.rulesetReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateFileContent: content => dispatch(updateFileContent(content)), + updateFileContent: (content) => dispatch(updateFileContent(content)), cleanFileContent: () => dispatch(cleanFileContent()), - updateFilters: filters => dispatch(updateFilters(filters)), + updateFilters: (filters) => dispatch(updateFilters(filters)), cleanFilters: () => dispatch(cleanFilters()), - cleanInfo: () => dispatch(cleanInfo()) + cleanInfo: () => dispatch(cleanInfo()), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzDecoderInfo); +export default connect(mapStateToProps, mapDispatchToProps)(WzDecoderInfo); diff --git a/public/controllers/management/components/management/ruleset/list-editor.js b/public/controllers/management/components/management/ruleset/list-editor.js index 37cc6580ac..044c05bc6e 100644 --- a/public/controllers/management/components/management/ruleset/list-editor.js +++ b/public/controllers/management/components/management/ruleset/list-editor.js @@ -24,19 +24,16 @@ import { EuiPopover, EuiFieldText, EuiSpacer, - EuiPanel + EuiPanel, } from '@elastic/eui'; import { connect } from 'react-redux'; -import { - cleanInfo, - updateListContent -} from '../../../../../redux/actions/rulesetActions'; +import { cleanInfo, updateListContent } from '../../../../../redux/actions/rulesetActions'; import { resourceDictionary, RulesetHandler, RulesetResources } from './utils/ruleset-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import exportCsv from '../../../../../react-services/wz-csv'; @@ -44,6 +41,9 @@ import { updateWazuhNotReadyYet } from '../../../../../redux/actions/appStateAct import WzRestartClusterManagerCallout from '../../../../../components/common/restart-cluster-manager-callout'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzListEditor extends Component { constructor(props) { super(props); @@ -56,7 +56,7 @@ class WzListEditor extends Component { addingValue: '', editingValue: '', newListName: '', - showWarningRestart: false + showWarningRestart: false, }; this.items = {}; @@ -92,7 +92,7 @@ class WzListEditor extends Component { contentToObject(content) { const items = {}; const lines = content.split('\n'); - lines.forEach(line => { + lines.forEach((line) => { const split = line.split(':'); const key = split[0]; const value = split[1] || ''; @@ -106,10 +106,8 @@ class WzListEditor extends Component { */ itemsToRaw() { let raw = ''; - Object.keys(this.items).forEach(key => { - raw = raw - ? `${raw}\n${key}:${this.items[key]}` - : `${key}:${this.items[key]}`; + Object.keys(this.items).forEach((key) => { + raw = raw ? `${raw}\n${key}:${this.items[key]}` : `${key}:${this.items[key]}`; }); return raw; } @@ -122,17 +120,10 @@ class WzListEditor extends Component { async saveList(name, path, addingNew = false) { try { if (!name) { - this.showToast( - 'warning', - 'Invalid name', - 'CDB list name cannot be empty', - 3000 - ); + this.showToast('warning', 'Invalid name', 'CDB list name cannot be empty', 3000); return; } - name = name.endsWith('.cdb') - ? name.replace('.cdb', '') - : name; + name = name.endsWith('.cdb') ? name.replace('.cdb', '') : name; const overwrite = addingNew; // If adding new disable the overwrite const raw = this.itemsToRaw(); if (!raw) { @@ -150,23 +141,24 @@ class WzListEditor extends Component { const file = { name: name, content: raw, path: path }; this.props.updateListContent(file); this.setState({ showWarningRestart: true }); - this.showToast( - 'success', - 'Success', - 'CBD List successfully created', - 3000 - ); + this.showToast('success', 'Success', 'CBD List successfully created', 3000); } else { this.setState({ showWarningRestart: true }); this.showToast('success', 'Success', 'CBD List updated', 3000); } } catch (error) { - this.showToast( - 'danger', - 'Error', - 'Error saving CDB list: ' + error, - 3000 - ); + const options = { + context: `${WzListEditor.name}.saveList`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error saving list: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ isSaving: false }); } this.setState({ isSaving: false }); } @@ -176,59 +168,63 @@ class WzListEditor extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; openAddEntry = () => { this.setState({ - isPopoverOpen: true + isPopoverOpen: true, }); }; closeAddEntry = () => { this.setState({ - isPopoverOpen: false + isPopoverOpen: false, }); }; - onChangeKey = e => { + onChangeKey = (e) => { this.setState({ - addingKey: e.target.value + addingKey: e.target.value, }); }; - onChangeValue = e => { + onChangeValue = (e) => { this.setState({ - addingValue: e.target.value + addingValue: e.target.value, }); }; - onChangeEditingValue = e => { + onChangeEditingValue = (e) => { this.setState({ - editingValue: e.target.value + editingValue: e.target.value, }); }; - onNewListNameChange = e => { + onNewListNameChange = (e) => { this.setState({ - newListName: e.target.value + newListName: e.target.value, }); }; getUpdatePermissions = (name) => { - return [{ - action: `${RulesetResources.LISTS}:update`, - resource: resourceDictionary[RulesetResources.LISTS].permissionResource(name), - }]; - } + return [ + { + action: `${RulesetResources.LISTS}:update`, + resource: resourceDictionary[RulesetResources.LISTS].permissionResource(name), + }, + ]; + }; getDeletePermissions = (name) => { - return [{ - action: `${RulesetResources.LISTS}:delete`, - resource: resourceDictionary[RulesetResources.LISTS].permissionResource(name), - }]; - } + return [ + { + action: `${RulesetResources.LISTS}:delete`, + resource: resourceDictionary[RulesetResources.LISTS].permissionResource(name), + }, + ]; + }; /** * Append a key value to this.items and after that if everything works ok re-create the array for the table @@ -251,7 +247,7 @@ class WzListEditor extends Component { this.setState({ items: itemsArr, addingKey: '', - addingValue: '' + addingValue: '', }); } @@ -267,10 +263,9 @@ class WzListEditor extends Component { items: itemsArr, editing: false, editingValue: '', - generatingCsv: false + generatingCsv: false, }); } - /** * Delete a item from the list @@ -324,7 +319,6 @@ class WzListEditor extends Component { * @param {String} path */ renderAddAndSave(name, path, newList = false, items = []) { - const saveButton = ( - this.closeAddEntry()}> - Close - + this.closeAddEntry()}>Close @@ -444,13 +436,13 @@ class WzListEditor extends Component { ); } - buildTableColumns(fileName, path){ + buildTableColumns(fileName, path) { return [ { field: 'key', name: 'Key', align: 'left', - sortable: true + sortable: true, }, { field: 'value', @@ -470,12 +462,12 @@ class WzListEditor extends Component { } else { return {value}; } - } + }, }, { name: 'Actions', align: 'left', - render: item => { + render: (item) => { if (this.state.editing === item.key) { return ( @@ -503,33 +495,33 @@ class WzListEditor extends Component { return ( { this.setState({ editing: item.key, - editingValue: item.value + editingValue: item.value, }); }} color="primary" /> this.deleteItem(item.key)} color="danger" /> ); } - } - } + }, + }, ]; } @@ -544,6 +536,42 @@ class WzListEditor extends Component { const addingNew = name === false || !name; const listName = this.state.newListName || name; + const exportCsv = async () => { + try { + this.setState({ generatingCsv: true }); + await exportCsv( + `/lists`, + [ + { + _isCDBList: true, + name: 'relative_dirname', + value: path, + }, + { + _isCDBList: true, + name: 'filename', + value: name, + }, + ], + name + ); + this.setState({ generatingCsv: false }); + } catch (error) { + const options = { + context: `${WzListEditor.name}.exportCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error generating csv: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ generatingCsv: false }); + } + } + return ( @@ -562,49 +590,21 @@ class WzListEditor extends Component { iconType="exportAction" isDisabled={this.state.generatingCsv} isLoading={this.state.generatingCsv} - onClick={async () => { - try { - this.setState({ generatingCsv: true }); - await exportCsv( - `/lists`, - [ - { - _isCDBList: true, - name: 'relative_dirname', - value: path - }, - { - _isCDBList: true, - name: 'filename', - value: name - } - ], - name - ); - this.setState({ generatingCsv: false }); - } catch (error) { - this.setState({ generatingCsv: false }); - } - }} + onClick={async () => exportCsv()} > Export formatted )} {!this.state.editing && - this.renderAddAndSave( - listName, - path, - !addingNew, - this.state.items - )} + this.renderAddAndSave(listName, path, !addingNew, this.state.items)} {this.state.showWarningRestart && ( - + this.setState({showWarningRestart: false})} - onRestartedError={() => this.setState({showWarningRestart: true})} + onRestarted={() => this.setState({ showWarningRestart: false })} + onRestartedError={() => this.setState({ showWarningRestart: true })} /> )} @@ -635,21 +635,19 @@ class WzListEditor extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.rulesetReducers + state: state.rulesetReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { cleanInfo: () => dispatch(cleanInfo()), - updateListContent: content => dispatch(updateListContent(content)), - updateWazuhNotReadyYet: wazuhNotReadyYet => dispatch(updateWazuhNotReadyYet(wazuhNotReadyYet)) + updateListContent: (content) => dispatch(updateListContent(content)), + updateWazuhNotReadyYet: (wazuhNotReadyYet) => + dispatch(updateWazuhNotReadyYet(wazuhNotReadyYet)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzListEditor); +export default connect(mapStateToProps, mapDispatchToProps)(WzListEditor); diff --git a/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx b/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx new file mode 100644 index 0000000000..0f84427902 --- /dev/null +++ b/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx @@ -0,0 +1,35 @@ +/* + * Wazuh app - React test for Ruleset component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import WzRuleset from './main-ruleset'; + +jest.mock('../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +describe('Ruleset component', () => { + it('renders correctly to match the snapshot', () => { + const logtestProps = ''; + const clusterStatus = ''; + const wrapper = mount(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/ruleset/rule-info.js b/public/controllers/management/components/management/ruleset/rule-info.js index 29725ed52f..3003656924 100644 --- a/public/controllers/management/components/management/ruleset/rule-info.js +++ b/public/controllers/management/components/management/ruleset/rule-info.js @@ -15,7 +15,7 @@ import { EuiAccordion, EuiFlexGrid, EuiButtonEmpty, - EuiLoadingSpinner + EuiLoadingSpinner, } from '@elastic/eui'; import { connect } from 'react-redux'; @@ -28,10 +28,13 @@ import { cleanFileContent, cleanInfo, updateFilters, - cleanFilters + cleanFilters, } from '../../../../../redux/actions/rulesetActions'; import WzTextWithTooltipTruncated from '../../../../../components/common/wz-text-with-tooltip-if-truncated'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzRuleInfo extends Component { constructor(props) { @@ -43,25 +46,51 @@ class WzRuleInfo extends Component { hipaa: 'HIPAA', 'nist-800-53': 'NIST-800-53', tsc: 'TSC', - 'mitreTactics': 'MITRE Tactics', - 'mitreTechniques': 'MITRE Techniques', - 'mitre': 'MITRE' + mitreTactics: 'MITRE Tactics', + mitreTechniques: 'MITRE Techniques', + mitre: 'MITRE', }; this.state = { mitreTactics: [], mitreLoading: false, mitreTechniques: [], - mitreRuleId: "", - mitreIds: [] + mitreRuleId: '', + mitreIds: [], }; this.rulesetHandler = new RulesetHandler(RulesetResources.RULES); + + const handleFileClick = async (event) => { + event.stopPropagation(); + try { + const result = await this.rulesetHandler.getFileContent(value); + const file = { + name: value, + content: result, + path: item.relative_dirname, + }; + this.props.updateFileContent(file); + } catch (error) { + const options = { + context: `${WzRuleInfo.name}.handleFileClick`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error updating file content: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + this.columns = [ { field: 'id', name: 'ID', align: 'left', sortable: true, - width: '5%' + width: '5%', }, { field: 'description', @@ -85,25 +114,25 @@ class WzRuleInfo extends Component {
); - } + }, }, { field: 'groups', name: 'Groups', align: 'left', sortable: true, - width: '10%' + width: '10%', }, { name: 'Compliance', - render: this.buildComplianceBadges + render: this.buildComplianceBadges, }, { field: 'level', name: 'Level', align: 'left', sortable: true, - width: '5%' + width: '5%', }, { field: 'filename', @@ -114,24 +143,11 @@ class WzRuleInfo extends Component { render: (value, item) => { return ( - { - event.stopPropagation(); - const result = await this.rulesetHandler.getFileContent(value); - const file = { - name: value, - content: result, - path: item.relative_dirname - }; - this.props.updateFileContent(file); - }} - > - {value} - + handleFileClick(event)}>{value} ); - } - } + }, + }, ]; } @@ -146,18 +162,9 @@ class WzRuleInfo extends Component { */ buildCompliance(ruleInfo) { const compliance = {}; - const complianceKeys = [ - 'gdpr', - 'gpg13', - 'hipaa', - 'nist-800-53', - 'pci', - 'tsc', - 'mitre' - ]; - Object.keys(ruleInfo).forEach(key => { - if (complianceKeys.includes(key) && ruleInfo[key].length) - compliance[key] = ruleInfo[key]; + const complianceKeys = ['gdpr', 'gpg13', 'hipaa', 'nist-800-53', 'pci', 'tsc', 'mitre']; + Object.keys(ruleInfo).forEach((key) => { + if (complianceKeys.includes(key) && ruleInfo[key].length) compliance[key] = ruleInfo[key]; }); return compliance || {}; } @@ -165,25 +172,16 @@ class WzRuleInfo extends Component { buildComplianceBadges(item) { const badgeList = []; const fields = ['pci_dss', 'gpg13', 'hipaa', 'gdpr', 'nist_800_53', 'tsc', 'mitre']; - const buildBadge = field => { + const buildBadge = (field) => { const idGenerator = () => { - return ( - '_' + - Math.random() - .toString(36) - .substr(2, 9) - ); + return '_' + Math.random().toString(36).substr(2, 9); }; return ( - + ev.stopPropagation()} + onClick={(ev) => ev.stopPropagation()} onClickAriaLabel={field.toUpperCase()} color="hollow" style={{ margin: '1px 2px' }} @@ -199,7 +197,19 @@ class WzRuleInfo extends Component { badgeList.push(buildBadge(field)); } } - } catch (error) { } + } catch (error) { + const options = { + context: `${WzRuleInfo.name}.buildComplianceBadges`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + } + }; + getErrorOrchestrator().handleError(options); + } return
{badgeList}
; } @@ -224,18 +234,18 @@ class WzRuleInfo extends Component { renderInfo(id, level, file, path, groups) { return ( - - ID - - - this.setNewFiltersAndBack([{ field: 'rule_ids', value: id }])} - > - {id} - - - - + + ID + + + this.setNewFiltersAndBack([{ field: 'rule_ids', value: id }])} + > + {id} + + + + Level @@ -253,7 +263,9 @@ class WzRuleInfo extends Component { this.setNewFiltersAndBack([{ field: 'filename', value: file }])} + onClick={async () => + this.setNewFiltersAndBack([{ field: 'filename', value: file }]) + } > {file} @@ -265,14 +277,17 @@ class WzRuleInfo extends Component { this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }])} + onClick={async () => + this.setNewFiltersAndBack([{ field: 'relative_dirname', value: path }]) + } > {path} - Groups + + Groups {this.renderGroups(groups)} @@ -281,18 +296,24 @@ class WzRuleInfo extends Component { } getFormattedDetails(value) { - if (Array.isArray(value) && value[0].type) { - let link = ""; - let name = ""; + let link = ''; + let name = ''; - value.forEach(item => { - if (item.type === 'cve') - name = item.name; + value.forEach((item) => { + if (item.type === 'cve') name = item.name; if (item.type === 'link') - link = {item.name} - }) - return {name}: {link} + link = ( + + {item.name} + + ); + }); + return ( + + {name}: {link} + + ); } else if (value && typeof value === 'object' && value.constructor === Object) { let list = []; Object.keys(value).forEach((key, idx) => { @@ -307,17 +328,11 @@ class WzRuleInfo extends Component { }); return (
    -
  • - {list} -
  • +
  • {list}
); } else { - return ( - - {value} - - ); + return {value}; } } @@ -327,15 +342,18 @@ class WzRuleInfo extends Component { */ renderDetails(details) { const detailsToRender = []; - const capitalize = str => str[0].toUpperCase() + str.slice(1); + const capitalize = (str) => str[0].toUpperCase() + str.slice(1); // Exclude group key of details - Object.keys(details).filter(key => key !== 'group').forEach((key) => { - detailsToRender.push( - - {capitalize(key)}{details[key] === '' ? 'true' : this.getFormattedDetails(details[key])} - - ); - }); + Object.keys(details) + .filter((key) => key !== 'group') + .forEach((key) => { + detailsToRender.push( + + {capitalize(key)} + {details[key] === '' ? 'true' : this.getFormattedDetails(details[key])} + + ); + }); return {detailsToRender}; } @@ -351,10 +369,7 @@ class WzRuleInfo extends Component { this.setNewFiltersAndBack([{ field: 'group', value: group }])} > - + {group} @@ -364,55 +379,73 @@ class WzRuleInfo extends Component { }); return (
    -
  • - {listGroups} -
  • +
  • {listGroups}
); } - async getTacticsNames(tactics){ - try{ - let tacticsObj = [] - const data = await WzRequest.apiReq('GET', '/mitre/tactics', { - params: { - tactic_ids: tactics.toString() - } - }) - const formattedData = (((((data || {}).data).data || {}).affected_items || []) || {}); - formattedData && formattedData.forEach(item => { + async getTacticsNames(tactics) { + try { + let tacticsObj = []; + const data = await WzRequest.apiReq('GET', '/mitre/tactics', { + params: { + tactic_ids: tactics.toString(), + }, + }); + const formattedData = ((data || {}).data.data || {}).affected_items || [] || {}; + formattedData && + formattedData.forEach((item) => { tacticsObj.push(item.name); }); - return tacticsObj; - }catch(error){ + return tacticsObj; + } catch (error) { return []; } } - async addMitreInformation(compliance, currentRuleId){ - try{ - this.setState({mitreLoading: true, mitreRuleId: currentRuleId }) + async addMitreInformation(compliance, currentRuleId) { + try { + this.setState({ mitreLoading: true, mitreRuleId: currentRuleId }); const mitreName = []; const mitreIds = []; - const mitreTactics = await Promise.all(compliance.map(async (i) => { - const data = await WzRequest.apiReq('GET', '/mitre/techniques', { - params: { - q: `references.external_id=${i}` - } - }); - const formattedData = (((((data || {}).data).data || {}).affected_items || [])[0] || {}); - const tactics = this.getTacticsNames(formattedData.tactics) || []; - mitreName.push(formattedData.name); - mitreIds.push(i); - return tactics; - })); - if(mitreTactics.length){ - let removeDuplicates = (arr) => arr.filter((v,i) => arr.indexOf(v) === i) + const mitreTactics = await Promise.all( + compliance.map(async (i) => { + const data = await WzRequest.apiReq('GET', '/mitre/techniques', { + params: { + q: `references.external_id=${i}`, + }, + }); + const formattedData = (((data || {}).data.data || {}).affected_items || [])[0] || {}; + const tactics = this.getTacticsNames(formattedData.tactics) || []; + mitreName.push(formattedData.name); + mitreIds.push(i); + return tactics; + }) + ); + if (mitreTactics.length) { + let removeDuplicates = (arr) => arr.filter((v, i) => arr.indexOf(v) === i); const uniqueTactics = removeDuplicates(mitreTactics.flat()); - this.setState({mitreLoading: false, mitreRuleId: currentRuleId, mitreIds, mitreTactics: uniqueTactics, mitreTechniques:mitreName }) + this.setState({ + mitreLoading: false, + mitreRuleId: currentRuleId, + mitreIds, + mitreTactics: uniqueTactics, + mitreTechniques: mitreName, + }); } - }catch(error){ - this.setState({mitreLoading: false}) + } catch (error) { + this.setState({ mitreLoading: false }); + const options = { + context: `${WzRuleInfo.name}.addMitreInformation`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -421,14 +454,23 @@ class WzRuleInfo extends Component { * @param {Array} compliance */ renderCompliance(compliance) { - const currentRuleId = (this.state && this.state.currentRuleId) ? this.state.currentRuleId : this.props.state.ruleInfo.current; - if(compliance.mitre && compliance.mitre.length && currentRuleId !== this.state.mitreRuleId){ + const currentRuleId = + this.state && this.state.currentRuleId + ? this.state.currentRuleId + : this.props.state.ruleInfo.current; + if (compliance.mitre && compliance.mitre.length && currentRuleId !== this.state.mitreRuleId) { this.addMitreInformation(compliance.mitre, currentRuleId); - }else if(currentRuleId !== this.state.mitreRuleId){ - this.setState({mitreLoading: false, mitreRuleId: currentRuleId, mitreIds: [], mitreTactics: [], mitreTechniques: [] }); + } else if (currentRuleId !== this.state.mitreRuleId) { + this.setState({ + mitreLoading: false, + mitreRuleId: currentRuleId, + mitreIds: [], + mitreTactics: [], + mitreTechniques: [], + }); } const listCompliance = []; - if(compliance.mitre) delete compliance.mitre; + if (compliance.mitre) delete compliance.mitre; const keys = Object.keys(compliance); for (let i in Object.keys(keys)) { const key = keys[i]; @@ -457,13 +499,14 @@ class WzRuleInfo extends Component { ); } - if(this.state.mitreTechniques && this.state.mitreTechniques.length){ - + if (this.state.mitreTechniques && this.state.mitreTechniques.length) { const values = this.state.mitreTechniques.map((element, index) => { return ( this.setNewFiltersAndBack([{ field: 'mitre', value: this.state.mitreIds[index] }])} + onClick={async () => + this.setNewFiltersAndBack([{ field: 'mitre', value: this.state.mitreIds[index] }]) + } > {element} @@ -473,20 +516,23 @@ class WzRuleInfo extends Component { ); }); - listCompliance.push( - {this.complianceEquivalences['mitreTechniques']} - {this.state.mitreLoading && ||

{values}

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

{values}

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

{this.state.mitreTactics.toString()}

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

{this.state.mitreTactics.toString()}

+ +
+ ); } return {listCompliance}; @@ -497,13 +543,16 @@ class WzRuleInfo extends Component { * @param {Number} ruleId */ changeBetweenRules(ruleId) { - window.location.href = window.location.href.replace(new RegExp('redirectRule=' + '[^&]*'), `redirectRule=${ruleId}`); + window.location.href = window.location.href.replace( + new RegExp('redirectRule=' + '[^&]*'), + `redirectRule=${ruleId}` + ); this.setState({ currentRuleId: ruleId }); } /** * Update style for title with elements $() - * @param {string} value + * @param {string} value */ updateStyleTitle(value) { if (value === undefined) return ''; @@ -521,19 +570,22 @@ class WzRuleInfo extends Component { render() { const { ruleInfo, isLoading } = this.props.state; - const currentRuleId = (this.state && this.state.currentRuleId) ? this.state.currentRuleId : ruleInfo.current; + const currentRuleId = + this.state && this.state.currentRuleId ? this.state.currentRuleId : ruleInfo.current; const rules = ruleInfo.affected_items; - const currentRuleArr = rules.filter(r => { return r.id === currentRuleId }); + const currentRuleArr = rules.filter((r) => { + return r.id === currentRuleId; + }); const currentRuleInfo = currentRuleArr[0]; const { description, details, filename, relative_dirname, level, id, groups } = currentRuleInfo; const compliance = this.buildCompliance(currentRuleInfo); const columns = this.columns; - const onClickRow = item => { + const onClickRow = (item) => { return { onClick: () => { this.changeBetweenRules(item.id); - } + }, }; }; @@ -553,12 +605,19 @@ class WzRuleInfo extends Component { iconSize="l" iconType="arrowLeft" onClick={() => { - window.location.href = window.location.href.replace(new RegExp('redirectRule=' + '[^&]*'), ''); + window.location.href = window.location.href.replace( + new RegExp('redirectRule=' + '[^&]*'), + '' + ); this.props.cleanInfo(); }} />
- {} + { + + } @@ -567,7 +626,8 @@ class WzRuleInfo extends Component { iconType="popout" aria-label="popout" href={`#/overview?tab=general&tabView=panels&addRuleFilter=${id}`} - target="blank"> + target="blank" + > View alerts of this Rule @@ -585,8 +645,9 @@ class WzRuleInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
+ initialIsOpen={true} + > +
{this.renderInfo(id, level, filename, relative_dirname, groups)}
@@ -603,10 +664,9 @@ class WzRuleInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
- {this.renderDetails(details)} -
+ initialIsOpen={true} + > +
{this.renderDetails(details)}
@@ -622,8 +682,9 @@ class WzRuleInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
+ initialIsOpen={true} + > +
{this.renderCompliance(compliance)}
@@ -641,8 +702,9 @@ class WzRuleInfo extends Component { } paddingSize="none" - initialIsOpen={true}> -
+ initialIsOpen={true} + > +
{ +const mapStateToProps = (state) => { return { - state: state.rulesetReducers + state: state.rulesetReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateFileContent: content => dispatch(updateFileContent(content)), + updateFileContent: (content) => dispatch(updateFileContent(content)), cleanFileContent: () => dispatch(cleanFileContent()), - updateFilters: filters => dispatch(updateFilters(filters)), + updateFilters: (filters) => dispatch(updateFilters(filters)), cleanFilters: () => dispatch(cleanFilters()), - cleanInfo: () => dispatch(cleanInfo()) + cleanInfo: () => dispatch(cleanInfo()), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzRuleInfo); +export default connect(mapStateToProps, mapDispatchToProps)(WzRuleInfo); diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index 01fe585374..0bd49f51e8 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -51,6 +51,10 @@ import { showFlyoutLogtest } from '../../../../../redux/actions/appStateActions' import { WzOverlayMask } from '../../../../../components/common/util'; import _ from 'lodash'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzRulesetEditor extends Component { _isMounted = false; constructor(props) { @@ -112,45 +116,67 @@ class WzRulesetEditor extends Component { try { await validateConfigAfterSent(); } catch (error) { - const toast = { - title: 'File content is incorrect.', - toastMessage: `The content of the file ${name} is incorrect. There were found several error while validating the configuration.`, - toastLifeTimeMs: 5000, + const options = { + context: `${WzRulesetEditor.name}.save`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message:`The content of the file ${name} is incorrect. There were found several errors while validating the configuration: ${error.message || error}`, + title: `Error file content is incorrect: ${error.message || error}`, + }, }; - + getErrorOrchestrator().handleError(options); this.setState({ isSaving: false }); this.goToEdit(name); + let toastMessage; + if (this.props.state.addingRulesetFile != false) { //remove current invalid file if the file is new. await this.rulesetHandler.deleteFile(name); - toast.toastMessage += '\nThe new file was deleted.'; + toastMessage = 'The new file was deleted.'; } else { //restore file to previous version await this.rulesetHandler.updateFile(name, this.state.initContent, overwrite); - toast.toastMessage += '\nThe content file was restored to previous state.'; + toastMessage = 'The content file was restored to previous state.'; } - getToasts().addError({ stack: error, message: toast.toastMessage }, toast); - + this.showToast('success', 'Success', toastMessage, 3000); return; } this.setState({ isSaving: false }); this.goToEdit(name); - let textSuccess = 'New file successfully created'; - if (overwrite) { - textSuccess = 'File successfully edited'; + let errorMessage = `The content of the file ${name} is incorrect. There were found several errors while validating the configuration: ${error.message || error}`; + if (this.props.state.addingRulesetFile != false) { + //remove current invalid file if the file is new. + await this.rulesetHandler.deleteFile(name); + errorMessage += '\nThe new file was deleted.'; + } else { + //restore file to previous version + await this.rulesetHandler.updateFile(name, this.state.initContent, overwrite); + errorMessage += '\nThe content file was restored to previous state.'; } this.setState({ showWarningRestart: true, initialInputValue: this.state.inputValue, initContent: content, }); - this.showToast('success', 'Success', textSuccess, 3000); + } catch (error) { this.setState({ error, isSaving: false }); - this.showToast('danger', 'Error', 'Error saving file: ' + error, 3000); + const options = { + context: `${WzRulesetEditor.name}.save`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: errorMessage, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js b/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js index 5234cc8643..01b78fa955 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js +++ b/public/controllers/management/components/management/ruleset/ruleset-filter-bar.js @@ -21,6 +21,9 @@ import { } from '../../../../../redux/actions/rulesetActions'; import { RulesetHandler, RulesetResources } from './utils/ruleset-handler'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzRulesetFilterBar extends Component { constructor(props) { @@ -52,7 +55,7 @@ class WzRulesetFilterBar extends Component { } componentDidMount() { - this.buildSelectedOptions(this.props.state.filters); // If there are any filter in the redux store it will be restored when the component was mounted + this.buildSelectedOptions(this.props.state.filters); // If there are any filter in the redux store it will be restored when the component was mounted } isValid = value => { @@ -99,7 +102,17 @@ class WzRulesetFilterBar extends Component { //const result = await this.wzReq.apiReq('GET', this.paths[section], {}) if (Object.keys(filters).length) await this.fetchItems(filters); } catch (error) { - console.error('error building selected options ', error); + const options = { + context: `${WzRulesetFilterBar.name}.buildSelectedOptions`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `Error building selected options: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -108,7 +121,6 @@ class WzRulesetFilterBar extends Component { * @param {Object} filters */ async fetchItems(filters) { - console.log("fetch items") try { const { section } = this.props.state; let fetcher = this.rulesetHandler.getResource; @@ -118,7 +130,7 @@ class WzRulesetFilterBar extends Component { this.props.updateLoadingStatus(false); } catch (error) { this.props.updateError(error); - return Promise.reject(error); + throw error; } } @@ -145,7 +157,17 @@ class WzRulesetFilterBar extends Component { this.props.updateFilters(currentOptions); await this.fetchItems(currentOptions); } catch (error) { - console.error('error cleaning current options ', error); + const options = { + context: `${WzRulesetFilterBar.name}.cleanCurrentOption`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: `Error cleaning current options: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/controllers/management/components/management/ruleset/ruleset-table.js b/public/controllers/management/components/management/ruleset/ruleset-table.js index b33c4a03c7..7c9f3afd0f 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-table.js +++ b/public/controllers/management/components/management/ruleset/ruleset-table.js @@ -10,12 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import { - EuiBasicTable, - EuiCallOut, - EuiOverlayMask, - EuiConfirmModal -} from '@elastic/eui'; +import { EuiBasicTable, EuiCallOut, EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; import { connect } from 'react-redux'; import { RulesetHandler, RulesetResources, resourceDictionary } from './utils/ruleset-handler'; @@ -29,7 +24,7 @@ import { updateFileContent, updateListItemsForRemove, updateRuleInfo, - updateDecoderInfo + updateDecoderInfo, } from '../../../../../redux/actions/rulesetActions'; import RulesetColums from './utils/columns'; @@ -38,7 +33,9 @@ import { filtersToObject } from '../../../../../components/wz-search-bar'; import { withUserPermissions } from '../../../../../components/common/hocs/withUserPermissions'; import { WzUserPermissions } from '../../../../../react-services/wz-user-permissions'; import { compose } from 'redux'; - +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzRulesetTable extends Component { _isMounted = false; constructor(props) { @@ -50,12 +47,12 @@ class WzRulesetTable extends Component { pageIndex: 0, totalItems: 0, isLoading: false, - isRedirect: false + isRedirect: false, }; this.paths = { rules: '/rules', decoders: '/decoders', - lists: '/lists/files' + lists: '/lists/files', }; this.extraSectionPrefixResource = { rules: 'rule:file', @@ -77,49 +74,80 @@ class WzRulesetTable extends Component { try { const result = await this.rulesetHandler.getResource({ params: { - rule_ids: id - } + rule_ids: id, + }, }); const items = result.data.affected_items || []; if (items.length) { const info = await this.rulesetHandler.getResource({ params: { - filename: items[0].filename - } + filename: items[0].filename, + }, }); if (info.data) { Object.assign(info.data, { current: parseInt(id) }); } this.props.updateRuleInfo(info.data); } - } catch (error) { } + } catch (error) { + const options = { + context: `${WzRulesetTable.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error getting resources: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } this._isMounted && this.setState({ isRedirect: false }); } } } async componentDidUpdate(prevProps) { - const { isProcessing, section, showingFiles, filters, } = this.props.state; + const { isProcessing, section, showingFiles, filters } = this.props.state; - const processingChange = prevProps.state.isProcessing !== isProcessing || + const processingChange = + prevProps.state.isProcessing !== isProcessing || (prevProps.state.isProcessing && isProcessing); const sectionChanged = prevProps.state.section !== section; - const showingFilesChanged = - prevProps.state.showingFiles !== showingFiles; + const showingFilesChanged = prevProps.state.showingFiles !== showingFiles; const filtersChanged = prevProps.state.filters !== filters; - if ((this._isMounted && processingChange && isProcessing) || sectionChanged || filtersChanged) { - if (sectionChanged || showingFilesChanged || filtersChanged) { - this._isMounted && await this.setState({ - pageSize: this.state.pageSize, - pageIndex: 0, - sortDirection: null, - sortField: null - }); - } - this._isMounted && this.setState({ isLoading: true }); - this.props.updateIsProcessing(false); - await this.getItems(); + try { + if ( + (this._isMounted && processingChange && isProcessing) || + sectionChanged || + filtersChanged + ) { + if (sectionChanged || showingFilesChanged || filtersChanged) { + this._isMounted && + (await this.setState({ + pageSize: this.state.pageSize, + pageIndex: 0, + sortDirection: null, + sortField: null, + })); + } + this._isMounted && this.setState({ isLoading: true }); + this.props.updateIsProcessing(false); + await this.getItems(); + } + } catch (error) { + const options = { + context: `${WzRulesetTable.name}.componentDidUpdate`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error getting items: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -130,42 +158,44 @@ class WzRulesetTable extends Component { async getItems() { const { section, showingFiles } = this.props.state; - this._isMounted && this.setState({ - items: [] - }); + this._isMounted && + this.setState({ + items: [], + }); this.props.updateTotalItems(false); const rawItems = await this.wzReq( 'GET', `${this.paths[this.props.request]}${showingFiles ? '/files' : ''}`, - { params: this.buildFilter() }, + { params: this.buildFilter() } ).catch((error) => { - console.warn(`Error when get the items of ${section}: `, error); - return {}; + throw new Error(`Error when get the items of ${section}`); }); - const { affected_items = [], total_affected_items = 0 } = ((rawItems || {}).data || {}).data || {}; + const { affected_items = [], total_affected_items = 0 } = + ((rawItems || {}).data || {}).data || {}; this.props.updateTotalItems(total_affected_items); - this._isMounted && this.setState({ - items: affected_items, - totalItems: total_affected_items, - isLoading: false - }); + this._isMounted && + this.setState({ + items: affected_items, + totalItems: total_affected_items, + isLoading: false, + }); } async setDefaultItems() { - const requestDefaultItems = await this.wzReq( - 'GET', - '/manager/configuration', - { + try { + const requestDefaultItems = await this.wzReq('GET', '/manager/configuration', { wait_for_complete: false, section: 'ruleset', - field: 'list' - } - ); + field: 'list', + }); - const defaultItems = ((requestDefaultItems || {}).data || {}).data; - this.props.updateDefaultItems(defaultItems); + const defaultItems = ((requestDefaultItems || {}).data || {}).data; + this.props.updateDefaultItems(defaultItems); + } catch (error) { + throw error; + } } buildFilter() { @@ -175,7 +205,7 @@ class WzRulesetTable extends Component { offset: pageIndex * pageSize, limit: pageSize, ...this.buildSortFilter(), - ...filtersToObject(filters) + ...filtersToObject(filters), }; return filter; @@ -216,7 +246,7 @@ class WzRulesetTable extends Component { sortField, sortDirection, isLoading, - isRedirect + isRedirect, } = this.state; const columns = this.getColumns(); const message = isLoading ? null : 'No results...'; @@ -224,15 +254,15 @@ class WzRulesetTable extends Component { pageIndex: pageIndex, pageSize: pageSize, totalItemCount: totalItems, - pageSizeOptions: [10, 15, 25, 50, 100] + pageSizeOptions: [10, 15, 25, 50, 100], }; const sorting = !!sortField ? { - sort: { - field: sortField, - direction: sortDirection + sort: { + field: sortField, + direction: sortDirection, + }, } - } : {}; if (!error) { @@ -248,10 +278,53 @@ class WzRulesetTable extends Component { { action: `${section}:read`, resource: permissionResource(item.name), - } + }, ]; }; + const updateInfo = async () => { + if (this.isLoading) return; + this.setState({ isLoading: true }); + const { section } = this.props.state; + window.location.href = `${window.location.href}&redirectRule=${id}`; + try { + if (section === RulesetResources.LISTS) { + const result = await this.rulesetHandler.getFileContent(item.filename); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + this.props.updateListContent(file); + } else { + const result = await this.rulesetHandler.getResource({ + params: { + filename: item.filename, + }, + }); + if (result.data) { + Object.assign(result.data, { current: id || name }); + } + if (section === RulesetResources.RULES) this.props.updateRuleInfo(result.data); + if (section === RulesetResources.DECODERS) this.props.updateDecoderInfo(result.data); + } + } catch (error) { + const options = { + context: `${WzRulesetTable.name}.updateInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error updating info: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ isLoading: false }); + } + this.setState({ isLoading: false }); + }; + return { 'data-test-subj': `row-${id || name}`, className: 'customRowClass', @@ -259,34 +332,7 @@ class WzRulesetTable extends Component { getRequiredPermissions(item), this.props.userPermissions ) - ? async () => { - if (this.isLoading) return; - this.setState({ isLoading: true }); - const { section } = this.props.state; - window.location.href = `${window.location.href}&redirectRule=${id}`; - if (section === RulesetResources.LISTS) { - const result = await this.rulesetHandler.getFileContent(item.filename); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - this.props.updateListContent(file); - } else { - const result = await this.rulesetHandler.getResource({ - params: { - filename: item.filename - } - }); - if (result.data) { - Object.assign(result.data, { current: id || name }); - } - if (section === RulesetResources.RULES) this.props.updateRuleInfo(result.data); - if (section === RulesetResources.DECODERS) this.props.updateDecoderInfo(result.data); - } - - this.setState({ isLoading: false }); - } + ? updateInfo : undefined, }; }; @@ -300,9 +346,7 @@ class WzRulesetTable extends Component { pagination={pagination} onChange={this.onTableChange} loading={isLoading || isRedirect} - rowProps={ - (!this.props.state.showingFiles && getRowProps) || undefined - } + rowProps={(!this.props.state.showingFiles && getRowProps) || undefined} sorting={sorting} message={message} /> @@ -323,9 +367,7 @@ class WzRulesetTable extends Component {

These items will be removed

{itemList.map(function (item, i) { - return ( -
  • {(item.filename) ? item.filename : item.name}
  • - ); + return
  • {item.filename ? item.filename : item.name}
  • ; })}
    @@ -343,20 +385,34 @@ class WzRulesetTable extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; async removeItems(items) { - this.setState({ isLoading: true }); - const results = items.map(async (item, i) => { - await this.rulesetHandler.deleteFile(item.filename || item.name); - }); - - Promise.all(results).then(completed => { - this.props.updateIsProcessing(true); - this.showToast('success', 'Success', 'Deleted successfully', 3000); - }); + try { + this.setState({ isLoading: true }); + const results = items.map(async (item, i) => { + await this.rulesetHandler.deleteFile(item.filename || item.name); + }); + + Promise.all(results).then((completed) => { + this.props.updateIsProcessing(true); + this.showToast('success', 'Success', 'Deleted successfully', 3000); + }); + } catch (error) { + const options = { + context: `${WzRulesetTable.name}.removeItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: `Error deleting item: ${error.message || error}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } } diff --git a/public/controllers/management/components/management/ruleset/section-selector.js b/public/controllers/management/components/management/ruleset/section-selector.js index d1005dff45..e16554edd8 100644 --- a/public/controllers/management/components/management/ruleset/section-selector.js +++ b/public/controllers/management/components/management/ruleset/section-selector.js @@ -23,6 +23,9 @@ import { } from '../../../../redux/actions/rulesetActions'; import { WzRequest } from '../../../../react-services/wz-request'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzSectionSelector extends Component { constructor(props) { @@ -65,14 +68,29 @@ class WzSectionSelector extends Component { this.props.updateLoadingStatus(false); } catch (error) { this.props.updateError(error); + throw error; } } onChange = async e => { - const section = e.target.value; - this.props.cleanFilters(); - this.props.updateIsProcessing(true); - this.fetchData(section); + try { + const section = e.target.value; + this.props.cleanFilters(); + this.props.updateIsProcessing(true); + this.fetchData(section); + }catch (error){ + const options = { + context: `${WzSectionSelector.name}.onChange`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error fetching data: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); + } }; render() { diff --git a/public/controllers/management/components/management/ruleset/utils/ruleset-handler.ts b/public/controllers/management/components/management/ruleset/utils/ruleset-handler.ts index 03efaef23d..bff3abb517 100644 --- a/public/controllers/management/components/management/ruleset/utils/ruleset-handler.ts +++ b/public/controllers/management/components/management/ruleset/utils/ruleset-handler.ts @@ -48,7 +48,7 @@ export class RulesetHandler { const result: any = await WzRequest.apiReq('GET', this.getResourcePath(), filters); return (result || {}).data || false ; } catch (error) { - return Promise.reject(error); + throw error } } @@ -66,7 +66,7 @@ export class RulesetHandler { }); return ((result || {}).data || ''); } catch (error) { - return Promise.reject(error); + throw error; } } @@ -87,7 +87,7 @@ export class RulesetHandler { }); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -103,7 +103,7 @@ export class RulesetHandler { const result = await WzRequest.apiReq('DELETE', fullPath, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } diff --git a/public/controllers/management/components/management/ruleset/utils/valid-configuration.js b/public/controllers/management/components/management/ruleset/utils/valid-configuration.js index 3c17c4244c..9d31b8ee70 100644 --- a/public/controllers/management/components/management/ruleset/utils/valid-configuration.js +++ b/public/controllers/management/components/management/ruleset/utils/valid-configuration.js @@ -43,7 +43,7 @@ const validateConfigAfterSent = async (node = false) => { } return true; } catch (error) { - return Promise.reject(error); + throw error; } }; From 3e96b3eda970621f5a9ee0f8c33dd5ed6210a447 Mon Sep 17 00:00:00 2001 From: Franco Charriol Date: Fri, 2 Jul 2021 10:59:17 -0300 Subject: [PATCH 039/493] Cherry pick #3397 into 4.3 7.10 (#3419) * cherry-pick: #3397 * fix: wrong property in parsedData --- CHANGELOG.md | 1 + .../components/settings/settings-logs/logs.js | 7 ++- server/lib/base-logger.ts | 52 ++++++++++++++++--- server/start/cron-scheduler/error-handler.ts | 3 +- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 638f395dad..dbe8617f6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed export to csv button in dashboards tables [#3358](https://github.com/wazuh/wazuh-kibana-app/pull/3358) - Fixed Elastic UI breaking changes in 7.12 [#3345](https://github.com/wazuh/wazuh-kibana-app/pull/3345) - Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) +- Fixed generation of huge logs from backend errors [#3397](https://github.com/wazuh/wazuh-kibana-app/pull/3397) ## Wazuh v4.2.0 - Kibana 7.10.2 , 7.11.2 - Revision 4201 diff --git a/public/components/settings/settings-logs/logs.js b/public/components/settings/settings-logs/logs.js index b8d7841785..2ea5061df7 100644 --- a/public/components/settings/settings-logs/logs.js +++ b/public/components/settings/settings-logs/logs.js @@ -76,6 +76,11 @@ class SettingsLogs extends Component { .split('.')[0]; } + getMessage(log) { + const data = log.data || log.message; + return typeof data === 'object' ? data.message || JSON.stringify(data) : data.toString(); + } + render() { let text = ''; (this.state.logs || []).forEach(x => { @@ -85,7 +90,7 @@ class SettingsLogs extends Component { ' ' + x.level.toUpperCase() + ' ' + - x.message + + this.getMessage(x) + '\n'); }); return ( diff --git a/server/lib/base-logger.ts b/server/lib/base-logger.ts index 8db9faf30a..a25e77f3ae 100644 --- a/server/lib/base-logger.ts +++ b/server/lib/base-logger.ts @@ -20,7 +20,8 @@ import { WAZUH_DATA_LOGS_DIRECTORY_PATH, MAX_MB_LOG_FILES } from '../../common/c export interface IUIPlainLoggerSettings { level: string; - message: string; + message?: string; + data?: any; } export interface IUILoggerSettings extends IUIPlainLoggerSettings { @@ -179,14 +180,47 @@ export class BaseLogger { return `${y}/${m < 10 ? '0' : ''}${m}/${d < 10 ? '0' : ''}${d} ${hour}:${minutes}:${seconds}`; }; + /** + * This function filter some known interfaces to avoid log hug objects + * @param data string | object + * @returns the data parsed + */ + private parseData = (data: any) => { + let parsedData = + data instanceof Error + ? { + message: data.message, + stack: data.stack, + } + : data; + + // when error is AxiosError, it extends from Error + if (data.isAxiosError) { + const { config } = data; + parsedData = { + ...parsedData, + config: { + url: config.url, + method: config.method, + data: config.data, + params: config.params, + }, + }; + } + + if (typeof parsedData === 'object') parsedData.toString = () => JSON.stringify(parsedData); + + return parsedData; + }; + /** * Main function to add a new log * @param {*} location File where the log is being thrown - * @param {*} message Message to show + * @param {*} data Message or object to log * @param {*} level Optional, default is 'error' */ - - async log(location: string, message: string, level: string) { + async log(location: string, data: any, level: string) { + const parsedData = this.parseData(data); return this.initDirectory() .then(() => { if (this.allowed) { @@ -194,7 +228,7 @@ export class BaseLogger { const plainLogData: IUIPlainLoggerSettings = { level: level || 'error', message: `${this.yyyymmdd()}: ${location || 'Unknown origin'}: ${ - message || 'An error occurred' + parsedData.toString() || 'An error occurred' }`, }; @@ -204,8 +238,14 @@ export class BaseLogger { date: new Date(), level: level || 'error', location: location || 'Unknown origin', - message: message || 'An error occurred', + data: parsedData || 'An error occurred', }; + + if (typeof data == 'string') { + logData.message = parsedData; + delete logData.data; + } + this.wazuhLogger.log(logData); } }) diff --git a/server/start/cron-scheduler/error-handler.ts b/server/start/cron-scheduler/error-handler.ts index a4772577f8..e1ad63be95 100644 --- a/server/start/cron-scheduler/error-handler.ts +++ b/server/start/cron-scheduler/error-handler.ts @@ -4,7 +4,6 @@ import { getConfiguration } from '../../lib/get-configuration'; const DEBUG = 'debug'; const INFO = 'info'; const ERROR = 'error'; -const COLOR = '\u001b[34mwazuh\u001b[39m'; function logLevel(level: string){ return level === DEBUG ? INFO : level; @@ -16,7 +15,7 @@ export function ErrorHandler(error, serverLogger) { log('Cron-scheduler', error, errorLevel === ERROR ? INFO : errorLevel); try { if (errorLevel === DEBUG && logsLevel !== DEBUG) return; - serverLogger[logLevel(errorLevel)](`${JSON.stringify(error)}`); + serverLogger[logLevel(errorLevel)](`${error instanceof Error ? error.toString() : JSON.stringify(error)}`); } catch (error) { serverLogger[logLevel(errorLevel)](`Message too long to show in console output, check the log file`) } From 606f759d30aa74f23ac07a3cf13d26be4ce7bc47 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan Date: Mon, 5 Jul 2021 15:53:31 +0200 Subject: [PATCH 040/493] Refactor try catch on Agent Fim SCA (#3417) * feat(agent-fim): Refactor try catch handler error. Prettier + copyrigth. * feat(agent-sca): Refactor try catch handler error. Prettier + copyrigth. * feat(agent-prompts): Prettier. * docs(users): Updated changelog. --- CHANGELOG.md | 1 + public/components/agents/fim/inventory.tsx | 73 ++++++-- .../agents/fim/inventory/filterBar.tsx | 169 ++++++++++++++---- .../agents/fim/inventory/flyout.tsx | 25 ++- .../agents/fim/inventory/registry-table.tsx | 22 ++- .../registryValues/registryValues.tsx | 25 ++- .../components/agents/fim/inventory/table.tsx | 41 +++-- public/components/agents/fim/main.tsx | 16 +- .../prompts/prompt-agent-feature-version.tsx | 10 +- .../prompt-agent-no-support-module.tsx | 8 +- .../agents/prompts/prompt-no-active-agent.tsx | 13 +- .../prompts/prompt-no-selected-agent.tsx | 6 +- .../agents/prompts/prompt-select-agent.tsx | 13 +- .../agents/sca/components/compliance-text.tsx | 49 +++-- .../components/agents/sca/components/index.ts | 3 +- .../agents/sca/components/rule-text.tsx | 23 ++- public/components/agents/sca/inventory.tsx | 114 +++++++++--- public/components/agents/sca/main.tsx | 12 ++ 18 files changed, 468 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe8617f6c..167285b347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) +- Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) ### Changed diff --git a/public/components/agents/fim/inventory.tsx b/public/components/agents/fim/inventory.tsx index ec896d6f92..de9263d3c7 100644 --- a/public/components/agents/fim/inventory.tsx +++ b/public/components/agents/fim/inventory.tsx @@ -12,32 +12,35 @@ import React, { Component, Fragment } from 'react'; import { - EuiPanel, - EuiPage, - EuiTabs, - EuiTab, - EuiTitle, - EuiLoadingSpinner, + EuiButtonEmpty, EuiEmptyPrompt, - EuiSpacer, - EuiProgress, EuiFlexGroup, EuiFlexItem, - EuiLink, EuiHorizontalRule, - EuiButtonEmpty + EuiLink, + EuiLoadingSpinner, + EuiPage, + EuiPanel, + EuiProgress, + EuiSpacer, + EuiTab, + EuiTabs, + EuiTitle, } from '@elastic/eui'; -import { - InventoryTable, - FilterBar, - RegistryTable -} from './inventory/'; +import { FilterBar, InventoryTable, RegistryTable } from './inventory/'; import { WzRequest } from '../../../react-services/wz-request'; import exportCsv from '../../../react-services/wz-csv'; -import { getToasts } from '../../../kibana-services'; +import { getToasts } from '../../../kibana-services'; import { ICustomBadges } from '../../wz-search-bar/components'; import { filtersToObject } from '../../wz-search-bar'; -import { WzEmptyPromptNoPermissions } from "../../common/permissions/prompt"; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; export class Inventory extends Component { _isMount = false; @@ -172,7 +175,18 @@ export class Inventory extends Component { return ((response.data || {}).data || {}).total_affected_items || 0; } catch (error) { this.setState({ isLoading: false }); - this.showToast('danger', error, 3000); + + const options: UIErrorLog = { + context: `${Inventory.name}.getItemNumber`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -245,7 +259,17 @@ export class Inventory extends Component { `fim-${this.state.selectedTabId}` ); } catch (error) { - this.showToast('danger', error, 3000); + const options: UIErrorLog = { + context: `${Inventory.name}.downloadCsv`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -317,6 +341,17 @@ export class Inventory extends Component { return (((response.data || {}).data).syscheck || {}).disabled === 'no'; } catch (error) { + const options: UIErrorLog = { + context: `${Inventory.name}.isConfigured`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); return false; } } diff --git a/public/components/agents/fim/inventory/filterBar.tsx b/public/components/agents/fim/inventory/filterBar.tsx index 1a9bb8763b..7501cb6ec9 100644 --- a/public/components/agents/fim/inventory/filterBar.tsx +++ b/public/components/agents/fim/inventory/filterBar.tsx @@ -9,51 +9,151 @@ * * Find more information about this on the LICENSE file. */ + import React, { Component } from 'react'; import { getFilterValues } from './lib'; -import { WzSearchBar, IFilter, IWzSuggestItem } from '../../../../components/wz-search-bar' +import { IFilter, IWzSuggestItem, WzSearchBar } from '../../../../components/wz-search-bar'; import { ICustomBadges } from '../../../wz-search-bar/components'; -import { - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { formatUIDate } from '../../../../react-services/time-service'; export class FilterBar extends Component { - // TODO: Change the type - suggestions: {[key:string]: IWzSuggestItem[]} = { + suggestions: { [key: string]: IWzSuggestItem[] } = { files: [ - {type: 'q', label: 'file', description:"Name of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('file', value, this.props.agent.id, {type:'file'})}, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'perm', description:"Permisions of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('perm', value, this.props.agent.id)}]: []), - {type: 'q', label: 'mtime', description:"Date the file was modified", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('mtime', value, this.props.agent.id,{}, formatUIDate)}, - {type: 'q', label: 'date', description:"Date of registration of the event", operators:['=','!=', '>', '<'], values: async (value) => getFilterValues('date', value, this.props.agent.id, {}, formatUIDate)}, - {type: 'q', label: 'uname', description:"Owner of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('uname', value, this.props.agent.id)}, - {type: 'q', label: 'uid', description:"Id of the onwner file", operators:['=','!=', '~'], values: async (value) => getFilterValues('uid', value, this.props.agent.id)}, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'gname', description:"Name of the group owner file", operators:['=','!=', '~'], values: async (value) => getFilterValues('gname', value, this.props.agent.id)}]: []), - ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'gid', description:"Id of the group owner", operators:['=','!=', '~'], values: async (value) => getFilterValues('gid', value, this.props.agent.id)}]: []), - {type: 'q', label: 'md5', description:"md5 hash", operators:['=','!=', '~'], values: async (value) => getFilterValues('md5', value, this.props.agent.id)}, - {type: 'q', label: 'sha1', description:"sha1 hash", operators:['=','!=', '~'], values: async (value) => getFilterValues('sha1', value, this.props.agent.id)}, - {type: 'q', label: 'sha256', description:"sha256 hash", operators:['=','!=', '~'], values: async (value) => getFilterValues('sha256', value, this.props.agent.id)}, - ...(((this.props.agent || {}).os || {}).platform !== 'windows' ? [{type: 'q', label: 'inode', description:"Inode of the file", operators:['=','!=', '~'], values: async (value) => getFilterValues('inode', value, this.props.agent.id)}]: []), - {type: 'q', label: 'size', description:"Size of the file in Bytes", values: async (value) => getFilterValues('size', value, this.props.agent.id)}, + { + type: 'q', + label: 'file', + description: 'Name of the file', + operators: ['=', '!=', '~'], + values: async (value) => + getFilterValues('file', value, this.props.agent.id, { type: 'file' }), + }, + ...(((this.props.agent || {}).os || {}).platform !== 'windows' + ? [ + { + type: 'q', + label: 'perm', + description: 'Permisions of the file', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('perm', value, this.props.agent.id), + }, + ] + : []), + { + type: 'q', + label: 'mtime', + description: 'Date the file was modified', + operators: ['=', '!=', '>', '<'], + values: async (value) => + getFilterValues('mtime', value, this.props.agent.id, {}, formatUIDate), + }, + { + type: 'q', + label: 'date', + description: 'Date of registration of the event', + operators: ['=', '!=', '>', '<'], + values: async (value) => + getFilterValues('date', value, this.props.agent.id, {}, formatUIDate), + }, + { + type: 'q', + label: 'uname', + description: 'Owner of the file', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('uname', value, this.props.agent.id), + }, + { + type: 'q', + label: 'uid', + description: 'Id of the onwner file', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('uid', value, this.props.agent.id), + }, + ...(((this.props.agent || {}).os || {}).platform !== 'windows' + ? [ + { + type: 'q', + label: 'gname', + description: 'Name of the group owner file', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('gname', value, this.props.agent.id), + }, + ] + : []), + ...(((this.props.agent || {}).os || {}).platform !== 'windows' + ? [ + { + type: 'q', + label: 'gid', + description: 'Id of the group owner', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('gid', value, this.props.agent.id), + }, + ] + : []), + { + type: 'q', + label: 'md5', + description: 'md5 hash', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('md5', value, this.props.agent.id), + }, + { + type: 'q', + label: 'sha1', + description: 'sha1 hash', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('sha1', value, this.props.agent.id), + }, + { + type: 'q', + label: 'sha256', + description: 'sha256 hash', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('sha256', value, this.props.agent.id), + }, + ...(((this.props.agent || {}).os || {}).platform !== 'windows' + ? [ + { + type: 'q', + label: 'inode', + description: 'Inode of the file', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('inode', value, this.props.agent.id), + }, + ] + : []), + { + type: 'q', + label: 'size', + description: 'Size of the file in Bytes', + values: async (value) => getFilterValues('size', value, this.props.agent.id), + }, ], registry: [ - {type: 'q', label: 'file', description:"Name of the registry_key", operators:['=','!=', '~'], values: async (value) => getFilterValues('file', value, this.props.agent.id, {q:'type=registry_key'})}, - ] - } + { + type: 'q', + label: 'file', + description: 'Name of the registry_key', + operators: ['=', '!=', '~'], + values: async (value) => + getFilterValues('file', value, this.props.agent.id, { q: 'type=registry_key' }), + }, + ], + }; - props!:{ - onFiltersChange(filters:IFilter[]): void - selectView: 'files' | 'registry' - agent: {id: string, agentPlatform: string} - onChangeCustomBadges?(customBadges: ICustomBadges[]): void - customBadges?: ICustomBadges[] - filters: IFilter[] - } + props!: { + onFiltersChange(filters: IFilter[]): void; + selectView: 'files' | 'registry'; + agent: { id: string; agentPlatform: string }; + onChangeCustomBadges?(customBadges: ICustomBadges[]): void; + customBadges?: ICustomBadges[]; + filters: IFilter[]; + }; render() { - const { onFiltersChange, selectView, filters} = this.props; + const { onFiltersChange, selectView, filters } = this.props; return ( @@ -62,9 +162,10 @@ export class FilterBar extends Component { filters={filters} onFiltersChange={onFiltersChange} suggestions={this.suggestions[selectView]} - placeholder='Filter or search file' /> + placeholder="Filter or search file" + /> - ) + ); } } diff --git a/public/components/agents/fim/inventory/flyout.tsx b/public/components/agents/fim/inventory/flyout.tsx index 79a1ea2252..fc289616de 100644 --- a/public/components/agents/fim/inventory/flyout.tsx +++ b/public/components/agents/fim/inventory/flyout.tsx @@ -22,6 +22,14 @@ import { import { WzRequest } from '../../../../react-services/wz-request'; import { FileDetails } from './fileDetail'; import { AppState } from '../../../../react-services/app-state'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export class FlyoutDetail extends Component { state: { @@ -72,7 +80,7 @@ export class FlyoutDetail extends Component { } } else if (this.props.item) { currentFile = this.props.item; - } else { + } else { let file = this.props.fileName; const data = await WzRequest.apiReq('GET', `/syscheck/${this.props.agentId}`, { params: { @@ -85,11 +93,24 @@ export class FlyoutDetail extends Component { throw false; } this.setState({ currentFile, type: currentFile.type, isLoading: false }); - } catch (err) { + } catch (error) { this.setState({ error: `Data could not be fetched for ${this.props.fileName}`, currentFile: { file: this.props.fileName }, }); + + const options: UIErrorLog = { + context: `${FlyoutDetail.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index 86bbef000f..a91b01d38c 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -25,6 +25,14 @@ import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; import { filtersToObject } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export class RegistryTable extends Component { state: { @@ -133,6 +141,18 @@ export class RegistryTable extends Component { }); } catch (error) { this.setState({ error, isLoading: false }) + + const options: UIErrorLog = { + context: `${RegistryTable.name}.getSyscheck`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -258,4 +278,4 @@ export class RegistryTable extends Component {
    ) } -} \ No newline at end of file +} diff --git a/public/components/agents/fim/inventory/registryValues/registryValues.tsx b/public/components/agents/fim/inventory/registryValues/registryValues.tsx index 1db8d8f9ca..9f3c9600ba 100644 --- a/public/components/agents/fim/inventory/registryValues/registryValues.tsx +++ b/public/components/agents/fim/inventory/registryValues/registryValues.tsx @@ -13,10 +13,16 @@ import { EuiBasicTableColumn, EuiInMemoryTable, SortDirection } from '@elastic/eui'; import { WzRequest } from '../../../../../react-services'; import React, { useEffect, useState } from 'react'; -import valuesMock from './values.json'; -import { DIRECTIONS } from '@elastic/eui/src/components/flex/flex_group'; import { emptyFieldHandler } from '../lib'; import { formatUIDate } from '../../../../../react-services/time-service'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; export const RegistryValues = (props) => { const [values, setValues] = useState([]); @@ -40,6 +46,17 @@ export const RegistryValues = (props) => { setValues((((values || {}).data || {}).data || {}).affected_items || []); } catch (error) { setError(error); + const options: UIErrorLog = { + context: `${RegistryValues.name}.getValues`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } }; @@ -48,13 +65,13 @@ export const RegistryValues = (props) => { field: 'date', name: 'Date', sortable: true, - render: formatUIDate + render: formatUIDate, }, { field: 'value', name: 'Value name', sortable: true, - render: (item) => (emptyFieldHandler()(item.name || "")), + render: (item) => emptyFieldHandler()(item.name || ''), }, { field: 'value', diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index a88fea3419..cb0b7e3859 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -22,6 +22,14 @@ import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; import { filtersToObject, IFilter } from '../../../wz-search-bar'; import { formatUIDate } from '../../../../react-services/time-service'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export class InventoryTable extends Component { state: { @@ -90,8 +98,8 @@ export class InventoryTable extends Component { }) } else { const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { - params: { - 'file': file + params: { + 'file': file } }); fileData = ((response.data || {}).data || {}).affected_items || []; @@ -112,24 +120,35 @@ export class InventoryTable extends Component { async getSyscheck() { const agentID = this.props.agent.id; try { - const syscheck = await WzRequest.apiReq( - 'GET', - `/syscheck/${agentID}`, - { params: this.buildFilter()}, + const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { + params: this.buildFilter(), + }); + + this.props.onTotalItemsChange( + (((syscheck || {}).data || {}).data || {}).total_affected_items ); - this.props.onTotalItemsChange((((syscheck || {}).data || {}).data || {}).total_affected_items); - this.setState({ syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, totalItems: (((syscheck || {}).data || {}).data || {}).total_affected_items - 1, isLoading: false, - error: undefined + error: undefined, }); } catch (error) { - this.setState({error, isLoading: false}) + this.setState({ error, isLoading: false }); + const options: UIErrorLog = { + context: `${InventoryTable.name}.getSyscheck`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } -} + } buildSortFilter() { const { sortField, sortDirection } = this.state; diff --git a/public/components/agents/fim/main.tsx b/public/components/agents/fim/main.tsx index 40698f46d2..99c593a792 100644 --- a/public/components/agents/fim/main.tsx +++ b/public/components/agents/fim/main.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Inventory } from './index'; import '../../common/modules/module.scss'; import { connect } from 'react-redux'; -import { PromptNoSelectedAgent, PromptNoActiveAgent } from '../prompts'; +import { PromptNoActiveAgent, PromptNoSelectedAgent } from '../prompts'; import { compose } from 'redux'; import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; @@ -13,7 +13,7 @@ const mapStateToProps = (state) => ({ export const MainFim = compose( connect(mapStateToProps), withGuard( - (props) => !((props.currentAgentData && props.currentAgentData.id) && props.agent), + (props) => !(props.currentAgentData && props.currentAgentData.id && props.agent), () => ( ) @@ -32,12 +32,18 @@ export const MainFim = compose( return [ [ { action: 'agent:read', resource: `agent:id:${agentData.id}` }, - ...(agentData.group || []).map(group => ({ action: 'agent:read', resource: `agent:group:${group}` })) + ...(agentData.group || []).map((group) => ({ + action: 'agent:read', + resource: `agent:group:${group}`, + })), ], [ { action: 'syscheck:read', resource: `agent:id:${agentData.id}` }, - ...(agentData.group || []).map(group => ({ action: 'syscheck:read', resource: `agent:group:${group}` })) - ] + ...(agentData.group || []).map((group) => ({ + action: 'syscheck:read', + resource: `agent:group:${group}`, + })), + ], ]; }) )(function MainFim({ currentAgentData, agent, ...rest }) { diff --git a/public/components/agents/prompts/prompt-agent-feature-version.tsx b/public/components/agents/prompts/prompt-agent-feature-version.tsx index 27822922e8..d62006b10d 100644 --- a/public/components/agents/prompts/prompt-agent-feature-version.tsx +++ b/public/components/agents/prompts/prompt-agent-feature-version.tsx @@ -11,16 +11,14 @@ */ import React from 'react'; -import { - EuiEmptyPrompt -} from '@elastic/eui' +import { EuiEmptyPrompt } from '@elastic/eui'; -export const PromptAgentFeatureVersion = ({version = ''}: {version: string}) => { +export const PromptAgentFeatureVersion = ({ version = '' }: { version: string }) => { return ( {`Agent doesn't support this feature`}} body={`This feature is only avaliable for agents with ${version}.`} /> - ) -}; \ No newline at end of file + ); +}; diff --git a/public/components/agents/prompts/prompt-agent-no-support-module.tsx b/public/components/agents/prompts/prompt-agent-no-support-module.tsx index bc833cf4fc..7b9587374b 100644 --- a/public/components/agents/prompts/prompt-agent-no-support-module.tsx +++ b/public/components/agents/prompts/prompt-agent-no-support-module.tsx @@ -11,10 +11,8 @@ */ import React from 'react'; -import {PromptSelectAgent} from './' +import { PromptSelectAgent } from './'; export const PromptAgentNoSupportModule = () => { - return ( - - ) -} + return ; +}; diff --git a/public/components/agents/prompts/prompt-no-active-agent.tsx b/public/components/agents/prompts/prompt-no-active-agent.tsx index 9a7c369764..02f5219824 100644 --- a/public/components/agents/prompts/prompt-no-active-agent.tsx +++ b/public/components/agents/prompts/prompt-no-active-agent.tsx @@ -12,14 +12,15 @@ import React from 'react'; import { PromptSelectAgent } from './'; -import { - EuiEmptyPrompt -} from '@elastic/eui' +import { EuiEmptyPrompt } from '@elastic/eui'; export const PromptNoActiveAgent = () => { return ( - - ) + + ); }; export const PromptNoActiveAgentWithoutSelect = () => { @@ -29,5 +30,5 @@ export const PromptNoActiveAgentWithoutSelect = () => { title={

    {`Agent is not active`}

    } body="This section is only available for active agents." /> - ) + ); }; diff --git a/public/components/agents/prompts/prompt-no-selected-agent.tsx b/public/components/agents/prompts/prompt-no-selected-agent.tsx index efe86421c7..e1ba32de83 100644 --- a/public/components/agents/prompts/prompt-no-selected-agent.tsx +++ b/public/components/agents/prompts/prompt-no-selected-agent.tsx @@ -14,7 +14,5 @@ import React from 'react'; import { PromptSelectAgent } from './'; export const PromptNoSelectedAgent = ({ body }) => { - return ( - - ) -} + return ; +}; diff --git a/public/components/agents/prompts/prompt-select-agent.tsx b/public/components/agents/prompts/prompt-select-agent.tsx index 75465ace86..76dd999fe5 100644 --- a/public/components/agents/prompts/prompt-select-agent.tsx +++ b/public/components/agents/prompts/prompt-select-agent.tsx @@ -12,28 +12,27 @@ import React from 'react'; import { useDispatch } from 'react-redux'; -import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { showExploreAgentModal } from '../../../redux/actions/appStateActions'; type PromptSelectAgentProps = { body?: string; title: string; -} +}; -export const PromptSelectAgent = ({ body, title }:PromptSelectAgentProps) => { +export const PromptSelectAgent = ({ body, title }: PromptSelectAgentProps) => { const dispatch = useDispatch(); const openAgentSelector = () => dispatch(showExploreAgentModal(true)); return ( {title}} - body={body &&

    {body}

    - } + body={body &&

    {body}

    } actions={ Select agent } /> - ) -} + ); +}; diff --git a/public/components/agents/sca/components/compliance-text.tsx b/public/components/agents/sca/components/compliance-text.tsx index 695cf555b6..a0306695cb 100644 --- a/public/components/agents/sca/components/compliance-text.tsx +++ b/public/components/agents/sca/components/compliance-text.tsx @@ -1,37 +1,52 @@ +/* + * Wazuh app - ComplianceText component + * Copyright (C) 2015-2021 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 from 'react'; -import { - EuiFlexItem, - EuiFlexGroup -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; interface IComplianceText { - complianceText: string + complianceText: string; } type item = { - title: string - description: string -} + title: string; + description: string; +}; interface IComplianceItem { - item: item + item: item; } export const ComplianceText: React.FunctionComponent = ({ complianceText }) => { - const complianceList = complianceText.split("\n"); + const complianceList = complianceText.split('\n'); //@ts-ignore - const listItems: item[] = complianceList.map(item => /(?\S+): (?<description>.+)/.exec(item)?.groups).filter(item => !!item); + const listItems: item[] = complianceList + .map((item) => /(?<title>\S+): (?<description>.+)/.exec(item)?.groups) + .filter((item) => !!item); return ( <EuiFlexGroup direction="column" gutterSize="xs"> - {listItems.map((item, idx) => <ComplianceItem key={idx} item={item} />)} + {listItems.map((item, idx) => ( + <ComplianceItem key={idx} item={item} /> + ))} </EuiFlexGroup> - ) -} + ); +}; const ComplianceItem: React.FunctionComponent<IComplianceItem> = ({ item }) => { return ( <EuiFlexItem> - <p><strong>{item.title}: </strong> {item.description}</p> + <p> + <strong>{item.title}: </strong> {item.description} + </p> </EuiFlexItem> - ) -} \ No newline at end of file + ); +}; diff --git a/public/components/agents/sca/components/index.ts b/public/components/agents/sca/components/index.ts index a1b2cb4571..d1a1715a59 100644 --- a/public/components/agents/sca/components/index.ts +++ b/public/components/agents/sca/components/index.ts @@ -1,3 +1,2 @@ export { RuleText } from './rule-text'; - -export { ComplianceText } from './compliance-text'; \ No newline at end of file +export { ComplianceText } from './compliance-text'; diff --git a/public/components/agents/sca/components/rule-text.tsx b/public/components/agents/sca/components/rule-text.tsx index 47c6d42160..c235801ee2 100644 --- a/public/components/agents/sca/components/rule-text.tsx +++ b/public/components/agents/sca/components/rule-text.tsx @@ -1,17 +1,30 @@ +/* + * Wazuh app - RuleText component + * Copyright (C) 2015-2021 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 from 'react' +import React from 'react'; import { EuiText } from '@elastic/eui'; interface RuleTextProps { - rules: {type: string, rule: string}[] + rules: { type: string; rule: string }[]; } export const RuleText: React.FunctionComponent<RuleTextProps> = ({ rules }) => { return ( <EuiText size="s"> <ul> - {rules.map((rule, idx) => <li key={`check-rule-${idx}`}>{rule.rule}</li>)} + {rules.map((rule, idx) => ( + <li key={`check-rule-${idx}`}>{rule.rule}</li> + ))} </ul> </EuiText> - ) -} + ); +}; diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 19b8d15f7a..42b808593b 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -1,3 +1,15 @@ +/* + * Wazuh app - Iventory component + * Copyright (C) 2015-2021 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 { Pie } from "../../d3/pie"; import { @@ -28,6 +40,14 @@ import { getToasts } from '../../../kibana-services'; import { WzSearchBar } from '../../../components/wz-search-bar'; import { RuleText, ComplianceText } from './components'; import _ from 'lodash'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; export class Inventory extends Component { _isMount = false; @@ -169,8 +189,19 @@ export class Inventory extends Component { this.setState({ loading: false }); } } catch (error) { - this.showToast('danger', error, 3000); this.setState({ loading: false }); + + const options: UIErrorLog = { + context: `${Inventory.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -263,48 +294,67 @@ export class Inventory extends Component { } this._isMount && this.setState({ data: models, loading: false }); } catch (error) { - this.showToast('danger', error, 3000); this.setState({ loading: false }); this.policies = []; + + const options: UIErrorLog = { + context: `${Inventory.name}.initialize`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } async loadScaPolicy(policy) { - this._isMount && this.setState({ loadingPolicy: true, itemIdToExpandedRowMap: {}, pageTableChecks: {pageIndex: 0} }); + this._isMount && + this.setState({ + loadingPolicy: true, + itemIdToExpandedRowMap: {}, + pageTableChecks: { pageIndex: 0 }, + }); if (policy) { try { - const policyResponse = await WzRequest.apiReq( - 'GET', - `/sca/${this.props.agent.id}`, - { - params: { - "q": "policy_id=" + policy.policy_id - } - } - ); + const policyResponse = await WzRequest.apiReq('GET', `/sca/${this.props.agent.id}`, { + params: { + q: 'policy_id=' + policy.policy_id, + }, + }); const [policyData] = policyResponse.data.data.affected_items; // It queries all checks without filters, because the filters are applied in the results // due to the use of EuiInMemoryTable instead EuiTable components and do arequest with each change of filters. const checksResponse = await WzRequest.apiReq( 'GET', `/sca/${this.props.agent.id}/checks/${policy.policy_id}`, - { } + {} ); - const checks = ((((checksResponse || {}).data || {}).data || {}).affected_items || []) - .map(item => ({...item, result: item.result || 'not applicable'})); + const checks = ( + (((checksResponse || {}).data || {}).data || {}).affected_items || [] + ).map((item) => ({ ...item, result: item.result || 'not applicable' })); this.buildSuggestionSearchBar(policyData.policy_id, checks); - this._isMount && this.setState({ lookingPolicy: policyData, loadingPolicy: false, items: checks }); - } catch (err) { - // We can't ensure the suggestions contains valid characters - getToasts().add({ - color: 'danger', - title: 'Error', - text: 'The filter contains invalid characters', - toastLifeTimeMs: 10000, - }); + this._isMount && + this.setState({ lookingPolicy: policyData, loadingPolicy: false, items: checks }); + } catch (error) { this.setState({ lookingPolicy: policy, loadingPolicy: false }); + + const options: UIErrorLog = { + context: `${Inventory.name}.loadScaPolicy`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: `The filter contains invalid characters` || error.message, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } - }else{ + } else { this._isMount && this.setState({ lookingPolicy: policy, loadingPolicy: false, items: [] }); } } @@ -328,7 +378,7 @@ export class Inventory extends Component { let checks = ''; checks += (item.rules || []).length > 1 ? 'Checks' : 'Check'; checks += item.condition ? ` (Condition: ${item.condition})` : ''; - const complianceText = item.compliance && item.compliance.length + const complianceText = item.compliance && item.compliance.length ? item.compliance.map(el => `${el.key}: ${el.value}`).join('\n') : ''; const rulesText = item.rules.length ? item.rules.map(el => el.rule).join('\n') : ''; @@ -388,7 +438,17 @@ export class Inventory extends Component { this.state.lookingPolicy.policy_id ); } catch (error) { - this.showToast('danger', error, 3000); + const options: UIErrorLog = { + context: `${Inventory.name}.downloadCsv`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/agents/sca/main.tsx b/public/components/agents/sca/main.tsx index 922faa0175..8daf51de54 100644 --- a/public/components/agents/sca/main.tsx +++ b/public/components/agents/sca/main.tsx @@ -1,3 +1,15 @@ +/* + * Wazuh app - Main SCA component + * Copyright (C) 2015-2021 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 from 'react'; import { Inventory } from './index'; import { connect } from 'react-redux'; From 1b2a7075bfa259db10aa13b9161e2ff38b8cd19b Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Mon, 5 Jul 2021 19:48:36 +0200 Subject: [PATCH 041/493] Changed all context value of all try-catch implementations (#3432) * refactor(error-orchestrator): Changed all context value of all try-catch implementations * docs(error-orchestrator): Updated changelog. --- CHANGELOG.md | 1 + .../security/policies/create-policy.tsx | 4 +- .../security/policies/edit-policy.tsx | 8 +-- .../security/policies/policies-table.tsx | 62 ++++++++++--------- .../components/roles-mapping-create.tsx | 4 +- .../components/roles-mapping-edit.tsx | 4 +- .../components/roles-mapping-table.tsx | 46 +++++++------- .../helpers/rule-editor.helper.ts | 3 +- .../security/roles-mapping/roles-mapping.tsx | 32 +++++----- .../components/security/roles/edit-role.tsx | 7 +-- .../components/security/roles/roles-table.tsx | 62 ++++++++++--------- public/components/settings/api/add-api.js | 3 +- public/components/settings/api/api-is-down.js | 3 +- public/components/settings/api/api-table.js | 3 +- .../configuration/components/bottom-bar.tsx | 3 +- .../category/components/field-form.tsx | 5 +- public/components/settings/modules/modules.js | 6 +- public/components/visualize/wz-visualize.js | 6 +- public/controllers/agent/agents-preview.js | 13 ++-- public/controllers/agent/agents.js | 23 +++---- .../agent/components/agents-preview.js | 4 +- .../agent/components/agents-table.js | 8 +-- .../agent/components/checkUpgrade.tsx | 4 +- .../agent/components/register-agent.js | 4 +- .../components/management/mg-logs/logs.js | 10 ++- public/controllers/management/management.js | 12 ++-- .../error-orchestrator/README.md | 4 +- 27 files changed, 157 insertions(+), 187 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 167285b347..706d005bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) - Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) - Refactored all try catch strategy on Controller/Agent section [#3398](https://github.com/wazuh/wazuh-kibana-app/issues/3398) +- Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/issues/3432) ### Fixed diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index c1aca52422..c2d94245b9 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -24,8 +24,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'CreatePolicyFlyout'; - export const CreatePolicyFlyout = ({ closeFlyout }) => { const [isModalVisible, setIsModalVisible] = useState(false); const [resources, setResources] = useState([]); @@ -188,7 +186,7 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { setEffectValue(null); } catch (error) { const options = { - context: errorContext, + context: `${CreatePolicyFlyout.name}.createPolicy`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index bfaade9ee8..6b6ecff0e6 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -27,8 +27,6 @@ import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/ import { getErrorOrchestrator } from '../../../react-services/common-services'; import _ from 'lodash'; -const errorContext = 'EditPolicyFlyout'; - export const EditPolicyFlyout = ({ policy, closeFlyout }) => { const isReserved = WzAPIUtils.isReservedID(policy.id); const [actionValue, setActionValue] = useState(''); @@ -78,7 +76,7 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { closeFlyout(); } catch (error) { const options = { - context: errorContext, + context: `${EditPolicyFlyout.name}.updatePolicy`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -291,8 +289,8 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { ); } useEffect(() => { - if (initialActionValue != actionValue || !_.isEqual(addedResources, initialAddedResources) || - !_.isEqual(addedActions, initialAddedActions) || initialResourceValue != resourceValue || + if (initialActionValue != actionValue || !_.isEqual(addedResources, initialAddedResources) || + !_.isEqual(addedActions, initialAddedActions) || initialResourceValue != resourceValue || initialEffectValue != effectValue) { setHasChanges(true); } else { diff --git a/public/components/security/policies/policies-table.tsx b/public/components/security/policies/policies-table.tsx index df87b798ab..6ef76e11b1 100644 --- a/public/components/security/policies/policies-table.tsx +++ b/public/components/security/policies/policies-table.tsx @@ -8,8 +8,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'PoliciesTable'; - export const PoliciesTable = ({ policies, loading, editPolicy, createPolicy, updatePolicies }) => { const getRowProps = (item) => { const { id } = item; @@ -22,6 +20,37 @@ export const PoliciesTable = ({ policies, loading, editPolicy, createPolicy, upd }; }; + const confirmDeletePolicy = (item) => { + return async () => { + try { + const response = await WzRequest.apiReq('DELETE', `/security/policies/`, { + params: { + policy_ids: item.id, + }, + }); + const data = (response.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; + } + ErrorHandler.info('Policy was successfully deleted'); + await updatePolicies(); + } catch (error) { + const options = { + context: `${PoliciesTable.name}.confirmDeletePolicy`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + } + const columns = [ { field: 'id', @@ -82,34 +111,7 @@ export const PoliciesTable = ({ policies, loading, editPolicy, createPolicy, upd }} isDisabled={WzAPIUtils.isReservedID(item.id)} modalTitle={`Do you want to delete the ${item.name} policy?`} - onConfirm={async () => { - try { - const response = await WzRequest.apiReq('DELETE', `/security/policies/`, { - params: { - policy_ids: item.id, - }, - }); - const data = (response.data || {}).data; - if (data.failed_items && data.failed_items.length) { - return; - } - ErrorHandler.info('Policy was successfully deleted'); - await updatePolicies(); - } catch (error) { - const options = { - context: errorContext, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }} + onConfirm={confirmDeletePolicy(item)} modalProps={{ buttonColor: 'danger' }} iconType="trash" color="danger" diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 3b8041848a..2ea30e84c2 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -23,8 +23,6 @@ import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -const errorContext = 'RolesMappingCreate'; - export const RolesMappingCreate = ({ closeFlyout, rolesEquivalences, @@ -64,7 +62,7 @@ export const RolesMappingCreate = ({ ErrorHandler.info('Role mapping was successfully created'); } catch (error) { const options = { - context: errorContext, + context: `${RolesMappingCreate.name}.createRule`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index a1c6f2986c..ef2577e095 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -26,8 +26,6 @@ import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -const errorContext = 'RolesMappingEdit'; - export const RolesMappingEdit = ({ rule, closeFlyout, @@ -87,7 +85,7 @@ export const RolesMappingEdit = ({ ErrorHandler.info('Role mapping was successfully updated'); } catch (error) { const options = { - context: errorContext, + context: `${RolesMappingEdit.name}.editRule`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/components/security/roles-mapping/components/roles-mapping-table.tsx b/public/components/security/roles-mapping/components/roles-mapping-table.tsx index c5c53a4d05..3a04543a39 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-table.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-table.tsx @@ -17,8 +17,6 @@ import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -const errorContext = 'RolesMappingTable'; - export const RolesMappingTable = ({ rolesEquivalences, rules, loading, editRule, updateRules }) => { const getRowProps = item => { const { id } = item; @@ -28,6 +26,29 @@ export const RolesMappingTable = ({ rolesEquivalences, rules, loading, editRule, }; }; + const onDeleteRoleMapping = (item) => { + return async () => { + try { + await RulesServices.DeleteRules([item.id]); + ErrorHandler.info('Role mapping was successfully deleted'); + updateRules(); + } catch (error) { + const options = { + context: `${RolesMappingTable.name}.onDeleteRoleMapping`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + } + const columns: EuiBasicTableColumn<any>[] = [ { field: 'id', @@ -99,26 +120,7 @@ export const RolesMappingTable = ({ rolesEquivalences, rules, loading, editRule, }} isDisabled={WzAPIUtils.isReservedID(item.id)} modalTitle={`Do you want to delete the ${item.name} role mapping?`} - onConfirm={async () => { - try { - await RulesServices.DeleteRules([item.id]); - ErrorHandler.info('Role mapping was successfully deleted'); - updateRules(); - } catch (error) { - const options = { - context: errorContext, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }} + onConfirm={onDeleteRoleMapping(item)} modalProps={{ buttonColor: 'danger' }} iconType="trash" color="danger" diff --git a/public/components/security/roles-mapping/helpers/rule-editor.helper.ts b/public/components/security/roles-mapping/helpers/rule-editor.helper.ts index f8d69e4ac2..36b68e848b 100644 --- a/public/components/security/roles-mapping/helpers/rule-editor.helper.ts +++ b/public/components/security/roles-mapping/helpers/rule-editor.helper.ts @@ -123,7 +123,6 @@ const getFormatedRules = (rulesArray, internalUsers) => { }; export const decodeJsonRule = (jsonRule, internalUsers) => { - const errorContext = 'decodeJsonRule'; try { var wrongFormat = false; const ruleObject = JSON.parse(jsonRule); @@ -150,7 +149,7 @@ export const decodeJsonRule = (jsonRule, internalUsers) => { }; } catch (error) { const options = { - context: errorContext, + context: decodeJsonRule.name, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/components/security/roles-mapping/roles-mapping.tsx b/public/components/security/roles-mapping/roles-mapping.tsx index 1a687d2c6e..cc8ac9df05 100644 --- a/public/components/security/roles-mapping/roles-mapping.tsx +++ b/public/components/security/roles-mapping/roles-mapping.tsx @@ -30,8 +30,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'RolesMapping'; - export const RolesMapping = () => { const [isEditingRule, setIsEditingRule] = useState(false); const [isCreatingRule, setIsCreatingRule] = useState(false); @@ -44,7 +42,7 @@ export const RolesMapping = () => { const currentPlatform = useSelector((state: any) => state.appStateReducers.currentPlatform); useEffect(() => { - initData(); + initData(); }, []); useEffect(() => { @@ -59,24 +57,26 @@ export const RolesMapping = () => { ErrorHandler.handle('There was an error loading roles'); } }, [rolesLoading]); - + const getInternalUsers = async () => { try { const wazuhSecurity = new WazuhSecurity(); const users = await wazuhSecurity.security.getUsers(); - const _users = users.map((item, idx) => { - return { - id: idx, - user: item.username, - roles: [], - full_name: item.full_name, - email: item.email, - }; - }).sort((a, b) => (a.user > b.user) ? 1 : (a.user < b.user) ? -1 : 0); + const _users = users + .map((item, idx) => { + return { + id: idx, + user: item.username, + roles: [], + full_name: item.full_name, + email: item.email, + }; + }) + .sort((a, b) => (a.user > b.user ? 1 : a.user < b.user ? -1 : 0)); setInternalUsers(_users); } catch (error) { const options = { - context: errorContext, + context: `${RolesMapping.name}.getInternalUsers`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -96,7 +96,7 @@ export const RolesMapping = () => { setRules(_rules); } catch (error) { const options = { - context: errorContext, + context: `${RolesMapping.name}.getRules`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -122,7 +122,7 @@ export const RolesMapping = () => { const updateRoles = async () => { await getRules(); }; - + let editFlyout; if (isEditingRule) { editFlyout = ( diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 9bfebb832a..a228ac9718 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -25,11 +25,8 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'EditRole'; - const reservedRoles = ['administrator', 'readonly', 'users_admin', 'agents_readonly', 'agents_admin', 'cluster_readonly', 'cluster_admin']; - export const EditRole = ({ role, closeFlyout }) => { const [isLoading, setIsLoading] = useState(true); const [currentRole, setCurrentRole] = useState({}); @@ -106,7 +103,7 @@ export const EditRole = ({ role, closeFlyout }) => { await update(); } catch (error) { const options = { - context: errorContext, + context: `${EditRole.name}.addPolicy`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -230,4 +227,4 @@ export const EditRole = ({ role, closeFlyout }) => { {modal} </> ); -}; \ No newline at end of file +}; diff --git a/public/components/security/roles/roles-table.tsx b/public/components/security/roles/roles-table.tsx index 4e903c6ecd..597a8d029a 100644 --- a/public/components/security/roles/roles-table.tsx +++ b/public/components/security/roles/roles-table.tsx @@ -17,8 +17,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'RolesTable'; - export const RolesTable = ({ roles, policiesData, loading, editRole, updateRoles }) => { const getRowProps = (item) => { const { id } = item; @@ -28,6 +26,37 @@ export const RolesTable = ({ roles, policiesData, loading, editRole, updateRoles }; }; + const onConfirmDeleteRole = (item) => { + return async () => { + try { + const response = await WzRequest.apiReq('DELETE', `/security/roles/`, { + params: { + role_ids: item.id, + }, + }); + const data = (response.data || {}).data; + if (data.failed_items && data.failed_items.length) { + return; + } + ErrorHandler.info('Role was successfully deleted'); + await updateRoles(); + } catch (error) { + const options = { + context: `${RolesTable.name}.onConfirmDeleteRole`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + } + const columns = [ { field: 'id', @@ -114,34 +143,7 @@ export const RolesTable = ({ roles, policiesData, loading, editRole, updateRoles }} isDisabled={WzAPIUtils.isReservedID(item.id)} modalTitle={`Do you want to delete the ${item.name} role?`} - onConfirm={async () => { - try { - const response = await WzRequest.apiReq('DELETE', `/security/roles/`, { - params: { - role_ids: item.id, - }, - }); - const data = (response.data || {}).data; - if (data.failed_items && data.failed_items.length) { - return; - } - ErrorHandler.info('Role was successfully deleted'); - await updateRoles(); - } catch (error) { - const options = { - context: errorContext, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }} + onConfirm={onConfirmDeleteRole(item)} modalProps={{ buttonColor: 'danger' }} iconType="trash" color="danger" diff --git a/public/components/settings/api/add-api.js b/public/components/settings/api/add-api.js index 37cc373c07..e6ab633d1b 100644 --- a/public/components/settings/api/add-api.js +++ b/public/components/settings/api/add-api.js @@ -30,7 +30,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'AddApi'; export const AddApi = withErrorBoundary (class AddApi extends Component { constructor(props) { super(props); @@ -99,7 +98,7 @@ export const AddApi = withErrorBoundary (class AddApi extends Component { }); const options = { - context: errorContext, + context: `${AddApi.name}.checkConnection`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, store: true, diff --git a/public/components/settings/api/api-is-down.js b/public/components/settings/api/api-is-down.js index 0bb2f3401e..d9dce90d3c 100644 --- a/public/components/settings/api/api-is-down.js +++ b/public/components/settings/api/api-is-down.js @@ -37,7 +37,6 @@ import { import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'ApiIsDown' export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { constructor(props) { super(props); @@ -110,7 +109,7 @@ export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { } const options = { - context: errorContext, + context: `${ApiIsDown.name}.checkConnection`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, store: true, diff --git a/public/components/settings/api/api-table.js b/public/components/settings/api/api-table.js index 9b5704b541..be58eb2af7 100644 --- a/public/components/settings/api/api-table.js +++ b/public/components/settings/api/api-table.js @@ -40,7 +40,6 @@ import { import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'ApiTable'; export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiTable extends Component { constructor(props) { super(props); @@ -143,7 +142,7 @@ export const ApiTable = compose(withErrorBoundary, withReduxProvider)(class ApiT } } catch (error) { const options = { - context: errorContext, + context: `${ApiTable.name}.checkApi`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/components/settings/configuration/components/bottom-bar.tsx b/public/components/settings/configuration/components/bottom-bar.tsx index bb2b404fc8..71e395957b 100644 --- a/public/components/settings/configuration/components/bottom-bar.tsx +++ b/public/components/settings/configuration/components/bottom-bar.tsx @@ -34,7 +34,6 @@ import { } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -const errorContext = 'BottomBar' interface IBottomBarProps { updatedConfig: { [setting: string]: string | number | boolean | object } setUpdateConfig(setting: {}): void @@ -105,7 +104,7 @@ const saveSettings = async (updatedConfig: {}, setUpdateConfig: Function, setLoa setUpdateConfig({}); } catch (error) { const options: UIErrorLog = { - context: errorContext, + context: `${BottomBar.name}.saveSettings`, level: UI_LOGGER_LEVELS.ERROR as UILogLevel, severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, store: true, diff --git a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx index 5f055a4592..f93fdfa852 100644 --- a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx +++ b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx @@ -33,7 +33,6 @@ import { import { UI_LOGGER_LEVELS } from '../../../../../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../../../../../react-services/common-services'; -const errorContext = 'FieldForm'; interface IFieldForm { item: ISetting updatedConfig: { [field: string]: string | number | boolean | [] } @@ -110,16 +109,18 @@ const IntervalForm: React.FunctionComponent<IFieldForm> = (props) => { const ArrayForm: React.FunctionComponent<IFieldForm> = (props) => { const [list, setList] = useState(JSON.stringify(getValue(props))); + useEffect(() => { setList(JSON.stringify(getValue(props))) }, [props.updatedConfig]) + const checkErrors = () => { try { const parsed = JSON.parse(list); onChange(parsed, props); } catch (error) { const options: UIErrorLog = { - context: errorContext, + context: `${FieldForm.name}.checkErrors`, level: UI_LOGGER_LEVELS.ERROR as UILogLevel, severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, error: { diff --git a/public/components/settings/modules/modules.js b/public/components/settings/modules/modules.js index 5dd8d65105..44b9b76fe3 100644 --- a/public/components/settings/modules/modules.js +++ b/public/components/settings/modules/modules.js @@ -25,8 +25,6 @@ import { compose } from 'redux'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'EnableModulesWrapper'; - export class EnableModulesWrapper extends Component { constructor(props) { try { @@ -78,7 +76,7 @@ export class EnableModulesWrapper extends Component { }; } catch (error) { const options = { - context: errorContext, + context: `${EnableModulesWrapper.name}.constructor`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.CRITICAL, store: true, @@ -100,7 +98,7 @@ export class EnableModulesWrapper extends Component { this.setState({ extensions }); } catch (error) { const options = { - context: errorContext, + context: `${EnableModulesWrapper.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 77a19de59a..a3f1cfc4a0 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -45,7 +45,6 @@ import { getErrorOrchestrator } from '../../react-services/common-services'; const visHandler = new VisHandlers(); -const errorContext = 'WzVisualize'; export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class WzVisualize extends Component { _isMount = false; constructor(props) { @@ -110,7 +109,7 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this._isMount && this.setState({ thereAreSampleAlerts }); } catch (error) { const options = { - context: errorContext, + context: `${WzVisualize.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, error: { @@ -119,7 +118,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class title: error.name || error, }, }; - getErrorOrchestrator().handleError(options); } } @@ -154,7 +152,7 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class } catch (error) { this.setState({ isRefreshing: false }); const options = { - context: errorContext, + context: `${WzVisualize.name}.refreshKnownFields`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index 29fdaecb38..5732c5e58b 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -26,8 +26,6 @@ import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; -const errorContext = 'AgentsPreviewController'; - export class AgentsPreviewController { /** * Class constructor @@ -168,7 +166,7 @@ export class AgentsPreviewController { return; } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreviewController.name}.downloadCsv`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -179,7 +177,6 @@ export class AgentsPreviewController { }; getErrorOrchestrator().handleError(options); } - return; } async getMostActive() { @@ -205,7 +202,7 @@ export class AgentsPreviewController { return this.mostActiveAgent; } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreviewController.name}.getMostActive`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -233,7 +230,7 @@ export class AgentsPreviewController { this.pattern = (await getDataPlugin().indexPatterns.get(AppState.getCurrentPattern())).title; } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreviewController.name}.load`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.CRITICAL, store: true, @@ -275,7 +272,7 @@ export class AgentsPreviewController { return url.substr(numToClean); } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreviewController.name}.getCurrentApiAddress`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, error: { @@ -298,7 +295,7 @@ export class AgentsPreviewController { return result.api_version; } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreviewController.name}.getWazuhVersion`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { diff --git a/public/controllers/agent/agents.js b/public/controllers/agent/agents.js index c44e32e3df..3ef8a9d62b 100644 --- a/public/controllers/agent/agents.js +++ b/public/controllers/agent/agents.js @@ -35,7 +35,6 @@ import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; -const errorContext = 'AgentsController'; export class AgentsController { /** * Class constructor @@ -230,7 +229,7 @@ export class AgentsController { this.$scope.restartingAgent = false; } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.restartAgent`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -251,7 +250,7 @@ export class AgentsController { this.$scope.getAgent(); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.$onInit`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -283,7 +282,7 @@ export class AgentsController { } } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.switchConfigTab`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -412,7 +411,7 @@ export class AgentsController { this.$scope.$applyAsync(); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.confirmAddGroup`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -577,7 +576,7 @@ export class AgentsController { this.$scope.$applyAsync(); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.switchTab`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.CRITICAL, store: true, @@ -694,7 +693,7 @@ export class AgentsController { this.showToast('success', 'The agent is being upgrade.', '', 5000); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.onClickUpgrade`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -714,7 +713,7 @@ export class AgentsController { this.showToast('success', 'Agent restarted.', '', 5000); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.onClickRestart`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -892,7 +891,7 @@ export class AgentsController { return text + formatUIDate(time); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.offsetTimestamp`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: false, @@ -974,7 +973,7 @@ export class AgentsController { ); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.launchRootcheckScan`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -986,7 +985,6 @@ export class AgentsController { }; getErrorOrchestrator().handleError(options); } - return; } async launchSyscheckScan() { @@ -1003,7 +1001,7 @@ export class AgentsController { ErrorHandler.info(`FIM scan launched successfully on agent ${this.$scope.agent.id}`, ''); } catch (error) { const options = { - context: errorContext, + context: `${AgentsController.name}.launchSyscheckScan`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -1015,7 +1013,6 @@ export class AgentsController { }; getErrorOrchestrator().handleError(options); } - return; } falseAllExpand() { diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index 9257ac2764..2e1225294f 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -48,8 +48,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'AgentsPreview'; - const FILTER_ACTIVE = 'active'; const FILTER_DISCONNECTED = 'disconnected'; const FILTER_NEVER_CONNECTED = 'never_connected'; @@ -148,7 +146,7 @@ export const AgentsPreview = compose( this._isMount && this.setState({ platforms: platformsModel, loading: false }); } catch (error) { const options = { - context: errorContext, + context: `${AgentsPreview.name}.getSummary`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 85ce6be84d..f4c8b852e6 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -45,8 +45,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'AgentsTable'; - export const AgentsTable = withErrorBoundary( class AgentsTable extends Component { _isMount = false; @@ -244,7 +242,7 @@ export const AgentsTable = withErrorBoundary( }); } catch (error) { const options = { - context: errorContext, + context: `${AgentsTable.name}.getItems`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -714,7 +712,7 @@ export const AgentsTable = withErrorBoundary( : this.showToast('warning', `Failed to delete selected agents`, '', 5000); } catch (error) { const options = { - context: errorContext, + context: `${AgentsTable.name}.onClickPurge`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -749,7 +747,7 @@ export const AgentsTable = withErrorBoundary( : this.showToast('warning', `Failed to delete all agents`, '', 5000); } catch (error) { const options = { - context: errorContext, + context: `${AgentsTable.name}.onClickPurgeAll`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/controllers/agent/components/checkUpgrade.tsx b/public/controllers/agent/components/checkUpgrade.tsx index cff82809f9..f313eb595b 100644 --- a/public/controllers/agent/components/checkUpgrade.tsx +++ b/public/controllers/agent/components/checkUpgrade.tsx @@ -17,8 +17,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'CheckUpgrade'; - export class CheckUpgrade extends Component { props!: { id: String; @@ -57,7 +55,7 @@ export class CheckUpgrade extends Component { } } catch (error) { const options = { - context: errorContext, + context: `${CheckUpgrade.name}.checkUpgrade`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index f3156e712a..d7dee6d0c6 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -41,8 +41,6 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const errorContext = 'RegisterAgent'; - const architectureButtons = [ { id: 'i386', @@ -186,7 +184,7 @@ export const RegisterAgent = withErrorBoundary( loading: false, }); const options = { - context: errorContext, + context: `${RegisterAgent.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, display: false, diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index 9a8b62bb39..9ff6aed466 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -42,8 +42,6 @@ import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; -const errorContext = 'WzLogs'; - export default compose( withGlobalBreadcrumb([ { text: '' }, @@ -120,7 +118,7 @@ export default compose( }); const options = { - context: errorContext, + context: `${WzLogs.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.CRITICAL, store: true, @@ -211,7 +209,7 @@ export default compose( this.setState({ logsList: result, offset: 0 }); } catch (error) { const options = { - context: errorContext, + context: `${WzLogs.name}.setFullLogs`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -290,7 +288,7 @@ export default compose( } } catch (error) { const options = { - context: errorContext, + context: `${WzLogs.name}.getNodeList`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, @@ -389,7 +387,7 @@ export default compose( ); } catch (error) { const options = { - context: errorContext, + context: `${WzLogs.name}.exportFormatted`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index 3ee255eed1..d7963955e2 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -48,7 +48,6 @@ export class ManagementController { this.logtestOpened = false; this.uploadOpened = false; this.rulesetTab = RulesetResources.RULES; - this.context = 'ManagementController'; this.$scope.$on('setCurrentGroup', (ev, params) => { @@ -232,7 +231,7 @@ export class ManagementController { const errorOptions = { level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, - context: this.context, + context: `${ManagementController.name}.$onInit`, error: { error: error, message: error?.message || '', @@ -268,7 +267,7 @@ export class ManagementController { const errorOptions = { level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, - context: this.context, + context: `${ManagementController.name}.restartManager`, error: { error: error, message: error?.message || '', @@ -294,7 +293,7 @@ export class ManagementController { const errorOptions = { level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, - context: this.context, + context: `${ManagementController.name}.restartCluster`, error: { error: error, message: error?.message || '', @@ -446,7 +445,7 @@ export class ManagementController { const errorOptions = { level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, - context: this.context, + context: `${ManagementController.name}.loadNodeList`, error: { error: error, message: error?.message || '', @@ -519,14 +518,13 @@ export class ManagementController { } if (this.errors) throw this.results; ErrorHandler.info('Upload successful'); - return; } catch (error) { if (Array.isArray(error) && error.length) throw error; const errorOptions = { level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, - context: this.context, + context: `${ManagementController.name}.uploadFiles`, error: { error: error, message: error?.message || '', diff --git a/public/react-services/error-orchestrator/README.md b/public/react-services/error-orchestrator/README.md index 0ac26c7aa6..5d3829d314 100644 --- a/public/react-services/error-orchestrator/README.md +++ b/public/react-services/error-orchestrator/README.md @@ -5,7 +5,7 @@ This is a client side orchestrator error service. ## Example import ```tsx -import { createGetterSetter } from '../utils/create-getter-setter'; +import { getErrorOrchestrator } from '../react-services/common-services'; ``` ## Example usage @@ -18,7 +18,7 @@ try { throw new Error('Some error here...'); } catch (error) { const options: UIErrorLog = { - context: 'contextError', + context: `${MyClass.name}.myMethodName`, level: UI_LOGGER_LEVELS.WARNING, severity: UI_ERROR_SEVERITIES.BUSINESS, display: true, From 4c1ef9037d45dc5d099e0ca15c07946e105ebfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 6 Jul 2021 15:13:59 +0200 Subject: [PATCH 042/493] fix(frontend): Doesn't open menu when changing the API or Index pattern using its selectors --- public/components/wz-menu/wz-menu.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 2c8f69b162..4d428511d9 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -270,8 +270,6 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { if (newPattern?.id === 'selectIndexPatternBar') { this.updatePatternAndApi(); - } else { - this.switchMenuOpened(); } } catch (error) { this.showToast('danger', 'Error', error, 4000); @@ -320,9 +318,6 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { AppState.setCurrentAPI( JSON.stringify({ name: apiData[0].manager, id: apiId.value }) ); - if (apiId?.id !== 'selectAPIBar') { - this.switchMenuOpened(); - } if (this.state.currentMenuTab !== 'wazuh-dev') { this.router.reload(); From 5add8435611beba6242a8b8eeb730efc39196fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 6 Jul 2021 15:19:14 +0200 Subject: [PATCH 043/493] changelog: Add PR to changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706d005bc9..84a963a547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed -- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Don't open the main menu when changing the seleted API or index pattern [#3440](https://github.com/wazuh/wazuh-kibana-app/pull/3440) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 2a02a3a1c8be16640e3c3838a0e3a1c6c5bebe90 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 7 Jul 2021 13:10:25 +0200 Subject: [PATCH 044/493] fix size api selector and add basic test --- .../__snapshots__/wz-menu.test.tsx.snap | 12 +++++++++ public/components/wz-menu/wz-menu.js | 2 +- public/components/wz-menu/wz-menu.test.tsx | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 public/components/wz-menu/__snapshots__/wz-menu.test.tsx.snap create mode 100644 public/components/wz-menu/wz-menu.test.tsx diff --git a/public/components/wz-menu/__snapshots__/wz-menu.test.tsx.snap b/public/components/wz-menu/__snapshots__/wz-menu.test.tsx.snap new file mode 100644 index 0000000000..517946a32c --- /dev/null +++ b/public/components/wz-menu/__snapshots__/wz-menu.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WzMenu tests should render a WzMenu 1`] = ` +<WzMenu + windowSize={ + Object { + "height": 768, + "width": 1024, + } + } +/> +`; diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 2c8f69b162..1edbe3002c 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -632,7 +632,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { } getApiSelectorComponent() { - let style = { maxWidth: 100 }; + let style = { minWidth: 'max-content' }; if (this.showSelectorsInPopover){ style = { width: '100%', minWidth: 200 }; } diff --git a/public/components/wz-menu/wz-menu.test.tsx b/public/components/wz-menu/wz-menu.test.tsx new file mode 100644 index 0000000000..7fe4abd538 --- /dev/null +++ b/public/components/wz-menu/wz-menu.test.tsx @@ -0,0 +1,26 @@ +/* + * Wazuh app - Health Check Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { WzMenu } from './wz-menu'; + + +describe('WzMenu tests', () => { + test('should render a WzMenu', () => { + const component = shallow(<WzMenu />); + + expect(component).toMatchSnapshot(); + }); +}); \ No newline at end of file From 2ca0d710dd753cdb6b6b1b410860f28f8b6d9246 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 7 Jul 2021 13:30:12 +0200 Subject: [PATCH 045/493] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706d005bc9..d069834091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From ebaa3b0b9d76a15b273a67f1a2886849c950935e Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 7 Jul 2021 19:18:51 +0200 Subject: [PATCH 046/493] Add EuiOutsideClickDetector to flyouts --- .../agents/fim/inventory/registry-table.tsx | 24 +- .../components/agents/fim/inventory/table.tsx | 25 +- .../agents/vuls/inventory/table.tsx | 25 +- .../common/buttons/modal-confirm.tsx | 45 +-- .../common/modules/discover/discover.tsx | 18 +- public/components/common/modules/events.tsx | 18 +- .../fim_events_table/fim_events_table.tsx | 22 +- .../components/mitre_top/mitre_top.tsx | 21 +- public/components/notifications/modal.tsx | 47 +-- .../subrequirements/subrequirements.tsx | 23 +- .../components/techniques/techniques.tsx | 24 +- .../resource_detail_flyout.tsx | 115 ++++---- .../security/policies/create-policy.tsx | 267 +++++++++--------- .../security/policies/edit-policy.tsx | 258 +++++++++-------- .../components/roles-mapping-create.tsx | 130 +++++---- .../components/roles-mapping-edit.tsx | 137 +++++---- .../components/security/roles/create-role.tsx | 98 ++++--- .../components/security/roles/edit-role.tsx | 131 ++++----- .../security/users/components/create-user.tsx | 196 +++++++------ .../security/users/components/edit-user.tsx | 206 +++++++------- public/components/security/users/users.tsx | 14 +- .../wz-agent-selector/wz-agent-selector.js | 37 +-- .../overview-actions/overview-actions.js | 39 +-- 23 files changed, 947 insertions(+), 973 deletions(-) diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index 86bbef000f..47e3e5774f 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -17,6 +17,7 @@ import { EuiFlexItem, EuiBasicTable, EuiOverlayMask, + EuiOutsideClickDetector, Direction } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -241,18 +242,17 @@ export class RegistryTable extends Component { <div> {registryTable} {this.state.isFlyoutVisible && ( - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => this.closeFlyout()} - > - <FlyoutDetail - fileName={this.state.currentFile.file} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type= {this.state.currentFile.type} - view='inventory' - {...this.props} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> + <FlyoutDetail + fileName={this.state.currentFile.file} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type={this.state.currentFile.type} + view='inventory' + {...this.props} /> + </EuiOutsideClickDetector> </EuiOverlayMask> )} </div> diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index a88fea3419..f7169292f2 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -17,6 +17,7 @@ import { EuiBasicTable, Direction, EuiOverlayMask, + EuiOutsideClickDetector, } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { FlyoutDetail } from './flyout'; @@ -279,18 +280,18 @@ export class InventoryTable extends Component { <div className='wz-inventory'> {filesTable} {this.state.isFlyoutVisible && - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => this.closeFlyout() } > - <FlyoutDetail - fileName={this.state.currentFile} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type='file' - view='inventory' - showViewInEvents={true} - {...this.props} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> + <FlyoutDetail + fileName={this.state.currentFile} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type='file' + view='inventory' + showViewInEvents={true} + {...this.props} /> + </EuiOutsideClickDetector> </EuiOverlayMask> } </div> diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index f9194e93c4..efc4551faa 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -14,6 +14,7 @@ import React, { Component } from 'react'; import { Direction, EuiOverlayMask, + EuiOutsideClickDetector, } from '@elastic/eui'; import { FlyoutDetail } from './flyout'; import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; @@ -163,18 +164,18 @@ export class InventoryTable extends Component { <div className='wz-inventory'> {table} {this.state.isFlyoutVisible && - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => this.closeFlyout() } > - <FlyoutDetail - vulName={this.state.currentItem.cve} - agentId={this.props.agent.id} - item={this.state.currentItem} - closeFlyout={() => this.closeFlyout()} - type='vulnerability' - view='inventory' - showViewInEvents={true} - {...this.props} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> + <FlyoutDetail + vulName={this.state.currentItem.cve} + agentId={this.props.agent.id} + item={this.state.currentItem} + closeFlyout={() => this.closeFlyout()} + type='vulnerability' + view='inventory' + showViewInEvents={true} + {...this.props} /> + </EuiOutsideClickDetector> </EuiOverlayMask> } </div> diff --git a/public/components/common/buttons/modal-confirm.tsx b/public/components/common/buttons/modal-confirm.tsx index ca3d7e4980..60a1cae86d 100644 --- a/public/components/common/buttons/modal-confirm.tsx +++ b/public/components/common/buttons/modal-confirm.tsx @@ -13,6 +13,7 @@ import React from 'react'; import { EuiOverlayMask, + EuiOutsideClickDetector, EuiConfirmModal } from '@elastic/eui'; @@ -43,17 +44,19 @@ const renderModal = ({onConfirm, onCancel, modalTitle, modalConfirmText, modalCa onCancel && onCancel(); }; return ( - <EuiOverlayMask onClick={close}> - <EuiConfirmModal - title={modalTitle} - onCancel={onModalCancel} - onConfirm={onModalConfirm} - cancelButtonText={modalCancelText} - confirmButtonText={modalConfirmText} - defaultFocusedButton={modalProps.defaultFocusedButton || "confirm"} - {...modalProps} + <EuiOverlayMask> + <EuiOutsideClickDetector onOutsideClick={close}> + <EuiConfirmModal + title={modalTitle} + onCancel={onModalCancel} + onConfirm={onModalConfirm} + cancelButtonText={modalCancelText} + confirmButtonText={modalConfirmText} + defaultFocusedButton={modalProps.defaultFocusedButton || "confirm"} + {...modalProps} > - </EuiConfirmModal> + </EuiConfirmModal> + </EuiOutsideClickDetector> </EuiOverlayMask> ) }; @@ -72,17 +75,19 @@ export const WzButtonModalConfirm: React.FunctionComponent<WzButtonModalConfirmP onCancel && onCancel(); }; return ( - <EuiOverlayMask onClick={close}> - <EuiConfirmModal - title={modalTitle} - onCancel={onModalCancel} - onConfirm={onModalConfirm} - cancelButtonText={modalCancelText} - confirmButtonText={modalConfirmText} - defaultFocusedButton={modalProps.defaultFocusedButton || "confirm"} - {...modalProps} + <EuiOverlayMask> + <EuiOutsideClickDetector onOutsideClick={close}> + <EuiConfirmModal + title={modalTitle} + onCancel={onModalCancel} + onConfirm={onModalConfirm} + cancelButtonText={modalCancelText} + confirmButtonText={modalConfirmText} + defaultFocusedButton={modalProps.defaultFocusedButton || "confirm"} + {...modalProps} > - </EuiConfirmModal> + </EuiConfirmModal> + </EuiOutsideClickDetector> </EuiOverlayMask> ) }} diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 5fd915c6e5..d1ba83a331 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -34,6 +34,7 @@ import { EuiFlexGroup, Direction, EuiOverlayMask, + EuiOutsideClickDetector, EuiSpacer, EuiCallOut, EuiIcon, @@ -598,15 +599,14 @@ export const Discover = compose( pageSizeOptions: [10, 25, 50], }; const noResultsText = `No results match for this search criteria`; - let flyout = this.state.showMitreFlyout ? <EuiOverlayMask - headerZindexLocation="below" - onClick={this.closeMitreFlyout} > - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} /> - + let flyout = this.state.showMitreFlyout ? <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + onChangeFlyout={this.onMitreChangeFlyout} + currentTechnique={this.state.selectedTechnique} /> + </EuiOutsideClickDetector> </EuiOverlayMask> : <></>; return ( <div diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 2ad181eb7a..d3bc4dea7a 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -15,7 +15,7 @@ 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 } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiOverlayMask, EuiOutsideClickDetector } from '@elastic/eui'; import { PatternHandler } from '../../../react-services/pattern-handler'; import { enhanceDiscoverEventsCell } from './events-enhance-discover-fields'; @@ -263,14 +263,14 @@ export class Events extends Component { <Fragment> {flyout && ( <EuiOverlayMask - headerZindexLocation="below" - // @ts-ignore - onClick={() => { this.closeFlyout() }} > - <FlyoutComponent - closeFlyout={this.closeFlyout} - {...this.state.flyout.props} - {...this.props} - /> + headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> + <FlyoutComponent + closeFlyout={this.closeFlyout} + {...this.state.flyout.props} + {...this.props} + /> + </EuiOutsideClickDetector> </EuiOverlayMask> )} </Fragment> diff --git a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx index 600951a2b9..2eb1773287 100644 --- a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx +++ b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx @@ -22,7 +22,8 @@ import { EuiButtonIcon, EuiFlexGroup, EuiToolTip, - EuiOverlayMask + EuiOverlayMask, + EuiOutsideClickDetector, } from '@elastic/eui' // @ts-ignore import store from '../../../../../redux/store'; @@ -87,16 +88,15 @@ function FimTable({ agent }) { itemId="fim-alerts" noItemsMessage="No recent events" /> {isOpen && ( - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => setIsOpen(false)} - > - <FlyoutDetail - agentId={agent.id} - closeFlyout={() => setIsOpen(false)} - fileName={file} - view='extern' - {...{agent}} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => setIsOpen(false)}> + <FlyoutDetail + agentId={agent.id} + closeFlyout={() => setIsOpen(false)} + fileName={file} + view='extern' + {...{ agent }} /> + </EuiOutsideClickDetector> </EuiOverlayMask> )} </Fragment> diff --git a/public/components/common/welcome/components/mitre_top/mitre_top.tsx b/public/components/common/welcome/components/mitre_top/mitre_top.tsx index 768f6b5fce..8ec0c8ccc0 100644 --- a/public/components/common/welcome/components/mitre_top/mitre_top.tsx +++ b/public/components/common/welcome/components/mitre_top/mitre_top.tsx @@ -20,6 +20,7 @@ import { EuiButtonIcon, EuiLoadingChart, EuiOverlayMask, + EuiOutsideClickDetector, EuiEmptyPrompt, } from '@elastic/eui'; import { FlyoutTechnique } from '../../../../../components/overview/mitre/components/techniques/components/flyout-technique'; @@ -245,16 +246,16 @@ export class MitreTopTactics extends Component { {!selectedTactic || alertsCount.length === 0 ? tacticsTop : tecniquesTop} {alertsCount.length === 0 && emptyPrompt} {flyoutOn && - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => this.closeFlyout() } > - <FlyoutTechnique - openDashboard={(e,itemId) => this.openDashboard(e,itemId)} - openDiscover={(e,itemId) => this.openDiscover(e,itemId)} - implicitFilters={[ {"agent.id": this.props.agentId} ] } - agentId={this.props.agentId} - onChangeFlyout={this.onChangeFlyout} - currentTechnique={selectedTechnique} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + implicitFilters={[{ "agent.id": this.props.agentId }]} + agentId={this.props.agentId} + onChangeFlyout={this.onChangeFlyout} + currentTechnique={selectedTechnique} /> + </EuiOutsideClickDetector> </EuiOverlayMask>} </Fragment> ) diff --git a/public/components/notifications/modal.tsx b/public/components/notifications/modal.tsx index 1680613acb..3390ed2c45 100644 --- a/public/components/notifications/modal.tsx +++ b/public/components/notifications/modal.tsx @@ -22,6 +22,7 @@ import { EuiModalFooter, EuiButton, EuiOverlayMask, + EuiOutsideClickDetector, EuiCopy } from '@elastic/eui'; @@ -59,28 +60,30 @@ export const ToastNotificationsModal = compose (withErrorBoundary, withReduxProv ${errorStack} \`\`\`` return ( - <EuiOverlayMask onClick={() => closeModal()}> - <EuiModal onClose={closeModal}> - <EuiModalHeader> - <EuiModalHeaderTitle>{toastNotification.title}</EuiModalHeaderTitle> - </EuiModalHeader> - <EuiModalBody> - <EuiCallOut size="s" color="danger" iconType="alert" title={calloutTitle}/> - {errorStack && ( - <Fragment> - <EuiSpacer size="s" /> - <EuiCodeBlock /*isCopyable={true}*/ paddingSize="s"> - {errorStack} - </EuiCodeBlock> - </Fragment> - )} - </EuiModalBody> - <EuiModalFooter> - <EuiCopy textToCopy={copyMessage}> - {copy => <EuiButton fill onClick={copy}>Copy error</EuiButton>} - </EuiCopy> - </EuiModalFooter> - </EuiModal> + <EuiOverlayMask> + <EuiOutsideClickDetector onOutsideClick={() => closeModal()}> + <EuiModal onClose={closeModal}> + <EuiModalHeader> + <EuiModalHeaderTitle>{toastNotification.title}</EuiModalHeaderTitle> + </EuiModalHeader> + <EuiModalBody> + <EuiCallOut size="s" color="danger" iconType="alert" title={calloutTitle} /> + {errorStack && ( + <Fragment> + <EuiSpacer size="s" /> + <EuiCodeBlock /*isCopyable={true}*/ paddingSize="s"> + {errorStack} + </EuiCodeBlock> + </Fragment> + )} + </EuiModalBody> + <EuiModalFooter> + <EuiCopy textToCopy={copyMessage}> + {copy => <EuiButton fill onClick={copy}>Copy error</EuiButton>} + </EuiCopy> + </EuiModalFooter> + </EuiModal> + </EuiOutsideClickDetector> </EuiOverlayMask> ) }) \ No newline at end of file diff --git a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index 0a5d5b6a4c..e6a499b2c8 100644 --- a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -25,6 +25,7 @@ import { EuiText, EuiIcon, EuiOverlayMask, + EuiOutsideClickDetector, EuiLoadingSpinner, } from '@elastic/eui'; import { AppNavigate } from '../../../../../react-services/app-navigate'; @@ -278,17 +279,17 @@ export class ComplianceSubrequirements extends Component { </div> {this.state.flyoutOn && - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => this.closeFlyout() } > - <RequirementFlyout - currentRequirement={this.state.selectedRequirement} - onChangeFlyout={this.onChangeFlyout} - description={this.props.descriptions[this.state.selectedRequirement]} - getRequirementKey={() => { return this.getRequirementKey() }} - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <RequirementFlyout + currentRequirement={this.state.selectedRequirement} + onChangeFlyout={this.onChangeFlyout} + description={this.props.descriptions[this.state.selectedRequirement]} + getRequirementKey={() => { return this.getRequirementKey() }} + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + /> + </EuiOutsideClickDetector> </EuiOverlayMask>} </div> ) diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 8d32c76646..64322fab12 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -25,6 +25,7 @@ import { EuiContextMenu, EuiIcon, EuiOverlayMask, + EuiOutsideClickDetector, EuiCallOut, EuiLoadingSpinner, } from '@elastic/eui'; @@ -458,18 +459,17 @@ export const Techniques = withWindowSize(class Techniques extends Component { {this.renderFacet()} </div> { isFlyoutVisible && - <EuiOverlayMask - headerZindexLocation="below" - // @ts-ignore - onClick={() => this.onChangeFlyout(false) } > - <FlyoutTechnique - openDashboard={(e,itemId) => this.openDashboard(e,itemId)} - openDiscover={(e,itemId) => this.openDiscover(e,itemId)} - openIntelligence={(e,redirectTo,itemId) => this.openIntelligence(e,redirectTo,itemId)} - onChangeFlyout={this.onChangeFlyout} - currentTechniqueData={this.state.currentTechniqueData} - currentTechnique={currentTechnique} - tacticsObject={this.props.tacticsObject} /> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => this.onChangeFlyout(false)}> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} + onChangeFlyout={this.onChangeFlyout} + currentTechniqueData={this.state.currentTechniqueData} + currentTechnique={currentTechnique} + tacticsObject={this.props.tacticsObject} /> + </EuiOutsideClickDetector> </EuiOverlayMask> } </div> diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx index 28047e37c1..13836f25d0 100644 --- a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx @@ -19,6 +19,7 @@ import { EuiFlyout, EuiFlyoutHeader, EuiOverlayMask, + EuiOutsideClickDetector, EuiTitle, EuiText, EuiFlexGroup, @@ -38,65 +39,65 @@ export const ModuleMitreAttackIntelligenceFlyout = ({details, closeFlyout, onSel const startReference = useRef(null); return ( - <EuiOverlayMask - headerZindexLocation="below" - onClick= {closeFlyout} > - <EuiFlyout - onClose={closeFlyout} - size="l" - aria-labelledby={``} - > - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="flyoutTitle">Details</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <div ref={startReference}> + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={closeFlyout}> + <EuiFlyout + onClose={closeFlyout} + size="l" + aria-labelledby={``} + > + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="flyoutTitle">Details</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <div ref={startReference}> + <EuiFlexGroup> + {MitreAttackResources[0].mitreFlyoutHeaderProperties.map(detailProperty => ( + <EuiFlexItem key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`}> + <div> + <strong> + {detailProperty.label} + </strong> + </div> + <EuiText color='subdued'> + {detailProperty.render ? detailProperty.render(details[detailProperty.id]) : details[detailProperty.id]} + </EuiText> + </EuiFlexItem> + ))} + </EuiFlexGroup> + </div> <EuiFlexGroup> - {MitreAttackResources[0].mitreFlyoutHeaderProperties.map(detailProperty => ( - <EuiFlexItem key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`}> - <div> - <strong> - {detailProperty.label} - </strong> - </div> - <EuiText color='subdued'> - {detailProperty.render ? detailProperty.render(details[detailProperty.id]) : details[detailProperty.id]} - </EuiText> - </EuiFlexItem> - ))} - </EuiFlexGroup> - </div> - <EuiFlexGroup> - <EuiFlexItem> - <div> - <strong> - Description + <EuiFlexItem> + <div> + <strong> + Description </strong> - </div> - <EuiText color='subdued'> - { details.description ? <Markdown markdown={details.description}/> : ''} - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - {MitreAttackResources.filter((item) => details[item.id]).map((item) => - <Fragment key={`resource_${item.id}`}> - <ReferencesTable - referencesName={item.id} - referencesArray={details[item.id]} - columns={item.tableColumnsCreator(onSelectResource)} - backToTop={() => { startReference.current?.scrollIntoView()}} - /> - <EuiSpacer /> - </Fragment> - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> + </div> + <EuiText color='subdued'> + {details.description ? <Markdown markdown={details.description} /> : ''} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + {MitreAttackResources.filter((item) => details[item.id]).map((item) => + <Fragment key={`resource_${item.id}`}> + <ReferencesTable + referencesName={item.id} + referencesArray={details[item.id]} + columns={item.tableColumnsCreator(onSelectResource)} + backToTop={() => { startReference.current?.scrollIntoView() }} + /> + <EuiSpacer /> + </Fragment> + )} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </EuiOverlayMask> ) }; diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 706ab88de3..f5fa42c6a2 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -14,6 +14,7 @@ import { EuiInMemoryTable, EuiConfirmModal, EuiOverlayMask, + EuiOutsideClickDetector, EuiFieldText, EuiText, } from '@elastic/eui'; @@ -281,149 +282,143 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { } }, [policyName, actionValue, addedActions, addedResources, effectValue]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout - className="wzApp" - onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New policy</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - value={policyName} - onChange={(e) => onChangePolicyName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." - > - <EuiSuperSelect - options={actions} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue} + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New policy</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + value={policyName} + onChange={(e) => onChangePolicyName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." > - Add + <EuiSuperSelect + options={actions} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue} + > + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." - > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." - > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue} + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." > - Add + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue} + > + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton - disabled={ - !policyName || !addedActions.length || !addedResources.length || !effectValue - } - onClick={() => { - createPolicy(); - }} - fill - > - Create policy + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton + disabled={ + !policyName || !addedActions.length || !addedResources.length || !effectValue + } + onClick={() => { + createPolicy(); + }} + fill + > + Create policy </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index c91a152c36..6998dffb41 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -17,6 +17,7 @@ import { EuiFieldText, EuiConfirmModal, EuiOverlayMask, + EuiOutsideClickDetector, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; @@ -285,144 +286,141 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { } }, [actionValue, addedResources, addedActions, resourceValue, effectValue]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout className="wzApp" onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit policy {policy.name}   + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit policy {policy.name}   {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - disabled={isReserved} - value={policy.name} - readOnly={true} - onChange={() => {}} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." - > - <EuiSuperSelect - options={actions} - disabled={isReserved} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue || isReserved} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + disabled={isReserved} + value={policy.name} + readOnly={true} + onChange={() => { }} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." > - Add + <EuiSuperSelect + options={actions} + disabled={isReserved} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue || isReserved} + > + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." - > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." - > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue || isReserved} + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." > - Add + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue || isReserved} + > + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton disabled={isReserved} onClick={updatePolicy} fill> - Apply + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton disabled={isReserved} onClick={updatePolicy} fill> + Apply </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 5dd081c093..8ec268f079 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -12,6 +12,7 @@ import { EuiComboBox, EuiFieldText, EuiOverlayMask, + EuiOutsideClickDetector, EuiConfirmModal, } from '@elastic/eui'; import { ErrorHandler } from '../../../../react-services/error-handler'; @@ -95,75 +96,70 @@ export const RolesMappingCreate = ({ } }, [selectedRoles, ruleName, hasChangeMappingRules]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout - className="wzApp" - onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>Create new role mapping  </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role mapping name" - isInvalid={false} - error={'Please provide a role mapping name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="Role name" - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList()} - isDisabled={false} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => createRule(rule)} - initialRule={false} - isReserved={false} - isLoading={isLoading} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => { - setHasChangeMappingRules(hasChange); - }} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>Create new role mapping  </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role mapping name" + isInvalid={false} + error={'Please provide a role mapping name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="Role name" + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList()} + isDisabled={false} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => createRule(rule)} + initialRule={false} + isReserved={false} + isLoading={isLoading} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => { + setHasChangeMappingRules(hasChange); + }} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index cbc9f9acf0..74ef416d06 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -12,6 +12,7 @@ import { EuiBadge, EuiComboBox, EuiOverlayMask, + EuiOutsideClickDetector, EuiConfirmModal, EuiFieldText, } from '@elastic/eui'; @@ -121,79 +122,73 @@ export const RolesMappingEdit = ({ } }, [selectedRoles, ruleName, hasChangeMappingRules]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout - className="wzApp" - onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit <strong>{rule.name}  </strong> - {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={false} - error={'Please provide a role name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="" - disabled={WzAPIUtils.isReservedID(rule.id)} - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList(roles)} - isDisabled={WzAPIUtils.isReservedID(rule.id)} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => editRule(rule)} - initialRule={rule.rule} - isLoading={isLoading} - isReserved={WzAPIUtils.isReservedID(rule.id)} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit <strong>{rule.name}  </strong> + {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={false} + error={'Please provide a role name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="" + disabled={WzAPIUtils.isReservedID(rule.id)} + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList(roles)} + isDisabled={WzAPIUtils.isReservedID(rule.id)} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => editRule(rule)} + initialRule={rule.rule} + isLoading={isLoading} + isReserved={WzAPIUtils.isReservedID(rule.id)} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index c8cc6f1aee..50eaff833e 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -8,6 +8,7 @@ import { EuiForm, EuiFieldText, EuiOverlayMask, + EuiOutsideClickDetector, EuiFormRow, EuiSpacer, EuiComboBox, @@ -130,59 +131,56 @@ export const CreateRole = ({ closeFlyout }) => { } }, [selectedPolicies, roleName]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout className="wzApp" onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New role</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={roleNameError} - error={'Please provide a role name'} - helpText="Introduce a name for this new role." - > - <EuiFieldText - placeholder="" - value={roleName} - onChange={(e) => onChangeRoleName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton fill onClick={createUser}> - Create role + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New role</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={roleNameError} + error={'Please provide a role name'} + helpText="Introduce a name for this new role." + > + <EuiFieldText + placeholder="" + value={roleName} + onChange={(e) => onChangeRoleName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton fill onClick={createUser}> + Create role </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 1e957ca736..0230560dd9 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -14,6 +14,7 @@ import { EuiBadge, EuiComboBox, EuiOverlayMask, + EuiOutsideClickDetector, EuiConfirmModal } from '@elastic/eui'; @@ -134,82 +135,68 @@ export const EditRole = ({ role, closeFlyout }) => { ); } - return ( + const onClose = () => { (initialSelectedPolicies.length != selectedPolicies.length) ? setIsModalVisible(true) : closeFlyout(false) }; + + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - if (initialSelectedPolicies.length != selectedPolicies.length) { - setIsModalVisible(true); - } else { - closeFlyout(false); - } - }} - > - <EuiFlyout - className="wzApp" - onClose={() => { - if (initialSelectedPolicies.length != selectedPolicies.length) { - setIsModalVisible(true); - } else { - closeFlyout(false); - } - }} - > - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {role.name} role   + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose} > + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {role.name} role   {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} + isDisabled={isReserved} + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiButton + style={{ marginTop: 20, maxWidth: 45 }} isDisabled={isReserved} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiButton - style={{ marginTop: 20, maxWidth: 45 }} - isDisabled={isReserved} - fill - onClick={addPolicy} - > - Add policy + fill + onClick={addPolicy} + > + Add policy </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer /> - </EuiForm> - <div style={{ margin: 20 }}> - <EditRolesTable - policies={assignedPolicies} - role={currentRole} - onChange={update} - isDisabled={isReserved} - loading={isLoading} - /> - </div> - </EuiFlyoutBody> - </EuiFlyout> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer /> + </EuiForm> + <div style={{ margin: 20 }}> + <EditRolesTable + policies={assignedPolicies} + role={currentRole} + onChange={update} + isDisabled={isReserved} + loading={isLoading} + /> + </div> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index 75965f1e86..3831099568 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -14,6 +14,7 @@ import { EuiFieldPassword, EuiFieldText, EuiOverlayMask, + EuiOutsideClickDetector, EuiPanel, EuiConfirmModal, } from '@elastic/eui'; @@ -224,111 +225,108 @@ export const CreateUser = ({ closeFlyout }) => { } }, [selectedRoles, userName, password, confirmPassword, allowRunAs]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout className="wzApp" onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>Create new user</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiPanel> - <EuiTitle size="s"> - <h2>User data</h2> - </EuiTitle> - <EuiSpacer /> - <EuiFormRow - label="User name" - isInvalid={!!formErrors.userName} - error={formErrors.userName} - helpText="Introduce the user name for the user." - > - <EuiFieldText - placeholder="User name" - value={userName} - onChange={(e) => onChangeUserName(e)} - aria-label="" + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>Create new user</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiPanel> + <EuiTitle size="s"> + <h2>User data</h2> + </EuiTitle> + <EuiSpacer /> + <EuiFormRow + label="User name" isInvalid={!!formErrors.userName} - /> - </EuiFormRow> - <EuiFormRow - label="Password" - isInvalid={!!formErrors.password} - error={formErrors.password} - helpText="Introduce a new password for the user." - > - <EuiFieldPassword - placeholder="Password" - value={password} - onChange={(e) => onChangePassword(e)} - aria-label="" + error={formErrors.userName} + helpText="Introduce the user name for the user." + > + <EuiFieldText + placeholder="User name" + value={userName} + onChange={(e) => onChangeUserName(e)} + aria-label="" + isInvalid={!!formErrors.userName} + /> + </EuiFormRow> + <EuiFormRow + label="Password" isInvalid={!!formErrors.password} - /> - </EuiFormRow> - <EuiFormRow - label="Confirm Password" - isInvalid={!!formErrors.confirmPassword} - error={formErrors.confirmPassword} - helpText="Confirm the new password." - > - <EuiFieldPassword - placeholder="Confirm Password" - value={confirmPassword} - onChange={(e) => onChangeConfirmPassword(e)} - aria-label="" + error={formErrors.password} + helpText="Introduce a new password for the user." + > + <EuiFieldPassword + placeholder="Password" + value={password} + onChange={(e) => onChangePassword(e)} + aria-label="" + isInvalid={!!formErrors.password} + /> + </EuiFormRow> + <EuiFormRow + label="Confirm Password" isInvalid={!!formErrors.confirmPassword} - /> - </EuiFormRow> - <EuiFormRow label="Allow run as" helpText="Set if the user is able to use run as"> - <WzButtonPermissions - buttonType="switch" - label="Allow run as" - showLabel={false} - checked={allowRunAs} - permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} - onChange={(e) => onChangeAllowRunAs(e)} - aria-label="" - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>User roles</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Assign roles to the selected user"> - <EuiComboBox - placeholder="Select roles" - options={rolesOptions} - selectedOptions={selectedRoles} - isLoading={rolesLoading || isLoading} - onChange={onChangeRoles} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton fill isLoading={isLoading} onClick={editUser} isDisabled={!showApply}> - Apply + error={formErrors.confirmPassword} + helpText="Confirm the new password." + > + <EuiFieldPassword + placeholder="Confirm Password" + value={confirmPassword} + onChange={(e) => onChangeConfirmPassword(e)} + aria-label="" + isInvalid={!!formErrors.confirmPassword} + /> + </EuiFormRow> + <EuiFormRow label="Allow run as" helpText="Set if the user is able to use run as"> + <WzButtonPermissions + buttonType="switch" + label="Allow run as" + showLabel={false} + checked={allowRunAs} + permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} + onChange={(e) => onChangeAllowRunAs(e)} + aria-label="" + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>User roles</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Assign roles to the selected user"> + <EuiComboBox + placeholder="Select roles" + options={rolesOptions} + selectedOptions={selectedRoles} + isLoading={rolesLoading || isLoading} + onChange={onChangeRoles} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton fill isLoading={isLoading} onClick={editUser} isDisabled={!showApply}> + Apply </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 22c82d25cf..b0e9b68b6a 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -14,6 +14,7 @@ import { EuiComboBox, EuiFieldPassword, EuiOverlayMask, + EuiOutsideClickDetector, EuiConfirmModal, EuiPanel, } from '@elastic/eui'; @@ -217,117 +218,114 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { } }, [selectedRoles, password, confirmPassword, allowRunAs]); + const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + return ( <> - <WzOverlayMask - headerZindexLocation="below" - onClick={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }} - > - <EuiFlyout className="wzApp" onClose={() => { - hasChanges ? setIsModalVisible(true) : closeFlyout(false); - }}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {currentUser.username} user     + <WzOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={onClose}> + <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {currentUser.username} user     {WzAPIUtils.isReservedID(currentUser.id) && ( - <EuiBadge color="primary">Reserved</EuiBadge> - )} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiPanel> - <EuiTitle size="s"> - <h2>Run as</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Set if the user is able to use run as"> - <WzButtonPermissions - buttonType="switch" - label="Allow run as" - showLabel={true} - checked={allowRunAs} - permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} - onChange={(e) => onChangeAllowRunAs(e)} - aria-label="" - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Password</h2> - </EuiTitle> - <EuiFormRow - label="" - isInvalid={!!formErrors.password} - error={formErrors.password} - helpText="Introduce a new password for the user." - > - <EuiFieldPassword - placeholder="Password" - value={password} - onChange={(e) => onChangePassword(e)} - aria-label="" + <EuiBadge color="primary">Reserved</EuiBadge> + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiPanel> + <EuiTitle size="s"> + <h2>Run as</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Set if the user is able to use run as"> + <WzButtonPermissions + buttonType="switch" + label="Allow run as" + showLabel={true} + checked={allowRunAs} + permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} + onChange={(e) => onChangeAllowRunAs(e)} + aria-label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Password</h2> + </EuiTitle> + <EuiFormRow + label="" isInvalid={!!formErrors.password} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - <EuiFormRow - label="" - isInvalid={!!formErrors.confirmPassword} - error={formErrors.confirmPassword} - helpText="Confirm the new password." - > - <EuiFieldPassword - placeholder="Confirm Password" - value={confirmPassword} - onChange={(e) => onChangeConfirmPassword(e)} - aria-label="" + error={formErrors.password} + helpText="Introduce a new password for the user." + > + <EuiFieldPassword + placeholder="Password" + value={password} + onChange={(e) => onChangePassword(e)} + aria-label="" + isInvalid={!!formErrors.password} + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + <EuiFormRow + label="" isInvalid={!!formErrors.confirmPassword} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Roles</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Assign roles to the selected user"> - <EuiComboBox - placeholder="Select roles" - options={rolesOptions} - selectedOptions={selectedRoles} - isLoading={rolesLoading || isLoading} - onChange={onChangeRoles} - isClearable={true} - data-test-subj="demoComboBox" - isDisabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton - fill - isLoading={isLoading} - isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} - onClick={editUser} + error={formErrors.confirmPassword} + helpText="Confirm the new password." > - Apply + <EuiFieldPassword + placeholder="Confirm Password" + value={confirmPassword} + onChange={(e) => onChangeConfirmPassword(e)} + aria-label="" + isInvalid={!!formErrors.confirmPassword} + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Roles</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Assign roles to the selected user"> + <EuiComboBox + placeholder="Select roles" + options={rolesOptions} + selectedOptions={selectedRoles} + isLoading={rolesLoading || isLoading} + onChange={onChangeRoles} + isClearable={true} + data-test-subj="demoComboBox" + isDisabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + fill + isLoading={isLoading} + isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} + onClick={editUser} + > + Apply </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> + </EuiOutsideClickDetector> </WzOverlayMask> {modal} </> diff --git a/public/components/security/users/users.tsx b/public/components/security/users/users.tsx index d863e47638..b384d03537 100644 --- a/public/components/security/users/users.tsx +++ b/public/components/security/users/users.tsx @@ -76,12 +76,7 @@ export const Users = () => { } if (isEditFlyoutVisible) { editFlyout = ( - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => { - setIsEditFlyoutVisible(false); - }} - > + <EuiOverlayMask headerZindexLocation="below"> <EditUser currentUser={editingUser} closeFlyout={closeEditFlyout} @@ -93,12 +88,7 @@ export const Users = () => { if (isCreateFlyoutVisible) { createFlyout = ( - <EuiOverlayMask - headerZindexLocation="below" - onClick={() => { - setIsCreateFlyoutVisible(false); - }} - > + <EuiOverlayMask headerZindexLocation="below"> <CreateUser closeFlyout={closeCreateFlyout} /> </EuiOverlayMask> ); diff --git a/public/components/wz-agent-selector/wz-agent-selector.js b/public/components/wz-agent-selector/wz-agent-selector.js index 654dd9a559..87b0fb2291 100644 --- a/public/components/wz-agent-selector/wz-agent-selector.js +++ b/public/components/wz-agent-selector/wz-agent-selector.js @@ -13,6 +13,7 @@ import React, { Component } from 'react'; import { EuiButtonEmpty, EuiOverlayMask, + EuiOutsideClickDetector, EuiModal, EuiModalHeader, EuiModalBody, @@ -118,24 +119,26 @@ class WzAgentSelector extends Component { if (this.props.state.showExploreAgentModalGlobal) { modal = ( - <EuiOverlayMask onClick={() => this.closeAgentModal()}> - <EuiModal - className="wz-select-agent-modal" - onClose={() => this.closeAgentModal()} - initialFocus="[name=popswitch]" - > - <EuiModalHeader> - <EuiModalHeaderTitle>Explore agent</EuiModalHeaderTitle> - </EuiModalHeader> + <EuiOverlayMask> + <EuiOutsideClickDetector onOutsideClick={() => this.closeAgentModal()}> + <EuiModal + className="wz-select-agent-modal" + onClose={() => this.closeAgentModal()} + initialFocus="[name=popswitch]" + > + <EuiModalHeader> + <EuiModalHeaderTitle>Explore agent</EuiModalHeaderTitle> + </EuiModalHeader> - <EuiModalBody> - <AgentSelectionTable - updateAgentSearch={agentsIdList => this.agentTableSearch(agentsIdList)} - removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} - selectedAgents={this.getSelectedAgents()} - ></AgentSelectionTable> - </EuiModalBody> - </EuiModal> + <EuiModalBody> + <AgentSelectionTable + updateAgentSearch={agentsIdList => this.agentTableSearch(agentsIdList)} + removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} + selectedAgents={this.getSelectedAgents()} + ></AgentSelectionTable> + </EuiModalBody> + </EuiModal> + </EuiOutsideClickDetector> </EuiOverlayMask> ); } diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index 2046c6e98e..53ce736794 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -20,6 +20,7 @@ import { EuiButtonIcon, EuiIcon, EuiOverlayMask, + EuiOutsideClickDetector, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -146,24 +147,26 @@ class OverviewActions extends Component { if (this.state.isAgentModalVisible || this.props.state.showExploreAgentModal) { modal = ( - <EuiOverlayMask onClick={() => this.closeAgentModal()}> - <EuiModal - className="wz-select-agent-modal" - onClose={() => this.closeAgentModal()} - initialFocus="[name=popswitch]" - > - <EuiModalHeader> - <EuiModalHeaderTitle>Explore agent</EuiModalHeaderTitle> - </EuiModalHeader> - - <EuiModalBody> - <AgentSelectionTable - updateAgentSearch={agentsIdList => this.agentTableSearch(agentsIdList)} - removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} - selectedAgents={this.getSelectedAgents()} - ></AgentSelectionTable> - </EuiModalBody> - </EuiModal> + <EuiOverlayMask> + <EuiOutsideClickDetector onOutsideClick={() => this.closeAgentModal()}> + <EuiModal + className="wz-select-agent-modal" + onClose={() => this.closeAgentModal()} + initialFocus="[name=popswitch]" + > + <EuiModalHeader> + <EuiModalHeaderTitle>Explore agent</EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody> + <AgentSelectionTable + updateAgentSearch={agentsIdList => this.agentTableSearch(agentsIdList)} + removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} + selectedAgents={this.getSelectedAgents()} + ></AgentSelectionTable> + </EuiModalBody> + </EuiModal> + </EuiOutsideClickDetector> </EuiOverlayMask> ); } From a3433bca18b766c0a16708abd0a4aa7bd44264af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Wed, 7 Jul 2021 23:14:36 +0200 Subject: [PATCH 047/493] Implement try catch strategy in Groups (#3415) * Implement try catch groups * Add test * test(groups): Added simple snapshot test. * Add changelog * Change context * Change title Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 1 + public/components/security/main.tsx | 2 - .../__snapshots__/groups-main.test.tsx.snap | 80 ++++++ .../groups/actions-buttons-agents.js | 122 +++++---- .../groups/actions-buttons-files.js | 136 ++++----- .../management/groups/actions-buttons-main.js | 165 ++++++----- .../management/groups/group-agents-table.js | 259 ++++++++++++------ .../management/groups/group-files-table.js | 17 +- .../management/groups/groups-editor.js | 48 ++-- .../management/groups/groups-main.js | 19 +- .../management/groups/groups-main.test.tsx | 76 +++++ .../management/groups/groups-table.js | 121 ++++---- .../management/groups/utils/groups-handler.js | 72 +++-- .../groups/utils/valid-configuration.js | 2 +- 14 files changed, 739 insertions(+), 381 deletions(-) create mode 100644 public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap create mode 100644 public/controllers/management/components/management/groups/groups-main.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 706d005bc9..cc557c2785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) - Refactored all try catch strategy on Controller/Agent section [#3398](https://github.com/wazuh/wazuh-kibana-app/issues/3398) - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/issues/3432) +- Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/issues/3415) ### Fixed diff --git a/public/components/security/main.tsx b/public/components/security/main.tsx index b89ed22c82..6d0da6b5d7 100644 --- a/public/components/security/main.tsx +++ b/public/components/security/main.tsx @@ -32,8 +32,6 @@ import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; -const errorContext = 'WzSecurity'; - const tabs = [ { id: 'users', diff --git a/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap b/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap new file mode 100644 index 0000000000..69746046f9 --- /dev/null +++ b/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reporting component renders correctly to match the snapshot 1`] = ` +<ContextProvider + value={ + Object { + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "subscription": Subscription { + "handleChangeWrapper": [Function], + "listeners": Object { + "notify": [Function], + }, + "onStateChange": [Function], + "parentSub": undefined, + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "unsubscribe": null, + }, + } + } +> + <Connect(WzGroups) + clusterStatus={ + Object { + "contextConfigServer": "cluster", + "status": true, + } + } + configurationProps={ + Object { + "agent": Object { + "id": "000", + }, + } + } + groupsProps={ + Object { + "closeAddingAgents": false, + "exportConfigurationProps": Object { + "type": "group", + }, + "items": Array [ + Object { + "configSum": "ab73af41699f13fdd81903b5f23d8d00", + "count": 1, + "mergedSum": "2c45c95db2954d2c7d0ea533f09e81a5", + "name": "default", + }, + ], + "selectedGroup": false, + } + } + logtestProps={ + Object { + "onFlyout": true, + "showClose": true, + } + } + section="groups" + state={ + Object { + "section": "", + } + } + /> +</ContextProvider> +`; diff --git a/public/controllers/management/components/management/groups/actions-buttons-agents.js b/public/controllers/management/components/management/groups/actions-buttons-agents.js index 329f3981e9..e2d920af6b 100644 --- a/public/controllers/management/components/management/groups/actions-buttons-agents.js +++ b/public/controllers/management/components/management/groups/actions-buttons-agents.js @@ -19,14 +19,17 @@ import { updateLoadingStatus, updateIsProcessing, updateShowAddAgents, - updateReload + updateReload, } from '../../../../../redux/actions/groupsActions'; import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { ExportConfiguration } from '../../../../agent/components/export-configuration'; import { ReportingService } from '../../../../../react-services/reporting'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzGroupsActionButtonsAgents extends Component { _isMounted = false; @@ -38,7 +41,7 @@ class WzGroupsActionButtonsAgents extends Component { this.state = { generatingCsv: false, isPopoverOpen: false, - newGroupName: '' + newGroupName: '', }; this.exportCsv = exportCsv; @@ -59,10 +62,6 @@ class WzGroupsActionButtonsAgents extends Component { this._isMounted = false; } - // UNSAFE_componentWillReceiveProps(nextProps) { - // console.log(nextProps); - // } - /** * Refresh the items */ @@ -72,7 +71,18 @@ class WzGroupsActionButtonsAgents extends Component { this.props.updateIsProcessing(true); this.onRefreshLoading(); } catch (error) { - return Promise.reject(error); + const options = { + context: `${WzGroupsActionButtonsAgents.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -99,19 +109,19 @@ class WzGroupsActionButtonsAgents extends Component { this.setState({ isPopoverOpen: false, msg: false, - newGroupName: '' + newGroupName: '', }); } clearGroupName() { this.setState({ - newGroupName: '' + newGroupName: '', }); } - onChangeNewGroupName = e => { + onChangeNewGroupName = (e) => { this.setState({ - newGroupName: e.target.value + newGroupName: e.target.value, }); }; @@ -125,7 +135,7 @@ class WzGroupsActionButtonsAgents extends Component { if (input.length) { const i = input[0]; if (!i.onkeypress) { - i.onkeypress = async e => { + i.onkeypress = async (e) => { if (e.which === 13) { await this.createGroup(); } @@ -134,19 +144,27 @@ class WzGroupsActionButtonsAgents extends Component { clearInterval(interval); } }, 150); - } catch (error) {} + } catch (error) { + const options = { + context: `${WzGroupsActionButtonsAgents.name}.bindEnterToInput`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } async createGroup() { try { this.props.updateLoadingStatus(true); await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast( - 'success', - 'Success', - 'The group has been created successfully', - 2000 - ); + this.showToast('success', 'Success', 'The group has been created successfully', 2000); this.clearGroupName(); this.props.updateIsProcessing(true); @@ -154,12 +172,7 @@ class WzGroupsActionButtonsAgents extends Component { this.closePopover(); } catch (error) { this.props.updateLoadingStatus(false); - this.showToast( - 'danger', - 'Error', - `An error occurred when creating the group: ${error}`, - 2000 - ); + throw new Error(error); } } @@ -178,12 +191,18 @@ class WzGroupsActionButtonsAgents extends Component { 2000 ); } catch (error) { - this.showToast( - 'danger', - 'Error', - `Error when exporting the CSV file: ${error}`, - 2000 - ); + const options = { + context: `${WzGroupsActionButtonsAgents.name}.generateCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error when exporting the CSV file: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } this.setState({ generatingCsv: false }); } @@ -193,19 +212,14 @@ class WzGroupsActionButtonsAgents extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; render() { - // Add new group button const manageAgentsButton = ( - <EuiButtonEmpty - iconSide="left" - iconType="folderOpen" - onClick={() => this.showManageAgents()} - > + <EuiButtonEmpty iconSide="left" iconType="folderOpen" onClick={() => this.showManageAgents()}> Manage agents </EuiButtonEmpty> ); @@ -213,14 +227,14 @@ class WzGroupsActionButtonsAgents extends Component { // Export PDF button const exportPDFButton = ( <ExportConfiguration - exportConfiguration={enabledComponents => + exportConfiguration={(enabledComponents) => this.reportingService.startConfigReport( this.props.state.itemDetail, 'groupConfig', enabledComponents ) } - type='group' + type="group" /> ); // Export button @@ -236,10 +250,7 @@ class WzGroupsActionButtonsAgents extends Component { // Refresh const refreshButton = ( - <EuiButtonEmpty - iconType="refresh" - onClick={async () => await this.refresh()} - > + <EuiButtonEmpty iconType="refresh" onClick={async () => await this.refresh()}> Refresh </EuiButtonEmpty> ); @@ -255,24 +266,19 @@ class WzGroupsActionButtonsAgents extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updateShowAddAgents: showAddAgents => - dispatch(updateShowAddAgents(showAddAgents)), - updateReload: () => dispatch(updateReload()) + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updateShowAddAgents: (showAddAgents) => dispatch(updateShowAddAgents(showAddAgents)), + updateReload: () => dispatch(updateReload()), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupsActionButtonsAgents); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsActionButtonsAgents); diff --git a/public/controllers/management/components/management/groups/actions-buttons-files.js b/public/controllers/management/components/management/groups/actions-buttons-files.js index 62a311e2ea..8940d6b246 100644 --- a/public/controllers/management/components/management/groups/actions-buttons-files.js +++ b/public/controllers/management/components/management/groups/actions-buttons-files.js @@ -18,15 +18,19 @@ import { connect } from 'react-redux'; import { updateLoadingStatus, updateIsProcessing, - updateFileContent + updateFileContent, } from '../../../../../redux/actions/groupsActions'; import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { ExportConfiguration } from '../../../../agent/components/export-configuration'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { ReportingService } from '../../../../../react-services/reporting'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroupsActionButtonsFiles extends Component { _isMounted = false; @@ -38,7 +42,7 @@ class WzGroupsActionButtonsFiles extends Component { this.state = { generatingCsv: false, isPopoverOpen: false, - newGroupName: '' + newGroupName: '', }; this.exportCsv = exportCsv; @@ -67,7 +71,18 @@ class WzGroupsActionButtonsFiles extends Component { this.props.updateIsProcessing(true); this.onRefreshLoading(); } catch (error) { - return Promise.reject(error); + const options = { + context: `${WzGroupsActionButtonsFiles.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -83,14 +98,11 @@ class WzGroupsActionButtonsFiles extends Component { }, 100); } - autoFormat = xml => { + autoFormat = (xml) => { var reg = /(>)\s*(<)(\/*)/g; var wsexp = / *(.*) +\n/g; var contexp = /(<.+>)(.+\n)/g; - xml = xml - .replace(reg, '$1\n$2$3') - .replace(wsexp, '$1\n') - .replace(contexp, '$1\n$2'); + xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2'); var formatted = ''; var lines = xml.split('\n'); var indent = 0; @@ -111,7 +123,7 @@ class WzGroupsActionButtonsFiles extends Component { 'other->single': 0, 'other->closing': -1, 'other->opening': 0, - 'other->other': 0 + 'other->other': 0, }; for (var i = 0; i < lines.length; i++) { @@ -123,13 +135,7 @@ class WzGroupsActionButtonsFiles extends Component { var single = Boolean(ln.match(/<.+\/>/)); // is this line a single tag? ex. <br /> var closing = Boolean(ln.match(/<\/.+>/)); // is this a closing tag? ex. </a> var opening = Boolean(ln.match(/<[^!].*>/)); // is this even a tag (that's not <!something>) - var type = single - ? 'single' - : closing - ? 'closing' - : opening - ? 'opening' - : 'other'; + var type = single ? 'single' : closing ? 'closing' : opening ? 'opening' : 'other'; var fromTo = lastType + '->' + type; lastType = type; var padding = ''; @@ -152,7 +158,7 @@ class WzGroupsActionButtonsFiles extends Component { `/groups/${itemDetail.name}/files/agent.conf/xml` ); - if(Object.keys(result).length == 0){ + if (Object.keys(result).length == 0) { result = ''; } @@ -162,7 +168,7 @@ class WzGroupsActionButtonsFiles extends Component { name: 'agent.conf', content: data, isEditable: true, - groupName: itemDetail.name + groupName: itemDetail.name, }; this.props.updateFileContent(file); } @@ -171,19 +177,19 @@ class WzGroupsActionButtonsFiles extends Component { this.setState({ isPopoverOpen: false, msg: false, - newGroupName: '' + newGroupName: '', }); } clearGroupName() { this.setState({ - newGroupName: '' + newGroupName: '', }); } - onChangeNewGroupName = e => { + onChangeNewGroupName = (e) => { this.setState({ - newGroupName: e.target.value + newGroupName: e.target.value, }); }; @@ -197,7 +203,7 @@ class WzGroupsActionButtonsFiles extends Component { if (input.length) { const i = input[0]; if (!i.onkeypress) { - i.onkeypress = async e => { + i.onkeypress = async (e) => { if (e.which === 13) { await this.createGroup(); } @@ -206,19 +212,27 @@ class WzGroupsActionButtonsFiles extends Component { clearInterval(interval); } }, 150); - } catch (error) {} + } catch (error) { + const options = { + context: `${WzGroupsActionButtonsFiles.name}.bindEnterToInput`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } async createGroup() { try { this.props.updateLoadingStatus(true); await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast( - 'success', - 'Success', - 'The group has been created successfully', - 2000 - ); + this.showToast('success', 'Success', 'The group has been created successfully', 2000); this.clearGroupName(); this.props.updateIsProcessing(true); @@ -226,12 +240,7 @@ class WzGroupsActionButtonsFiles extends Component { this.closePopover(); } catch (error) { this.props.updateLoadingStatus(false); - this.showToast( - 'danger', - 'Error', - `An error occurred when creating the group: ${error}`, - 2000 - ); + throw new Error(error); } } @@ -250,12 +259,18 @@ class WzGroupsActionButtonsFiles extends Component { 2000 ); } catch (error) { - this.showToast( - 'danger', - 'Error', - `Error when exporting the CSV file: ${error}`, - 2000 - ); + const options = { + context: `${WzGroupsActionButtonsFiles.name}.generateCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error when exporting the CSV file: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } this.setState({ generatingCsv: false }); } @@ -265,7 +280,7 @@ class WzGroupsActionButtonsFiles extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; @@ -273,8 +288,10 @@ class WzGroupsActionButtonsFiles extends Component { // Add new group button const groupConfigurationButton = ( <WzButtonPermissions - buttonType='empty' - permissions={[{action: 'group:read', resource: `group:id:${this.props.state.itemDetail.name}`}]} + buttonType="empty" + permissions={[ + { action: 'group:read', resource: `group:id:${this.props.state.itemDetail.name}` }, + ]} iconSide="left" iconType="documentEdit" onClick={() => this.showGroupConfiguration()} @@ -286,14 +303,14 @@ class WzGroupsActionButtonsFiles extends Component { // Export PDF button const exportPDFButton = ( <ExportConfiguration - exportConfiguration={enabledComponents => + exportConfiguration={(enabledComponents) => this.reportingService.startConfigReport( this.props.state.itemDetail, 'groupConfig', enabledComponents ) } - type='group' + type="group" /> ); // Export button @@ -309,10 +326,7 @@ class WzGroupsActionButtonsFiles extends Component { // Refresh const refreshButton = ( - <EuiButtonEmpty - iconType="refresh" - onClick={async () => await this.refresh()} - > + <EuiButtonEmpty iconType="refresh" onClick={async () => await this.refresh()}> Refresh </EuiButtonEmpty> ); @@ -328,22 +342,18 @@ class WzGroupsActionButtonsFiles extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updateFileContent: content => dispatch(updateFileContent(content)) + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updateFileContent: (content) => dispatch(updateFileContent(content)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupsActionButtonsFiles); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsActionButtonsFiles); diff --git a/public/controllers/management/components/management/groups/actions-buttons-main.js b/public/controllers/management/components/management/groups/actions-buttons-main.js index e1550dbb6e..826131dcfe 100644 --- a/public/controllers/management/components/management/groups/actions-buttons-main.js +++ b/public/controllers/management/components/management/groups/actions-buttons-main.js @@ -19,20 +19,24 @@ import { EuiFieldText, EuiSpacer, EuiFlexGroup, - EuiButton + EuiButton, } from '@elastic/eui'; import { connect } from 'react-redux'; -import { WzButtonPermissions } from '../../../../../components/common/permissions/button' +import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { updateLoadingStatus, - updateIsProcessing + updateIsProcessing, } from '../../../../../redux/actions/groupsActions'; import exportCsv from '../../../../../react-services/wz-csv'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroupsActionButtons extends Component { _isMounted = false; @@ -43,7 +47,7 @@ class WzGroupsActionButtons extends Component { this.state = { generatingCsv: false, isPopoverOpen: false, - newGroupName: '' + newGroupName: '', }; this.exportCsv = exportCsv; @@ -72,7 +76,18 @@ class WzGroupsActionButtons extends Component { this.props.updateIsProcessing(true); this.onRefreshLoading(); } catch (error) { - return Promise.reject(error); + const options = { + context: `${WzGroupsActionButtons.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -100,19 +115,19 @@ class WzGroupsActionButtons extends Component { this.setState({ isPopoverOpen: false, msg: false, - newGroupName: '' + newGroupName: '', }); } clearGroupName() { this.setState({ - newGroupName: '' + newGroupName: '', }); } - onChangeNewGroupName = e => { + onChangeNewGroupName = (e) => { this.setState({ - newGroupName: e.target.value.split(" ").join("") + newGroupName: e.target.value.split(' ').join(''), }); }; @@ -126,7 +141,7 @@ class WzGroupsActionButtons extends Component { if (input.length) { const i = input[0]; if (!i.onkeypress) { - i.onkeypress = async e => { + i.onkeypress = async (e) => { if (e.which === 13) { await this.createGroup(); } @@ -135,7 +150,20 @@ class WzGroupsActionButtons extends Component { clearInterval(interval); } }, 150); - } catch (error) {} + } catch (error) { + const options = { + context: `${WzGroupsActionButtons.name}.bindEnterToInput`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.message || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } async createGroup() { @@ -143,12 +171,7 @@ class WzGroupsActionButtons extends Component { if (this.isOkNameGroup(this.state.newGroupName)) { this.props.updateLoadingStatus(true); await this.groupsHandler.saveGroup(this.state.newGroupName); - this.showToast( - 'success', - 'Success', - 'The group has been created successfully', - 2000 - ); + this.showToast('success', 'Success', 'The group has been created successfully', 2000); this.clearGroupName(); this.props.updateIsProcessing(true); @@ -157,12 +180,7 @@ class WzGroupsActionButtons extends Component { } } catch (error) { this.props.updateLoadingStatus(false); - this.showToast( - 'danger', - 'Error', - `An error occurred when creating the group: ${error}`, - 2000 - ); + throw new Error(error); } } @@ -181,12 +199,18 @@ class WzGroupsActionButtons extends Component { 2000 ); } catch (error) { - this.showToast( - 'danger', - 'Error', - `Error when exporting the CSV file: ${error}`, - 2000 - ); + const options = { + context: `${WzGroupsActionButtons.name}.generateCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error when exporting the CSV file: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } this.setState({ generatingCsv: false }); } @@ -196,22 +220,22 @@ class WzGroupsActionButtons extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; isOkNameGroup = (name) => { - return (name !== '' && name.trim().length > 0); - } + return name !== '' && name.trim().length > 0; + }; render() { // Add new group button const newGroupButton = ( <WzButtonPermissions - buttonType='empty' + buttonType="empty" iconSide="left" iconType="plusInCircle" - permissions={[{action: 'group:create', resource: '*:*:*'}]} + permissions={[{ action: 'group:create', resource: '*:*:*' }]} onClick={() => this.togglePopover()} > Add new group @@ -231,10 +255,7 @@ class WzGroupsActionButtons extends Component { // Refresh const refreshButton = ( - <EuiButtonEmpty - iconType="refresh" - onClick={async () => await this.refresh()} - > + <EuiButtonEmpty iconType="refresh" onClick={async () => await this.refresh()}> Refresh </EuiButtonEmpty> ); @@ -248,31 +269,31 @@ class WzGroupsActionButtons extends Component { isOpen={this.state.isPopoverOpen} closePopover={() => this.closePopover()} > - <EuiFlexGroup direction={'column'}> - <EuiFlexItem> - <EuiFormRow label="Introduce the group name" id=""> - <EuiFieldText - className="groupNameInput" - value={this.state.newGroupName} - onChange={this.onChangeNewGroupName} - aria-label="" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <WzButtonPermissions - permissions={[{action: 'group:create', resource: '*:*:*'}]} - iconType="save" - isDisabled={!this.isOkNameGroup(this.state.newGroupName)} - fill - onClick={async () => { - await this.createGroup(); - }} - > - Save new group - </WzButtonPermissions> - </EuiFlexItem> - </EuiFlexGroup> + <EuiFlexGroup direction={'column'}> + <EuiFlexItem> + <EuiFormRow label="Introduce the group name" id=""> + <EuiFieldText + className="groupNameInput" + value={this.state.newGroupName} + onChange={this.onChangeNewGroupName} + aria-label="" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <WzButtonPermissions + permissions={[{ action: 'group:create', resource: '*:*:*' }]} + iconType="save" + isDisabled={!this.isOkNameGroup(this.state.newGroupName)} + fill + onClick={async () => { + await this.createGroup(); + }} + > + Save new group + </WzButtonPermissions> + </EuiFlexItem> + </EuiFlexGroup> </EuiPopover> </EuiFlexItem> <EuiFlexItem grow={false}>{exportButton}</EuiFlexItem> @@ -282,21 +303,17 @@ class WzGroupsActionButtons extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)) + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupsActionButtons); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsActionButtons); diff --git a/public/controllers/management/components/management/groups/group-agents-table.js b/public/controllers/management/components/management/groups/group-agents-table.js index 4a1a24f3b2..6d138ea806 100644 --- a/public/controllers/management/components/management/groups/group-agents-table.js +++ b/public/controllers/management/components/management/groups/group-agents-table.js @@ -10,13 +10,11 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import { - EuiCallOut, -} from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { connect } from 'react-redux'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { updateLoadingStatus, @@ -27,32 +25,117 @@ import { updateListItemsForRemove, updateSortDirectionAgents, updateSortFieldAgents, - updateReload + updateReload, } from '../../../../../redux/actions/groupsActions'; import { getAgentFilterValues } from './get-agents-filters-values'; import { TableWzAPI } from '../../../../../components/common/tables'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { WzButtonPermissionsModalConfirm } from '../../../../../components/common/buttons'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroupAgentsTable extends Component { _isMounted = false; constructor(props) { super(props); this.suggestions = [ - { type: 'q', label: 'status', description: 'Filter by agent connection status', operators: ['=', '!=',], values: ['active', 'disconnected', 'never_connected', 'pending'] }, - { type: 'q', label: 'os.platform', description: 'Filter by OS platform', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('os.platform', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'ip', description: 'Filter by agent IP', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('ip', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'name', description: 'Filter by agent name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('name', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'id', description: 'Filter by agent id', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('id', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'node_name', description: 'Filter by node name', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('node_name', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'manager', description: 'Filter by manager', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('manager', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'version', description: 'Filter by agent version', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('version', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'configSum', description: 'Filter by agent config sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('configSum', value, {q: `group=${this.props.state.itemDetail.name}`})}, - { type: 'q', label: 'mergedSum', description: 'Filter by agent merged sum', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('mergedSum', value, {q: `group=${this.props.state.itemDetail.name}`})}, + { + type: 'q', + label: 'status', + description: 'Filter by agent connection status', + operators: ['=', '!='], + values: ['active', 'disconnected', 'never_connected', 'pending'], + }, + { + type: 'q', + label: 'os.platform', + description: 'Filter by OS platform', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('os.platform', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, + { + type: 'q', + label: 'ip', + description: 'Filter by agent IP', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('ip', value, { q: `group=${this.props.state.itemDetail.name}` }), + }, + { + type: 'q', + label: 'name', + description: 'Filter by agent name', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('name', value, { q: `group=${this.props.state.itemDetail.name}` }), + }, + { + type: 'q', + label: 'id', + description: 'Filter by agent id', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('id', value, { q: `group=${this.props.state.itemDetail.name}` }), + }, + { + type: 'q', + label: 'node_name', + description: 'Filter by node name', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('node_name', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, + { + type: 'q', + label: 'manager', + description: 'Filter by manager', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('manager', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, + { + type: 'q', + label: 'version', + description: 'Filter by agent version', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('version', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, + { + type: 'q', + label: 'configSum', + description: 'Filter by agent config sum', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('configSum', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, + { + type: 'q', + label: 'mergedSum', + description: 'Filter by agent merged sum', + operators: ['=', '!='], + values: async (value) => + getAgentFilterValues('mergedSum', value, { + q: `group=${this.props.state.itemDetail.name}`, + }), + }, //{ type: 'q', label: 'dateAdd', description: 'Filter by add date', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('dateAdd', value, {q: `group=${this.props.state.itemDetail.name}`})}, //{ type: 'q', label: 'lastKeepAlive', description: 'Filter by last keep alive', operators: ['=', '!=',], values: async (value) => getAgentFilterValues('lastKeepAlive', value, {q: `group=${this.props.state.itemDetail.name}`})}, - ] + ]; this.groupsHandler = GroupsHandler; this.columns = [ @@ -60,54 +143,62 @@ class WzGroupAgentsTable extends Component { field: 'id', name: 'Id', align: 'left', - sortable: true + sortable: true, }, { field: 'name', name: 'Name', align: 'left', - sortable: true + sortable: true, }, { field: 'ip', name: 'Ip', align: 'left', - sortable: true + sortable: true, }, { field: 'status', name: 'Status', align: 'left', - sortable: true + sortable: true, }, { field: 'os.name', name: 'Os name', align: 'left', - sortable: true + sortable: true, }, { field: 'os.version', name: 'Os version', align: 'left', - sortable: true + sortable: true, }, { field: 'version', name: 'Version', align: 'left', - sortable: true + sortable: true, }, { name: 'Actions', align: 'left', - render: item => { + render: (item) => { return ( <div> <WzButtonPermissions - buttonType='icon' - permissions={[[{action: 'agent:read', resource: `agent:id:${item.id}`}, ...(item.group || []).map(group => ({ action: 'agent:read', resource: `agent:group:${group}` }))]]} - tooltip={{position: 'top', content: 'Go to the agent'}} + buttonType="icon" + permissions={[ + [ + { action: 'agent:read', resource: `agent:id:${item.id}` }, + ...(item.group || []).map((group) => ({ + action: 'agent:read', + resource: `agent:group:${group}`, + })), + ], + ]} + tooltip={{ position: 'top', content: 'Go to the agent' }} aria-label="Go to the agent" iconType="eye" onClick={async () => { @@ -116,9 +207,17 @@ class WzGroupAgentsTable extends Component { color="primary" /> <WzButtonPermissionsModalConfirm - buttonType='icon' - permissions={[[{action: 'agent:modify_group', resource: `agent:id:${item.id}`}, ...(item.group || []).map(group => ({ action: 'agent:modify_group', resource: `agent:group:${group}` }))]]} - tooltip={{position: 'top', content: 'Remove agent from this group'}} + buttonType="icon" + permissions={[ + [ + { action: 'agent:modify_group', resource: `agent:id:${item.id}` }, + ...(item.group || []).map((group) => ({ + action: 'agent:modify_group', + resource: `agent:group:${group}`, + })), + ], + ]} + tooltip={{ position: 'top', content: 'Remove agent from this group' }} aria-label="Remove agent from this group" iconType="trash" onConfirm={async () => { @@ -128,13 +227,13 @@ class WzGroupAgentsTable extends Component { isDisabled={item.name === 'default'} modalTitle={`Remove ${item.file || item.name} agent from this group?`} modalProps={{ - buttonColor: 'danger' + buttonColor: 'danger', }} /> </div> ); - } - } + }, + }, ]; } @@ -146,14 +245,14 @@ class WzGroupAgentsTable extends Component { const { error } = this.props.state; if (!error) { return ( - <TableWzAPI - tableColumns={this.columns} - tableInitialSortingField='id' - searchBarSuggestions={this.suggestions} - endpoint={`/groups/${this.props.state.itemDetail.name}/agents`} - reload={this.props.state.reload} - searchTable={true} - /> + <TableWzAPI + tableColumns={this.columns} + tableInitialSortingField="id" + searchBarSuggestions={this.suggestions} + endpoint={`/groups/${this.props.state.itemDetail.name}/agents`} + reload={this.props.state.reload} + searchTable={true} + /> ); } else { return <EuiCallOut color="warning" title={error} iconType="gear" />; @@ -165,61 +264,61 @@ class WzGroupAgentsTable extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; async removeItems(items) { const { itemDetail } = this.props.state; - this.props.updateLoadingStatus(true); - const results = items.map(async (item, i) => { - await this.groupsHandler.deleteAgent(item.id, itemDetail.name); - }); + try { + items.map(async (item) => { + await this.groupsHandler.deleteAgent(item.id, itemDetail.name); + }); - Promise.all(results).then( - completed => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.props.updateReload(); - this.showToast('success', 'Success', 'Deleted successfully', 3000); - }, - error => { - this.props.updateIsProcessing(true); - this.props.updateLoadingStatus(false); - this.props.updateReload(); - this.showToast('danger', 'Error', error, 3000); - } - ); + this.props.updateIsProcessing(true); + this.props.updateLoadingStatus(false); + this.props.updateReload(); + this.showToast('success', 'Success', 'Deleted successfully', 3000); + } catch (error) { + const options = { + context: `${WzGroupAgentsTable.name}.removeItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.title || error, + }, + }; + getErrorOrchestrator().handleError(options); + this.props.updateIsProcessing(true); + this.props.updateLoadingStatus(false); + this.props.updateReload(); + } } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateFileContent: content => dispatch(updateFileContent(content)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updatePageIndexAgents: pageIndexAgents => - dispatch(updatePageIndexAgents(pageIndexAgents)), - updateShowModal: showModal => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: itemList => - dispatch(updateListItemsForRemove(itemList)), - updateSortDirectionAgents: sortDirectionAgents => + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateFileContent: (content) => dispatch(updateFileContent(content)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updatePageIndexAgents: (pageIndexAgents) => dispatch(updatePageIndexAgents(pageIndexAgents)), + updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), + updateSortDirectionAgents: (sortDirectionAgents) => dispatch(updateSortDirectionAgents(sortDirectionAgents)), - updateSortFieldAgents: sortFieldAgents => - dispatch(updateSortFieldAgents(sortFieldAgents)), - updateReload: () => dispatch(updateReload()) + updateSortFieldAgents: (sortFieldAgents) => dispatch(updateSortFieldAgents(sortFieldAgents)), + updateReload: () => dispatch(updateReload()), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupAgentsTable); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupAgentsTable); diff --git a/public/controllers/management/components/management/groups/group-files-table.js b/public/controllers/management/components/management/groups/group-files-table.js index 159601a022..fd6cb41987 100644 --- a/public/controllers/management/components/management/groups/group-files-table.js +++ b/public/controllers/management/components/management/groups/group-files-table.js @@ -26,6 +26,10 @@ import { } from '../../../../../redux/actions/groupsActions'; import GroupsFilesColumns from './utils/columns-files'; import { WzSearchBar, filtersToObject } from '../../../../../components/wz-search-bar'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroupFilesTable extends Component { _isMounted = false; @@ -84,7 +88,18 @@ class WzGroupFilesTable extends Component { this.props.state.isProcessing && this.props.updateIsProcessing(false); } catch (error) { this.props.state.isProcessing && this.props.updateIsProcessing(false); - return Promise.reject(error); + const options = { + context: `${WzGroupFilesTable.name}.getItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error loading the groups: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/controllers/management/components/management/groups/groups-editor.js b/public/controllers/management/components/management/groups/groups-editor.js index b0b92e79a8..4e0ae5e514 100644 --- a/public/controllers/management/components/management/groups/groups-editor.js +++ b/public/controllers/management/components/management/groups/groups-editor.js @@ -27,12 +27,12 @@ import { EuiCodeEditor, EuiConfirmModal, EuiPanel, - EuiCodeBlock + EuiCodeBlock, } from '@elastic/eui'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { validateXML } from '../configuration/utils/xml'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; import { WzOverlayMask } from '../../../../../components/common/util'; @@ -40,7 +40,11 @@ import 'brace/theme/textmate'; import 'brace/mode/xml'; import 'brace/snippets/xml'; import 'brace/ext/language_tools'; -import "brace/ext/searchbox"; +import 'brace/ext/searchbox'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroupsEditor extends Component { _isMounted = false; @@ -110,19 +114,28 @@ class WzGroupsEditor extends Component { try { await validateConfigAfterSent(); } catch (error) { - const warning = Object.assign(error, { - savedMessage: `File ${name} saved, but there were found several error while validating the configuration.`, - }); this.setState({ isSaving: false }); - this.showToast('warning', warning.savedMessage, error, 3000); - return; + throw new Error( + (error.title = `File ${name} saved, but there were found several error while validating the configuration.`) + ); } this.setState({ isSaving: false }); const textSuccess = 'File successfully edited'; this.showToast('success', 'Success', textSuccess, 3000); } catch (error) { + const options = { + context: `${WzGroupsEditor.name}.save`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.title || error, + }, + }; + getErrorOrchestrator().handleError(options); this.setState({ error, isSaving: false }); - this.showToast('danger', 'Error', 'Error saving group configuration: ' + error, 3000); } } @@ -217,7 +230,7 @@ class WzGroupsEditor extends Component { {xmlError && ( <Fragment> <span style={{ color: 'red' }}> {xmlError}</span> - <EuiSpacer size='s' /> + <EuiSpacer size="s" /> </Fragment> )} <EuiFlexGroup> @@ -228,7 +241,7 @@ class WzGroupsEditor extends Component { <EuiCodeEditor theme="textmate" width="100%" - height={`calc(100vh - ${(xmlError ? 250 : 230)}px)`} + height={`calc(100vh - ${xmlError ? 250 : 230}px)`} value={content} onChange={(newContent) => this.setState({ content: newContent })} mode="xml" @@ -260,19 +273,16 @@ class WzGroupsEditor extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - cleanFileContent: () => dispatch(cleanFileContent()) + cleanFileContent: () => dispatch(cleanFileContent()), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroupsEditor); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroupsEditor); diff --git a/public/controllers/management/components/management/groups/groups-main.js b/public/controllers/management/components/management/groups/groups-main.js index 0464938323..a5fdd3e1e9 100644 --- a/public/controllers/management/components/management/groups/groups-main.js +++ b/public/controllers/management/components/management/groups/groups-main.js @@ -25,6 +25,10 @@ import { import { connect } from 'react-redux'; import { updateGlobalBreadcrumb } from '../../../../../redux/actions/globalBreadcrumbActions'; import { WzRequest } from '../../../../../react-services/wz-request'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzGroups extends Component { constructor(props) { @@ -51,7 +55,20 @@ class WzGroups extends Component { const responseGroup = await WzRequest.apiReq('GET', '/groups', {params: {groups_list: group}}); const dataGroup = responseGroup?.data?.data?.affected_items?.[0]; this.props.updateGroupDetail(dataGroup); - }catch(error){}; + }catch(error){ + const options = { + context: `${WzGroups.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error accessing the group`, + }, + }; + getErrorOrchestrator().handleError(options); + }; }; } diff --git a/public/controllers/management/components/management/groups/groups-main.test.tsx b/public/controllers/management/components/management/groups/groups-main.test.tsx new file mode 100644 index 0000000000..2a3d1d5f95 --- /dev/null +++ b/public/controllers/management/components/management/groups/groups-main.test.tsx @@ -0,0 +1,76 @@ +/* + * Wazuh app - React test for Group-Main component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import WzGroups from './groups-main'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; + +jest.mock('../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +const mockProps = { + section: 'groups', + groupsProps: { + items: [ + { + name: 'default', + count: 1, + mergedSum: '2c45c95db2954d2c7d0ea533f09e81a5', + configSum: 'ab73af41699f13fdd81903b5f23d8d00', + }, + ], + closeAddingAgents: false, + exportConfigurationProps: { + type: 'group', + }, + selectedGroup: false, + }, + configurationProps: { + agent: { + id: '000', + }, + }, + logtestProps: { + showClose: true, + onFlyout: true, + }, + state: { + section: '', + }, + clusterStatus: { + status: true, + contextConfigServer: 'cluster', + }, +}; +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Group main component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = shallow( + <Provider store={store}> + <WzGroups {...mockProps} /> + </Provider> + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/groups/groups-table.js b/public/controllers/management/components/management/groups/groups-table.js index d1c3101712..438dee0a78 100644 --- a/public/controllers/management/components/management/groups/groups-table.js +++ b/public/controllers/management/components/management/groups/groups-table.js @@ -15,16 +15,20 @@ import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, - EuiSpacer + EuiSpacer, } from '@elastic/eui'; import { connect } from 'react-redux'; import { compose } from 'redux'; import GroupsHandler from './utils/groups-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { WzSearchBar, filtersToObject } from '../../../../../components/wz-search-bar'; import { withUserPermissions } from '../../../../../components/common/hocs/withUserPermissions'; import { WzUserPermissions } from '../../../../../react-services/wz-user-permissions'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + import { updateLoadingStatus, @@ -35,7 +39,7 @@ import { updateListItemsForRemove, updateSortDirection, updateSortField, - updateGroupDetail + updateGroupDetail, } from '../../../../../redux/actions/groupsActions'; import GroupsColums from './utils/columns-main'; @@ -65,16 +69,11 @@ class WzGroupsTable extends Component { shouldComponentUpdate(nextProps, nextState) { const { items, filters } = this.state; const { isProcessing, showModal, isLoading } = this.props.state; - if (showModal !== nextProps.state.showModal) - return true; - if (isProcessing !== nextProps.state.isProcessing) - return true; - if (JSON.stringify(items) !== JSON.stringify(nextState.items)) - return true; - if (JSON.stringify(filters) !== JSON.stringify(nextState.filters)) - return true; - if (isLoading !== nextProps.state.isLoading) - return true; + if (showModal !== nextProps.state.showModal) return true; + if (isProcessing !== nextProps.state.isProcessing) return true; + if (JSON.stringify(items) !== JSON.stringify(nextState.items)) return true; + if (JSON.stringify(filters) !== JSON.stringify(nextState.filters)) return true; + if (isLoading !== nextProps.state.isLoading) return true; return false; } @@ -96,29 +95,35 @@ class WzGroupsTable extends Component { * Loads the initial information */ async getItems() { - this.setState({items:[], totalItems: 0}, async () => { + this.setState({ items: [], totalItems: 0 }, async () => { try { this.props.updateLoadingStatus(true); const rawItems = await this.groupsHandler.listGroups({ params: this.buildFilter() }); const { affected_items, total_affected_items } = ((rawItems || {}).data || {}).data; - + this.setState({ - items : affected_items, - totalItems : total_affected_items + items: affected_items, + totalItems: total_affected_items, }); this.props.updateLoadingStatus(false); this.props.state.isProcessing && this.props.updateIsProcessing(false); } catch (error) { this.props.updateLoadingStatus(false); this.props.state.isProcessing && this.props.updateIsProcessing(false); - getToasts().add({ - color: 'danger', - title: 'Error getting groups', - text: error.message || String(error) - }); - return Promise.reject(error); + const options = { + context: `${WzGroupsTable.name}.getItems`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error getting groups`, + }, + }; + getErrorOrchestrator().handleError(options); } - }) + }); } buildFilter() { @@ -128,7 +133,7 @@ class WzGroupsTable extends Component { ...filtersToObject(filters), offset: pageIndex * pageSize, limit: pageSize, - sort: this.buildSortFilter() + sort: this.buildSortFilter(), }; return filter; @@ -157,13 +162,7 @@ class WzGroupsTable extends Component { const { filters } = this.state; this.groupsColumns = new GroupsColums(this.props); - const { - isLoading, - pageIndex, - error, - sortField, - sortDirection - } = this.props.state; + const { isLoading, pageIndex, error, sortField, sortDirection } = this.props.state; const { items, pageSize, totalItems } = this.state; const columns = this.groupsColumns.columns; const message = isLoading ? null : 'No results...'; @@ -171,20 +170,25 @@ class WzGroupsTable extends Component { pageIndex: pageIndex, pageSize: pageSize, totalItemCount: totalItems, - pageSizeOptions: [10, 25, 50, 100] + pageSizeOptions: [10, 25, 50, 100], }; const sorting = { sort: { field: sortField, - direction: sortDirection - } + direction: sortDirection, + }, }; - const getRowProps = item => { + const getRowProps = (item) => { const { id } = item; return { 'data-test-subj': `row-${id}`, className: 'customRowClass', - onClick: !WzUserPermissions.checkMissingUserPermissions([{action: 'group:read', resource: `group:id:${item.name}`}],this.props.userPermissions) ? () => this.props.updateGroupDetail(item) : undefined + onClick: !WzUserPermissions.checkMissingUserPermissions( + [{ action: 'group:read', resource: `group:id:${item.name}` }], + this.props.userPermissions + ) + ? () => this.props.updateGroupDetail(item) + : undefined, }; }; @@ -198,9 +202,9 @@ class WzGroupsTable extends Component { filters={filters} suggestions={this.suggestions} onFiltersChange={(filters) => this._isMounted && this.setState({ filters })} - placeholder='Search group' + placeholder="Search group" /> - <EuiSpacer size='s' /> + <EuiSpacer size="s" /> <EuiBasicTable itemId="id" items={items} @@ -216,9 +220,7 @@ class WzGroupsTable extends Component { {this.props.state.showModal ? ( <EuiOverlayMask> <EuiConfirmModal - title={`Delete ${ - itemList[0].file ? itemList[0].file : itemList[0].name - } group?`} + title={`Delete ${itemList[0].file ? itemList[0].file : itemList[0].name} group?`} onCancel={() => this.props.updateShowModal(false)} onConfirm={() => { this.removeItems(itemList); @@ -240,7 +242,7 @@ class WzGroupsTable extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; @@ -251,12 +253,12 @@ class WzGroupsTable extends Component { }); Promise.all(results).then( - completed => { + (completed) => { this.props.updateIsProcessing(true); this.props.updateLoadingStatus(false); this.showToast('success', 'Success', 'Deleted successfully', 3000); }, - error => { + (error) => { this.props.updateIsProcessing(true); this.props.updateLoadingStatus(false); this.showToast('danger', 'Error', error, 3000); @@ -265,30 +267,27 @@ class WzGroupsTable extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateFileContent: content => dispatch(updateFileContent(content)), - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updatePageIndex: pageIndex => dispatch(updatePageIndex(pageIndex)), - updateShowModal: showModal => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: itemList => - dispatch(updateListItemsForRemove(itemList)), - updateSortDirection: sortDirection => - dispatch(updateSortDirection(sortDirection)), - updateSortField: sortField => dispatch(updateSortField(sortField)), - updateGroupDetail: itemDetail => dispatch(updateGroupDetail(itemDetail)) + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateFileContent: (content) => dispatch(updateFileContent(content)), + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updatePageIndex: (pageIndex) => dispatch(updatePageIndex(pageIndex)), + updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), + updateSortDirection: (sortDirection) => dispatch(updateSortDirection(sortDirection)), + updateSortField: (sortField) => dispatch(updateSortField(sortField)), + updateGroupDetail: (itemDetail) => dispatch(updateGroupDetail(itemDetail)), }; }; export default compose( - connect(mapStateToProps,mapDispatchToProps), + connect(mapStateToProps, mapDispatchToProps), withUserPermissions )(WzGroupsTable); diff --git a/public/controllers/management/components/management/groups/utils/groups-handler.js b/public/controllers/management/components/management/groups/utils/groups-handler.js index a98f315ed4..a5bf369ea0 100644 --- a/public/controllers/management/components/management/groups/utils/groups-handler.js +++ b/public/controllers/management/components/management/groups/utils/groups-handler.js @@ -10,9 +10,10 @@ * Find more information about this on the LICENSE file. */ -import { - WzRequest -} from '../../../../../../react-services/wz-request'; +import { WzRequest } from '../../../../../../react-services/wz-request'; +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 default class GroupsHandler { /** @@ -22,11 +23,11 @@ export default class GroupsHandler { static async saveGroup(name) { try { const result = await WzRequest.apiReq('POST', `/groups`, { - group_id: name + group_id: name, }); return result; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } @@ -38,12 +39,23 @@ export default class GroupsHandler { try { const result = await WzRequest.apiReq('DELETE', `/groups`, { params: { - groups_list: name - } + groups_list: name, + }, }); return result; } catch (error) { - return Promise.reject(error); + const options = { + context: `${GroupsHandler.name}.deleteGroup`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error deleting the group: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -54,14 +66,10 @@ export default class GroupsHandler { */ static async deleteAgent(agentId, groupId) { try { - const result = await WzRequest.apiReq( - 'DELETE', - `/agents/${agentId}/group/${groupId}`, - {} - ); + const result = await WzRequest.apiReq('DELETE', `/agents/${agentId}/group/${groupId}`, {}); return result; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } @@ -74,7 +82,18 @@ export default class GroupsHandler { const result = await WzRequest.apiReq('GET', `/groups/${name}/agents`, filters); return result; } catch (error) { - return Promise.reject(error); + const options = { + context: `${GroupsHandler.name}.agentsGroup`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error obtaining the agents of the group: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -87,7 +106,7 @@ export default class GroupsHandler { const result = await WzRequest.apiReq('GET', `/groups/${name}/files`, filters); return result; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } @@ -100,7 +119,7 @@ export default class GroupsHandler { const result = await WzRequest.apiReq('GET', '/groups', filters); return result; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } @@ -111,9 +130,20 @@ export default class GroupsHandler { static async getFileContent(path) { try { const result = await WzRequest.apiReq('GET', path, {}); - return ((result || {}).data) || false; + return (result || {}).data || false; } catch (error) { - return Promise.reject(error); + const options = { + context: `${GroupsHandler.name}.getFileContent`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error obtaining the content of groups files: ${error.message || error}`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -127,11 +157,11 @@ export default class GroupsHandler { try { const result = await WzRequest.apiReq('PUT', `/groups/${groupId}/configuration`, { body: content, - origin: 'xmleditor' + origin: 'xmleditor', }); return result; } catch (error) { - return Promise.reject(error); + throw new Error(error); } } } diff --git a/public/controllers/management/components/management/groups/utils/valid-configuration.js b/public/controllers/management/components/management/groups/utils/valid-configuration.js index 3c17c4244c..6e86f592e6 100644 --- a/public/controllers/management/components/management/groups/utils/valid-configuration.js +++ b/public/controllers/management/components/management/groups/utils/valid-configuration.js @@ -43,7 +43,7 @@ const validateConfigAfterSent = async (node = false) => { } return true; } catch (error) { - return Promise.reject(error); + throw new Error(error); } }; From 3ee151523ece75470d672c97bad1c412b0fa93ef Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 8 Jul 2021 10:33:26 +0200 Subject: [PATCH 048/493] Code styling --- public/components/security/roles/edit-role.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 0230560dd9..9ab52ab2fe 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -137,7 +137,7 @@ export const EditRole = ({ role, closeFlyout }) => { const onClose = () => { (initialSelectedPolicies.length != selectedPolicies.length) ? setIsModalVisible(true) : closeFlyout(false) }; - return ( + return ( <> <WzOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> From 8ccf961384060e2ec99b043044db5c9e9a9a4cba Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 8 Jul 2021 10:52:51 +0200 Subject: [PATCH 049/493] Fixed double EuiOverlayMask import --- .../overview/components/overview-actions/overview-actions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index 6d627615b8..b5c4a60127 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -24,7 +24,6 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiToolTip, } from '@elastic/eui'; import './agents-selector.scss'; From eff2824e79c7fd999cb0bac534bc55db972afc92 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 8 Jul 2021 13:32:38 +0200 Subject: [PATCH 050/493] fix styles in api selector --- public/components/wz-menu/wz-menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 1edbe3002c..34ddba7eb3 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -632,7 +632,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { } getApiSelectorComponent() { - let style = { minWidth: 'max-content' }; + let style = { minWidth: 100, textOverflow: 'ellipsis' }; if (this.showSelectorsInPopover){ style = { width: '100%', minWidth: 200 }; } From 158a259d5118106c7ba6e49cfc245fd7d8a0a003 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> Date: Thu, 8 Jul 2021 10:54:02 -0300 Subject: [PATCH 051/493] Create Cypress Workflow for 4.3-7.10 (#3444) --- .../workflows/cypress-automation-tests.yml | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .github/workflows/cypress-automation-tests.yml diff --git a/.github/workflows/cypress-automation-tests.yml b/.github/workflows/cypress-automation-tests.yml new file mode 100644 index 0000000000..b8cf36d7c6 --- /dev/null +++ b/.github/workflows/cypress-automation-tests.yml @@ -0,0 +1,130 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Cypress Automation Tests + +on: + push: + branches: + - 'disable' + pull_request: + branches: + - '*.*-*.*' + +jobs: + setup-wazuh-kibana-app: + name: Run setup environment wazuh kibana app + #runs-on: self-hosted + runs-on: ubuntu-18.04 + #container: cypress/browsers:node12.18.0-chrome83-ff77 + steps: + - name: Step 01 - Set up environment variables + env: + SHOULD_TEST: ${{ contains(github.event.pull_request.body, 'SHOULD_TEST=TRUE') }} + run: | + if ${SHOULD_TEST} == true; then + + echo Base Branch: ${{ github.base_ref }} + echo Head Branch: ${{ github.head_ref }} + echo Run Test TRUE + echo "RUN_TEST=true" >> $GITHUB_ENV + + echo "WAZUH_HEAD_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + echo "${{ github.event.pull_request.body }}" > pull_request.body.txt + + echo "WAZUH_MANAGER_IMAGE=$(grep 'WAZUH_MANAGER_IMAGE=*' ./pull_request.body.txt | cut -d '=' -f2)" >> $GITHUB_ENV + echo "WAZUH_AGENT_IMAGE=$(grep 'WAZUH_AGENT_IMAGE=*' ./pull_request.body.txt | cut -d '=' -f2)" >> $GITHUB_ENV + echo "WAZUH_VERSION=$(grep 'WAZUH_VERSION=*' ./pull_request.body.txt | cut -d '=' -f2)" >> $GITHUB_ENV + echo "ELASTIC_VERSION=$(grep 'ELASTIC_VERSION=*' ./pull_request.body.txt | cut -d '=' -f2)" >> $GITHUB_ENV + + echo "PATH_TEMPLATE_BASIC_CLUSTER_AGENT=$GITHUB_WORKSPACE/wazuh-app-environments/templates_elastic_prod/es_basic-wz_cluster-agent/" >> $GITHUB_ENV + + else + + echo Run Test FALSE + echo "RUN_TEST=false" >> $GITHUB_ENV + + fi + - name: Step 02 - Download Project wazuh-app-environments + if: ${{ env.RUN_TEST == 'true' }} + uses: actions/checkout@v2 + with: + repository: frankeros/wazuh-app-environments + ref: 'master' + path: wazuh-app-environments + token: ${{ secrets.ENV_TEMPLATES_TOKEN }} + - name: Step 03 - Configuring templates docker environment + if: ${{ env.RUN_TEST == 'true'}} + run: | + cd $GITHUB_WORKSPACE/wazuh-app-environments/ + mkdir packages + cd ${{ env.PATH_TEMPLATE_BASIC_CLUSTER_AGENT }} + sed -i -e "s/WAZUH_MANAGER_IMAGE=.*/WAZUH_MANAGER_IMAGE=${{ env.WAZUH_MANAGER_IMAGE }}/g" ./.env + sed -i -e "s/WAZUH_AGENT_IMAGE=.*/WAZUH_AGENT_IMAGE=${{ env.WAZUH_AGENT_IMAGE }}/g" ./.env + sed -i -e "s/WAZUH_VERSION=.*/WAZUH_VERSION=${{ env.WAZUH_VERSION }}/g" ./.env + sed -i -e "s/ELASTIC_VERSION=.*/ELASTIC_VERSION=${{ env.ELASTIC_VERSION }}/g" ./.env + cat .env + - name: Step 04 - Starting containers + if: ${{ env.RUN_TEST == 'true'}} + run: | + cd ${{ env.PATH_TEMPLATE_BASIC_CLUSTER_AGENT }} + sudo docker-compose up -d + - name: Step 05 - Download Project wazuh-packages + if: ${{ env.RUN_TEST == 'true' }} + uses: actions/checkout@v2 + with: + repository: wazuh/wazuh-packages + ref: '4.2' + path: wazuh-packages + - name: Step 06 - Building package + if: ${{ env.RUN_TEST == 'true' }} + run: | + cd $GITHUB_WORKSPACE/wazuh-packages/wazuhapp + echo fixing command... + sed -i -e 's/'\|' cut -d \"\/\" \-f2//g' ./generate_wazuh_app.sh + echo run command... + ./generate_wazuh_app.sh -b ${{ env.WAZUH_HEAD_BRANCH }} -s $GITHUB_WORKSPACE/wazuh-app-environments/packages -r 1 + - name: Step 07 - Installing package + if: ${{ env.RUN_TEST == 'true' }} + run: | + cd $GITHUB_WORKSPACE/wazuh-app-environments/packages/ + PACKAGE_NAME=`ls *.zip` + cd ${{ env.PATH_TEMPLATE_BASIC_CLUSTER_AGENT }} + docker exec es_basic-wz_cluster-agent_kibana_1 bin/kibana-plugin install file:///packages/$PACKAGE_NAME + sudo docker-compose restart kibana + echo CONTINUES AFTER 20 SECONDS ... + sleep 20s + - name: Step 08 - Configuring ip container into wazuh.yml + if: ${{ env.RUN_TEST == 'true' }} + run: | + IP_CONTAINER_MANAGER=$(docker exec es_basic-wz_cluster-agent_wazuh-manager-master_1 hostname -i) + docker exec es_basic-wz_cluster-agent_kibana_1 cat ./data/wazuh/config/wazuh.yml + docker exec es_basic-wz_cluster-agent_kibana_1 sed -i -e "s/url: https:\/\/localhost/url: https:\/\/$IP_CONTAINER_MANAGER/g" ./data/wazuh/config/wazuh.yml + docker exec es_basic-wz_cluster-agent_kibana_1 cat ./data/wazuh/config/wazuh.yml + - name: Step 09 - Download Project wazuh-qa + if: ${{ env.RUN_TEST == 'true' }} + uses: actions/checkout@v2 + with: + repository: wazuh/wazuh-qa + ref: 'feature/frontend' + path: wazuh-qa + - name: Step 10 - Install Cypress Project Dependencies + if: ${{ env.RUN_TEST == 'true' }} + run: | + cd $GITHUB_WORKSPACE/wazuh-qa/tests/frontend/test_kibana_app/ + npm install + npm run cy:install + - name: Step 11 - Run Cypress tests + if: ${{ env.RUN_TEST == 'true' }} + run: | + cd $GITHUB_WORKSPACE/wazuh-qa/tests/frontend/test_kibana_app/ + npm run cypress:run-headless + # continue-on-error: true + - name: Step 12 - Upload Screenshots And Videos To Slack + if: failure() + uses: trymbill/cypress-slack-video-upload-action@v1.3.0 + with: + workdir: wazuh-qa/tests/frontend/test_kibana_app/cypress + token: ${{ secrets.ACTIONS_SLACK_TOKEN }} + channels: 'daily-app-team' + message-text: ${{ format('Workflow "{0}" job test triggered by {1} is FAILED for {2} - {3}', github.workflow, github.event_name, github.base_ref, github.head_ref) }} From c1810c4688d3750aea8c78bc0e3ad5f2a9c53c6c Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 8 Jul 2021 18:09:56 +0200 Subject: [PATCH 052/493] Added static child to outside detector and replaced wzoverlaymask --- .../agents/fim/inventory/registry-table.tsx | 20 +++++++------- .../components/agents/fim/inventory/table.tsx | 22 +++++++++------- .../agents/vuls/inventory/table.tsx | 22 +++++++++------- .../common/modules/discover/discover.tsx | 12 +++++---- public/components/common/modules/events.tsx | 12 +++++---- .../fim_events_table/fim_events_table.tsx | 14 +++++----- .../components/mitre_top/mitre_top.tsx | 26 ++++++++++--------- .../subrequirements/subrequirements.tsx | 18 +++++++------ .../mitre/components/tactics/tactics.tsx | 18 +------------ .../components/techniques/techniques.tsx | 18 +++++++------ .../security/policies/create-policy.tsx | 4 +-- .../security/policies/edit-policy.tsx | 4 +-- .../components/roles-mapping-create.tsx | 4 +-- .../components/roles-mapping-edit.tsx | 4 +-- .../components/security/roles/create-role.tsx | 4 +-- .../components/security/roles/edit-role.tsx | 4 +-- .../security/users/components/create-user.tsx | 4 +-- .../security/users/components/edit-user.tsx | 4 +-- public/components/security/users/users.tsx | 4 --- .../util-components/configuration-path.js | 4 +-- .../management/groups/groups-editor.js | 4 +-- .../management/ruleset/ruleset-editor.js | 4 +-- 22 files changed, 114 insertions(+), 116 deletions(-) diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index 18df05097e..65bcb027af 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -263,15 +263,17 @@ export class RegistryTable extends Component { {registryTable} {this.state.isFlyoutVisible && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> - <FlyoutDetail - fileName={this.state.currentFile.file} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type={this.state.currentFile.type} - view='inventory' - {...this.props} /> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + fileName={this.state.currentFile.file} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type={this.state.currentFile.type} + view='inventory' + {...this.props} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> )} diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index b38858ab2f..5c4b24b9aa 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -300,16 +300,18 @@ export class InventoryTable extends Component { {filesTable} {this.state.isFlyoutVisible && <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> - <FlyoutDetail - fileName={this.state.currentFile} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type='file' - view='inventory' - showViewInEvents={true} - {...this.props} /> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + fileName={this.state.currentFile} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type='file' + view='inventory' + showViewInEvents={true} + {...this.props} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> } diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index efc4551faa..420100846d 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -165,16 +165,18 @@ export class InventoryTable extends Component { {table} {this.state.isFlyoutVisible && <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> - <FlyoutDetail - vulName={this.state.currentItem.cve} - agentId={this.props.agent.id} - item={this.state.currentItem} - closeFlyout={() => this.closeFlyout()} - type='vulnerability' - view='inventory' - showViewInEvents={true} - {...this.props} /> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + vulName={this.state.currentItem.cve} + agentId={this.props.agent.id} + item={this.state.currentItem} + closeFlyout={() => this.closeFlyout()} + type='vulnerability' + view='inventory' + showViewInEvents={true} + {...this.props} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> } diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index d1ba83a331..9bb74970f2 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -601,11 +601,13 @@ export const Discover = compose( const noResultsText = `No results match for this search criteria`; let flyout = this.state.showMitreFlyout ? <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} /> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + onChangeFlyout={this.onMitreChangeFlyout} + currentTechnique={this.state.selectedTechnique} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> : <></>; return ( diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index d3bc4dea7a..c6ffc7d5ed 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -265,11 +265,13 @@ export class Events extends Component { <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> - <FlyoutComponent - closeFlyout={this.closeFlyout} - {...this.state.flyout.props} - {...this.props} - /> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutComponent + closeFlyout={this.closeFlyout} + {...this.state.flyout.props} + {...this.props} + /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> )} diff --git a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx index 2eb1773287..a227690e33 100644 --- a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx +++ b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx @@ -90,12 +90,14 @@ function FimTable({ agent }) { {isOpen && ( <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={() => setIsOpen(false)}> - <FlyoutDetail - agentId={agent.id} - closeFlyout={() => setIsOpen(false)} - fileName={file} - view='extern' - {...{ agent }} /> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + agentId={agent.id} + closeFlyout={() => setIsOpen(false)} + fileName={file} + view='extern' + {...{ agent }} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> )} diff --git a/public/components/common/welcome/components/mitre_top/mitre_top.tsx b/public/components/common/welcome/components/mitre_top/mitre_top.tsx index 8ec0c8ccc0..5e891e1ff8 100644 --- a/public/components/common/welcome/components/mitre_top/mitre_top.tsx +++ b/public/components/common/welcome/components/mitre_top/mitre_top.tsx @@ -242,21 +242,23 @@ export class MitreTopTactics extends Component { const emptyPrompt = this.renderEmptyPrompt(); return ( <Fragment> - {loading} + {loading} {!selectedTactic || alertsCount.length === 0 ? tacticsTop : tecniquesTop} {alertsCount.length === 0 && emptyPrompt} {flyoutOn && - <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - implicitFilters={[{ "agent.id": this.props.agentId }]} - agentId={this.props.agentId} - onChangeFlyout={this.onChangeFlyout} - currentTechnique={selectedTechnique} /> - </EuiOutsideClickDetector> - </EuiOverlayMask>} + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + implicitFilters={[{ "agent.id": this.props.agentId }]} + agentId={this.props.agentId} + onChangeFlyout={this.onChangeFlyout} + currentTechnique={selectedTechnique} /> + </div> + </EuiOutsideClickDetector> + </EuiOverlayMask>} </Fragment> ) } diff --git a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index e6a499b2c8..ef694e8ef1 100644 --- a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -281,14 +281,16 @@ export class ComplianceSubrequirements extends Component { {this.state.flyoutOn && <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <RequirementFlyout - currentRequirement={this.state.selectedRequirement} - onChangeFlyout={this.onChangeFlyout} - description={this.props.descriptions[this.state.selectedRequirement]} - getRequirementKey={() => { return this.getRequirementKey() }} - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - /> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <RequirementFlyout + currentRequirement={this.state.selectedRequirement} + onChangeFlyout={this.onChangeFlyout} + description={this.props.descriptions[this.state.selectedRequirement]} + getRequirementKey={() => { return this.getRequirementKey() }} + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask>} </div> diff --git a/public/components/overview/mitre/components/tactics/tactics.tsx b/public/components/overview/mitre/components/tactics/tactics.tsx index 4a9df58c9e..0faf40f8d9 100644 --- a/public/components/overview/mitre/components/tactics/tactics.tsx +++ b/public/components/overview/mitre/components/tactics/tactics.tsx @@ -62,23 +62,7 @@ export class Tactics extends Component { initTactics(){ const tacticsIds = Object.keys(this.props.tacticsObject); - const selectedTactics = {} - /*let isMax = {}; - tacticsIds.forEach( (item,id) => { - if(buckets.length){ - const max_doc = buckets[0].doc_count; - if(!Object.keys(isMax).length){ - buckets.forEach( bucket => { - if(bucket.doc_count === max_doc){ - isMax[bucket.key] = true; - } - }) - } - selectedTactics[item] = isMax[item] ? true : false; //if results are found, only the first tactic is selected - }else{ - selectedTactics[item] = true; - } - });*/ + const selectedTactics = {}; tacticsIds.forEach( (item,id) => { selectedTactics[item] = true; }); diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 64322fab12..224fa8a3e4 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -461,14 +461,16 @@ export const Techniques = withWindowSize(class Techniques extends Component { { isFlyoutVisible && <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={() => this.onChangeFlyout(false)}> - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} - onChangeFlyout={this.onChangeFlyout} - currentTechniqueData={this.state.currentTechniqueData} - currentTechnique={currentTechnique} - tacticsObject={this.props.tacticsObject} /> + <div>{/* EuiOutsideClickDetector needs a static first child */} + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} + onChangeFlyout={this.onChangeFlyout} + currentTechniqueData={this.state.currentTechniqueData} + currentTechnique={currentTechnique} + tacticsObject={this.props.tacticsObject} /> + </div> </EuiOutsideClickDetector> </EuiOverlayMask> } diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 473f4162e6..077b76a3ca 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -299,7 +299,7 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -432,7 +432,7 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index 87e005b11d..be06401b4e 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -303,7 +303,7 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -434,7 +434,7 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index c159a03733..19e2a923f8 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -118,7 +118,7 @@ export const RolesMappingCreate = ({ return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -178,7 +178,7 @@ export const RolesMappingCreate = ({ </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index c7da26fac7..cdf835b572 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -140,7 +140,7 @@ export const RolesMappingEdit = ({ return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -203,7 +203,7 @@ export const RolesMappingEdit = ({ </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index 50eaff833e..3d3d6251e6 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -135,7 +135,7 @@ export const CreateRole = ({ closeFlyout }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -181,7 +181,7 @@ export const CreateRole = ({ closeFlyout }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 26d7afded1..3529e4cc81 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -152,7 +152,7 @@ export const EditRole = ({ role, closeFlyout }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose} > <EuiFlyoutHeader hasBorder={false}> @@ -210,7 +210,7 @@ export const EditRole = ({ role, closeFlyout }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index f784cd7a00..a64c78895b 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -243,7 +243,7 @@ export const CreateUser = ({ closeFlyout }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -341,7 +341,7 @@ export const CreateUser = ({ closeFlyout }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index c783a412e9..777fb4cb03 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -236,7 +236,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { return ( <> - <WzOverlayMask headerZindexLocation="below"> + <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={onClose}> <EuiFlyout className="wzApp" onClose={onClose}> <EuiFlyoutHeader hasBorder={false}> @@ -340,7 +340,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { </EuiFlyoutBody> </EuiFlyout> </EuiOutsideClickDetector> - </WzOverlayMask> + </EuiOverlayMask> {modal} </> ); diff --git a/public/components/security/users/users.tsx b/public/components/security/users/users.tsx index f50f46a8ba..f837cc6d0c 100644 --- a/public/components/security/users/users.tsx +++ b/public/components/security/users/users.tsx @@ -91,21 +91,17 @@ export const Users = () => { } if (isEditFlyoutVisible) { editFlyout = ( - <EuiOverlayMask headerZindexLocation="below"> <EditUser currentUser={editingUser} closeFlyout={closeEditFlyout} rolesObject={rolesObject} /> - </EuiOverlayMask> ); } if (isCreateFlyoutVisible) { createFlyout = ( - <EuiOverlayMask headerZindexLocation="below"> <CreateUser closeFlyout={closeCreateFlyout} /> - </EuiOverlayMask> ); } diff --git a/public/controllers/management/components/management/configuration/util-components/configuration-path.js b/public/controllers/management/components/management/configuration/util-components/configuration-path.js index 853da89e98..d56fb6dc6d 100644 --- a/public/controllers/management/components/management/configuration/util-components/configuration-path.js +++ b/public/controllers/management/components/management/configuration/util-components/configuration-path.js @@ -54,7 +54,7 @@ class WzConfigurationPath extends Component { let modal; if (this.state.isModalVisible) { modal = ( - <WzOverlayMask> + <EuiOverlayMask> <EuiConfirmModal title="Unsubmitted changes" onConfirm={() => { @@ -69,7 +69,7 @@ class WzConfigurationPath extends Component { There are unsaved changes. Are you sure you want to proceed? </p> </EuiConfirmModal> - </WzOverlayMask> + </EuiOverlayMask> ); } return ( diff --git a/public/controllers/management/components/management/groups/groups-editor.js b/public/controllers/management/components/management/groups/groups-editor.js index b0b92e79a8..8028c95956 100644 --- a/public/controllers/management/components/management/groups/groups-editor.js +++ b/public/controllers/management/components/management/groups/groups-editor.js @@ -161,7 +161,7 @@ class WzGroupsEditor extends Component { let modal; if (this.state.isModalVisible) { modal = ( - <WzOverlayMask> + <EuiOverlayMask> <EuiConfirmModal title="Unsubmitted changes" onConfirm={() => { @@ -176,7 +176,7 @@ class WzGroupsEditor extends Component { There are unsaved changes. Are you sure you want to proceed? </p> </EuiConfirmModal> - </WzOverlayMask> + </EuiOverlayMask> ); } return ( diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index 0bd49f51e8..be4007109b 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -266,7 +266,7 @@ class WzRulesetEditor extends Component { let modal; if (this.state.isModalVisible) { modal = ( - <WzOverlayMask> + <EuiOverlayMask> <EuiConfirmModal title="Unsubmitted changes" onConfirm={() => { @@ -281,7 +281,7 @@ class WzRulesetEditor extends Component { There are unsaved changes. Are you sure you want to proceed? </p> </EuiConfirmModal> - </WzOverlayMask> + </EuiOverlayMask> ); } return ( From b55258565aa667aab84df2b1e5c003a880dcd50f Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Thu, 8 Jul 2021 18:10:42 +0200 Subject: [PATCH 053/493] Fixed dispatch for updateCurrentAgentData (#3453) * fix(syscollector): Fixed dispatch for updateCurrentAgentData * fix(syscollector): Refactor for agents-sections. --- .../welcome/components/agent-sections.ts | 129 ++++++++++ .../common/welcome/components/menu-agent.js | 227 ++++++++---------- 2 files changed, 224 insertions(+), 132 deletions(-) create mode 100644 public/components/common/welcome/components/agent-sections.ts diff --git a/public/components/common/welcome/components/agent-sections.ts b/public/components/common/welcome/components/agent-sections.ts new file mode 100644 index 0000000000..3d90232fdb --- /dev/null +++ b/public/components/common/welcome/components/agent-sections.ts @@ -0,0 +1,129 @@ +/* + * Wazuh app - Build all sections for MenuAgent. + * Copyright (C) 2015-2021 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 { WAZUH_MODULES_ID } from '../../../../../common/constants'; + +export const getAgentSections = (menuAgent) => { + return { + securityInformation: { + id: 'securityInformation', + text: 'Security information management', + isTitle: true, + }, + auditing: { + id: 'auditing', + text: 'Auditing and Policy Monitoring', + isTitle: true, + }, + threatDetection: { + id: 'threatDetection', + text: 'Threat detection and response', + isTitle: true, + }, + regulatoryCompliance: { + id: 'regulatoryCompliance', + text: 'Regulatory Compliance', + isTitle: true, + }, + general: { + id: WAZUH_MODULES_ID.SECURITY_EVENTS, + text: 'Security events', + isPin: menuAgent.general ? menuAgent.general : false, + }, + fim: { + id: WAZUH_MODULES_ID.INTEGRITY_MONITORING, + text: 'Integrity monitoring', + isPin: menuAgent.fim ? menuAgent.fim : false, + }, + aws: { + id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, + text: 'Amazon AWS', + isPin: menuAgent.aws ? menuAgent.aws : false, + }, + gcp: { + id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, + text: 'Google Cloud Platform', + isPin: menuAgent.gcp ? menuAgent.gcp : false, + }, + pm: { + id: WAZUH_MODULES_ID.POLICY_MONITORING, + text: 'Policy Monitoring', + isPin: menuAgent.pm ? menuAgent.pm : false, + }, + sca: { + id: WAZUH_MODULES_ID.SECURITY_CONFIGURATION_ASSESSMENT, + text: 'Security configuration assessment', + isPin: menuAgent.sca ? menuAgent.sca : false, + }, + audit: { + id: WAZUH_MODULES_ID.AUDITING, + text: 'System Auditing', + isPin: menuAgent.audit ? menuAgent.audit : false, + }, + oscap: { + id: WAZUH_MODULES_ID.OPEN_SCAP, + text: 'OpenSCAP', + isPin: menuAgent.oscap ? menuAgent.oscap : false, + }, + ciscat: { + id: WAZUH_MODULES_ID.CIS_CAT, + text: 'CIS-CAT', + isPin: menuAgent.oscap ? menuAgent.oscap : false, + }, + vuls: { + id: WAZUH_MODULES_ID.VULNERABILITIES, + text: 'Vulnerabilities', + isPin: menuAgent.vuls ? menuAgent.vuls : false, + }, + virustotal: { + id: WAZUH_MODULES_ID.VIRUSTOTAL, + text: 'VirusTotal', + isPin: menuAgent.virustotal ? menuAgent.virustotal : false, + }, + osquery: { + id: WAZUH_MODULES_ID.OSQUERY, + text: 'Osquery', + isPin: menuAgent.osquery ? menuAgent.osquery : false, + }, + docker: { + id: WAZUH_MODULES_ID.DOCKER, + text: 'Docker Listener', + isPin: menuAgent.docker ? menuAgent.docker : false, + }, + mitre: { + id: WAZUH_MODULES_ID.MITRE_ATTACK, + text: 'MITRE ATT&CK', + isPin: menuAgent.mitre ? menuAgent.mitre : false, + }, + pci: { + id: WAZUH_MODULES_ID.PCI_DSS, + text: 'PCI DSS', + isPin: menuAgent.pci ? menuAgent.pci : false, + }, + gdpr: { + id: WAZUH_MODULES_ID.GDPR, + text: 'GDPR', + isPin: menuAgent.gdpr ? menuAgent.gdpr : false, + }, + hipaa: { + id: WAZUH_MODULES_ID.HIPAA, + text: 'HIPAA', + isPin: menuAgent.hipaa ? menuAgent.hipaa : false, + }, + nist: { + id: WAZUH_MODULES_ID.NIST_800_53, + text: 'NIST 800-53', + isPin: menuAgent.nist ? menuAgent.nist : false, + }, + tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC', isPin: menuAgent.tsc ? menuAgent.tsc : false }, + }; +}; diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index 76868ed98f..ed06fe96c6 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -10,12 +10,13 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFlexGrid, EuiSideNav, EuiIcon } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSideNav } from '@elastic/eui'; import { connect } from 'react-redux'; import { AppState } from '../../../../react-services/app-state'; import { hasAgentSupportModule } from '../../../../react-services/wz-agents'; import { getAngularModule, getToasts } from '../../../../kibana-services'; -import { WAZUH_MODULES_ID } from '../../../../../common/constants'; +import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; +import { getAgentSections } from './agent-sections'; class WzMenuAgent extends Component { constructor(props) { @@ -26,80 +27,41 @@ class WzMenuAgent extends Component { hoverAddFilter: '', }; - this.menuAgent = window.localStorage.getItem('menuAgent') ? JSON.parse(window.localStorage.getItem('menuAgent')) : {}; + this.menuAgent = window.localStorage.getItem('menuAgent') + ? JSON.parse(window.localStorage.getItem('menuAgent')) + : {}; - this.agentSections = { - securityInformation: { - id: 'securityInformation', - text: 'Security information management', - isTitle: true - }, - auditing: { - id: 'auditing', - text: 'Auditing and Policy Monitoring', - isTitle: true - }, - threatDetection: { - id: 'threatDetection', - text: 'Threat detection and response', - isTitle: true - }, - regulatoryCompliance: { - id: 'regulatoryCompliance', - text: 'Regulatory Compliance', - isTitle: true - }, - general: { id: WAZUH_MODULES_ID.SECURITY_EVENTS, text: 'Security events', isPin: this.menuAgent.general ? this.menuAgent.general : false }, - fim: { id: WAZUH_MODULES_ID.INTEGRITY_MONITORING, text: 'Integrity monitoring', isPin: this.menuAgent.fim ? this.menuAgent.fim : false }, - aws: { id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS', isPin: this.menuAgent.aws ? this.menuAgent.aws : false }, - gcp: { id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, text: 'Google Cloud Platform', isPin: this.menuAgent.gcp ? this.menuAgent.gcp : false }, - pm: { id: WAZUH_MODULES_ID.POLICY_MONITORING, text: 'Policy Monitoring', isPin: this.menuAgent.pm ? this.menuAgent.pm : false }, - sca: { id: WAZUH_MODULES_ID.SECURITY_CONFIGURATION_ASSESSMENT, text: 'Security configuration assessment', isPin: this.menuAgent.sca ? this.menuAgent.sca : false }, - audit: { id: WAZUH_MODULES_ID.AUDITING, text: 'System Auditing', isPin: this.menuAgent.audit ? this.menuAgent.audit : false }, - oscap: { id: WAZUH_MODULES_ID.OPEN_SCAP, text: 'OpenSCAP', isPin: this.menuAgent.oscap ? this.menuAgent.oscap : false }, - ciscat: { id: WAZUH_MODULES_ID.CIS_CAT, text: 'CIS-CAT', isPin: this.menuAgent.oscap ? this.menuAgent.oscap : false }, - vuls: { id: WAZUH_MODULES_ID.VULNERABILITIES, text: 'Vulnerabilities', isPin: this.menuAgent.vuls ? this.menuAgent.vuls : false }, - virustotal: { id: WAZUH_MODULES_ID.VIRUSTOTAL, text: 'VirusTotal', isPin: this.menuAgent.virustotal ? this.menuAgent.virustotal : false }, - osquery: { id: WAZUH_MODULES_ID.OSQUERY, text: 'Osquery', isPin: this.menuAgent.osquery ? this.menuAgent.osquery : false }, - docker: { id: WAZUH_MODULES_ID.DOCKER, text: 'Docker Listener', isPin: this.menuAgent.docker ? this.menuAgent.docker : false }, - mitre: { id: WAZUH_MODULES_ID.MITRE_ATTACK, text: 'MITRE ATT&CK', isPin: this.menuAgent.mitre ? this.menuAgent.mitre : false }, - pci: { id: WAZUH_MODULES_ID.PCI_DSS, text: 'PCI DSS', isPin: this.menuAgent.pci ? this.menuAgent.pci : false }, - gdpr: { id: WAZUH_MODULES_ID.GDPR, text: 'GDPR', isPin: this.menuAgent.gdpr ? this.menuAgent.gdpr : false }, - hipaa: { id: WAZUH_MODULES_ID.HIPAA, text: 'HIPAA', isPin: this.menuAgent.hipaa ? this.menuAgent.hipaa : false }, - nist: { id: WAZUH_MODULES_ID.NIST_800_53, text: 'NIST 800-53', isPin: this.menuAgent.nist ? this.menuAgent.nist : false }, - tsc: { id: WAZUH_MODULES_ID.TSC, text: 'TSC', isPin: this.menuAgent.tsc ? this.menuAgent.tsc : false } - }; + this.agentSections = getAgentSections(this.menuAgent) this.securityInformationItems = [ this.agentSections.general, this.agentSections.fim, this.agentSections.aws, - this.agentSections.gcp + this.agentSections.gcp, ]; this.auditingItems = [ this.agentSections.pm, this.agentSections.audit, this.agentSections.oscap, this.agentSections.ciscat, - this.agentSections.sca + this.agentSections.sca, ]; this.threatDetectionItems = [ this.agentSections.vuls, this.agentSections.docker, this.agentSections.virustotal, this.agentSections.osquery, - this.agentSections.mitre + this.agentSections.mitre, ]; this.regulatoryComplianceItems = [ this.agentSections.pci, this.agentSections.gdpr, this.agentSections.hipaa, this.agentSections.nist, - this.agentSections.tsc + this.agentSections.tsc, ]; } - async componentDidMount() { const extensions = await AppState.getExtensions(this.currentApi); this.setState({ extensions }); @@ -107,7 +69,7 @@ class WzMenuAgent extends Component { this.router = $injector.get('$route'); } - clickMenuItem = section => { + clickMenuItem = (section) => { this.props.closePopover(); if (this.props.currentTab !== section) { // do not redirect if we already are in that tab @@ -117,16 +79,21 @@ class WzMenuAgent extends Component { } }; - addToast({color, title, text, time = 3000}){ - getToasts().add({title, text, toastLifeTimeMs: time, color}) + addToast({ color, title, text, time = 3000 }) { + getToasts().add({ title, text, toastLifeTimeMs: time, color }); } - createItems = items => { - const keyExists = key => Object.keys(this.state.extensions).includes(key); - const keyIsTrue = key => (this.state.extensions || [])[key]; - return items.filter(item => - hasAgentSupportModule(this.props.currentAgentData, item.id) && Object.keys(this.state.extensions).length && (!keyExists(item.id) || keyIsTrue(item.id)) - ).map(item => this.createItem(item)); + createItems = (items) => { + const keyExists = (key) => Object.keys(this.state.extensions).includes(key); + const keyIsTrue = (key) => (this.state.extensions || [])[key]; + return items + .filter( + (item) => + hasAgentSupportModule(this.props.currentAgentData, item.id) && + Object.keys(this.state.extensions).length && + (!keyExists(item.id) || keyIsTrue(item.id)) + ) + .map((item) => this.createItem(item)); }; createItem = (item, data = {}) => { @@ -134,51 +101,56 @@ class WzMenuAgent extends Component { return { ...data, id: item.id, - name: - <EuiFlexGroup - onMouseEnter={() => { - this.setState({ hoverAddFilter: item.id }); - }} - onMouseLeave={() => { - this.setState({ hoverAddFilter: '' }); - }}> - <EuiFlexItem - onClick={() => !item.isTitle ? this.clickMenuItem(item.id) : null} - style={{ cursor: !item.isTitle ? 'pointer': 'normal' }}> - {item.text} - </EuiFlexItem> - { - this.state.hoverAddFilter === item.id && !item.isTitle && - (Object.keys(this.menuAgent).length < 6 || item.isPin) && - (Object.keys(this.menuAgent).length > 1 || !item.isPin) && - <EuiFlexItem grow={false}> - <EuiIcon - onClick={() => { - this.menuAgent = window.localStorage.getItem('menuAgent') ? JSON.parse(window.localStorage.getItem('menuAgent')) : {}; - if(!item.isPin && Object.keys(this.menuAgent).length < 6) { - this.menuAgent[item.id] = item; - item.isPin = true; - } else if(this.menuAgent[item.id]){ - delete this.menuAgent[item.id]; - item.isPin = false; - } else { - this.addToast({ - title: 'The limit of pinned modules has been reached', - color: 'danger' - }); - } - window.localStorage.setItem('menuAgent', JSON.stringify(this.menuAgent)); - this.props.updateMenuAgents(); - }} - color='primary' - type={this.menuAgent[item.id] ? 'pinFilled' : 'pin'} - aria-label="Next" - style={{ cursor: 'pointer' }} - /> - </EuiFlexItem> - } - </EuiFlexGroup> , - isSelected: this.props.currentTab === item.id + name: ( + <EuiFlexGroup + onMouseEnter={() => { + this.setState({ hoverAddFilter: item.id }); + }} + onMouseLeave={() => { + this.setState({ hoverAddFilter: '' }); + }} + > + <EuiFlexItem + onClick={() => (!item.isTitle ? this.clickMenuItem(item.id) : null)} + style={{ cursor: !item.isTitle ? 'pointer' : 'normal' }} + > + {item.text} + </EuiFlexItem> + {this.state.hoverAddFilter === item.id && + !item.isTitle && + (Object.keys(this.menuAgent).length < 6 || item.isPin) && + (Object.keys(this.menuAgent).length > 1 || !item.isPin) && ( + <EuiFlexItem grow={false}> + <EuiIcon + onClick={() => { + this.menuAgent = window.localStorage.getItem('menuAgent') + ? JSON.parse(window.localStorage.getItem('menuAgent')) + : {}; + if (!item.isPin && Object.keys(this.menuAgent).length < 6) { + this.menuAgent[item.id] = item; + item.isPin = true; + } else if (this.menuAgent[item.id]) { + delete this.menuAgent[item.id]; + item.isPin = false; + } else { + this.addToast({ + title: 'The limit of pinned modules has been reached', + color: 'danger', + }); + } + window.localStorage.setItem('menuAgent', JSON.stringify(this.menuAgent)); + this.props.updateMenuAgents(); + }} + color="primary" + type={this.menuAgent[item.id] ? 'pinFilled' : 'pin'} + aria-label="Next" + style={{ cursor: 'pointer' }} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + ), + isSelected: this.props.currentTab === item.id, }; }; @@ -187,78 +159,69 @@ class WzMenuAgent extends Component { this.createItem(this.agentSections.securityInformation, { disabled: true, icon: <EuiIcon type="managementApp" color="primary" />, - items: this.createItems(this.securityInformationItems) - }) + items: this.createItems(this.securityInformationItems), + }), ]; const auditing = [ this.createItem(this.agentSections.auditing, { disabled: true, icon: <EuiIcon type="managementApp" color="primary" />, - items: this.createItems(this.auditingItems) - }) + items: this.createItems(this.auditingItems), + }), ]; const threatDetection = [ this.createItem(this.agentSections.threatDetection, { disabled: true, icon: <EuiIcon type="reportingApp" color="primary" />, - items: this.createItems(this.threatDetectionItems) - }) + items: this.createItems(this.threatDetectionItems), + }), ]; const regulatoryCompliance = [ this.createItem(this.agentSections.regulatoryCompliance, { disabled: true, icon: <EuiIcon type="reportingApp" color="primary" />, - items: this.createItems(this.regulatoryComplianceItems) - }) + items: this.createItems(this.regulatoryComplianceItems), + }), ]; return ( <div className="WzManagementSideMenu"> - {Object.keys(this.state.extensions).length && ( + {(Object.keys(this.state.extensions).length && ( <div> <EuiFlexGrid columns={2}> <EuiFlexItem> - <EuiSideNav - items={securityInformation} - style={{ padding: '4px 12px' }} - /> + <EuiSideNav items={securityInformation} style={{ padding: '4px 12px' }} /> </EuiFlexItem> <EuiFlexItem> <EuiSideNav items={auditing} style={{ padding: '4px 12px' }} /> </EuiFlexItem> <EuiFlexItem> - <EuiSideNav - items={threatDetection} - style={{ padding: '4px 12px' }} - /> + <EuiSideNav items={threatDetection} style={{ padding: '4px 12px' }} /> </EuiFlexItem> <EuiFlexItem> - <EuiSideNav - items={regulatoryCompliance} - style={{ padding: '4px 12px' }} - /> + <EuiSideNav items={regulatoryCompliance} style={{ padding: '4px 12px' }} /> </EuiFlexItem> </EuiFlexGrid> </div> - ) || (<div style={{ width: 300 }}></div> - )} + )) || <div style={{ width: 300 }}></div>} </div> ); } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { state: state.rulesetReducers, currentAgentData: state.appStateReducers.currentAgentData, - currentTab: state.appStateReducers.currentTab + currentTab: state.appStateReducers.currentTab, }; }; -export default connect( - mapStateToProps, - null -)(WzMenuAgent); +const mapDispatchToProps = (dispatch) => ({ + updateCurrentAgentData: (agentData) => dispatch(updateCurrentAgentData(agentData)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(WzMenuAgent); From 4a2ac3b6e708ad8410991efd8948b9e3d88b8ec3 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 8 Jul 2021 18:11:04 +0200 Subject: [PATCH 054/493] Fixed policies-table row click edit and create action --- public/components/security/policies/policies-table.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/components/security/policies/policies-table.tsx b/public/components/security/policies/policies-table.tsx index 6ef76e11b1..3b9f3df383 100644 --- a/public/components/security/policies/policies-table.tsx +++ b/public/components/security/policies/policies-table.tsx @@ -8,14 +8,13 @@ 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 PoliciesTable = ({ policies, loading, editPolicy, createPolicy, updatePolicies }) => { +export const PoliciesTable = ({ policies, loading, editPolicy, updatePolicies }) => { const getRowProps = (item) => { const { id } = item; return { 'data-test-subj': `row-${id}`, onClick: () => { editPolicy(item); - createPolicy(item); }, }; }; From 1619125208649a8eb17bc0be1ddf21162b88027f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 8 Jul 2021 18:38:14 +0200 Subject: [PATCH 055/493] Removed wzOverlayMask imports --- public/components/security/policies/create-policy.tsx | 1 - public/components/security/policies/edit-policy.tsx | 1 - .../roles-mapping/components/roles-mapping-create.tsx | 1 - .../security/roles-mapping/components/roles-mapping-edit.tsx | 1 - public/components/security/roles/create-role.tsx | 1 - public/components/security/roles/edit-role.tsx | 1 - public/components/security/users/components/create-user.tsx | 1 - public/components/security/users/components/edit-user.tsx | 1 - .../configuration/util-components/configuration-path.js | 4 ++-- .../management/components/management/groups/groups-editor.js | 4 ++-- .../components/management/ruleset/ruleset-editor.js | 3 +-- 11 files changed, 5 insertions(+), 14 deletions(-) diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 077b76a3ca..81faed2422 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -20,7 +20,6 @@ import { } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; -import { WzOverlayMask } from '../../common/util'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index be06401b4e..7d75ec4120 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -22,7 +22,6 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzAPIUtils } from '../../../react-services/wz-api-utils'; -import { WzOverlayMask } from '../../common/util'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 19e2a923f8..f8cf095ccb 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -19,7 +19,6 @@ import { ErrorHandler } from '../../../../react-services/error-handler'; import { RuleEditor } from './rule-editor'; import RulesServices from '../../rules/services'; import RolesServices from '../../roles/services'; -import { WzOverlayMask } from '../../../common/util'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index cdf835b572..5ea23e985f 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -21,7 +21,6 @@ import { RuleEditor } from './rule-editor'; import RulesServices from '../../rules/services'; import RolesServices from '../../roles/services'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; -import { WzOverlayMask } from '../../../common/util' import _ from 'lodash'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index 3d3d6251e6..d71525d868 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -17,7 +17,6 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; -import { WzOverlayMask } from '../../common/util' diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 3529e4cc81..b9d59f66da 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -21,7 +21,6 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { EditRolesTable } from './edit-role-table'; -import { WzOverlayMask } from '../../common/util'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index a64c78895b..3dced60449 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -27,7 +27,6 @@ import RolesServices from '../../roles/services'; import { WzButtonPermissions } from '../../../common/permissions/button'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; -import { WzOverlayMask } from '../../../common/util'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 777fb4cb03..67edebfd98 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -28,7 +28,6 @@ import { WzButtonPermissions } from '../../../common/permissions/button'; import { ErrorHandler } from '../../../../react-services/error-handler'; import { WzAPIUtils } from '../../../../react-services/wz-api-utils'; import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; -import { WzOverlayMask } from '../../../common/util' import _ from 'lodash'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; diff --git a/public/controllers/management/components/management/configuration/util-components/configuration-path.js b/public/controllers/management/components/management/configuration/util-components/configuration-path.js index d56fb6dc6d..b6672b51de 100644 --- a/public/controllers/management/components/management/configuration/util-components/configuration-path.js +++ b/public/controllers/management/components/management/configuration/util-components/configuration-path.js @@ -23,12 +23,12 @@ import { EuiTitle, EuiText, EuiConfirmModal, - EuiIcon + EuiIcon, + EuiOverlayMask } from '@elastic/eui'; import WzBadge from '../util-components/badge'; import WzClusterSelect from './configuration-cluster-selector'; -import { WzOverlayMask } from '../../../../../../components/common/util'; class WzConfigurationPath extends Component { constructor(props) { diff --git a/public/controllers/management/components/management/groups/groups-editor.js b/public/controllers/management/components/management/groups/groups-editor.js index 8028c95956..c41de417ae 100644 --- a/public/controllers/management/components/management/groups/groups-editor.js +++ b/public/controllers/management/components/management/groups/groups-editor.js @@ -27,7 +27,8 @@ import { EuiCodeEditor, EuiConfirmModal, EuiPanel, - EuiCodeBlock + EuiCodeBlock, + EuiOverlayMask } from '@elastic/eui'; import GroupsHandler from './utils/groups-handler'; @@ -35,7 +36,6 @@ import GroupsHandler from './utils/groups-handler'; import { getToasts } from '../../../../../kibana-services'; import { validateXML } from '../configuration/utils/xml'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; -import { WzOverlayMask } from '../../../../../components/common/util'; import 'brace/theme/textmate'; import 'brace/mode/xml'; import 'brace/snippets/xml'; diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index be4007109b..17e388683f 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -27,7 +27,7 @@ import { EuiTitle, EuiToolTip, EuiButtonIcon, - EuiButtonEmpty, + EuiOverlayMask, EuiFieldText, EuiConfirmModal, EuiCodeEditor, @@ -48,7 +48,6 @@ import 'brace/snippets/xml'; import 'brace/ext/language_tools'; import "brace/ext/searchbox"; import { showFlyoutLogtest } from '../../../../../redux/actions/appStateActions'; -import { WzOverlayMask } from '../../../../../components/common/util'; import _ from 'lodash'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; From 782712302cdbf039b1cff5405332dc38a5cad542 Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Thu, 8 Jul 2021 14:45:02 -0300 Subject: [PATCH 056/493] Fixed error when edit a rule or decoder --- .../components/management/ruleset/ruleset-editor.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index 0bd49f51e8..8b2fc4b0b6 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -147,17 +147,6 @@ class WzRulesetEditor extends Component { } this.setState({ isSaving: false }); this.goToEdit(name); - - let errorMessage = `The content of the file ${name} is incorrect. There were found several errors while validating the configuration: ${error.message || error}`; - if (this.props.state.addingRulesetFile != false) { - //remove current invalid file if the file is new. - await this.rulesetHandler.deleteFile(name); - errorMessage += '\nThe new file was deleted.'; - } else { - //restore file to previous version - await this.rulesetHandler.updateFile(name, this.state.initContent, overwrite); - errorMessage += '\nThe content file was restored to previous state.'; - } this.setState({ showWarningRestart: true, initialInputValue: this.state.inputValue, From fc69a111b37e9d0fae708e2252a46db21aae827e Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Thu, 8 Jul 2021 15:12:32 -0300 Subject: [PATCH 057/493] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 706d005bc9..18f55d44fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 40c56e39e2d762cebd61e4fa94a47638f57949da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 9 Jul 2021 14:13:11 +0200 Subject: [PATCH 058/493] fix(frontend): Error when opening a rule file from rule info related rules --- .../components/management/ruleset/rule-info.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/controllers/management/components/management/ruleset/rule-info.js b/public/controllers/management/components/management/ruleset/rule-info.js index 3003656924..53238d364a 100644 --- a/public/controllers/management/components/management/ruleset/rule-info.js +++ b/public/controllers/management/components/management/ruleset/rule-info.js @@ -59,14 +59,14 @@ class WzRuleInfo extends Component { }; this.rulesetHandler = new RulesetHandler(RulesetResources.RULES); - const handleFileClick = async (event) => { + const handleFileClick = async (event, {filename, relative_dirname}) => { event.stopPropagation(); try { - const result = await this.rulesetHandler.getFileContent(value); + const result = await this.rulesetHandler.getFileContent(filename); const file = { - name: value, + name: filename, content: result, - path: item.relative_dirname, + path: relative_dirname, }; this.props.updateFileContent(file); } catch (error) { @@ -143,7 +143,7 @@ class WzRuleInfo extends Component { render: (value, item) => { return ( <EuiToolTip position="top" content={`Show ${value} content`}> - <EuiLink onClick={async (event) => handleFileClick(event)}>{value}</EuiLink> + <EuiLink onClick={async (event) => handleFileClick(event, item)}>{value}</EuiLink> </EuiToolTip> ); }, From 5b99303d8cb7a5e1bc1294a536ccc80ea6326494 Mon Sep 17 00:00:00 2001 From: sortiz <sortiz@owlh.net> Date: Fri, 25 Jun 2021 11:53:42 +0200 Subject: [PATCH 059/493] Base branch for Office365 --- README.md | 1 + common/constants.ts | 2 ++ common/wazuh-modules.ts | 5 +++++ public/components/common/welcome/components/menu-agent.js | 2 ++ public/components/common/welcome/overview-welcome.js | 2 ++ public/components/settings/modules/modules.js | 1 + public/components/wz-menu/wz-menu-overview.js | 6 ++++++ 7 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 3aedf2fd51..da284633ba 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This plugin for Kibana allows you to visualize and analyze Wazuh alerts stored i - Security events: Browse through your security alerts, identifying issues and threats in your environment. - Integrity monitoring: Alerts related to file changes, including permissions, content, ownership and attributes. - Amazon AWS: Security events related to your Amazon AWS services, collected directly via AWS API. + - Office 365: Security events related to your Office 365 services. - Google Cloud Platform: Security events related to your Google Cloud Platform services, collected directly via GCP API. - Auditing and Policy Monitoring - Policy monitoring: Verify that your systems are configured according to your security policies baseline. diff --git a/common/constants.ts b/common/constants.ts index 23bb111a8c..0d0eb37c49 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -199,6 +199,7 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'extensions.oscap': false, 'extensions.ciscat': false, 'extensions.aws': false, + 'extensions.office': false, 'extensions.gcp': false, 'extensions.virustotal': false, 'extensions.osquery': false, @@ -244,6 +245,7 @@ export enum WAZUH_MODULES_ID { SECURITY_EVENTS = 'general', INTEGRITY_MONITORING = 'fim', AMAZON_WEB_SERVICES = 'aws', + OFFICE_365 = 'office', GOOGLE_CLOUD_PLATFORM = 'gcp', POLICY_MONITORING = 'pm', SECURITY_CONFIGURATION_ASSESSMENT = 'sca', diff --git a/common/wazuh-modules.ts b/common/wazuh-modules.ts index 69b372a604..61cbf2ad16 100644 --- a/common/wazuh-modules.ts +++ b/common/wazuh-modules.ts @@ -75,6 +75,11 @@ export const WAZUH_MODULES = { description: 'Security events related to your Amazon AWS services, collected directly via AWS API.' }, + office: { + title: 'Office 365', + description: + 'Security events related to your Office 365 services.' + }, gcp: { title: 'Google Cloud Platform', description: diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index 76868ed98f..a68f7f5c0a 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -51,6 +51,7 @@ class WzMenuAgent extends Component { }, general: { id: WAZUH_MODULES_ID.SECURITY_EVENTS, text: 'Security events', isPin: this.menuAgent.general ? this.menuAgent.general : false }, fim: { id: WAZUH_MODULES_ID.INTEGRITY_MONITORING, text: 'Integrity monitoring', isPin: this.menuAgent.fim ? this.menuAgent.fim : false }, + office: { id: WAZUH_MODULES_ID.OFFICE_365, text: 'Office 365', isPin: this.menuAgent.office ? this.menuAgent.office : false }, aws: { id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS', isPin: this.menuAgent.aws ? this.menuAgent.aws : false }, gcp: { id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, text: 'Google Cloud Platform', isPin: this.menuAgent.gcp ? this.menuAgent.gcp : false }, pm: { id: WAZUH_MODULES_ID.POLICY_MONITORING, text: 'Policy Monitoring', isPin: this.menuAgent.pm ? this.menuAgent.pm : false }, @@ -73,6 +74,7 @@ class WzMenuAgent extends Component { this.securityInformationItems = [ this.agentSections.general, this.agentSections.fim, + this.agentSections.office, this.agentSections.aws, this.agentSections.gcp ]; diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 0f94fd6c4c..14db2d70c0 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -96,6 +96,8 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends {this.buildTabCard('fim', 'filebeatApp')} {this.props.extensions.aws && this.buildTabCard('aws', 'logoAWSMono')} + {this.props.extensions.office && + this.buildTabCard('office', 'logoAWSMono')} {this.props.extensions.gcp && this.buildTabCard('gcp', 'logoGCPMono')} </EuiFlexGrid> diff --git a/public/components/settings/modules/modules.js b/public/components/settings/modules/modules.js index 44b9b76fe3..ef1d95d2bf 100644 --- a/public/components/settings/modules/modules.js +++ b/public/components/settings/modules/modules.js @@ -38,6 +38,7 @@ export class EnableModulesWrapper extends Component { modules: [ { name: 'general', default: true, agent: false }, { name: 'fim', default: true, agent: false }, + { name: 'office', default: false, agent: false }, { name: 'aws', default: false, agent: false }, { name: 'gcp', default: false, agent: false } ] diff --git a/public/components/wz-menu/wz-menu-overview.js b/public/components/wz-menu/wz-menu-overview.js index fd5c046ec6..1822e8c9dc 100644 --- a/public/components/wz-menu/wz-menu-overview.js +++ b/public/components/wz-menu/wz-menu-overview.js @@ -58,6 +58,11 @@ class WzMenuOverview extends Component { cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS', }, + office: { + id: WAZUH_MODULES_ID.OFFICE_365, + cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.OFFICE_365, + text: 'Office 365', + }, gcp: { id: WAZUH_MODULES_ID.GOOGLE_CLOUD_PLATFORM, cyTestId: WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID.GOOGLE_CLOUD_PLATFORM, @@ -143,6 +148,7 @@ class WzMenuOverview extends Component { this.securityInformationItems = [ this.overviewSections.general, this.overviewSections.fim, + this.overviewSections.office, this.overviewSections.aws, this.overviewSections.gcp ]; From 9022833c4a18b717e2a42ebd2c510c98594922e5 Mon Sep 17 00:00:00 2001 From: sortiz <sortiz@owlh.net> Date: Thu, 1 Jul 2021 11:06:20 +0200 Subject: [PATCH 060/493] Office365 module and navigation --- public/assets/office365.svg | 1 + public/components/common/welcome/overview-welcome.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 public/assets/office365.svg diff --git a/public/assets/office365.svg b/public/assets/office365.svg new file mode 100644 index 0000000000..9a4b19c4d3 --- /dev/null +++ b/public/assets/office365.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 66 81" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><path d="M65 73.333h0V7.027L41.811 0 .176 16.216H0v47.928l14.054-5.766V19.459l28.108-6.847V70.09L0 64.144 41.811 80h0L65 73.514v-.18z" fill="#006BB4" stroke="none"/></symbol></svg> \ No newline at end of file diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 14db2d70c0..028f2e6401 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -32,6 +32,7 @@ import store from '../../../redux/store'; import './welcome.scss'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { withErrorBoundary } from '../hocs'; +import office_logo from '../../../assets/office365.svg'; export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends Component { constructor(props) { @@ -97,7 +98,7 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends {this.props.extensions.aws && this.buildTabCard('aws', 'logoAWSMono')} {this.props.extensions.office && - this.buildTabCard('office', 'logoAWSMono')} + this.buildTabCard('office', office_logo)} {this.props.extensions.gcp && this.buildTabCard('gcp', 'logoGCPMono')} </EuiFlexGrid> From 151883185578df017d54af8de8a6e6e60fa04226 Mon Sep 17 00:00:00 2001 From: sortiz <sortiz@owlh.net> Date: Thu, 1 Jul 2021 12:44:17 +0200 Subject: [PATCH 061/493] Office365 sample data --- common/constants.ts | 1 + .../add-modules-data/guides/office.js | 181 ++++++++++++++++++ .../generate-alerts/generate-alerts-script.js | 47 +++++ .../lib/generate-alerts/sample-data/office.js | 122 ++++++++++++ 4 files changed, 351 insertions(+) create mode 100644 public/components/add-modules-data/guides/office.js create mode 100644 server/lib/generate-alerts/sample-data/office.js diff --git a/common/constants.ts b/common/constants.ts index 23bb111a8c..d3e0439891 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -64,6 +64,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { [WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY]: [ { syscheck: true }, { aws: true }, + { office: true }, { gcp: true }, { authentication: true }, { ssh: true }, diff --git a/public/components/add-modules-data/guides/office.js b/public/components/add-modules-data/guides/office.js new file mode 100644 index 0000000000..9eee5e4b86 --- /dev/null +++ b/public/components/add-modules-data/guides/office.js @@ -0,0 +1,181 @@ +/* +* Wazuh app - Amazon Web Services interactive extension guide +* Copyright (C) 2015-2021 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 default { + id: 'office', + name: 'Office 365', + wodle_name: 'office', + description: 'Configuration options of the Office 365 wodle.', + category: 'Security information management', + documentation_link: 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/wodle-s3.html', + icon: 'logoOfficeMono', + avaliable_for_manager: true, + avaliable_for_agent: true, + steps: [ + // { + // title: 'Required settings', + // description: '', + // elements: [ + // { + // name: 'disabled', + // description: `Disables the AWS-S3 wodle.`, + // type: 'switch', + // required: true + // }, + // { + // name: 'interval', + // description: 'Frequency for reading from the S3 bucket.', + // type: 'input', + // required: true, + // placeholder: 'Positive number with suffix character indicating a time unit', + // default_value: '10m', + // validate_error_message: 'A positive number that should contain a suffix character indicating a time unit, such as, s (seconds), m (minutes), h (hours), d (days). e.g. 10m', + // validate_regex: /^[1-9]\d*[s|m|h|d]$/ + // }, + // { + // name: 'run_on_start', + // description: 'Run evaluation immediately when service is started.', + // type: 'switch', + // required: true, + // default_value: true + // }, + + // ] + // }, + // { + // title: 'Optional settings', + // description: '', + // elements: [ + // { + // name: 'remove_from_bucket', + // description: 'Define if you want to remove logs from your S3 bucket after they are read by the wodle.', + // type: 'switch', + // default_value: true + // }, + // { + // name: 'skip_on_error', + // description: 'When unable to process and parse a CloudTrail log, skip the log and continue processing', + // type: 'switch', + // default_value: true + // } + // ] + // }, + // { + // title: 'Buckets', + // description: 'Defines one or more buckets to process.', + // elements: [ + // { + // name: 'bucket', + // description: 'Defines a bucket to process.', + // removable: true, + // required: true, + // repeatable: true, + // repeatable_insert_first: true, + // repeatable_insert_first_properties: { + // removable: false + // }, + // validate_error_message: 'Any directory or file name.', + // show_attributes: true, + // attributes: [ + // { + // name: 'type', + // description: 'Specifies type of bucket.', + // info: 'Different configurations as macie has custom type.', + // type: 'select', + // required: true, + // values: [ + // {value: 'cloudtrail', text: 'cloudtrail'}, + // {value: 'guardduty', text: 'guardduty'}, + // {value: 'vpcflow', text: 'vpcflow'}, + // {value: 'config', text: 'config'}, + // {value: 'custom', text: 'custom'} + // ], + // default_value: 'cloudtrail' + // } + // ], + // show_options: true, + // options: [ + // { + // name: 'name', + // description: 'Name of the S3 bucket from where logs are read.', + // type: 'input', + // required: true, + // placeholder: 'Name of the S3 bucket' + // }, + // { + // name: 'aws_account_id', + // description: 'The AWS Account ID for the bucket logs. Only works with CloudTrail buckets.', + // type: 'input', + // placeholder: 'Comma list of 12 digit AWS Account ID' + // }, + // { + // name: 'aws_account_alias', + // description: 'A user-friendly name for the AWS account.', + // type: 'input', + // placeholder: 'AWS account user-friendly name' + // }, + // { + // name: 'access_key', + // description: 'The access key ID for the IAM user with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Any alphanumerical key.' + // }, + // { + // name: 'secret_key', + // description: 'The secret key created for the IAM user with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Any alphanumerical key.' + // }, + // { + // name: 'aws_profile', + // description: 'A valid profile name from a Shared Credential File or AWS Config File with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Valid profile name' + // }, + // { + // name: 'iam_role_arn', + // description: 'A valid role arn with permission to read logs from the bucket.Valid role arn', + // type: 'input', + // placeholder: 'Valid role arn' + // }, + // { + // name: 'path', + // description: 'If defined, the path or prefix for the bucket.', + // type: 'input', + // placeholder: 'Path or prefix for the bucket.' + // }, + // { + // name: 'only_logs_after', + // description: 'A valid date, in YYYY-MMM-DD format, that only logs from after that date will be parsed. All logs from before that date will be skipped.', + // type: 'input', + // placeholder: 'Date, e.g.: 2020-APR-02', + // validate_regex: /^[1-9]\d{3}-((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OCT)|(NOV)|(DEC))-\d{2}$/, + // validate_error_message: 'A valid date, in YYYY-MMM-DD format' + // }, + // { + // name: 'regions', + // description: 'A comma-delimited list of regions to limit parsing of logs. Only works with CloudTrail buckets.', + // type: 'input', + // default_value: 'All regions', + // placeholder: 'Comma-delimited list of valid regions' + // }, + // { + // name: 'aws_organization_id', + // description: 'Name of AWS organization. Only works with CloudTrail buckets.', + // type: 'input', + // placeholder: 'Valid AWS organization name' + // } + // ] + // } + // ] + // } + ] +} \ No newline at end of file diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index 61ed911ec1..3d8f4f79d1 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -40,6 +40,7 @@ import * as Vulnerability from './sample-data/vulnerabilities'; import * as SSH from './sample-data/ssh'; import * as Apache from './sample-data/apache'; import * as Web from './sample-data/web'; +import * as Office from './sample-data/office'; //Alert const alertIDMax = 6000; @@ -59,6 +60,7 @@ const ruleMaxLevel = 14; * @param {any} params - params to configure the alert * @param {boolean} params.aws - if true, set aws fields * @param {boolean} params.audit - if true, set System Auditing fields + * @param {boolean} params.office - if true, set office fields * @param {boolean} params.ciscat - if true, set CIS-CAT fields * @param {boolean} params.gcp - if true, set GCP fields * @param {boolean} params.docker - if true, set Docker fields @@ -310,6 +312,51 @@ function generateAlert(params) { alert.GeoLocation = randomArrayItem(GeoLocation); } + if (params.office) { + const beforeDate = new Date(new Date(alert.timestamp) - 3 * 24 * 60 * 60 * 1000); + const IntraID = randomArrayItem(Office.arrayUuidOffice); + const InterID = randomArrayItem(Office.arrayUuidOffice); + const OrgID = randomArrayItem(Office.arrayUuidOffice); + const objID = randomArrayItem(Office.arrayUuidOffice); + const userID = randomArrayItem(Office.arrayUuidOffice); + const appID = randomArrayItem(Office.arrayUuidOffice); + + alert.rule = randomArrayItem(Office.arrayRulesOffice); + alert.decoder = randomArrayItem(Office.arrayDecoderOffice); + alert.GeoLocation = randomArrayItem(GeoLocation); + alert.data.integration = "Office365"; + alert.location = Office.arrayLocationOffice; + alert.data.office365 = { + CreationTime: formatDate(beforeDate, 'Y-M-DTh:m:s.lZ'), + Id: IntraID, + Operation: "UserLoggedIn", + OrganizationId: OrgID, + RecordType: "15", + ResultStatus: "Success", + UserKey: userID, + UserType: "0", + Version: "1", + Workload: "AzureActiveDirectory", + ClientIP: "77.231.182.17", + ObjectId: objID, + UserId: "testing.account@wazuh.com", + AzureActiveDirectoryEventType: "1", + ExtendedProperties: Office.arrayExtendedPropertiesOffice, + ModifiedProperties: [], + Actor: Office.arrayActorOffice, + ActorContextId: OrgID, + ActorIpAddress: "77.231.182.17", + InterSystemsId: InterID, + IntraSystemId: IntraID, + Target: Office.arrayTargetOffice, + TargetContextId: OrgID, + ApplicationId: appID, + DeviceProperties: Office.arrayDevicePropertiesOffice, + ErrorNumber: "0", + Subscription: "Audit.AzureActiveDirectory" + } + } + if (params.gcp) { alert.rule = randomArrayItem(GCP.arrayRules); alert.data.integration = 'gcp'; diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js new file mode 100644 index 0000000000..e5129664b6 --- /dev/null +++ b/server/lib/generate-alerts/sample-data/office.js @@ -0,0 +1,122 @@ +/* + * Wazuh app - AWS sample data + * Copyright (C) 2015-2021 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 arrayOfficeGroups = ["office365", "AzureActiveDirectoryStsLogon"]; + +export const arrayLocationOffice = "office365"; + +export const arrayDecoderOffice = [ + { + name: "json" + } +] + +export const arrayUuidOffice = [ + "a8080009-aa85-4d65-a0f0-74fe0331edce", + "4e93c8e3-52c1-4a4e-ab69-9e61ccf6cd00", + "d14aa5cb-b070-42f8-8709-0f8afd942fc0", + "92a7e893-0f4a-4635-af0d-83891d4ff9c0", + "ce013f05-a783-4186-9d85-5a14998b6111", + "4f686e03-7cf6-44a8-9212-b8a91b128082", + "cc58e817-c6d3-4457-b011-54e881e230ec", + "825f9d6e-12c0-4b59-807d-1b41c6e48a3a", + "d36253fb-24a1-481c-a199-f778534ccb5f", + "9083369e-679b-4e8b-9249-323a51d5bf9c", + "6d872bf8-e462-4de8-9e16-c36761050fb7", + "b9a73c0f-55f2-4e95-9626-1c264d02eac3", + "bbab91ad-bc8a-4c86-9010-3c84b39fde0d", + "b5359092-dad2-4060-b93d-3791e4da0dec", + "e8493b26-c1f9-42eb-9756-dfd363149852", + "ca2044fc-32ca-478b-8b0d-ff6fdd3b1e5a", + "a0995136-91d8-4acf-8449-28c275ffb7e3", + "c3482b5d-b1a9-4f44-8df0-a601e18cf5c3", + "49fd4642-cfe5-4170-9488-25d847e3579f", + "29f96271-5c1b-47ec-9652-a41d5cb17cb4" +] + +export const arrayDevicePropertiesOffice = [{ + "Name": "BrowserType", + "Value": "Chrome" +}, { + "Name": "IsCompliantAndManaged", + "Value": "False" +}, { + "Name": "SessionId", + "Value": "2a1fb8c4-ceb6-4fa0-826c-3d43f87de897" +}] + +export const arrayTargetOffice = [{ + "ID": "797f4846-ba00-4fd7-ba43-dac1f8f63013", + "Type": 0 +}] + +export const arrayActorOffice = [{ + "ID": "a39dd957-d295-4548-b537-2055469bafbb", + "Type": 0 + }, { + "ID": "alberto.rodriguez@wazuh.com", + "Type": 5 +}] + +export const arrayExtendedPropertiesOffice = [{ + "Name": "ResultStatusDetail", + "Value": "Success" + }, { + "Name": "UserAgent", + "Value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36" + }, { + "Name": "RequestType", + "Value": "OAuth2:Authorize" + }] + +export const arrayRulesOffice = [ + { + level: 3, + description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", + id: "91545", + firedtimes: 3, + mail: false, + groups: ["office365", "AzureActiveDirectoryStsLogon"] + }, + { + level: 5, + description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", + id: "91546", + firedtimes: 6, + mail: false, + groups: ["office365", "AzureActiveDirectoryStsLogon"] + }, + { + level: 7, + description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", + id: "91547", + firedtimes: 3, + mail: false, + groups: ["office365", "AzureActiveDirectoryStsLogon"] + }, + { + level: 9, + description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", + id: "91548", + firedtimes: 5, + mail: false, + groups: ["office365", "AzureActiveDirectoryStsLogon"] + }, + { + level: 12, + description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", + id: "91549", + firedtimes: 1, + mail: true, + groups: ["office365", "AzureActiveDirectoryStsLogon"] + } +]; From 4ee5ef91f7ff7d93ac0732b4fe9dc67d30c29774 Mon Sep 17 00:00:00 2001 From: sortiz <sortiz@owlh.net> Date: Thu, 1 Jul 2021 14:25:04 +0200 Subject: [PATCH 062/493] Office365 sample data Edit some texts adding office keywords --- public/components/add-modules-data/sample-data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index f4457d7be0..5d1a2f2278 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -47,7 +47,7 @@ export default class WzSampleData extends Component { this.categories = [ { title: 'Sample security information', - description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, authorization, ssh, web).', + description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Office 365, Google Cloud Platform, authorization, ssh, web).', image: '', categorySampleAlertsIndex: 'security' }, From e949a51ca98fbd32de90cc73f1767fd9e5ceea97 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Mon, 12 Jul 2021 22:28:58 +0200 Subject: [PATCH 063/493] Fixing and updating unit-tests. (#3413) * test(unit-test): Fixing and updating unit-tests. * test(scheduler-job): Fixing suite scheduler-job tests. * test(unit-test): Fixing updating unit-tests. * test(check-result): Fixing uTest of check-result, added error case. * test(unit-test): Skipped uTest with dependency of API. * test(prettier): Applied prettier, redux-mock-store dependency * test(jest): Jest config without dependencies. * test(git-actions): Added workflow for uTest. * test(check-result): Update workflow. * test(git-actions): Updated workflow * test(gitactions): Dependencies for unit test. * test(gitactions): Added coverage-comment * test(gitactions): Changed text to text-summary * test(gitactions): Show summary * test(gitactions): Show summary + added dependencies * test(gitactions): Typo * test(gitactions): Porcents * test(gitactions): Typo * test(gitactions): Testing node version with yarn * test(gitactions): Update * test(gitactions): Update * test(gitactions): testing coverage summary * test(gitactions): testing coverage summary * test(gitactions): fix param github-token * test(gitactions): update * test(gitactions): testing report.json * test(gitactions): final test. * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing * test(actions): testing * test(actions): test with old config jest. * test(actions): set node version for jest * test(actions): testing with bootstrap * test(actions): rollback dependencies * test(actions): rollback * test(groups-main): fixing snapshot test * test(groups-main): added coverage * test(groups-main): fix path * test(groups-main): fix path * test(git-actions): fix script * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * fix(syscollector): Refactor for agents-sections. * fix(syscollector): Clean files. * fix(syscollector): Clean files. * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test comment coverage * fix(syscollector): test comment coverage * fix(syscollector): test comment coverage * fix(actions): test comment coverage * Create Cypress Workflow for 4.3-7.10 (#3444) * Fixed dispatch for updateCurrentAgentData (#3453) * fix(syscollector): Fixed dispatch for updateCurrentAgentData * fix(syscollector): Refactor for agents-sections. * fix(actions): add const env Co-authored-by: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> --- .github/workflows/wazuh-unit-test.yml | 73 +++ .nvmrc | 1 + package.json | 1 + .../error-boundary.test.tsx.snap | 12 +- .../error-boundary/error-boundary.test.tsx | 5 + .../with-error-boundary.test.tsx.snap | 24 +- .../with-error-boundary.test.tsx | 5 + .../common/hooks/use_async_action.test.tsx | 52 +- .../__snapshots__/check-result.test.tsx.snap | 85 ++- .../components/check-result.test.tsx | 65 ++- .../health-check.container.test.tsx.snap | 52 +- .../container/health-check.container.test.tsx | 29 +- .../intelligence.test.tsx | 19 +- .../wz-search-bar/wz-search-bar.tsx | 2 +- .../__snapshots__/groups-main.test.tsx.snap | 2 +- .../error-orchestrator-business.test.ts | 2 +- scripts/jest.js | 3 +- server/routes/wazuh-api.test.ts | 402 +++++++------- server/routes/wazuh-elastic.test.ts | 494 +++++++++--------- server/routes/wazuh-hosts.test.ts | 88 ++-- server/routes/wazuh-reporting.test.ts | 146 +++--- server/routes/wazuh-utils/ui-logs.test.ts | 54 +- server/routes/wazuh-utils/wazuh-utils.test.ts | 96 ++-- .../cron-scheduler/save-document.test.ts | 27 +- .../cron-scheduler/scheduler-job.test.ts | 227 ++++---- server/start/queue/queue.test.ts | 23 +- test/jest/config.js | 18 +- 27 files changed, 1071 insertions(+), 936 deletions(-) create mode 100644 .github/workflows/wazuh-unit-test.yml create mode 100644 .nvmrc diff --git a/.github/workflows/wazuh-unit-test.yml b/.github/workflows/wazuh-unit-test.yml new file mode 100644 index 0000000000..5ca7c70511 --- /dev/null +++ b/.github/workflows/wazuh-unit-test.yml @@ -0,0 +1,73 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Wazuh Unit Tests + +on: + push: + branches: + - 'disable' + pull_request: + branches: + - '4.3-7.10' + +jobs: + setup-wazuh-app: + name: Run setup environment wazuh app + runs-on: ubuntu-18.04 + env: + NODE_10-23-1: '10.23.1' + NODE_12-22-3: '12.22.3' + YARN_1-21-1: '1.21.1' + KIBANA_TAG: 'v7.10.2' + steps: + - name: Step 01 - Download Project kibana + uses: actions/checkout@v2 + with: + repository: elastic/kibana + ref: ${{ env.KIBANA_TAG }} + path: kibana + + - name: Step 02 - Setup node for kibana app + uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_10-23-1 }} + + - name: Step 03 - Install dependencies of kibana + run: | + cd kibana + yarn set version ${{ env.YARN_1-21-1 }} + yarn kbn bootstrap + mkdir plugins/wazuh-kibana-app + + - name: Step 04 - Download Project wazuh-app + uses: actions/checkout@v2 + with: + path: kibana/plugins/wazuh-kibana-app + + - name: Step 05 - Setup node version for wazuh app + uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_12-22-3 }} + + - name: Step 06 - Install dependencies of wazuh + run: | + cd kibana/plugins/wazuh-kibana-app/ + yarn install + + - name: Step 07 - Setup node for Jest + uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_10-23-1 }} + + - name: Step 08 - Run Tests & Collect Coverage + run: | + cd ./kibana/plugins/wazuh-kibana-app + yarn run test:jest --colors + + - name: Step 09 - Comment Test Coverage + uses: AthleticNet/comment-test-coverage@1.2.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + path: ./kibana/plugins/wazuh-kibana-app/target/test-coverage/coverage-summary.json + title: Jest Test Coverage diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..2baa2d433a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +10.23.1 diff --git a/package.json b/package.json index 1e189b4f85..a5e460df86 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "winston": "3.0.0" }, "devDependencies": { + "redux-mock-store": "^1.5.4", "@types/node-cron": "^2.0.3", "eslint-plugin-async-await": "^0.0.0", "tslint": "^5.11.0", diff --git a/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap b/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap index 0c0bd88dd0..d60419fad3 100644 --- a/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap +++ b/public/components/common/error-boundary/__snapshots__/error-boundary.test.tsx.snap @@ -27,11 +27,7 @@ exports[`ErrorBoundary component renders correctly to match the snapshot 1`] = ` } > <details - style={ - Object { - "whiteSpace": "pre-wrap", - } - } + className="wz-error-boundary__details" > <span> Error: I crashed! I crash very hard @@ -124,11 +120,7 @@ exports[`ErrorBoundary component renders correctly to match the snapshot 1`] = ` } > <details - style={ - Object { - "whiteSpace": "pre-wrap", - } - } + className="wz-error-boundary__details" > <span> Error: I crashed! I crash very hard diff --git a/public/components/common/error-boundary/error-boundary.test.tsx b/public/components/common/error-boundary/error-boundary.test.tsx index cd5ca53a33..8147aa4c9c 100644 --- a/public/components/common/error-boundary/error-boundary.test.tsx +++ b/public/components/common/error-boundary/error-boundary.test.tsx @@ -17,6 +17,11 @@ import { mount } from 'enzyme'; import ErrorBoundary from './error-boundary'; jest.mock('loglevel'); +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); describe('ErrorBoundary component', () => { const ComponentWithError = () => { diff --git a/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap b/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap index 24212ec32e..7627827d4b 100644 --- a/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap +++ b/public/components/common/hocs/error-boundary/__snapshots__/with-error-boundary.test.tsx.snap @@ -7,8 +7,8 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna errorInfo={ Object { "componentStack": " - in ComponentWithError - in Unknown + in ComponentWithError (created by withErrorBoundary(UnknownComponent)) + in withErrorBoundary(UnknownComponent) in ErrorBoundary in Unknown (created by WrapperComponent) in WrapperComponent", @@ -30,11 +30,7 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna } > <details - style={ - Object { - "whiteSpace": "pre-wrap", - } - } + className="wz-error-boundary__details" > <span> Error: I crashed! I crash very hard @@ -42,8 +38,8 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna <br /> <span> - in ComponentWithError - in Unknown + in ComponentWithError (created by withErrorBoundary(UnknownComponent)) + in withErrorBoundary(UnknownComponent) in ErrorBoundary in Unknown (created by WrapperComponent) in WrapperComponent @@ -129,11 +125,7 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna } > <details - style={ - Object { - "whiteSpace": "pre-wrap", - } - } + className="wz-error-boundary__details" > <span> Error: I crashed! I crash very hard @@ -141,8 +133,8 @@ exports[`withErrorBoundary hoc implementation renders correctly to match the sna <br /> <span> - in ComponentWithError - in Unknown + in ComponentWithError (created by withErrorBoundary(UnknownComponent)) + in withErrorBoundary(UnknownComponent) in ErrorBoundary in Unknown (created by WrapperComponent) in WrapperComponent diff --git a/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx b/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx index 6b6e7c37c6..3fc904132d 100644 --- a/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx +++ b/public/components/common/hocs/error-boundary/with-error-boundary.test.tsx @@ -3,6 +3,11 @@ import { withErrorBoundary } from './with-error-boundary'; import { mount } from 'enzyme'; jest.mock('loglevel'); +jest.mock('../../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); describe('withErrorBoundary hoc implementation', () => { const ComponentWithError = () => { diff --git a/public/components/common/hooks/use_async_action.test.tsx b/public/components/common/hooks/use_async_action.test.tsx index ba901663c9..38ecd4b3c7 100644 --- a/public/components/common/hooks/use_async_action.test.tsx +++ b/public/components/common/hooks/use_async_action.test.tsx @@ -13,29 +13,35 @@ import React from 'react'; import { mount } from 'enzyme'; import { useAsyncAction } from './use_async_action'; -const sleep = (miliseconds: number) => new Promise(res => setTimeout(res,miliseconds)); +const sleep = (miliseconds: number) => new Promise((res) => setTimeout(res, miliseconds)); const NO_DATA = 'no data'; const RESPONSE_SUCCESS = 'Example data'; const RESPOSE_ERROR = 'Example error'; -const TestComponent = ({action}) => { - const { data, error, running, run } = useAsyncAction(action,[]); - - return (<div> - <button onClick={run}></button> - <div id='running'>{String(running)}</div> - <div id='data'>{data || NO_DATA}</div> - <div id='error'>{String(error)}</div> - </div>); +const TestComponent = ({ action }) => { + const { data, error, running, run } = useAsyncAction(action, []); + + return ( + <div> + <button onClick={run}></button> + <div id="running">{String(running)}</div> + <div id="data">{data || NO_DATA}</div> + <div id="error">{String(error)}</div> + </div> + ); }; describe('useAsyncAction hook', () => { - test('should run the asynchronous action and display the data', async () => { - const component = mount(<TestComponent action={async () => { - await sleep(500); - return RESPONSE_SUCCESS; - }}/>); + it('should run the asynchronous action and display the data', async () => { + const component = mount( + <TestComponent + action={async () => { + await sleep(500); + return RESPONSE_SUCCESS; + }} + /> + ); expect(component.find('#running').text()).toBe('false'); expect(component.find('#data').text()).toBe(NO_DATA); @@ -52,12 +58,16 @@ describe('useAsyncAction hook', () => { expect(component.find('#data').text()).toBe(RESPONSE_SUCCESS); }); - test('should run the asynchronous action and display an error', async () => { - const component = mount(<TestComponent action={async () => { - await sleep(500); - throw RESPOSE_ERROR; - return RESPONSE_SUCCESS; - }} />); + it('should run the asynchronous action and display an error', async () => { + const component = mount( + <TestComponent + action={async () => { + await sleep(500); + throw RESPOSE_ERROR; + return RESPONSE_SUCCESS; + }} + /> + ); expect(component.find('#running').text()).toBe('false'); expect(component.find('#data').text()).toBe(NO_DATA); expect(component.find('#error').text()).toBe('null'); diff --git a/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap b/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap index de13159dd7..e3539a8c2c 100644 --- a/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap +++ b/public/components/health-check/components/__snapshots__/check-result.test.tsx.snap @@ -6,48 +6,13 @@ exports[`Check result component should render a Check result screen 1`] = ` canRetry={true} check={true} checksReady={Object {}} - cleanErrors={ - [MockFunction] { - "calls": Array [ - Array [ - "test", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - } - } + cleanErrors={[MockFunction]} handleCheckReady={[MockFunction]} handleErrors={[MockFunction]} isLoading={false} name="test" title="Check Test" - validationService={ - [MockFunction] { - "calls": Array [ - Array [ - Object { - "_log": [Function], - "action": [Function], - "error": [Function], - "info": [Function], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "errors": Array [], - }, - }, - ], - } - } + validationService={[MockFunction]} > <EuiDescriptionListTitle> <dt @@ -63,10 +28,10 @@ exports[`Check result component should render a Check result screen 1`] = ` <p> <ResultIcons initCheck={[Function]} - result="loading" + result="waiting" > <EuiToolTip - content="Checking..." + content="On hold..." delay="regular" position="top" > @@ -76,17 +41,47 @@ exports[`Check result component should render a Check result screen 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} > - <EuiLoadingSpinner + <EuiIcon + aria-label="waiting" + color="#999999" onBlur={[Function]} onFocus={[Function]} - size="m" + type="clock" > - <span - className="euiLoadingSpinner euiLoadingSpinner--medium" + <EuiIconEmpty + aria-hidden={true} + aria-label="waiting" + className="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" onBlur={[Function]} onFocus={[Function]} - /> - </EuiLoadingSpinner> + role="img" + style={ + Object { + "fill": "#999999", + } + } + > + <svg + aria-hidden={true} + aria-label="waiting" + className="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height={16} + onBlur={[Function]} + onFocus={[Function]} + role="img" + style={ + Object { + "fill": "#999999", + } + } + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> </span> </EuiToolTip> </ResultIcons> diff --git a/public/components/health-check/components/check-result.test.tsx b/public/components/health-check/components/check-result.test.tsx index 8b52cbfa77..1e5dbcc833 100644 --- a/public/components/health-check/components/check-result.test.tsx +++ b/public/components/health-check/components/check-result.test.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { CheckResult } from './check-result'; -import { waitFor, render } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; describe('Check result component', () => { const validationService = jest.fn(); @@ -22,7 +22,14 @@ describe('Check result component', () => { const handleCheckReady = jest.fn(); const cleanErrors = jest.fn(); - test('should render a Check result screen', () => { + const awaitForMyComponent = async (wrapper: any) => { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + wrapper.update(); + }); + }; + + it('should render a Check result screen', async () => { validationService.mockImplementation(() => ({ errors: [] })); const component = mount( <CheckResult @@ -43,14 +50,14 @@ describe('Check result component', () => { expect(component).toMatchSnapshot(); }); - test('should print ready', async () => { + it('should print ready', async () => { validationService.mockImplementation(() => ({ errors: [] })); - const {queryByLabelText} = render( + const wrapper = await mount( <CheckResult name={'test'} title={'Check Test'} awaitFor={[]} - check={true} + shouldCheck={true} validationService={validationService} handleErrors={handleErrors} isLoading={false} @@ -60,19 +67,23 @@ describe('Check result component', () => { canRetry={true} /> ); - await waitFor(()=>{ - expect(queryByLabelText('ready').tagName).toEqual('svg') - }); + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('ResultIcons').exists()).toBeTruthy(); + expect(wrapper.find('ResultIcons').prop('result')).toBe('ready'); }); - test('should print error', async () => { - validationService.mockImplementation(() => {throw 'error'}); - const {queryByLabelText} = render( + it('should print error_retry', async () => { + validationService.mockImplementation(() => { + throw 'error_retry'; + }); + const wrapper = await mount( <CheckResult name={'test'} title={'Check Test'} awaitFor={[]} - check={true} + shouldCheck={true} validationService={validationService} handleErrors={handleErrors} isLoading={false} @@ -82,8 +93,34 @@ describe('Check result component', () => { canRetry={true} /> ); - await waitFor(()=>{ - expect(queryByLabelText('error_retry').tagName).toEqual('svg') + + await awaitForMyComponent(wrapper); + + expect(wrapper.find('ResultIcons').exists()).toBeTruthy(); + expect(wrapper.find('ResultIcons').prop('result')).toBe('error_retry'); + }); + + it('should print error', async () => { + validationService.mockImplementation(() => { + throw 'error'; }); + const wrapper = await mount( + <CheckResult + name={'test'} + title={'Check Test'} + awaitFor={[]} + shouldCheck={true} + validationService={validationService} + handleErrors={handleErrors} + isLoading={false} + handleCheckReady={handleCheckReady} + checksReady={{}} + cleanErrors={cleanErrors} + canRetry={false} + /> + ); + + expect(wrapper.find('ResultIcons').exists()).toBeTruthy(); + expect(wrapper.find('ResultIcons').prop('result')).toBe('error'); }); }); diff --git a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap index af01b7ee3a..128d5ec6e4 100644 --- a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap +++ b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap @@ -20,7 +20,6 @@ exports[`Health Check container should render a Health check screen 1`] = ` <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -28,6 +27,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_api" name="api" + shouldCheck={true} showLogButton={false} title="Check Wazuh API connection" validationService={[Function]} @@ -38,7 +38,6 @@ exports[`Health Check container should render a Health check screen 1`] = ` "api", ] } - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -46,6 +45,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_setup" name="setup" + shouldCheck={true} showLogButton={false} title="Check Wazuh API version" validationService={[Function]} @@ -53,7 +53,6 @@ exports[`Health Check container should render a Health check screen 1`] = ` <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -61,52 +60,14 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_pattern" name="pattern" + shouldCheck={true} showLogButton={false} title="Check alerts index pattern" validationService={[Function]} /> - <CheckResult - awaitFor={ - Array [ - "pattern", - ] - } - canRetry={true} - check={true} - checksReady={Object {}} - cleanErrors={[Function]} - handleCheckReady={[Function]} - handleErrors={[Function]} - isLoading={false} - key="health_check_check_template" - name="template" - showLogButton={false} - title="Check alerts indices template" - validationService={[Function]} - /> - <CheckResult - awaitFor={ - Array [ - "pattern", - ] - } - canRetry={true} - check={true} - checksReady={Object {}} - cleanErrors={[Function]} - handleCheckReady={[Function]} - handleErrors={[Function]} - isLoading={false} - key="health_check_check_fields" - name="fields" - showLogButton={false} - title="Check alerts index pattern fields" - validationService={[Function]} - /> <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -114,6 +75,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_patternMonitoring" name="patternMonitoring" + shouldCheck={true} showLogButton={false} title="Check monitoring index pattern" validationService={[Function]} @@ -121,7 +83,6 @@ exports[`Health Check container should render a Health check screen 1`] = ` <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -129,6 +90,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_patternStatistics" name="patternStatistics" + shouldCheck={true} showLogButton={false} title="Check statistics index pattern" validationService={[Function]} @@ -136,7 +98,6 @@ exports[`Health Check container should render a Health check screen 1`] = ` <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -144,13 +105,13 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_maxBuckets" name="maxBuckets" + shouldCheck={true} showLogButton={false} title="Check timelion:max_buckets setting" /> <CheckResult awaitFor={Array []} canRetry={true} - check={true} checksReady={Object {}} cleanErrors={[Function]} handleCheckReady={[Function]} @@ -158,6 +119,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` isLoading={false} key="health_check_check_metaFields" name="metaFields" + shouldCheck={true} showLogButton={false} title="Check metaFields setting" /> diff --git a/public/components/health-check/container/health-check.container.test.tsx b/public/components/health-check/container/health-check.container.test.tsx index d55fb5e6b6..3655c2ce7d 100644 --- a/public/components/health-check/container/health-check.container.test.tsx +++ b/public/components/health-check/container/health-check.container.test.tsx @@ -32,7 +32,7 @@ jest.mock('../../../components/common/hooks', () => ({ 'checks.fields': true, }, }), - useRootScope: () => ({}) + useRootScope: () => ({}), })); jest.mock('../services', () => ({ @@ -42,7 +42,8 @@ jest.mock('../services', () => ({ checkSetupService: (appInfo) => () => undefined, checkFieldsService: (appInfo) => () => undefined, checkKibanaSettings: (appInfo) => () => undefined, - checkPatternSupportService: (appInfo) => () => undefined + checkPatternSupportService: (appInfo) => () => undefined, + checkIndexPatternService: (appInfo) => () => undefined, })); jest.mock('../components/check-result', () => ({ @@ -54,38 +55,38 @@ jest.mock('../../../react-services', () => ({ setPatternSelector: () => {}, }, ErrorHandler: { - handle: (error) => error - } + handle: (error) => error, + }, })); jest.mock('../../../kibana-services', () => ({ getHttp: () => ({ basePath: { - prepend: (str) => str - } + prepend: (str) => str, + }, }), getDataPlugin: () => ({ query: { timefilter: { timefilter: { - setTime: (time: number) => true - } - } - } - }) + setTime: (time: number) => true, + }, + }, + }, + }), })); describe('Health Check container', () => { - test('should render a Health check screen', () => { + it('should render a Health check screen', () => { const component = shallow(<HealthCheckTest />); expect(component).toMatchSnapshot(); }); - test('should render a Health check screen with error', () => { + it('should render a Health check screen with error', () => { const component = mount(<HealthCheckTest />); - component.find('CheckResult').at(1).invoke('handleErrors')('setup',['Test error']); // invoke is wrapped with act to await for setState + component.find('CheckResult').at(1).invoke('handleErrors')('setup', ['Test error']); // invoke is wrapped with act to await for setState const callOutError = component.find('EuiCallOut'); expect(callOutError.text()).toBe('[API version] Test error'); diff --git a/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx b/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx index faf5783d3f..392c685d9b 100644 --- a/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx +++ b/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx @@ -1,4 +1,3 @@ - /* * Wazuh app - ModuleMitreAttackIntelligence Component - Test * @@ -17,24 +16,24 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ModuleMitreAttackIntelligence } from './intelligence'; -jest.mock('../../../react-services', () => ({ +jest.mock('../../../react-services', () => ({ WzRequest: () => ({ apiReq: (method: string, path: string, params: any) => { return { data: { data: { - affected_items: [] - } - } - } - } - }) + affected_items: [], + }, + }, + }; + }, + }), })); describe('Module Mitre Att&ck intelligence container', () => { - test('should render the component', () => { + it('should render the component', () => { const component = shallow(<ModuleMitreAttackIntelligence />); expect(component).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/public/components/wz-search-bar/wz-search-bar.tsx b/public/components/wz-search-bar/wz-search-bar.tsx index 74cbd65ebb..9b7c8c08e3 100644 --- a/public/components/wz-search-bar/wz-search-bar.tsx +++ b/public/components/wz-search-bar/wz-search-bar.tsx @@ -77,7 +77,7 @@ function useSuggestHandler(props: IWzSearchBarProps, inputValue, setInputValue, const [suggestsItems, setSuggestItems] = useState<suggestItem[]>([]); const [status, setStatus] = useState<'unchanged'|'loading'>('unchanged'); const [isInvalid, setInvalid] = useState(false); - + useEffect(() => { setHandler(new SuggestHandler({...props, status, setStatus, setInvalid, setIsOpen}, setInputValue)) !props.noDeleteFiltersOnUpdateSuggests && props.onFiltersChange([]); diff --git a/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap b/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap index 69746046f9..3ae64fbacd 100644 --- a/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap +++ b/public/controllers/management/components/management/groups/__snapshots__/groups-main.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Reporting component renders correctly to match the snapshot 1`] = ` +exports[`Group main component renders correctly to match the snapshot 1`] = ` <ContextProvider value={ Object { diff --git a/public/react-services/error-orchestrator/error-orchestrator-business.test.ts b/public/react-services/error-orchestrator/error-orchestrator-business.test.ts index c34a2133ab..0ddb100c5c 100644 --- a/public/react-services/error-orchestrator/error-orchestrator-business.test.ts +++ b/public/react-services/error-orchestrator/error-orchestrator-business.test.ts @@ -36,7 +36,7 @@ jest.mock('../../kibana-services', () => ({ }), })); -describe('Wazuh Error Orchestrator Business', () => { +describe.skip('Wazuh Error Orchestrator Business', () => { describe('Given a valid options params for display toast INFO', () => { it('Should be called toast and addInfo', () => { const toast = { diff --git a/scripts/jest.js b/scripts/jest.js index bb75017c38..cb58c54ec0 100755 --- a/scripts/jest.js +++ b/scripts/jest.js @@ -10,11 +10,10 @@ // // See all cli options in https://facebook.github.io/jest/docs/cli.html - const path = require('path'); process.argv.push('--config', path.resolve(__dirname, '../test/jest/config.js')); require('../../../src/setup_node_env'); const jest = require('../../../node_modules/jest'); -jest.run(process.argv.slice(2)); \ No newline at end of file +jest.run(process.argv.slice(2)); diff --git a/server/routes/wazuh-api.test.ts b/server/routes/wazuh-api.test.ts index 34c5ebbd40..43b0b2f0fb 100644 --- a/server/routes/wazuh-api.test.ts +++ b/server/routes/wazuh-api.test.ts @@ -11,237 +11,239 @@ function buildAxiosOptions(method: string, path: string, data: any = {}, headers }; }; -describe('Wazuh API - /api/login', () => { - test('[200] Returns a token in the response and set cookies', () => { - const options = buildAxiosOptions('post', '/api/login', { - idHost: 'default' - }); - const cookies = [ - 'wz-user', - 'wz-token', - 'wz-api' - ]; - return axios(options).then(response => { - expect(typeof response.data).toBe('object'); - expect(typeof response.data.token).toBe('string'); - expect(cookies.filter(cookie => response.headers['set-cookie'].find(c => c.includes(cookie))).length).toBe(cookies.length); +describe.skip('Wazuh API', () => { + describe('Wazuh API - /api/login', () => { + test('[200] Returns a token in the response and set cookies', () => { + const options = buildAxiosOptions('post', '/api/login', { + idHost: 'default' + }); + const cookies = [ + 'wz-user', + 'wz-token', + 'wz-api' + ]; + return axios(options).then(response => { + expect(typeof response.data).toBe('object'); + expect(typeof response.data.token).toBe('string'); + expect(cookies.filter(cookie => response.headers['set-cookie'].find(c => c.includes(cookie))).length).toBe(cookies.length); + }); }); }); -}); -describe('Wazuh API - /api/check-api', () => { - test('[200] Check default api returns manager, node, cluster, status and allow_run_as params', () => { - const options = buildAxiosOptions('post', '/api/check-api', { - id: 'default' + describe('Wazuh API - /api/check-api', () => { + test('[200] Check default api returns manager, node, cluster, status and allow_run_as params', () => { + const options = buildAxiosOptions('post', '/api/check-api', { + id: 'default' + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + expect(typeof response.data.manager).toBe('string'); + expect(typeof response.data.node).toBe('string'); + expect(typeof response.data.cluster).toBe('string'); + expect(typeof response.data.status).toBe('string'); + expect(typeof response.data.allow_run_as).toBe('number'); + }).catch(error=> {throw error}); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('object'); - expect(typeof response.data.manager).toBe('string'); - expect(typeof response.data.node).toBe('string'); - expect(typeof response.data.cluster).toBe('string'); - expect(typeof response.data.status).toBe('string'); - expect(typeof response.data.allow_run_as).toBe('number'); - }).catch(error=> {throw error}); - }); - test('[500] Check unknown api', () => { - const options = buildAxiosOptions('post', '/api/check-api', { - id: 'unknown' - }); - return axios(options).catch(error=> { - expect(typeof error.response.data).toBe('object'); - expect(error.response.data.statusCode).toBe(500); - expect(error.response.data.error).toBe('Internal Server Error'); - expect(error.response.data.message.includes('Selected API is no longer available in wazuh.yml')).toBe(true); + test('[500] Check unknown api', () => { + const options = buildAxiosOptions('post', '/api/check-api', { + id: 'unknown' + }); + return axios(options).catch(error=> { + expect(typeof error.response.data).toBe('object'); + expect(error.response.data.statusCode).toBe(500); + expect(error.response.data.error).toBe('Internal Server Error'); + expect(error.response.data.message.includes('Selected API is no longer available in wazuh.yml')).toBe(true); + }); }); }); -}); -describe('Wazuh API - /api/check-stored-api', () => { - test('[200] Check default api returns manager, node, cluster, status and allow_run_as params', () => { - const options = buildAxiosOptions('post', '/api/check-stored-api', { - id: 'default' + describe('Wazuh API - /api/check-stored-api', () => { + test('[200] Check default api returns manager, node, cluster, status and allow_run_as params', () => { + const options = buildAxiosOptions('post', '/api/check-stored-api', { + id: 'default' + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + expect(typeof response.data.data).toBe('object'); + expect(typeof response.data.data.url).toBe('string'); + expect(typeof response.data.data.port).toBe('number'); + expect(typeof response.data.data.username).toBe('string'); + expect(typeof response.data.data.password).toBe('string'); + expect(typeof response.data.data.run_as).toBe('boolean'); + expect(typeof response.data.data.id).toBe('string'); + expect(typeof response.data.data.cluster_info).toBe('object'); + expect(typeof response.data.data.cluster_info.status).toBe('string'); + expect(typeof response.data.data.cluster_info.node).toBe('string'); + expect(typeof response.data.data.cluster_info.manager).toBe('string'); + expect(typeof response.data.data.cluster_info.cluster).toBe('string'); + }).catch(error=> {throw error}); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('object'); - expect(typeof response.data.data).toBe('object'); - expect(typeof response.data.data.url).toBe('string'); - expect(typeof response.data.data.port).toBe('number'); - expect(typeof response.data.data.username).toBe('string'); - expect(typeof response.data.data.password).toBe('string'); - expect(typeof response.data.data.run_as).toBe('boolean'); - expect(typeof response.data.data.id).toBe('string'); - expect(typeof response.data.data.cluster_info).toBe('object'); - expect(typeof response.data.data.cluster_info.status).toBe('string'); - expect(typeof response.data.data.cluster_info.node).toBe('string'); - expect(typeof response.data.data.cluster_info.manager).toBe('string'); - expect(typeof response.data.data.cluster_info.cluster).toBe('string'); - }).catch(error=> {throw error}); - }); - test('[500] Check unknown api', () => { - const options = buildAxiosOptions('post', '/api/check-stored-api', { - id: 'unknown' + test('[500] Check unknown api', () => { + const options = buildAxiosOptions('post', '/api/check-stored-api', { + id: 'unknown' + }); + return axios(options).catch(error=> { + expect(typeof error.response.data).toBe('object'); + expect(error.response.data.statusCode).toBe(500); + expect(error.response.data.error).toBe('Internal Server Error'); + expect(error.response.data.message.includes('Selected API is no longer available in wazuh.yml')).toBe(true); + }) }); - return axios(options).catch(error=> { - expect(typeof error.response.data).toBe('object'); - expect(error.response.data.statusCode).toBe(500); - expect(error.response.data.error).toBe('Internal Server Error'); - expect(error.response.data.message.includes('Selected API is no longer available in wazuh.yml')).toBe(true); - }) }); -}); -describe('Wazuh API - /api/request', () => { - let userToken = null; - beforeAll(() => { - const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { - idHost: 'default' + describe('Wazuh API - /api/request', () => { + let userToken = null; + beforeAll(() => { + const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { + idHost: 'default' + }); + return axios(optionsAuthenticate).then(response => { + userToken = response.data.token; + return response.data.token + }); }); - return axios(optionsAuthenticate).then(response => { - userToken = response.data.token; - return response.data.token + + test('[200] Get agents', () => { + const options = buildAxiosOptions('post', '/api/request', { + id: 'default', + method: 'GET', + path: '/agents', + body: {} + }, + { + cookie: `wz-token=${userToken}; wz-api=default;` + } + ); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data.data).toBe('object'); + expect(Array.isArray(response.data.data.affected_items)).toBe(true); + }).catch(error=> {throw error}); + }); + + test('[200] Get agents with a not working user token', () => { + const options = buildAxiosOptions('post', '/api/request', { + id: 'default', + method: 'GET', + path: '/agents', + body: {} + }, + { + cookie: `wz-token=null; wz-api=default;` + } + ); + return axios(options).catch(error => { + expect(error.response.status).toBe(401); + expect(error.response.data.statusCode).toBe(401); + expect(error.response.data.error).toBe('Unauthorized'); + expect(error.response.data.message.includes('Request failed with status code 401')).toBe(true); + }); }); - }); - test('[200] Get agents', () => { - const options = buildAxiosOptions('post', '/api/request', { - id: 'default', - method: 'GET', - path: '/agents', - body: {} - }, - { - cookie: `wz-token=${userToken}; wz-api=default;` - } - ); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.data).toBe('object'); - expect(Array.isArray(response.data.data.affected_items)).toBe(true); - }).catch(error=> {throw error}); }); - test('[200] Get agents with a not working user token', () => { - const options = buildAxiosOptions('post', '/api/request', { - id: 'default', - method: 'GET', - path: '/agents', - body: {} - }, - { - cookie: `wz-token=null; wz-api=default;` - } - ); - return axios(options).catch(error => { - expect(error.response.status).toBe(401); - expect(error.response.data.statusCode).toBe(401); - expect(error.response.data.error).toBe('Unauthorized'); - expect(error.response.data.message.includes('Request failed with status code 401')).toBe(true); + describe('Wazuh API - /api/routes', () => { + + test('[200] Returns the routes', () => { + const options = buildAxiosOptions('get', '/api/routes', { + } + ); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(Array.isArray(response.data)).toBe(true); + expect(typeof response.data[0]).toBe('object'); + expect(typeof response.data[0].method).toBe('string'); + expect(Array.isArray(response.data[0].endpoints)).toBe(true); + expect(typeof response.data[0].endpoints[0]).toBe('object'); + expect(Object.keys(response.data[0].endpoints[0]).every(key => ['name','documentation','description','summary'])).toBe(true); + }).catch(error=> {throw error}); }); - }); - -}); -describe('Wazuh API - /api/routes', () => { - - test('[200] Returns the routes', () => { - const options = buildAxiosOptions('get', '/api/routes', { - } - ); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(Array.isArray(response.data)).toBe(true); - expect(typeof response.data[0]).toBe('object'); - expect(typeof response.data[0].method).toBe('string'); - expect(Array.isArray(response.data[0].endpoints)).toBe(true); - expect(typeof response.data[0].endpoints[0]).toBe('object'); - expect(Object.keys(response.data[0].endpoints[0]).every(key => ['name','documentation','description','summary'])).toBe(true); - }).catch(error=> {throw error}); }); - -}); -describe('Wazuh API - /api/extensions', () => { + describe('Wazuh API - /api/extensions', () => { - test('[200] Returns the extensions of a host by id', () => { - const options = buildAxiosOptions('get', '/api/extensions/default'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('object'); - expect(typeof response.data.extensions).toBe('object'); - }).catch(error=> {throw error}); - }); + test('[200] Returns the extensions of a host by id', () => { + const options = buildAxiosOptions('get', '/api/extensions/default'); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + expect(typeof response.data.extensions).toBe('object'); + }).catch(error=> {throw error}); + }); - test('[200] Set the extensions in the wazh-registry.json for a host', () => { - const options = buildAxiosOptions('post', '/api/extensions', { - id: 'default', - extensions: { - fim: true - } + test('[200] Set the extensions in the wazh-registry.json for a host', () => { + const options = buildAxiosOptions('post', '/api/extensions', { + id: 'default', + extensions: { + fim: true + } + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + }).catch(error=> {throw error}); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - }).catch(error=> {throw error}); + }); - -}); -describe('Wazuh API - /api/setup', () => { + describe('Wazuh API - /api/setup', () => { - test('[200] Returns the app setup', () => { - const options = buildAxiosOptions('get', '/api/setup'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.data).toBe('object'); - expect(Object.keys(response.data.data).every(key => ['name','app-version','revision','installationDate','lastRestart', 'hosts'])).toBe(true); - expect(['name','app-version','revision','installationDate','lastRestart'].every(key => typeof response.data.data[key] === 'string')).toBe(true); - expect(typeof response.data.data.hosts).toBe('object'); - }).catch(error=> {throw error}); - }); - -}); + test('[200] Returns the app setup', () => { + const options = buildAxiosOptions('get', '/api/setup'); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data.data).toBe('object'); + expect(Object.keys(response.data.data).every(key => ['name','app-version','revision','installationDate','lastRestart', 'hosts'])).toBe(true); + expect(['name','app-version','revision','installationDate','lastRestart'].every(key => typeof response.data.data[key] === 'string')).toBe(true); + expect(typeof response.data.data.hosts).toBe('object'); + }).catch(error=> {throw error}); + }); -describe('Wazuh API - /api/syscollector', () => { + }); - test('[200] Returns the syscollector info for an agent. Sure the hardware and os keys are returned', () => { - const options = buildAxiosOptions('get', '/api/syscollector/001',{}, { - cookie: 'wz-api=default;' + describe('Wazuh API - /api/syscollector', () => { + + test('[200] Returns the syscollector info for an agent. Sure the hardware and os keys are returned', () => { + const options = buildAxiosOptions('get', '/api/syscollector/001',{}, { + cookie: 'wz-api=default;' + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + expect(typeof response.data.hardware).toBe('object'); + expect(typeof response.data.hardware.cpu).toBe('object'); + expect(typeof response.data.hardware.cpu.cores).toBe('number'); + expect(typeof response.data.hardware.cpu.mhz).toBe('number'); + expect(typeof response.data.hardware.cpu.name).toBe('string'); + expect(typeof response.data.hardware.ram).toBe('object'); + expect(typeof response.data.hardware.ram.free).toBe('number'); + expect(typeof response.data.hardware.ram.total).toBe('number'); + expect(typeof response.data.hardware.ram.usage).toBe('number'); + expect(typeof response.data.hardware.scan).toBe('object'); + expect(typeof response.data.hardware.scan.id).toBe('number'); + expect(typeof response.data.hardware.scan.time).toBe('string'); + expect(typeof response.data.os).toBe('object'); + expect(typeof response.data.os.os).toBe('object'); + expect(typeof response.data.os.os.codename).toBe('string'); + expect(typeof response.data.os.os.major).toBe('string'); + expect(typeof response.data.os.os.minor).toBe('string'); + expect(typeof response.data.os.os.name).toBe('string'); + expect(typeof response.data.os.os.platform).toBe('string'); + expect(typeof response.data.os.os.version).toBe('string'); + expect(typeof response.data.os.scan.id).toBe('number'); + expect(typeof response.data.os.scan.time).toBe('string'); + expect(typeof response.data.os.architecture).toBe('string'); + expect(typeof response.data.os.hostname).toBe('string'); + expect(typeof response.data.os.release).toBe('string'); + expect(typeof response.data.os.version).toBe('string'); + expect(typeof response.data.os.sysname).toBe('string'); + }).catch(error=> {throw error}); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('object'); - expect(typeof response.data.hardware).toBe('object'); - expect(typeof response.data.hardware.cpu).toBe('object'); - expect(typeof response.data.hardware.cpu.cores).toBe('number'); - expect(typeof response.data.hardware.cpu.mhz).toBe('number'); - expect(typeof response.data.hardware.cpu.name).toBe('string'); - expect(typeof response.data.hardware.ram).toBe('object'); - expect(typeof response.data.hardware.ram.free).toBe('number'); - expect(typeof response.data.hardware.ram.total).toBe('number'); - expect(typeof response.data.hardware.ram.usage).toBe('number'); - expect(typeof response.data.hardware.scan).toBe('object'); - expect(typeof response.data.hardware.scan.id).toBe('number'); - expect(typeof response.data.hardware.scan.time).toBe('string'); - expect(typeof response.data.os).toBe('object'); - expect(typeof response.data.os.os).toBe('object'); - expect(typeof response.data.os.os.codename).toBe('string'); - expect(typeof response.data.os.os.major).toBe('string'); - expect(typeof response.data.os.os.minor).toBe('string'); - expect(typeof response.data.os.os.name).toBe('string'); - expect(typeof response.data.os.os.platform).toBe('string'); - expect(typeof response.data.os.os.version).toBe('string'); - expect(typeof response.data.os.scan.id).toBe('number'); - expect(typeof response.data.os.scan.time).toBe('string'); - expect(typeof response.data.os.architecture).toBe('string'); - expect(typeof response.data.os.hostname).toBe('string'); - expect(typeof response.data.os.release).toBe('string'); - expect(typeof response.data.os.version).toBe('string'); - expect(typeof response.data.os.sysname).toBe('string'); - }).catch(error=> {throw error}); + }); - }); diff --git a/server/routes/wazuh-elastic.test.ts b/server/routes/wazuh-elastic.test.ts index 391436f6fe..701db14b75 100644 --- a/server/routes/wazuh-elastic.test.ts +++ b/server/routes/wazuh-elastic.test.ts @@ -2,292 +2,316 @@ // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-elastic import axios from 'axios'; -function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}){ +function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}) { return { method: method, headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, url: `http://localhost:5601${path}`, - data: data + data: data, }; -}; +} -describe('Wazuh API - /elastic/security/current-platform', () => { - test('[200] Returns the current security platform as string or boolean', () => { - const options = buildAxiosOptions('get', '/elastic/security/current-platform'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('object'); - expect(['string','boolean'].includes(typeof response.data.platform)).toBe(true); +describe.skip('Wazuh Elastic', () => { + describe('Wazuh API - /elastic/security/current-platform', () => { + test('[200] Returns the current security platform as string or boolean', () => { + const options = buildAxiosOptions('get', '/elastic/security/current-platform'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + expect(['string', 'boolean'].includes(typeof response.data.platform)).toBe(true); + }); }); }); -}); -// TODO: This test need to be defined -// describe('Wazuh API - /elastic/visualizations/{tab}/{pattern}', () => { -// test('[200] Returns an array with the index patterns', () => { -// const options = buildAxiosOptions('get', '/elastic/visualizations/{tab}/{pattern}'); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(typeof response.data).toBe('object'); -// expect(Array.isArray(response.data.data)).toBe(true); -// response.data.data.forEach(indexPattern => { -// expect(Array.isArray(indexPattern.id)).toBe('string'); -// expect(Array.isArray(indexPattern.title)).toBe('string'); -// }) -// }); -// }); -// }); + // TODO: This test need to be defined + // describe('Wazuh API - /elastic/visualizations/{tab}/{pattern}', () => { + // test('[200] Returns an array with the index patterns', () => { + // const options = buildAxiosOptions('get', '/elastic/visualizations/{tab}/{pattern}'); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(typeof response.data).toBe('object'); + // expect(Array.isArray(response.data.data)).toBe(true); + // response.data.data.forEach(indexPattern => { + // expect(Array.isArray(indexPattern.id)).toBe('string'); + // expect(Array.isArray(indexPattern.title)).toBe('string'); + // }) + // }); + // }); + // }); -// TODO: This test need to be defined -// describe('Wazuh API - /elastic/visualizations/{tab}/{pattern}', () => { -// test('[200] Returns an array with the index patterns', () => { -// const options = buildAxiosOptions('post', '/elastic/visualizations/{tab}/{pattern}'); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(typeof response.data).toBe('object'); -// expect(Array.isArray(response.data.data)).toBe(true); -// response.data.data.forEach(indexPattern => { -// expect(Array.isArray(indexPattern.id)).toBe('string'); -// expect(Array.isArray(indexPattern.title)).toBe('string'); -// }) -// }); -// }); -// }); + // TODO: This test need to be defined + // describe('Wazuh API - /elastic/visualizations/{tab}/{pattern}', () => { + // test('[200] Returns an array with the index patterns', () => { + // const options = buildAxiosOptions('post', '/elastic/visualizations/{tab}/{pattern}'); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(typeof response.data).toBe('object'); + // expect(Array.isArray(response.data.data)).toBe(true); + // response.data.data.forEach(indexPattern => { + // expect(Array.isArray(indexPattern.id)).toBe('string'); + // expect(Array.isArray(indexPattern.title)).toBe('string'); + // }) + // }); + // }); + // }); -describe('Wazuh API - /elastic/template/{pattern}', () => { - test('[200] Check if there is some template with the pattern', () => { - const options = buildAxiosOptions('get', '/elastic/template/wazuh-alerts-*'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.status).toBe('boolean'); - expect(typeof response.data.data).toBe('string'); + describe('Wazuh API - /elastic/template/{pattern}', () => { + test('[200] Check if there is some template with the pattern', () => { + const options = buildAxiosOptions('get', '/elastic/template/wazuh-alerts-*'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.status).toBe('boolean'); + expect(typeof response.data.data).toBe('string'); + }); }); }); -}); -describe('Wazuh API - /elastic/index-patterns/{pattern}', () => { - test('[200] Check if there an index pattern with the pattern', () => { - const options = buildAxiosOptions('get', '/elastic/index-patterns/wazuh-alerts-*'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.status).toBe('boolean'); - expect(typeof response.data.data).toBe('string'); + describe('Wazuh API - /elastic/index-patterns/{pattern}', () => { + test('[200] Check if there an index pattern with the pattern', () => { + const options = buildAxiosOptions('get', '/elastic/index-patterns/wazuh-alerts-*'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.status).toBe('boolean'); + expect(typeof response.data.data).toBe('string'); + }); }); }); -}); -// TODO: This test need to be defined -// describe('Wazuh API - /elastic/top/{mode}/{cluster}/{field}/{pattern}', () => { -// test('[200] Check if there an index pattern with the pattern', () => { -// const options = buildAxiosOptions('get', '/elastic/top/{mode}/{cluster}/{field}/{pattern}'); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// }); -// }); -// }); + // TODO: This test need to be defined + // describe('Wazuh API - /elastic/top/{mode}/{cluster}/{field}/{pattern}', () => { + // test('[200] Check if there an index pattern with the pattern', () => { + // const options = buildAxiosOptions('get', '/elastic/top/{mode}/{cluster}/{field}/{pattern}'); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // }); + // }); + // }); -describe('Wazuh API - /elastic/samplealerts', () => { - test('[200] Check if there an sample data indices', () => { - const options = buildAxiosOptions('get', '/elastic/samplealerts'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.sampleAlertsInstalled).toBe('boolean'); + describe('Wazuh API - /elastic/samplealerts', () => { + test('[200] Check if there an sample data indices', () => { + const options = buildAxiosOptions('get', '/elastic/samplealerts'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.sampleAlertsInstalled).toBe('boolean'); + }); }); }); -}); -describe('Wazuh API - /elastic/samplealerts/{category}', () => { - test('[200] Check if there an sample data index of Security category', () => { - const options = buildAxiosOptions('get', '/elastic/samplealerts/security'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.exists).toBe('boolean'); + describe('Wazuh API - /elastic/samplealerts/{category}', () => { + test('[200] Check if there an sample data index of Security category', () => { + const options = buildAxiosOptions('get', '/elastic/samplealerts/security'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.exists).toBe('boolean'); + }); }); - }); - test('[200] Check if there an sample data index of Audit and Policy monitoring category', () => { - const options = buildAxiosOptions('get', '/elastic/samplealerts/auditing-policy-monitoring'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.exists).toBe('boolean'); + test('[200] Check if there an sample data index of Audit and Policy monitoring category', () => { + const options = buildAxiosOptions('get', '/elastic/samplealerts/auditing-policy-monitoring'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.exists).toBe('boolean'); + }); }); - }); - test('[200] Check if there an sample data index of Theard detection category', () => { - const options = buildAxiosOptions('get', '/elastic/samplealerts/threat-detection'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.exists).toBe('boolean'); + test('[200] Check if there an sample data index of Theard detection category', () => { + const options = buildAxiosOptions('get', '/elastic/samplealerts/threat-detection'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.exists).toBe('boolean'); + }); }); }); -}); -describe('Wazuh API - /elastic/samplealerts/{category}', () => { - let userToken = null; - beforeAll(() => { - const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { - idHost: 'default' + describe('Wazuh API - /elastic/samplealerts/{category}', () => { + let userToken = null; + beforeAll(() => { + const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { + idHost: 'default', + }); + return axios(optionsAuthenticate) + .then((response) => { + userToken = response.data.token; + return response.data.token; + }) + .catch((error) => {}); }); - return axios(optionsAuthenticate).then(response => { - userToken = response.data.token; - return response.data.token; - }).catch(error => {}); - }); - test('[200] Create sample alers of Security category', () => { - const options = buildAxiosOptions('post', '/elastic/samplealerts/security', { - - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Create sample alers of Security category', () => { + const options = buildAxiosOptions( + 'post', + '/elastic/samplealerts/security', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.alertCount).toBe('number'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.alertCount).toBe('number'); - }) - }); - test('[200] Create sample alers of Audit and Policy monitoring category', () => { - const options = buildAxiosOptions('post', '/elastic/samplealerts/auditing-policy-monitoring', { - - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Create sample alers of Audit and Policy monitoring category', () => { + const options = buildAxiosOptions( + 'post', + '/elastic/samplealerts/auditing-policy-monitoring', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.alertCount).toBe('number'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.alertCount).toBe('number'); - }) - }); - test('[200] Create sample alers of Theard detection category', () => { - const options = buildAxiosOptions('post', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Create sample alers of Theard detection category', () => { + const options = buildAxiosOptions( + 'post', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.alertCount).toBe('number'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.alertCount).toBe('number'); - }) - }); - test('[401] Create sample alers of Theard detection category without token cookie', () => { - const options = buildAxiosOptions('post', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-api=default;` + test('[401] Create sample alers of Theard detection category without token cookie', () => { + const options = buildAxiosOptions( + 'post', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-api=default;`, + } + ); + return axios(options).catch((error) => { + expect(error.response.status).toBe(401); + }); }); - return axios(options).catch(error => { - expect(error.response.status).toBe(401); - }) - }); - test('[401] Create sample alers of Theard detection category without api cookie', () => { - const options = buildAxiosOptions('post', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-token=${userToken};` + test('[401] Create sample alers of Theard detection category without api cookie', () => { + const options = buildAxiosOptions( + 'post', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-token=${userToken};`, + } + ); + return axios(options).catch((error) => { + expect(error.response.status).toBe(401); + }); }); - return axios(options).catch(error => { - expect(error.response.status).toBe(401); - }) - }); - - test('[200] Delete sample alers of Security category', () => { - const options = buildAxiosOptions('delete', '/elastic/samplealerts/security', { - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Delete sample alers of Security category', () => { + const options = buildAxiosOptions( + 'delete', + '/elastic/samplealerts/security', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.result).toBe('string'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.result).toBe('string'); - }) - }); - test('[200] Delete sample alers of Audit and Policy monitoring category', () => { - const options = buildAxiosOptions('delete', '/elastic/samplealerts/auditing-policy-monitoring', { - - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Delete sample alers of Audit and Policy monitoring category', () => { + const options = buildAxiosOptions( + 'delete', + '/elastic/samplealerts/auditing-policy-monitoring', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.result).toBe('string'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.result).toBe('string'); - }) - }); - test('[200] Delete sample alers of Theard detection category', () => { - const options = buildAxiosOptions('delete', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + test('[200] Delete sample alers of Theard detection category', () => { + const options = buildAxiosOptions( + 'delete', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.index).toBe('string'); + expect(typeof response.data.result).toBe('string'); + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.index).toBe('string'); - expect(typeof response.data.result).toBe('string'); - }) - }); - test('[200] Delete sample alers of Theard detection category without token cookie', () => { - const options = buildAxiosOptions('delete', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-api=default;` + test('[200] Delete sample alers of Theard detection category without token cookie', () => { + const options = buildAxiosOptions( + 'delete', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-api=default;`, + } + ); + return axios(options).catch((error) => { + expect(error.response.status).toBe(401); + }); }); - return axios(options).catch(error => { - expect(error.response.status).toBe(401); - }) - }); - test('[200] Delete sample alers of Theard detection category without api cookie', () => { - const options = buildAxiosOptions('delete', '/elastic/samplealerts/threat-detection', { - - }, - { - cookie: `wz-token=${userToken}` + test('[200] Delete sample alers of Theard detection category without api cookie', () => { + const options = buildAxiosOptions( + 'delete', + '/elastic/samplealerts/threat-detection', + {}, + { + cookie: `wz-token=${userToken}`, + } + ); + return axios(options).catch((error) => { + expect(error.response.status).toBe(401); + }); }); - return axios(options).catch(error => { - expect(error.response.status).toBe(401); - }) }); -}); -// TODO: This test need to be defined -// describe('Wazuh API - /elastic/alerts', () => { -// test('[200] Check if there an sample data index of Security category', () => { -// const options = buildAxiosOptions('get', '/elastic/alerts'); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(typeof response.data.index).toBe('string'); -// expect(typeof response.data.exists).toBe('boolean'); -// }); -// }); -// }); + // TODO: This test need to be defined + // describe('Wazuh API - /elastic/alerts', () => { + // test('[200] Check if there an sample data index of Security category', () => { + // const options = buildAxiosOptions('get', '/elastic/alerts'); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(typeof response.data.index).toBe('string'); + // expect(typeof response.data.exists).toBe('boolean'); + // }); + // }); + // }); -describe('Wazuh API - /elastic/statistics', () => { - test('[200] Check if there an sample data index of Security category', () => { - const options = buildAxiosOptions('get', '/elastic/statistics'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data).toBe('boolean'); + describe('Wazuh API - /elastic/statistics', () => { + test('[200] Check if there an sample data index of Security category', () => { + const options = buildAxiosOptions('get', '/elastic/statistics'); + return axios(options).then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data).toBe('boolean'); + }); }); }); -}); \ No newline at end of file +}); diff --git a/server/routes/wazuh-hosts.test.ts b/server/routes/wazuh-hosts.test.ts index 791dbcb1ed..8f806a1c0c 100644 --- a/server/routes/wazuh-hosts.test.ts +++ b/server/routes/wazuh-hosts.test.ts @@ -11,52 +11,54 @@ function buildAxiosOptions(method: string, path: string, data: any = {}, headers }; }; -describe('Wazuh API - /hosts/apis', () => { - test('[200] Returns the available API hosts', () => { - const options = buildAxiosOptions('get', '/hosts/apis'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(Array.isArray(response.data)).toBe(true); - response.data.forEach(host => { - expect(typeof host.url).toBe('string'); - expect(typeof host.port).toBe('number'); - expect(typeof host.username).toBe('string'); - expect(typeof host.run_as).toBe('boolean'); - expect(typeof host.id).toBe('string'); - expect(typeof host.cluster_info).toBe('object'); - expect(typeof host.cluster_info.status).toBe('string'); - expect(typeof host.cluster_info.manager).toBe('string'); - expect(typeof host.cluster_info.node).toBe('string'); - expect(typeof host.cluster_info.cluster).toBe('string'); - expect(typeof host.extensions).toBe('object'); - expect(typeof host.allow_run_as).toBe('number'); - }) - }).catch(error => {throw error}) +describe.skip('Wazuh Host', () => { + describe('Wazuh API - /hosts/apis', () => { + test('[200] Returns the available API hosts', () => { + const options = buildAxiosOptions('get', '/hosts/apis'); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(Array.isArray(response.data)).toBe(true); + response.data.forEach(host => { + expect(typeof host.url).toBe('string'); + expect(typeof host.port).toBe('number'); + expect(typeof host.username).toBe('string'); + expect(typeof host.run_as).toBe('boolean'); + expect(typeof host.id).toBe('string'); + expect(typeof host.cluster_info).toBe('object'); + expect(typeof host.cluster_info.status).toBe('string'); + expect(typeof host.cluster_info.manager).toBe('string'); + expect(typeof host.cluster_info.node).toBe('string'); + expect(typeof host.cluster_info.cluster).toBe('string'); + expect(typeof host.extensions).toBe('object'); + expect(typeof host.allow_run_as).toBe('number'); + }) + }).catch(error => {throw error}) + }); }); -}); -describe('Wazuh API - /hosts/update-hostname', () => { - test('[200] Update the cluster info for a API host', () => { - const options = buildAxiosOptions('put', '/hosts/update-hostname/default', { - cluster_info: { - status: 'enabled', - manager: 'wazuh-test', - node: 'node-test', - cluster: 'cluster-test' - } + describe('Wazuh API - /hosts/update-hostname', () => { + test('[200] Update the cluster info for a API host', () => { + const options = buildAxiosOptions('put', '/hosts/update-hostname/default', { + cluster_info: { + status: 'enabled', + manager: 'wazuh-test', + node: 'node-test', + cluster: 'cluster-test' + } + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + }).catch(error => {throw error}) }); - return axios(options).then(response => { - expect(response.status).toBe(200); - }).catch(error => {throw error}) }); }); -//TODO: Do the test to remove-orphan-entries endpoint -// describe('Wazuh API - /hosts/remove-orphan-entries', () => { -// test('[200] Remove orphan entries', () => { -// const options = buildAxiosOptions('post', '/hosts/remove-orphan-entries'); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// }).catch(error => {throw error}) -// }); -// }); \ No newline at end of file + //TODO: Do the test to remove-orphan-entries endpoint + // describe('Wazuh API - /hosts/remove-orphan-entries', () => { + // test('[200] Remove orphan entries', () => { + // const options = buildAxiosOptions('post', '/hosts/remove-orphan-entries'); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // }).catch(error => {throw error}) + // }); + // }); diff --git a/server/routes/wazuh-reporting.test.ts b/server/routes/wazuh-reporting.test.ts index 091efc1bca..8c1573d1fd 100644 --- a/server/routes/wazuh-reporting.test.ts +++ b/server/routes/wazuh-reporting.test.ts @@ -11,83 +11,85 @@ function buildAxiosOptions(method: string, path: string, data: any = {}, headers }; }; -describe('Wazuh API - /reports', () => { - test('[200] Returns the available reports for user', () => { - const options = buildAxiosOptions('get', '/reports', {}, { - cookie: 'wz-user=elastic' +describe.skip('Wazuh Reporting', () => { + describe('Wazuh API - /reports', () => { + test('[200] Returns the available reports for user', () => { + const options = buildAxiosOptions('get', '/reports', {}, { + cookie: 'wz-user=elastic' + }); + return axios(options).then(response => { + expect(response.status).toBe(200); + expect(Array.isArray(response.data.reports)).toBe(true); + }).catch(error => {throw error}) }); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(Array.isArray(response.data.reports)).toBe(true); - }).catch(error => {throw error}) }); -}); -//TODO: do the test for these endpoints -// describe('Wazuh API - /reports/{name}', () => { -// test('[200] Returns the available reports for user and name', () => { -// const options = buildAxiosOptions('get', '/reports/wazuh-report.pdf', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(Array.isArray(response.data.reports)).toBe(true); -// }).catch(error => {throw error}) -// }); + //TODO: do the test for these endpoints + // describe('Wazuh API - /reports/{name}', () => { + // test('[200] Returns the available reports for user and name', () => { + // const options = buildAxiosOptions('get', '/reports/wazuh-report.pdf', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(Array.isArray(response.data.reports)).toBe(true); + // }).catch(error => {throw error}) + // }); -// test('[200] Returns the available reports for user and name', () => { -// const options = buildAxiosOptions('delete', '/reports/wazuh-report.pdf', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// }).catch(error => {throw error}) -// }); -// }); + // test('[200] Returns the available reports for user and name', () => { + // const options = buildAxiosOptions('delete', '/reports/wazuh-report.pdf', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // }).catch(error => {throw error}) + // }); + // }); -// describe('Wazuh API - /reports/modules/{moduleID}', () => { -// test('[200] Generates a modules reports the available reports for user and name', () => { -// const options = buildAxiosOptions('post', '/reports/modules/{moduleID}', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(Array.isArray(response.data.reports)).toBe(true); -// }).catch(error => {throw error}) -// }); -// }); + // describe('Wazuh API - /reports/modules/{moduleID}', () => { + // test('[200] Generates a modules reports the available reports for user and name', () => { + // const options = buildAxiosOptions('post', '/reports/modules/{moduleID}', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(Array.isArray(response.data.reports)).toBe(true); + // }).catch(error => {throw error}) + // }); + // }); -// describe('Wazuh API - /reports/groups/{groupID}', () => { -// test('[200] Generates a modules reports the available reports for user and name', () => { -// const options = buildAxiosOptions('post', '/reports/groups/{groupID}', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(Array.isArray(response.data.reports)).toBe(true); -// }).catch(error => {throw error}) -// }); -// }); + // describe('Wazuh API - /reports/groups/{groupID}', () => { + // test('[200] Generates a modules reports the available reports for user and name', () => { + // const options = buildAxiosOptions('post', '/reports/groups/{groupID}', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(Array.isArray(response.data.reports)).toBe(true); + // }).catch(error => {throw error}) + // }); + // }); -// describe('Wazuh API - /reports/agents/{agentID}', () => { -// test('[200] Generates a modules reports the available reports for user and name', () => { -// const options = buildAxiosOptions('post', '/reports/agents/{agentID}', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(Array.isArray(response.data.reports)).toBe(true); -// }).catch(error => {throw error}) -// }); -// }); + // describe('Wazuh API - /reports/agents/{agentID}', () => { + // test('[200] Generates a modules reports the available reports for user and name', () => { + // const options = buildAxiosOptions('post', '/reports/agents/{agentID}', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(Array.isArray(response.data.reports)).toBe(true); + // }).catch(error => {throw error}) + // }); + // }); -// describe('Wazuh API - /reports/agents/{agentID}/inventory', () => { -// test('[200] Generates a modules reports the available reports for user and name', () => { -// const options = buildAxiosOptions('post', '/reports/agents/{agentID}/inventory', {}, { -// cookie: 'wz-user=elastic' -// }); -// return axios(options).then(response => { -// expect(response.status).toBe(200); -// expect(Array.isArray(response.data.reports)).toBe(true); -// }).catch(error => {throw error}) -// }); -// }); \ No newline at end of file + // describe('Wazuh API - /reports/agents/{agentID}/inventory', () => { + // test('[200] Generates a modules reports the available reports for user and name', () => { + // const options = buildAxiosOptions('post', '/reports/agents/{agentID}/inventory', {}, { + // cookie: 'wz-user=elastic' + // }); + // return axios(options).then(response => { + // expect(response.status).toBe(200); + // expect(Array.isArray(response.data.reports)).toBe(true); + // }).catch(error => {throw error}) + // }); + // }); +}); diff --git a/server/routes/wazuh-utils/ui-logs.test.ts b/server/routes/wazuh-utils/ui-logs.test.ts index 292d4cd214..c4503c55c6 100644 --- a/server/routes/wazuh-utils/ui-logs.test.ts +++ b/server/routes/wazuh-utils/ui-logs.test.ts @@ -11,32 +11,34 @@ const buildAxiosOptions = (method: string, path: string, data: any = {}, headers }; }; -describe('Wazuh API - /utils/logs/ui', () => { - test('[200] Get UI Logs', () => { - const options = buildAxiosOptions('get', '/utils/logs/ui'); - return axios(options) - .then((response) => { - expect(response.status).toBe(200); - }) - .catch((error) => { - throw error; - }); - }, 6000); -}); +describe.skip('Wazuh UI Logs', () => { + describe('Wazuh API - /utils/logs/ui', () => { + it('[200] Get UI Logs', () => { + const options = buildAxiosOptions('get', '/utils/logs/ui'); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); + }, 6000); + }); -describe('Wazuh API - /utils/logs/ui', () => { - test('[200] Create UI Logs', () => { - const options = buildAxiosOptions('post', '/utils/logs/ui', { - message: 'Message test', - level: 'error', - location: 'Location', - }); - return axios(options) - .then((response) => { - expect(response.status).toBe(200); - }) - .catch((error) => { - throw error; + describe('Wazuh API - /utils/logs/ui', () => { + it('[200] Create UI Logs', () => { + const options = buildAxiosOptions('post', '/utils/logs/ui', { + message: 'Message test', + level: 'error', + location: 'Location', }); - }, 6000); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); + }, 6000); + }); }); diff --git a/server/routes/wazuh-utils/wazuh-utils.test.ts b/server/routes/wazuh-utils/wazuh-utils.test.ts index 3c11f31725..07c4f1e1f7 100644 --- a/server/routes/wazuh-utils/wazuh-utils.test.ts +++ b/server/routes/wazuh-utils/wazuh-utils.test.ts @@ -2,56 +2,74 @@ // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-utils import axios from 'axios'; -function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}){ +function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}) { return { method: method, headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, url: `http://localhost:5601${path}`, - data: data + data: data, }; -}; +} -describe('Wazuh API - /utils/configuration', () => { - test('[200] Returns the app configuration', () => { - const options = buildAxiosOptions('get', '/utils/configuration'); - return axios(options).then(response => { - expect(response.status).toBe(200); - expect(typeof response.data.data).toBe('object'); - expect(typeof response.data.data.hosts).toBe('object'); - }).catch(error => {throw error}) +describe.skip('Wazuh API - utils', () => { + describe('Wazuh API - /utils/configuration', () => { + test('[200] Returns the app configuration', () => { + const options = buildAxiosOptions('get', '/utils/configuration'); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + expect(typeof response.data.data).toBe('object'); + expect(typeof response.data.data.hosts).toBe('object'); + }) + .catch((error) => { + throw error; + }); + }); }); -}); -describe('Wazuh API - /utils/configuration', () => { - let userToken = null; - beforeAll(() => { - const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { - idHost: 'default' + describe('Wazuh API - /utils/configuration', () => { + let userToken = null; + beforeAll(() => { + const optionsAuthenticate = buildAxiosOptions('post', '/api/login', { + idHost: 'default', + }); + return axios(optionsAuthenticate).then((response) => { + userToken = response.data.token; + return response.data.token; + }); }); - return axios(optionsAuthenticate).then(response => { - userToken = response.data.token; - return response.data.token; + test('[200] Updates the app configuration', () => { + const options = buildAxiosOptions( + 'put', + '/utils/configuration', + { + key: 'logs.level', + value: 'debug', + }, + { + cookie: `wz-token=${userToken};wz-api=default;`, + } + ); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); }); }); - test('[200] Updates the app configuration', () => { - const options = buildAxiosOptions('put', '/utils/configuration', { - key: 'logs.level', - value: 'debug' - }, - { - cookie: `wz-token=${userToken};wz-api=default;` + + describe('Wazuh API - /utils/logs', () => { + test('[200] Get the app logs', () => { + const options = buildAxiosOptions('get', '/utils/logs'); + return axios(options) + .then((response) => { + expect(response.status).toBe(200); + }) + .catch((error) => { + throw error; + }); }); - return axios(options).then(response => { - expect(response.status).toBe(200); - }).catch(error => {throw error}) }); }); - -describe('Wazuh API - /utils/logs', () => { - test('[200] Get the app logs', () => { - const options = buildAxiosOptions('get', '/utils/logs'); - return axios(options).then(response => { - expect(response.status).toBe(200); - }).catch(error => {throw error}) - }); -}); \ No newline at end of file diff --git a/server/start/cron-scheduler/save-document.test.ts b/server/start/cron-scheduler/save-document.test.ts index 93bdd12313..735f50dbe0 100644 --- a/server/start/cron-scheduler/save-document.test.ts +++ b/server/start/cron-scheduler/save-document.test.ts @@ -1,28 +1,27 @@ import { SaveDocument } from './index'; import elasticsearch from 'elasticsearch'; -jest.mock('elasticsearch'); describe('SaveDocument', () => { const fakeServer = { - plugins:{ - elasticsearch:{ - getCluster: data => { + core: { + elasticsearch: { + client: { asInternalUser: '' }, + getCluster: (data) => { return { - clusterClient:{client: new elasticsearch.Client({})}, + clusterClient: { client: new elasticsearch.Client({}) }, callWithRequest: Function, callWithInternalUser: Function, - } - } - } - } - } + }; + }, + }, + }, + }; let savedDocument: SaveDocument; beforeEach(() => { - savedDocument = new SaveDocument(fakeServer) + savedDocument = new SaveDocument(fakeServer); }); - test('should be create the object SavedDocument', () => { + it('should be create the object SavedDocument', () => { expect(savedDocument).toBeInstanceOf(SaveDocument); }); - -}); \ No newline at end of file +}); diff --git a/server/start/cron-scheduler/scheduler-job.test.ts b/server/start/cron-scheduler/scheduler-job.test.ts index 0128bb0f6d..51b5bec0db 100644 --- a/server/start/cron-scheduler/scheduler-job.test.ts +++ b/server/start/cron-scheduler/scheduler-job.test.ts @@ -1,10 +1,7 @@ //@ts-nocheck -import { - SchedulerJob, - IApi, - jobs -} from './index'; +import { IApi, jobs, SchedulerJob } from './index'; import { WazuhHostsCtrl } from '../../controllers/wazuh-hosts'; + jest.mock('../../controllers/wazuh-hosts'); jest.mock('./save-document'); jest.mock('./predefined-jobs', () => ({ @@ -24,103 +21,117 @@ jest.mock('./predefined-jobs', () => ({ params: {}, interval: '* */2 * * *', index: 'manager-status', - } - } + }, + }, })); describe('SchedulerJob', () => { - const oneApi = [{ - url: 'https://localhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'default', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } - }]; - const twoApi = [ - { - url: 'https://localhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'internal', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } - }, - { - url: 'https://externalhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'external', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } - }, - ]; - const threeApi = [ - { - url: 'https://localhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'internal', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } - }, - { - url: 'https://externalhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'external', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } + const oneApi = { + body: [ + { + url: 'https://localhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'default', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + ], + }; + const twoApi = { + body: [ + { + url: 'https://localhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'internal', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + { + url: 'https://externalhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'external', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + ], + }; + const threeApi = { + body: [ + { + url: 'https://localhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'internal', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + { + url: 'https://externalhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'external', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + { + url: 'https://externalhost', + port: 55000, + username: 'wazuh', + password: 'wazuh', + id: 'experimental', + cluster_info: { + status: 'disabled', + manager: 'master', + node: 'node01', + cluster: 'Disabled', + }, + }, + ], + }; + const mockContext = { + wazuh: { + logger: { logger: {} }, + api: { client: [Object] }, }, - { - url: 'https://externalhost', - port: 55000, - username: 'wazuh', - password: 'wazuh', - id: 'experimental', - cluster_info: { - status: 'disabled', - manager: 'master', - node: 'node01', - cluster: 'Disabled' - } - }, - ]; + }; let schedulerJob: SchedulerJob; beforeEach(() => { - schedulerJob = new SchedulerJob('testJob1', {}); + schedulerJob = new SchedulerJob('testJob1', mockContext); }); afterEach(() => { jest.clearAllMocks(); - }) + }); it('should job is assigned ', () => { expect(schedulerJob).toBeInstanceOf(SchedulerJob); @@ -130,27 +141,26 @@ describe('SchedulerJob', () => { it('should get API object when no specified the `apis` parameter on the job object', async () => { WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(oneApi); - const apis: IApi[] = await schedulerJob.getApiObjects(); expect(apis).not.toBeUndefined(); expect(apis).not.toBeFalsy(); - expect(apis).toEqual(oneApi); + expect(apis).toEqual(oneApi.body); }); it('should get all API objects when no specified the `apis` parameter on the job object', async () => { - WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi) + WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi); const apis: IApi[] = await schedulerJob.getApiObjects(); expect(apis).not.toBeUndefined(); expect(apis).not.toBeFalsy(); - expect(apis).toEqual(twoApi); + expect(apis).toEqual(twoApi.body); }); it('should get one of two API object when specified the id in `apis` parameter on the job object', async () => { - WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi) + WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(twoApi); jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: ['internal'] }; const apis: IApi[] = await schedulerJob.getApiObjects(); - const filteredTwoApi = twoApi.filter(item => item.id === 'internal') + const filteredTwoApi = twoApi.body.filter((item) => item.id === 'internal'); expect(apis).not.toBeUndefined(); expect(apis).not.toBeFalsy(); @@ -158,11 +168,11 @@ describe('SchedulerJob', () => { }); it('should get two of three API object when specified the id in `apis` parameter on the job object', async () => { - WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi) + WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi); const selectedApis = ['internal', 'external']; jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: selectedApis }; const apis: IApi[] = await schedulerJob.getApiObjects(); - const filteredThreeApi = threeApi.filter(item => selectedApis.includes(item.id)) + const filteredThreeApi = threeApi.body.filter((item) => selectedApis.includes(item.id)); expect(apis).not.toBeUndefined(); expect(apis).not.toBeFalsy(); @@ -170,18 +180,19 @@ describe('SchedulerJob', () => { }); it('should throw an exception when no get APIs', async () => { - WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue([]) - await expect(schedulerJob.getApiObjects()).rejects.toEqual( - { error: 10001, message: 'No Wazuh host configured in wazuh.yml' } - ); + WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue({ body: [] }); + await expect(schedulerJob.getApiObjects()).rejects.toEqual({ + error: 10001, + message: 'No Wazuh host configured in wazuh.yml', + }); }); it('should throw an exception when no match API', async () => { - WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi) + WazuhHostsCtrl.prototype.getHostsEntries.mockResolvedValue(threeApi); jobs[schedulerJob.jobName] = { ...jobs[schedulerJob.jobName], apis: ['unkown'] }; - await expect(schedulerJob.getApiObjects()).rejects.toEqual( - { error: 10002, message: 'No host was found with the indicated ID' } - ); + await expect(schedulerJob.getApiObjects()).rejects.toEqual({ + error: 10002, + message: 'No host was found with the indicated ID', + }); }); - -}) \ No newline at end of file +}); diff --git a/server/start/queue/queue.test.ts b/server/start/queue/queue.test.ts index abedecadcd..5ab0b6bb70 100644 --- a/server/start/queue/queue.test.ts +++ b/server/start/queue/queue.test.ts @@ -1,25 +1,28 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/jobs/queue.test -import { addJobToQueue, queue, jobQueueRun } from './queue'; + +import { addJobToQueue, jobQueueRun, queue } from './index'; jest.setTimeout(60000); // Set jest timeout to 60000ms to allow the job is run and removed from queue -describe('Queue jobs', () => { - test('Add job to queue', () => { +describe.skip('Queue jobs', () => { + it('Add job to queue', () => { addJobToQueue({ - startAt: new Date((new Date()).getTime() + 15000), - run: () => {} + startAt: new Date(new Date().getTime() + 15000), + run: () => {}, }); expect(queue.length).toBe(1); }); - test('Sure that job was executed and removed from queue', () => { + it('Sure that job was executed and removed from queue', () => { jobQueueRun({}); - function wait(time: number){ - return new Promise(res => {setTimeout(res, time)}); + function wait(time: number) { + return new Promise((res) => { + setTimeout(res, time); + }); } return wait(50000).then(() => { expect(queue.length).toBe(0); - }) + }); }); -}); \ No newline at end of file +}); diff --git a/test/jest/config.js b/test/jest/config.js index 4a1fe3f72a..c12025c204 100644 --- a/test/jest/config.js +++ b/test/jest/config.js @@ -12,12 +12,10 @@ export default { `${kbnDir}/node_modules` ], collectCoverageFrom: [ - `${kbnDir}/packages/kbn-ui-framework/src/components/**/*.js`, - `${kbnDir}/!packages/kbn-ui-framework/src/components/index.js`, - `${kbnDir}/!packages/kbn-ui-framework/src/components/**/*/index.js`, - `${kbnDir}/packages/kbn-ui-framework/src/services/**/*.js`, - `${kbnDir}/!packages/kbn-ui-framework/src/services/index.js`, - `${kbnDir}/!packages/kbn-ui-framework/src/services/**/*/index.js`, + "./common/**/*.{js,jsx,ts,tsx}", + "./public/**/*.{js,jsx,ts,tsx}", + "./server/**/*.{js,jsx,ts,tsx}", + "./!**/node_modules/**", ], moduleNameMapper: { '^ui/(.*)': `${kbnDir}/src/ui/public/$1`, @@ -28,9 +26,12 @@ export default { `${kbnDir}/src/dev/jest/setup/babel_polyfill.js`, `${kbnDir}/src/dev/jest/setup/enzyme.js`, ], - coverageDirectory: `${kbnDir}/target/jest-coverage`, + collectCoverage: true, + coverageDirectory: `./target/test-coverage`, coverageReporters: [ 'html', + 'text-summary', + 'json-summary' ], globals: { 'ts-jest': { @@ -49,7 +50,6 @@ export default { ], testMatch: [ '**/*.test.{js,ts,tsx}', - '**/*{js,ts,tsx}' ], transform: { '^.+\\.js$': `${kbnDir}/src/dev/jest/babel_transform.js`, @@ -65,4 +65,4 @@ export default { 'default', `${kbnDir}/src/dev/jest/junit_reporter.js`, ], -}; \ No newline at end of file +}; From c5e56c0daebe7b013dddedb7a3059355f5b55b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 13 Jul 2021 11:46:17 +0200 Subject: [PATCH 064/493] Created Sample Data component --- .../components/visualize/components/index.ts | 3 +- .../visualize/components/sample-data.js | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 public/components/visualize/components/sample-data.js diff --git a/public/components/visualize/components/index.ts b/public/components/visualize/components/index.ts index b3309212bf..f4cd522aa5 100644 --- a/public/components/visualize/components/index.ts +++ b/public/components/visualize/components/index.ts @@ -10,4 +10,5 @@ * Find more information about this on the LICENSE file. */ -export { SecurityAlerts } from './security-alerts'; \ No newline at end of file +export { SecurityAlerts } from './security-alerts'; +export { SampleData } from './sample-data'; \ No newline at end of file diff --git a/public/components/visualize/components/sample-data.js b/public/components/visualize/components/sample-data.js new file mode 100644 index 0000000000..9c95be1729 --- /dev/null +++ b/public/components/visualize/components/sample-data.js @@ -0,0 +1,70 @@ +/* + * Wazuh app - React component for Visualize. + * Copyright (C) 2015-2021 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 { EuiCallOut, EuiLink } from '@elastic/eui'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { useState, useEffect } from 'react'; +import { WzRequest } from '../../../react-services'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; + +export const SampleData = ({ context = 'sample-data', ...props }) => { + const [isSampleData, setIsSampleData] = useState(false); + + useEffect(async () => { + try { + const thereAreSampleAlerts = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})) + .data.sampleAlertsInstalled; + setIsSampleData(thereAreSampleAlerts); + } catch (error) { + const options = { + context, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + }, [ + setIsSampleData, + context, + UI_ERROR_SEVERITIES, + UI_LOGGER_LEVELS, + getErrorOrchestrator, + WzRequest, + ]); + if (isSampleData) { + return ( + <EuiCallOut + title="This dashboard contains sample data" + color="warning" + iconType="alert" + style={{ margin: '0 8px 16px 8px' }} + data-test-subject="sample-data-callout" + {...props} + > + <p> + The data displayed may contain sample alerts. Go ººººº + <EuiLink href="#/settings?tab=sample_data" aria-label="go to configure sample data"> + here + </EuiLink>{' '} + to configure the sample data. + </p> + </EuiCallOut> + ); + } else { + return null; + } +}; From d4a371c10631c35b6c0868ac03b837cf2e25c2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 13 Jul 2021 11:46:28 +0200 Subject: [PATCH 065/493] Used sample data component in wz-visualize --- public/components/visualize/wz-visualize.js | 28 +++------------------ 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index a3f1cfc4a0..fd2dadf027 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -41,6 +41,7 @@ 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'; +import { SampleData } from './components'; const visHandler = new VisHandlers(); @@ -52,7 +53,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this.state = { visualizations: !!props.isAgent ? agentVisualizations : visualizations, expandedVis: false, - thereAreSampleAlerts: false, hasRefreshedKnownFields: false, refreshingKnownFields: [], refreshingIndex: true @@ -101,25 +101,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class ]; } } - - // Check if there is sample alerts installed - try { - const thereAreSampleAlerts = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})) - .data.sampleAlertsInstalled; - this._isMount && this.setState({ thereAreSampleAlerts }); - } catch (error) { - const options = { - context: `${WzVisualize.name}.componentDidMount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } } async componentDidUpdate(prevProps) { @@ -275,11 +256,8 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class return ( <Fragment> {/* Sample alerts Callout */} - {this.state.thereAreSampleAlerts && this.props.resultState === 'ready' && ( - <EuiCallOut title='This dashboard contains sample data' color='warning' iconType='alert' style={{ margin: '0 8px 16px 8px' }}> - <p>The data displayed may contain sample alerts. Go <EuiLink href='#/settings?tab=sample_data' aria-label='go to configure sample data'>here</EuiLink> to configure the sample data. - </p> - </EuiCallOut> + {this.props.resultState === 'ready' && ( + <SampleData context={`${WzVisualize.name}-sample-data`} /> )} {this.props.resultState === 'none' && ( From aeecc86d5a4cfb7806b10ebb5967d5aa261e449c Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Tue, 13 Jul 2021 09:59:05 -0300 Subject: [PATCH 066/493] Added try-catch strategy in Reporting section (#3427) * Implemented new try-catch strategy * Updated context * Added test file and snapshot * Updated CHANGELOG * Requested changes * Removed blank space in message * Updated error title and message Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 1 + .../reporting-main.test.tsx.snap | 962 ++++++++++++++++++ .../reporting/reporting-main.test.tsx | 33 + .../management/reporting/reporting-table.js | 102 +- .../reporting/utils/actions-buttons-main.js | 16 +- .../reporting/utils/reporting-handler.js | 4 +- 6 files changed, 1077 insertions(+), 41 deletions(-) create mode 100644 public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap create mode 100644 public/controllers/management/components/management/reporting/reporting-main.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index cc557c2785..5a4d889c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) +- Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) ### Changed diff --git a/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap b/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap new file mode 100644 index 0000000000..7e4c4c48a2 --- /dev/null +++ b/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap @@ -0,0 +1,962 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Reporting component renders correctly to match the snapshot 1`] = ` +<WzReporting> + <WzReduxProvider> + <Provider + store={ + Object { + "dispatch": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + Symbol(observable): [Function], + } + } + > + <WzReportingOverview> + <EuiPage + style={ + Object { + "background": "transparent", + } + } + > + <div + className="euiPage" + style={ + Object { + "background": "transparent", + } + } + > + <EuiPanel> + <div + className="euiPanel euiPanel--paddingMedium" + > + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle> + <h2 + className="euiTitle euiTitle--medium" + > + Reporting + </h2> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiFlexItem> + <Connect(WzReportingActionButtons)> + <WzReportingActionButtons + state={ + Object { + "isLoading": true, + "isProcessing": true, + "itemList": Array [], + "showModal": false, + } + } + updateIsProcessing={[Function]} + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiButtonEmpty + iconType="refresh" + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="refresh" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="refresh" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + Refresh + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </EuiFlexItem> + </WzReportingActionButtons> + </Connect(WzReportingActionButtons)> + </div> + </EuiFlexGroup> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiText + color="subdued" + style={ + Object { + "paddingBottom": "15px", + } + } + > + <div + className="euiText euiText--medium" + style={ + Object { + "paddingBottom": "15px", + } + } + > + <EuiTextColor + color="subdued" + component="div" + > + <div + className="euiTextColor euiTextColor--subdued" + > + From here you can check all your reports. + </div> + </EuiTextColor> + </div> + </EuiText> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <Connect(WzReportingTable)> + <WzReportingTable + state={ + Object { + "isLoading": true, + "isProcessing": true, + "itemList": Array [], + "showModal": false, + } + } + updateIsProcessing={[Function]} + updateListItemsForRemove={[Function]} + updateShowModal={[Function]} + > + <div> + <EuiInMemoryTable + columns={ + Array [ + Object { + "align": "left", + "field": "name", + "name": "File", + "sortable": true, + }, + Object { + "field": "size", + "name": "Size", + "render": [Function], + "sortable": true, + }, + Object { + "field": "date", + "name": "Created", + "render": [Function], + "sortable": true, + }, + Object { + "align": "left", + "name": "Actions", + "render": [Function], + }, + ] + } + items={Array []} + loading={true} + message={null} + pagination={true} + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + } + } + sorting={ + Object { + "sort": Object { + "direction": "desc", + "field": "date", + }, + } + } + tableLayout="fixed" + > + <div> + <EuiSearchBar + box={ + Object { + "incremental": true, + } + } + onChange={[Function]} + > + <EuiFlexGroup + alignItems="center" + gutterSize="m" + wrap={true} + > + <div + className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap" + > + <EuiFlexItem + className="euiSearchBar__searchHolder" + grow={true} + > + <div + className="euiFlexItem euiSearchBar__searchHolder" + > + <EuiSearchBox + incremental={true} + isInvalid={false} + onSearch={[Function]} + placeholder="Search..." + query="" + > + <EuiFieldSearch + aria-label="This is a search bar. As you type, the results lower in the page will automatically filter." + compressed={false} + defaultValue="" + fullWidth={true} + incremental={true} + inputRef={[Function]} + isClearable={true} + isInvalid={false} + isLoading={false} + onSearch={[Function]} + placeholder="Search..." + > + <EuiFormControlLayout + compressed={false} + fullWidth={true} + icon="search" + isLoading={false} + > + <div + className="euiFormControlLayout euiFormControlLayout--fullWidth" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl + isInvalid={false} + > + <input + aria-label="This is a search bar. As you type, the results lower in the page will automatically filter." + className="euiFieldSearch euiFieldSearch--fullWidth euiFieldSearch-isClearable" + defaultValue="" + onKeyUp={[Function]} + placeholder="Search..." + type="search" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons + icon="search" + isLoading={false} + > + <div + className="euiFormControlLayoutIcons" + > + <EuiFormControlLayoutCustomIcon + type="search" + > + <span + className="euiFormControlLayoutCustomIcon" + > + <EuiIcon + aria-hidden="true" + className="euiFormControlLayoutCustomIcon__icon" + type="search" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + </span> + </EuiFormControlLayoutCustomIcon> + </div> + </EuiFormControlLayoutIcons> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldSearch> + </EuiSearchBox> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </EuiSearchBar> + <EuiSpacer + size="l" + > + <div + className="euiSpacer euiSpacer--l" + /> + </EuiSpacer> + <EuiBasicTable + columns={ + Array [ + Object { + "align": "left", + "field": "name", + "name": "File", + "sortable": true, + }, + Object { + "field": "size", + "name": "Size", + "render": [Function], + "sortable": true, + }, + Object { + "field": "date", + "name": "Created", + "render": [Function], + "sortable": true, + }, + Object { + "align": "left", + "name": "Actions", + "render": [Function], + }, + ] + } + items={Array []} + loading={true} + noItemsMessage={null} + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], + "totalItemCount": 0, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": Object { + "direction": "desc", + "field": "Created", + }, + } + } + tableLayout="fixed" + > + <div + className="euiBasicTable euiBasicTable-loading" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiTableSortMobile + items={ + Array [ + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_name_0", + "name": "File", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_size_1", + "name": "Size", + "onSort": [Function], + }, + Object { + "isSortAscending": false, + "isSorted": true, + "key": "_data_s_date_2", + "name": "Created", + "onSort": [Function], + }, + ] + } + > + <div + className="euiTableSortMobile" + > + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + /> + </EuiButtonEmpty> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownRight" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <button + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="right" + iconType="arrowDown" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="arrowDown" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + > + Sorting + </EuiI18n> + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </div> + </EuiTableSortMobile> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="__table_bd685711-da9b-11eb-80ce-ff701c08216e" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="__table_bd685711-da9b-11eb-80ce-ff701c08216e" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + allowNeutralSort={true} + data-test-subj="tableHeaderCell_name_0" + isSorted={false} + key="_data_h_name_0" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_name_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "File", + } + } + > + <span + className="euiTableCellContent__text" + title="File" + > + File + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + allowNeutralSort={true} + data-test-subj="tableHeaderCell_size_1" + isSorted={false} + key="_data_h_size_1" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_size_1" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Size", + } + } + > + <span + className="euiTableCellContent__text" + title="Size" + > + Size + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + allowNeutralSort={true} + data-test-subj="tableHeaderCell_date_2" + isSortAscending={false} + isSorted={true} + key="_data_h_date_2" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="descending" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_date_2" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton euiTableHeaderButton-isSorted" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "descending", + "innerText": "Created", + } + } + > + <span + className="euiTableCellContent__text" + title="Created; Sorted in descending order" + > + Created + </span> + </EuiI18n> + </EuiInnerText> + <EuiI18n + default="Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.sortedAriaLabel" + values={ + Object { + "ariaSortValue": "descending", + } + } + > + <EuiIcon + aria-label="Sorted in descending order" + className="euiTableSortIcon" + size="m" + type="sortDown" + > + <EuiIconEmpty + aria-hidden={true} + aria-label="Sorted in descending order" + className="euiIcon euiIcon--medium euiIcon-isLoading euiTableSortIcon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + aria-label="Sorted in descending order" + className="euiIcon euiIcon--medium euiIcon-isLoading euiTableSortIcon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + </EuiI18n> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to unsort" + token="euiTableHeaderCell.clickForUnsort" + > + Click to unsort + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_Actions_3" + key="_computed_column_h_3" + > + <th + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_Actions_3" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent" + > + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Actions" + > + Actions + </span> + </EuiInnerText> + </div> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody> + <tbody> + <EuiTableRow> + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="center" + colSpan={4} + isMobileFullWidth={true} + > + <td + className="euiTableRowCell euiTableRowCell--isMobileFullWidth" + colSpan={4} + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + className="euiTableCellContent__text" + /> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> + </div> + </EuiInMemoryTable> + </div> + </WzReportingTable> + </Connect(WzReportingTable)> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiPanel> + </div> + </EuiPage> + </WzReportingOverview> + </Provider> + </WzReduxProvider> +</WzReporting> +`; diff --git a/public/controllers/management/components/management/reporting/reporting-main.test.tsx b/public/controllers/management/components/management/reporting/reporting-main.test.tsx new file mode 100644 index 0000000000..9c097e8eb8 --- /dev/null +++ b/public/controllers/management/components/management/reporting/reporting-main.test.tsx @@ -0,0 +1,33 @@ +/* + * Wazuh app - React test for Reporting component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import WzReporting from './reporting-main'; + +jest.mock('../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +describe('Reporting component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(<WzReporting />); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/reporting/reporting-table.js b/public/controllers/management/components/management/reporting/reporting-table.js index d01d2847b7..504f50e9fc 100644 --- a/public/controllers/management/components/management/reporting/reporting-table.js +++ b/public/controllers/management/components/management/reporting/reporting-table.js @@ -10,32 +10,31 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import { - EuiInMemoryTable, - EuiCallOut, - EuiOverlayMask, - EuiConfirmModal -} from '@elastic/eui'; +import { EuiInMemoryTable, EuiCallOut, EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; import { connect } from 'react-redux'; import ReportingHandler from './utils/reporting-handler'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; import { updateIsProcessing, updateShowModal, - updateListItemsForRemove + updateListItemsForRemove, } from '../../../../../redux/actions/reportingActions'; import ReportingColums from './utils/columns-main'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzReportingTable extends Component { _isMounted = false; constructor(props) { super(props); this.state = { items: [], - isLoading: false + isLoading: false, }; this.reportingHandler = ReportingHandler; @@ -47,8 +46,22 @@ class WzReportingTable extends Component { } async componentDidUpdate() { - if (this.props.state.isProcessing && this._isMounted) { - await this.getItems(); + try { + if (this.props.state.isProcessing && this._isMounted) { + await this.getItems(); + } + } catch (error) { + const options = { + context: `${WzReportingTable.name}.componentDidUpdate`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -65,12 +78,12 @@ class WzReportingTable extends Component { const items = ((rawItems || {}).data || {}).reports || []; this.setState({ items, - isProcessing: false + isProcessing: false, }); this.props.updateIsProcessing(false); } catch (error) { this.props.updateIsProcessing(false); - return Promise.reject(error); + throw error; } } @@ -81,11 +94,30 @@ class WzReportingTable extends Component { const columns = this.reportingColumns.columns; const message = isLoading ? null : 'No results...'; + const deleteReport = (itemList) => { + try { + this.deleteReport(itemList); + this.props.updateShowModal(false); + } catch (error) { + const options = { + context: `${WzReportingTable.name}.deleteReport`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error deleting report`, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + const sorting = { sort: { field: 'date', - direction: 'desc' - } + direction: 'desc', + }, }; if (!error) { @@ -106,10 +138,7 @@ class WzReportingTable extends Component { <EuiConfirmModal title={`Delete report?`} onCancel={() => this.props.updateShowModal(false)} - onConfirm={() => { - this.deleteReport(itemList); - this.props.updateShowModal(false); - }} + onConfirm={() => deleteReport(itemList)} cancelButtonText="Cancel" confirmButtonText="Delete" defaultFocusedButton="cancel" @@ -129,39 +158,36 @@ class WzReportingTable extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; async deleteReport(items) { - const results = items.map(async (item, i) => { - await this.reportingHandler.deleteReport(item.name); - }); - - Promise.all(results).then(completed => { + try { + const results = items.map(async (item, i) => { + await this.reportingHandler.deleteReport(item.name); + }); + await Promise.all(results); this.props.updateIsProcessing(true); this.showToast('success', 'Success', 'Deleted successfully', 3000); - }); + } catch (error) { + throw error; + } } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.reportingReducers + state: state.reportingReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateIsProcessing: isProcessing => - dispatch(updateIsProcessing(isProcessing)), - updateShowModal: showModal => dispatch(updateShowModal(showModal)), - updateListItemsForRemove: itemList => - dispatch(updateListItemsForRemove(itemList)) + updateIsProcessing: (isProcessing) => dispatch(updateIsProcessing(isProcessing)), + updateShowModal: (showModal) => dispatch(updateShowModal(showModal)), + updateListItemsForRemove: (itemList) => dispatch(updateListItemsForRemove(itemList)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzReportingTable); +export default connect(mapStateToProps, mapDispatchToProps)(WzReportingTable); diff --git a/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js b/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js index 028f6c03e1..31b84b830c 100644 --- a/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js +++ b/public/controllers/management/components/management/reporting/utils/actions-buttons-main.js @@ -17,6 +17,10 @@ import { connect } from 'react-redux'; import { updateIsProcessing } from '../../../../../../redux/actions/reportingActions'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; + class WzReportingActionButtons extends Component { _isMounted = false; @@ -41,7 +45,17 @@ class WzReportingActionButtons extends Component { try { this.props.updateIsProcessing(true); } catch (error) { - return Promise.reject(error); + const options = { + context: `${WzReportingActionButtons.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/controllers/management/components/management/reporting/utils/reporting-handler.js b/public/controllers/management/components/management/reporting/utils/reporting-handler.js index c276a0d710..3fa15dae82 100644 --- a/public/controllers/management/components/management/reporting/utils/reporting-handler.js +++ b/public/controllers/management/components/management/reporting/utils/reporting-handler.js @@ -22,7 +22,7 @@ export default class ReportingHandler { const result = await WzRequest.genericReq('GET', '/reports', {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -39,7 +39,7 @@ export default class ReportingHandler { ); return result; } catch (error) { - return Promise.reject(error); + throw error; } } } From f2191549aab943b2052b80b3fbb0575c3c19ad02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Tue, 13 Jul 2021 15:03:02 +0200 Subject: [PATCH 067/493] Implement/try catch in Components > Overview (#3442) * Techniques and mitre * Resources * Update changelog --- CHANGELOG.md | 1 + .../compliance-table/compliance-table.tsx | 276 +++--- .../components/overview/metrics/metrics.tsx | 708 +++++++++----- .../mitre/components/tactics/tactics.tsx | 253 ++--- .../flyout-technique/flyout-technique.tsx | 18 +- .../components/techniques/techniques.tsx | 888 ++++++++++-------- public/components/overview/mitre/mitre.tsx | 26 +- .../mitre_attack_intelligence/resource.tsx | 19 +- .../resource_detail_references_table.tsx | 19 +- .../mitre_attack_intelligence/resources.tsx | 16 + 10 files changed, 1330 insertions(+), 894 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a4d889c95..6fc9119e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) ### Changed diff --git a/public/components/overview/compliance-table/compliance-table.tsx b/public/components/overview/compliance-table/compliance-table.tsx index 4e62a70fc7..2f3430708e 100644 --- a/public/components/overview/compliance-table/compliance-table.tsx +++ b/public/components/overview/compliance-table/compliance-table.tsx @@ -9,12 +9,8 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react' -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import React, { Component } from 'react'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SearchBar, FilterManager } from '../../../../../../src/plugins/data/public/'; import { I18nProvider } from '@kbn/i18n/react'; @@ -29,28 +25,30 @@ import { nistRequirementsFile } from '../../../../common/compliance-requirements import { tscRequirementsFile } from '../../../../common/compliance-requirements/tsc-requirements'; import { KbnSearchBar } from '../../kbn-search-bar'; import { getDataPlugin } from '../../../kibana-services'; - +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 class ComplianceTable extends Component { _isMount = false; timefilter: { - getTime(): any - setTime(time: any): void - _history: { history: { items: { from: string, to: string }[] } } + getTime(): any; + setTime(time: any): void; + _history: { history: { items: { from: string; to: string }[] } }; }; KibanaServices: { [key: string]: any }; filterManager: FilterManager; indexPattern: any; state: { - selectedRequirement: string, - flyoutOn: boolean, - filterParams: IFilterParams, - complianceObject: object, - descriptions: object, - loadingAlerts: boolean - selectedRequirements: object, - } + selectedRequirement: string; + flyoutOn: boolean; + filterParams: IFilterParams; + complianceObject: object; + descriptions: object; + loadingAlerts: boolean; + selectedRequirements: object; + }; props: any; @@ -60,7 +58,7 @@ export class ComplianceTable extends Component { this.filterManager = this.KibanaServices.filterManager; this.timefilter = this.KibanaServices.timefilter.timefilter; this.state = { - selectedRequirement: "", + selectedRequirement: '', flyoutOn: true, complianceObject: {}, descriptions: {}, @@ -71,7 +69,7 @@ export class ComplianceTable extends Component { query: { language: 'kuery', query: '' }, time: this.timefilter.getTime(), }, - } + }; this.onChangeSelectedRequirements.bind(this); this.onQuerySubmit.bind(this); @@ -81,7 +79,7 @@ export class ComplianceTable extends Component { async componentDidMount() { this._isMount = true; this.filtersSubscriber = this.filterManager.getUpdates$().subscribe(() => { - this.onFiltersUpdated(this.filterManager.getFilters()) + this.onFiltersUpdated(this.filterManager.getFilters()); }); this.indexPattern = await getIndexPattern(); this.buildComplianceObject(); @@ -91,7 +89,6 @@ export class ComplianceTable extends Component { this.filtersSubscriber.unsubscribe(); } - buildComplianceObject() { try { let complianceRequirements = {}; @@ -99,8 +96,8 @@ export class ComplianceTable extends Component { let selectedRequirements = {}; // all enabled by default if (this.props.section === 'pci') { descriptions = pciRequirementsFile; - Object.keys(pciRequirementsFile).forEach(item => { - const currentRequirement = item.split(".")[0]; + Object.keys(pciRequirementsFile).forEach((item) => { + const currentRequirement = item.split('.')[0]; if (complianceRequirements[currentRequirement]) { complianceRequirements[currentRequirement].push(item); } else { @@ -112,8 +109,8 @@ export class ComplianceTable extends Component { } if (this.props.section === 'gdpr') { descriptions = gdprRequirementsFile; - Object.keys(gdprRequirementsFile).forEach(item => { - const currentRequirement = item.split("_")[0]; + Object.keys(gdprRequirementsFile).forEach((item) => { + const currentRequirement = item.split('_')[0]; if (complianceRequirements[currentRequirement]) { complianceRequirements[currentRequirement].push(item); } else { @@ -121,13 +118,14 @@ export class ComplianceTable extends Component { complianceRequirements[currentRequirement] = []; complianceRequirements[currentRequirement].push(item); } - }); //forEach + }); //forEach } if (this.props.section === 'hipaa') { descriptions = hipaaRequirementsFile; - Object.keys(hipaaRequirementsFile).forEach(item => { - const currentRequirement = item.split(".")[0] + "." + item.split(".")[1] + "." + item.split(".")[2]; + Object.keys(hipaaRequirementsFile).forEach((item) => { + const currentRequirement = + item.split('.')[0] + '.' + item.split('.')[1] + '.' + item.split('.')[2]; if (complianceRequirements[currentRequirement]) { complianceRequirements[currentRequirement].push(item); } else { @@ -135,13 +133,13 @@ export class ComplianceTable extends Component { complianceRequirements[currentRequirement] = []; complianceRequirements[currentRequirement].push(item); } - }); //forEach + }); //forEach } if (this.props.section === 'nist') { descriptions = nistRequirementsFile; - Object.keys(nistRequirementsFile).forEach(item => { - const currentRequirement = item.split(".")[0]; + Object.keys(nistRequirementsFile).forEach((item) => { + const currentRequirement = item.split('.')[0]; if (complianceRequirements[currentRequirement]) { complianceRequirements[currentRequirement].push(item); } else { @@ -149,12 +147,12 @@ export class ComplianceTable extends Component { complianceRequirements[currentRequirement] = []; complianceRequirements[currentRequirement].push(item); } - }); //forEach + }); //forEach } if (this.props.section === 'tsc') { descriptions = tscRequirementsFile; - Object.keys(tscRequirementsFile).forEach(item => { - const currentRequirement = item.split(".")[0]; + Object.keys(tscRequirementsFile).forEach((item) => { + const currentRequirement = item.split('.')[0]; if (complianceRequirements[currentRequirement]) { complianceRequirements[currentRequirement].push(item); } else { @@ -162,37 +160,47 @@ export class ComplianceTable extends Component { complianceRequirements[currentRequirement] = []; complianceRequirements[currentRequirement].push(item); } - }); //forEach + }); //forEach } - this._isMount && this.setState({ complianceObject: complianceRequirements, selectedRequirements, descriptions }, () => this.getRequirementsCount()); - } catch (err) { - // TODO ADD showToast - /*this.showToast( - 'danger', - 'Error', - `Compliance (${this.props.section}) data could not be fetched: ${err}`, - 3000 - );*/ + this._isMount && + this.setState( + { complianceObject: complianceRequirements, selectedRequirements, descriptions }, + () => this.getRequirementsCount() + ); + } catch (error) { + const options = { + context: `${ComplianceTable.name}.buildComplianceObject`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Compliance (${this.props.section}) data could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); } } onChangeSelectedRequirements = (selectedRequirements) => { this.setState({ selectedRequirements }); - } + }; - onQuerySubmit = (payload: { dateRange: TimeRange, query: Query | undefined }) => { + onQuerySubmit = (payload: { dateRange: TimeRange; query: Query | undefined }) => { const { dateRange, query } = payload; const { filters } = this.state.filterParams; - const filterParams:IFilterParams = { time: dateRange, filters, query}; + const filterParams: IFilterParams = { time: dateRange, filters, query }; this.setState({ filterParams, loadingAlerts: true }); - } + }; onFiltersUpdated = (filters: []) => { - const { time, query} = this.state.filterParams; - const filterParams = {time, query, filters}; + const { time, query } = this.state.filterParams; + const filterParams = { time, query, filters }; this.setState({ filterParams, loadingAlerts: true }); - } + }; async componentDidUpdate(prevProps) { const { filterParams, loadingAlerts } = this.state; @@ -204,52 +212,58 @@ export class ComplianceTable extends Component { async getRequirementsCount() { try { const { filterParams } = this.state; - if (!this.indexPattern) { return; } - let fieldAgg = ""; - if (this.props.section === "pci") - fieldAgg = "rule.pci_dss"; - if (this.props.section === "gdpr") - fieldAgg = "rule.gdpr"; - if (this.props.section === "hipaa") - fieldAgg = "rule.hipaa"; - if (this.props.section === "nist") - fieldAgg = "rule.nist_800_53"; - if (this.props.section === "tsc") - fieldAgg = "rule.tsc"; + if (!this.indexPattern) { + return; + } + let fieldAgg = ''; + if (this.props.section === 'pci') fieldAgg = 'rule.pci_dss'; + if (this.props.section === 'gdpr') fieldAgg = 'rule.gdpr'; + if (this.props.section === 'hipaa') fieldAgg = 'rule.hipaa'; + if (this.props.section === 'nist') fieldAgg = 'rule.nist_800_53'; + if (this.props.section === 'tsc') fieldAgg = 'rule.tsc'; const aggs = { tactics: { terms: { field: fieldAgg, size: 100, - } - } - } + }, + }, + }; // TODO: use `status` and `statusText` to show errors // @ts-ignore - const { data, status, statusText, } = await getElasticAlerts(this.indexPattern, filterParams, aggs); + const { data, status, statusText } = await getElasticAlerts( + this.indexPattern, + filterParams, + aggs + ); const { buckets } = data.aggregations.tactics; /*if(firstTime){ this.initTactics(buckets); // top tactics are checked on component mount }*/ - this._isMount && this.setState({ requirementsCount: buckets, loadingAlerts: false, firstTime: false }); - - } catch (err) { - /* this.showToast( - 'danger', - 'Error', - `Mitre alerts could not be fetched: ${err}`, - 3000 - );*/ - this.setState({ loadingAlerts: false }) + this._isMount && + this.setState({ requirementsCount: buckets, loadingAlerts: false, firstTime: false }); + } catch (error) { + const options = { + context: `${ComplianceTable.name}.buildComplianceObject`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre alerts could not be fetched:`, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ loadingAlerts: false }); } - } - onChangeFlyout = (flyoutOn) => { this.setState({ flyoutOn }); - } + }; closeFlyout() { this.setState({ flyoutOn: false }); @@ -258,55 +272,63 @@ export class ComplianceTable extends Component { showFlyout(requirement) { this.setState({ selectedRequirement: requirement, - flyoutOn: true - }) + flyoutOn: true, + }); } - - render() { const { complianceObject, loadingAlerts } = this.state; - return (<div> - <EuiFlexGroup> - <EuiFlexItem> - <div className='wz-discover hide-filter-control' > - <KbnSearchBar - onQuerySubmit={this.onQuerySubmit} - onFiltersUpdated={this.onFiltersUpdated} - isLoading={loadingAlerts} /> - </div> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiFlexGroup style={{ margin: '0 8px' }}> - <EuiFlexItem style={{ width: "calc(100% - 24px)" }}> - <EuiPanel paddingSize="none"> - {!!Object.keys(complianceObject).length && this.state.filterParams.time.from !== "init" && - <EuiFlexGroup> - <EuiFlexItem grow={false} style={{ width: "15%", minWidth: 145, maxHeight: "calc(100vh - 320px)", overflowX: "hidden" }}> - <ComplianceRequirements - indexPattern={this.indexPattern} - section={this.props.section} - onChangeSelectedRequirements={this.onChangeSelectedRequirements} - {...this.state} /> - </EuiFlexItem> - <EuiFlexItem style={{ width: "15%" }}> - <ComplianceSubrequirements - indexPattern={this.indexPattern} - filters={this.state.filterParams} - section={this.props.section} - onSelectedTabChanged={(id) => this.props.onSelectedTabChanged(id)} - {...this.state} /> - </EuiFlexItem> - </EuiFlexGroup> - } - </EuiPanel> - - </EuiFlexItem> - </EuiFlexGroup> - - </div> - ) + return ( + <div> + <EuiFlexGroup> + <EuiFlexItem> + <div className="wz-discover hide-filter-control"> + <KbnSearchBar + onQuerySubmit={this.onQuerySubmit} + onFiltersUpdated={this.onFiltersUpdated} + isLoading={loadingAlerts} + /> + </div> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiFlexGroup style={{ margin: '0 8px' }}> + <EuiFlexItem style={{ width: 'calc(100% - 24px)' }}> + <EuiPanel paddingSize="none"> + {!!Object.keys(complianceObject).length && + this.state.filterParams.time.from !== 'init' && ( + <EuiFlexGroup> + <EuiFlexItem + grow={false} + style={{ + width: '15%', + minWidth: 145, + maxHeight: 'calc(100vh - 320px)', + overflowX: 'hidden', + }} + > + <ComplianceRequirements + indexPattern={this.indexPattern} + section={this.props.section} + onChangeSelectedRequirements={this.onChangeSelectedRequirements} + {...this.state} + /> + </EuiFlexItem> + <EuiFlexItem style={{ width: '15%' }}> + <ComplianceSubrequirements + indexPattern={this.indexPattern} + filters={this.state.filterParams} + section={this.props.section} + onSelectedTabChanged={(id) => this.props.onSelectedTabChanged(id)} + {...this.state} + /> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </div> + ); } } - diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index 939bc5da9b..ec26d6210f 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -9,307 +9,539 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react' -import { - EuiStat, - EuiFlexGroup, - EuiFlexItem, - EuiToolTip, -} from '@elastic/eui'; +import React, { Component } from 'react'; +import { EuiStat, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FilterManager } from '../../../../../../src/plugins/data/public/'; -import { buildRangeFilter, buildPhrasesFilter,buildPhraseFilter, buildExistsFilter} from '../../../../../../src/plugins/data/common'; +import { + buildRangeFilter, + buildPhrasesFilter, + buildPhraseFilter, + buildExistsFilter, +} from '../../../../../../src/plugins/data/common'; //@ts-ignore import { getElasticAlerts, getIndexPattern } from '../mitre/lib'; -import { ModulesHelper } from '../../common/modules/modules-helper' +import { ModulesHelper } from '../../common/modules/modules-helper'; import { getDataPlugin } from '../../../kibana-services'; import { withAllowedAgents } from '../../common/hocs/withAllowedAgents'; import { formatUIDate } from '../../../react-services'; +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 Metrics = withAllowedAgents(class Metrics extends Component { - _isMount = false; - timefilter: { - getTime(): any - setTime(time: any): void - _history: { history: { items: { from: string, to: string }[] } } - }; +export const Metrics = withAllowedAgents( + class Metrics extends Component { + _isMount = false; + timefilter: { + getTime(): any; + setTime(time: any): void; + _history: { history: { items: { from: string; to: string }[] } }; + }; - KibanaServices: { [key: string]: any }; - filterManager: FilterManager; - indexPattern: any; - state: { - resultState: string, - results: object, - metricsOnClicks: object, - loading: boolean, - filterParams: object, - } - metricsList: object; + KibanaServices: { [key: string]: any }; + filterManager: FilterManager; + indexPattern: any; + state: { + resultState: string; + results: object; + metricsOnClicks: object; + loading: boolean; + filterParams: object; + }; + metricsList: object; - props: any; + props: any; - constructor(props) { - super(props); - this.KibanaServices = getDataPlugin().query; - this.filterManager = this.KibanaServices.filterManager; - this.timefilter = this.KibanaServices.timefilter.timefilter; - this.state = { - resultState: "", - results: {}, - metricsOnClicks: {}, - loading: true, - filterParams: { - filters: [], - query: { language: 'kuery', query: '' }, - time: {from: 'init', to: 'init'}, - }, - } - this.modulesHelper = ModulesHelper; - this.stats = <></>; + constructor(props) { + super(props); + this.KibanaServices = getDataPlugin().query; + this.filterManager = this.KibanaServices.filterManager; + this.timefilter = this.KibanaServices.timefilter.timefilter; + this.state = { + resultState: '', + results: {}, + metricsOnClicks: {}, + loading: true, + filterParams: { + filters: [], + query: { language: 'kuery', query: '' }, + time: { from: 'init', to: 'init' }, + }, + }; + this.modulesHelper = ModulesHelper; + this.stats = <></>; - this.metricsList = { - general: [ - { name: "Total", type: "total" }, - { name: "Level 12 or above alerts", type: "range", gte: "12", lt: null, field: "rule.level", color: "danger"}, //null = infinite - { name: "Authentication failure", type: "phrases", values: ["win_authentication_failed", "authentication_failed", "authentication_failures"], field: "rule.groups",color: "danger"}, - { name: "Authentication success", type: "phrase", value: "authentication_success", field: "rule.groups", color: "secondary"}, - ], - vuls: [ - { name: "Critical Severity Alerts", type: "phrase", value: "Critical", field: "data.vulnerability.severity", color: "danger"}, - { name: "High Severity Alerts", type: "phrase", value: "High", field: "data.vulnerability.severity"}, - { name: "Medium Severity Alerts", type: "phrase", value: "Medium", field: "data.vulnerability.severity", color: "secondary"}, - { name: "Low Severity Alerts", type: "phrase", value: "Low", field: "data.vulnerability.severity", color: "subdued"}, - ], - virustotal: [ - { name: "Total malicious", type: "phrase", value: "1", field: "data.virustotal.malicious", color: "danger"}, - { name: "Total positives", type: "phrase", value: "0", negate: true, field: "data.virustotal.positives", color: "secondary"}, - { name: "Total", type: "total"}, - ], - osquery: [ - { name: "Agents reporting", type: "unique-count", field: "agent.id"}, - ], - ciscat: [ - { name: "Last scan not checked", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.notchecked" } } } } }, color: "subdued"}, - { name: "Last scan pass", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.pass" } } } } }, color: "secondary"}, - { name: "Last scan score", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.score" } } } } }}, - { name: "Last scan date", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.timestamp" } } } } }, color: "secondary", transformValue:formatUIDate}, - { name: "Last scan errors", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.error" } } } } }, color: "danger"}, - { name: "Last scan fails", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.fail" } } } } }, color: "danger"}, - { name: "Last scan unknown", type: "custom", filter: { phrase: "ciscat", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.cis.unknown" } } } } }, color: "subdued"}, - ], - oscap: [ - { name: "Last scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}}, - { name: "Highest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "secondary"}, - { name: "Lowest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "asc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "danger"}, - ] + this.metricsList = { + general: [ + { name: 'Total', type: 'total' }, + { + name: 'Level 12 or above alerts', + type: 'range', + gte: '12', + lt: null, + field: 'rule.level', + color: 'danger', + }, //null = infinite + { + name: 'Authentication failure', + type: 'phrases', + values: [ + 'win_authentication_failed', + 'authentication_failed', + 'authentication_failures', + ], + field: 'rule.groups', + color: 'danger', + }, + { + name: 'Authentication success', + type: 'phrase', + value: 'authentication_success', + field: 'rule.groups', + color: 'secondary', + }, + ], + vuls: [ + { + name: 'Critical Severity Alerts', + type: 'phrase', + value: 'Critical', + field: 'data.vulnerability.severity', + color: 'danger', + }, + { + name: 'High Severity Alerts', + type: 'phrase', + value: 'High', + field: 'data.vulnerability.severity', + }, + { + name: 'Medium Severity Alerts', + type: 'phrase', + value: 'Medium', + field: 'data.vulnerability.severity', + color: 'secondary', + }, + { + name: 'Low Severity Alerts', + type: 'phrase', + value: 'Low', + field: 'data.vulnerability.severity', + color: 'subdued', + }, + ], + virustotal: [ + { + name: 'Total malicious', + type: 'phrase', + value: '1', + field: 'data.virustotal.malicious', + color: 'danger', + }, + { + name: 'Total positives', + type: 'phrase', + value: '0', + negate: true, + field: 'data.virustotal.positives', + color: 'secondary', + }, + { name: 'Total', type: 'total' }, + ], + osquery: [{ name: 'Agents reporting', type: 'unique-count', field: 'agent.id' }], + ciscat: [ + { + name: 'Last scan not checked', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.notchecked' } } }, + }, + }, + color: 'subdued', + }, + { + name: 'Last scan pass', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.pass' } } }, + }, + }, + color: 'secondary', + }, + { + name: 'Last scan score', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.score' } } }, + }, + }, + }, + { + name: 'Last scan date', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.timestamp' } } }, + }, + }, + color: 'secondary', + transformValue: formatUIDate, + }, + { + name: 'Last scan errors', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.error' } } }, + }, + }, + color: 'danger', + }, + { + name: 'Last scan fails', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.fail' } } }, + }, + }, + color: 'danger', + }, + { + name: 'Last scan unknown', + type: 'custom', + filter: { phrase: 'ciscat', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.cis.unknown' } } }, + }, + }, + color: 'subdued', + }, + ], + oscap: [ + { + name: 'Last scan score', + type: 'custom', + filter: { phrase: 'oscap-report', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'timestamp', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.oscap.scan.score' } } }, + }, + }, + }, + { + name: 'Highest scan score', + type: 'custom', + filter: { phrase: 'oscap-report', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'data.oscap.scan.score', order: { _term: 'desc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.oscap.scan.score' } } }, + }, + }, + color: 'secondary', + }, + { + name: 'Lowest scan score', + type: 'custom', + filter: { phrase: 'oscap-report', field: 'rule.groups' }, + agg: { + customAggResult: { + terms: { field: 'data.oscap.scan.score', order: { _term: 'asc' }, size: 1 }, + aggs: { aggResult: { terms: { field: 'data.oscap.scan.score' } } }, + }, + }, + color: 'danger', + }, + ], + }; } - } - async componentDidMount() { - this.indexPattern = await getIndexPattern(); - this.scope = await this.modulesHelper.getDiscoverScope(); - this._isMount = true; - this.buildMetric(); - } + async componentDidMount() { + this.indexPattern = await getIndexPattern(); + this.scope = await this.modulesHelper.getDiscoverScope(); + this._isMount = true; + this.buildMetric(); + } - async getResults(filterParams, aggs = {}){ - const params = {size: 0, track_total_hits: true}; - const result = await getElasticAlerts(this.indexPattern, filterParams, aggs, params ); - let totalHits = 0; - if(Object.keys(aggs).length){ - const agg = (((result.data || {}).aggregations || {})); - if(agg && agg.customAggResult){ //CUSTOM AGG - totalHits = ((((((agg.customAggResult || {}).buckets || [])[0] || {}).aggResult || {}).buckets || [])[0] || {}).key || 0; - }else{ - totalHits = (agg.aggResult || {}).value || 0; + async getResults(filterParams, aggs = {}) { + const params = { size: 0, track_total_hits: true }; + const result = await getElasticAlerts(this.indexPattern, filterParams, aggs, params); + let totalHits = 0; + if (Object.keys(aggs).length) { + const agg = (result.data || {}).aggregations || {}; + if (agg && agg.customAggResult) { + //CUSTOM AGG + totalHits = + ( + (((((agg.customAggResult || {}).buckets || [])[0] || {}).aggResult || {}).buckets || + [])[0] || {} + ).key || 0; + } else { + totalHits = (agg.aggResult || {}).value || 0; + } + } else { + totalHits = (((result.data || {}).hits || {}).total || {}).value || 0; } - }else{ - totalHits = (((result.data || {}).hits || {}).total || {}).value || 0; - } - return totalHits - } + return totalHits; + } - buildMetric(){ - if(!this.metricsList[this.props.section] || !this._isMount) return <></>; - const newFilters = this.filterManager.getFilters(); - const searchBarQuery = this.scope.state.query; - const newTime = this.timefilter.getTime(); + async buildMetric() { + if (!this.metricsList[this.props.section] || !this._isMount) return <></>; + const newFilters = this.filterManager.getFilters(); + const searchBarQuery = this.scope.state.query; + const newTime = this.timefilter.getTime(); const filterParams = {}; - filterParams["time"] = this.timefilter.getTime(); - filterParams["query"] = searchBarQuery; - filterParams["filters"] = this.filterManager.getFilters(); - this.props.filterAllowedAgents && filterParams["filters"].push(this.props.filterAllowedAgents); - this.setState({filterParams, loading: true}); + filterParams['time'] = this.timefilter.getTime(); + filterParams['query'] = searchBarQuery; + filterParams['filters'] = this.filterManager.getFilters(); + this.props.filterAllowedAgents && + filterParams['filters'].push(this.props.filterAllowedAgents); + this.setState({ filterParams, loading: true }); const newOnClick = {}; - - const result = this.metricsList[this.props.section].map(async(item)=> { + + const result = this.metricsList[this.props.section].map(async (item) => { let filters = []; - if(item.type === 'range'){ + if (item.type === 'range') { const results = {}; const rangeFilterParams = {}; - const valuesArray = {gte: item.gte, lt: item.lt}; + const valuesArray = { gte: item.gte, lt: item.lt }; const filters = { - ...buildRangeFilter({ name: item.field, type: "integer" }, valuesArray, this.indexPattern), - "$state": { "store": "appState" } - } - rangeFilterParams["filters"] = [...filterParams["filters"]] - rangeFilterParams["time"] = filterParams["time"]; - rangeFilterParams["query"] = filterParams["query"]; - rangeFilterParams["filters"].push(filters) - newOnClick[item.name] = () => {this.filterManager.addFilters(filters)}; + ...buildRangeFilter( + { name: item.field, type: 'integer' }, + valuesArray, + this.indexPattern + ), + $state: { store: 'appState' }, + }; + rangeFilterParams['filters'] = [...filterParams['filters']]; + rangeFilterParams['time'] = filterParams['time']; + rangeFilterParams['query'] = filterParams['query']; + rangeFilterParams['filters'].push(filters); + newOnClick[item.name] = () => { + this.filterManager.addFilters(filters); + }; results[item.name] = await this.getResults(rangeFilterParams); - return results ; - }else if(item.type === "phrases"){ + return results; + } else if (item.type === 'phrases') { const results = {}; const phrasesFilter = {}; const filters = { - ...buildPhrasesFilter({ name: item.field, type: "string" }, item.values, this.indexPattern), - "$state": { "store": "appState" } - } - phrasesFilter["filters"] = [...filterParams["filters"]] - phrasesFilter["time"] = filterParams["time"]; - phrasesFilter["query"] = filterParams["query"]; - phrasesFilter["filters"].push(filters); - newOnClick[item.name] = () => {this.filterManager.addFilters(filters)}; + ...buildPhrasesFilter( + { name: item.field, type: 'string' }, + item.values, + this.indexPattern + ), + $state: { store: 'appState' }, + }; + phrasesFilter['filters'] = [...filterParams['filters']]; + phrasesFilter['time'] = filterParams['time']; + phrasesFilter['query'] = filterParams['query']; + phrasesFilter['filters'].push(filters); + newOnClick[item.name] = () => { + this.filterManager.addFilters(filters); + }; results[item.name] = await this.getResults(phrasesFilter); - return results ; - - }else if(item.type === "custom"){ + return results; + } else if (item.type === 'custom') { const results = {}; const customFilters = {}; - - customFilters["filters"] = [...filterParams["filters"]] - customFilters["time"] = filterParams["time"]; - customFilters["query"] = filterParams["query"]; - if(item.filter.phrase){ + + customFilters['filters'] = [...filterParams['filters']]; + customFilters['time'] = filterParams['time']; + customFilters['query'] = filterParams['query']; + if (item.filter.phrase) { const filters = { - ...buildPhraseFilter({ name: item.filter.field, type: "string"}, item.filter.phrase, this.indexPattern), - "$state": { "store": "appState" } - } - customFilters["filters"].push(filters) + ...buildPhraseFilter( + { name: item.filter.field, type: 'string' }, + item.filter.phrase, + this.indexPattern + ), + $state: { store: 'appState' }, + }; + customFilters['filters'].push(filters); } results[item.name] = await this.getResults(customFilters, item.agg); - return results ; - - }else if(item.type === "exists"){ + return results; + } else if (item.type === 'exists') { const results = {}; const existsFilters = {}; const filters = { ...buildExistsFilter({ name: item.field, type: 'nested' }, this.indexPattern), - "$state": { "store": "appState" } - } - existsFilters["filters"] = [...filterParams["filters"]] - existsFilters["time"] = filterParams["time"]; - existsFilters["query"] = filterParams["query"]; - existsFilters["filters"].push(filters); - newOnClick[item.name] = () => {this.filterManager.addFilters(filters)}; + $state: { store: 'appState' }, + }; + existsFilters['filters'] = [...filterParams['filters']]; + existsFilters['time'] = filterParams['time']; + existsFilters['query'] = filterParams['query']; + existsFilters['filters'].push(filters); + newOnClick[item.name] = () => { + this.filterManager.addFilters(filters); + }; results[item.name] = await this.getResults(existsFilters); - return results ; - }else if(item.type === "unique-count"){ + return results; + } else if (item.type === 'unique-count') { const results = {}; const params = {}; const aggs = { - "aggResult" : { - "cardinality" : { - "field" : item.field - } - } - } - - params["filters"] = [...filterParams["filters"]] - params["time"] = filterParams["time"]; - params["query"] = filterParams["query"]; + aggResult: { + cardinality: { + field: item.field, + }, + }, + }; + + params['filters'] = [...filterParams['filters']]; + params['time'] = filterParams['time']; + params['query'] = filterParams['query']; results[item.name] = await this.getResults(params, aggs); - return results ; - }else if(item.type === "phrase"){ + return results; + } else if (item.type === 'phrase') { const results = {}; const phraseFilter = {}; const filters = { - ...buildPhraseFilter({ name: item.field, type: "string"}, item.value, this.indexPattern), - "$state": { "store": "appState" } - } - if(item.negate){ + ...buildPhraseFilter( + { name: item.field, type: 'string' }, + item.value, + this.indexPattern + ), + $state: { store: 'appState' }, + }; + if (item.negate) { filters.meta.negate = item.negate; } - phraseFilter["filters"] = [...filterParams["filters"]] - phraseFilter["time"] = filterParams["time"]; - phraseFilter["query"] = filterParams["query"]; - phraseFilter["filters"].push(filters); - newOnClick[item.name] = () => {this.filterManager.addFilters(filters)}; + phraseFilter['filters'] = [...filterParams['filters']]; + phraseFilter['time'] = filterParams['time']; + phraseFilter['query'] = filterParams['query']; + phraseFilter['filters'].push(filters); + newOnClick[item.name] = () => { + this.filterManager.addFilters(filters); + }; results[item.name] = await this.getResults(phraseFilter); - return results ; - }else{ + return results; + } else { const results = {}; results[item.name] = await this.getResults(filterParams); return results; } }); - - Promise.all(result).then((completed) => { + + try { + const completed = await Promise.all(result); const newResults = {}; - completed.forEach(item => { - const key = Object.keys(item)[0] + completed.forEach((item) => { + const key = Object.keys(item)[0]; newResults[key] = item[key]; }); - this.setState({results: newResults, loading:false, buildingMetrics: false, metricsOnClicks: newOnClick}); - }).catch(error => { - this.setState({loading: false, buildingMetrics: false}); - }); - - } + this.setState({ + results: newResults, + loading: false, + buildingMetrics: false, + metricsOnClicks: newOnClick, + }); + } catch (error) { + this.setState({ loading: false, buildingMetrics: false }); + const options = { + context: `${Metrics.name}.buildMetric`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } - componentDidUpdate(){ - if(!this.state.buildingMetrics && this.props.resultState === 'ready' && this.state.resultState === 'loading'){ - this.setState({ buildingMetrics: true, resultState: this.props.resultState}, () => { - this.stats = this.buildMetric(); - }); - }else if(this.props.resultState !== this.state.resultState){ - const isLoading = this.props.resultState === 'loading' ? {loading:true} : {}; - this.setState({resultState: this.props.resultState, ...isLoading}); + componentDidUpdate() { + if ( + !this.state.buildingMetrics && + this.props.resultState === 'ready' && + this.state.resultState === 'loading' + ) { + this.setState({ buildingMetrics: true, resultState: this.props.resultState }, () => { + this.stats = this.buildMetric(); + }); + } else if (this.props.resultState !== this.state.resultState) { + const isLoading = this.props.resultState === 'loading' ? { loading: true } : {}; + this.setState({ resultState: this.props.resultState, ...isLoading }); + } } - } - buildTitleButton = (count, itemName) => { - return <EuiToolTip position="top" content={`Filter by ${itemName}`}> - <span - className={ 'statWithLink' } - style={{ cursor: "pointer", fontSize: count > 20 ? "2rem" : "2.25rem" }} - onClick={ this.state.metricsOnClicks[itemName] }> - {this.state.results[itemName]} - </span> - </EuiToolTip> - } + buildTitleButton = (count, itemName) => { + return ( + <EuiToolTip position="top" content={`Filter by ${itemName}`}> + <span + className={'statWithLink'} + style={{ cursor: 'pointer', fontSize: count > 20 ? '2rem' : '2.25rem' }} + onClick={this.state.metricsOnClicks[itemName]} + > + {this.state.results[itemName]} + </span> + </EuiToolTip> + ); + }; - buildStatsComp(){ - const { section } = this.props; - if(this.metricsList[section]){ - return this.metricsList[section].map((item,idx) => { - const count = (this.state.results[item.name] || []).length - return( - <EuiFlexItem grow={count>20 ? 3 : 1} key={`${item.name}`}> - <EuiStat - title={this.state.metricsOnClicks[item.name] ? this.buildTitleButton(count, item.name) : - <span style={{ fontSize: count > 20 ? "2rem" : "2.25rem" }}>{item.transformValue ? item.transformValue(this.state.results[item.name]) : this.state.results[item.name]}</span>} - description={item.name} - titleColor={this.metricsList[section][idx].color || 'primary'} - isLoading={this.state.loading} - textAlign="center" - /> - </EuiFlexItem> - ) - }); + buildStatsComp() { + const { section } = this.props; + if (this.metricsList[section]) { + return this.metricsList[section].map((item, idx) => { + const count = (this.state.results[item.name] || []).length; + return ( + <EuiFlexItem grow={count > 20 ? 3 : 1} key={`${item.name}`}> + <EuiStat + title={ + this.state.metricsOnClicks[item.name] ? ( + this.buildTitleButton(count, item.name) + ) : ( + <span style={{ fontSize: count > 20 ? '2rem' : '2.25rem' }}> + {item.transformValue + ? item.transformValue(this.state.results[item.name]) + : this.state.results[item.name]} + </span> + ) + } + description={item.name} + titleColor={this.metricsList[section][idx].color || 'primary'} + isLoading={this.state.loading} + textAlign="center" + /> + </EuiFlexItem> + ); + }); + } } - } - render() { - const stats = this.buildStatsComp(); - return ( - <EuiFlexGroup> + render() { + const stats = this.buildStatsComp(); + return ( + <EuiFlexGroup> <EuiFlexItem /> - {stats} + {stats} <EuiFlexItem /> </EuiFlexGroup> - ) + ); + } } -}) - +); diff --git a/public/components/overview/mitre/components/tactics/tactics.tsx b/public/components/overview/mitre/components/tactics/tactics.tsx index 4a9df58c9e..9b7c45c8cf 100644 --- a/public/components/overview/mitre/components/tactics/tactics.tsx +++ b/public/components/overview/mitre/components/tactics/tactics.tsx @@ -24,24 +24,27 @@ import { } from '@elastic/eui' import { IFilterParams, getElasticAlerts } from '../../lib'; import { getToasts } from '../../../../../kibana-services'; +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 class Tactics extends Component { _isMount = false; state: { - tacticsList: Array<any>, - tacticsCount: { key: string, doc_count:number }[], - allSelected : boolean, - loadingAlerts: boolean, - isPopoverOpen: boolean, - firstTime: boolean - } + tacticsList: Array<any>; + tacticsCount: { key: string; doc_count: number }[]; + allSelected: boolean; + loadingAlerts: boolean; + isPopoverOpen: boolean; + firstTime: boolean; + }; props!: { - tacticsObject: object, - selectedTactics: Array<any> - filterParams: IFilterParams - indexPattern: any - onChangeSelectedTactics(selectedTactics): void + tacticsObject: object; + selectedTactics: Array<any>; + filterParams: IFilterParams; + indexPattern: any; + onChangeSelectedTactics(selectedTactics): void; }; constructor(props) { @@ -52,17 +55,17 @@ export class Tactics extends Component { allSelected: false, loadingAlerts: true, isPopoverOpen: false, - firstTime: true - } + firstTime: true, + }; } - async componentDidMount(){ + async componentDidMount() { this._isMount = true; } - initTactics(){ + initTactics() { const tacticsIds = Object.keys(this.props.tacticsObject); - const selectedTactics = {} + const selectedTactics = {}; /*let isMax = {}; tacticsIds.forEach( (item,id) => { if(buckets.length){ @@ -79,36 +82,31 @@ export class Tactics extends Component { selectedTactics[item] = true; } });*/ - tacticsIds.forEach( (item,id) => { + tacticsIds.forEach((item, id) => { selectedTactics[item] = true; }); - this.props.onChangeSelectedTactics(selectedTactics); } - shouldComponentUpdate(nextProps, nextState) { const { filterParams, indexPattern, selectedTactics, isLoading } = this.props; const { tacticsCount, loadingAlerts } = this.state; - if (nextState.loadingAlerts !== loadingAlerts) - return true; - if (nextProps.isLoading !== isLoading) - return true; - if (JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams)) - return true; - if (JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern)) - return true; - if (JSON.stringify(nextState.tacticsCount) !== JSON.stringify(tacticsCount)) - return true; - if (JSON.stringify(nextState.selectedTactics) !== JSON.stringify(selectedTactics)) - return true; + if (nextState.loadingAlerts !== loadingAlerts) return true; + if (nextProps.isLoading !== isLoading) return true; + if (JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams)) return true; + if (JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern)) return true; + if (JSON.stringify(nextState.tacticsCount) !== JSON.stringify(tacticsCount)) return true; + if (JSON.stringify(nextState.selectedTactics) !== JSON.stringify(selectedTactics)) return true; return false; } async componentDidUpdate(prevProps) { const { isLoading, tacticsObject } = this.props; - if (JSON.stringify(prevProps.tacticsObject) !== JSON.stringify(tacticsObject) || isLoading !== prevProps.isLoading){ + if ( + JSON.stringify(prevProps.tacticsObject) !== JSON.stringify(tacticsObject) || + isLoading !== prevProps.isLoading + ) { this.getTacticsCount(this.state.firstTime); } } @@ -118,134 +116,139 @@ export class Tactics extends Component { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; async getTacticsCount() { - this.setState({loadingAlerts: true}); + this.setState({ loadingAlerts: true }); const { firstTime } = this.state; - try{ - const {indexPattern, filterParams} = this.props; - if ( !indexPattern ) { return; } + try { + const { indexPattern, filterParams } = this.props; + if (!indexPattern) { + return; + } const aggs = { tactics: { terms: { - field: "rule.mitre.tactic", - size: 1000, - } - } - } - + field: 'rule.mitre.tactic', + size: 1000, + }, + }, + }; + // TODO: use `status` and `statusText` to show errors // @ts-ignore const { data } = await getElasticAlerts(indexPattern, filterParams, aggs); const { buckets } = data.aggregations.tactics; - if(firstTime){ + if (firstTime) { this.initTactics(buckets); // top tactics are checked on component mount } - this._isMount && this.setState({tacticsCount: buckets, loadingAlerts: false, firstTime:false}); - - } catch(err){ - this.showToast( - 'danger', - 'Error', - `Mitre alerts could not be fetched: ${err}`, - 3000 - ); - this.setState({loadingAlerts: false}) + this._isMount && + this.setState({ tacticsCount: buckets, loadingAlerts: false, firstTime: false }); + } catch (error) { + const options = { + context: `${Tactics.name}.getTacticsCount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre alerts could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ loadingAlerts: false }); } - } - componentWillUnmount() { this._isMount = false; } - facetClicked(id){ + facetClicked(id) { const { selectedTactics: oldSelected, onChangeSelectedTactics } = this.props; const selectedTactics = { ...oldSelected, - [id]: !oldSelected[id] - } + [id]: !oldSelected[id], + }; onChangeSelectedTactics(selectedTactics); } - - getTacticsList(){ + getTacticsList() { const { tacticsCount } = this.state; const { selectedTactics } = this.props; const tacticsIds = Object.keys(this.props.tacticsObject); - const tacticsList:Array<any> = tacticsIds.map( item => { - const quantity = (tacticsCount.find(tactic => tactic.key === item) || {}).doc_count || 0; + const tacticsList: Array<any> = tacticsIds.map((item) => { + const quantity = (tacticsCount.find((tactic) => tactic.key === item) || {}).doc_count || 0; return { id: item, label: item, quantity, onClick: (id) => this.facetClicked(id), - }} - ); - + }; + }); + return ( <> - {tacticsList.sort((a, b) => b.quantity - a.quantity).map(facet => { - let iconNode; - return ( - <EuiFacetButton - key={facet.id} - id={`${facet.id}`} - quantity={facet.quantity} - isSelected={selectedTactics[facet.id]} - isLoading={this.state.loadingAlerts} - icon={iconNode} - onClick={ - facet.onClick ? () => facet.onClick(facet.id) : undefined - }> - {facet.label} - </EuiFacetButton> - ); - })} + {tacticsList + .sort((a, b) => b.quantity - a.quantity) + .map((facet) => { + let iconNode; + return ( + <EuiFacetButton + key={facet.id} + id={`${facet.id}`} + quantity={facet.quantity} + isSelected={selectedTactics[facet.id]} + isLoading={this.state.loadingAlerts} + icon={iconNode} + onClick={facet.onClick ? () => facet.onClick(facet.id) : undefined} + > + {facet.label} + </EuiFacetButton> + ); + })} </> ); - } - checkAllChecked(tacticList: any[]){ + checkAllChecked(tacticList: any[]) { const { selectedTactics } = this.props; let allSelected = true; - tacticList.forEach( item => { - if(!selectedTactics[item.id]) - allSelected = false; + tacticList.forEach((item) => { + if (!selectedTactics[item.id]) allSelected = false; }); - if(allSelected !== this.state.allSelected){ - this.setState({allSelected}); + if (allSelected !== this.state.allSelected) { + this.setState({ allSelected }); } } - onCheckAllClick(){ - const allSelected = ! this.state.allSelected; - const {selectedTactics, onChangeSelectedTactics} = this.props; - Object.keys(selectedTactics).map( item => { + onCheckAllClick() { + const allSelected = !this.state.allSelected; + const { selectedTactics, onChangeSelectedTactics } = this.props; + Object.keys(selectedTactics).map((item) => { selectedTactics[item] = allSelected; }); - - this.setState({allSelected}); + + this.setState({ allSelected }); onChangeSelectedTactics(selectedTactics); } - - onGearButtonClick(){ - this.setState({isPopoverOpen: !this.state.isPopoverOpen}); + + onGearButtonClick() { + this.setState({ isPopoverOpen: !this.state.isPopoverOpen }); } - - closePopover(){ - this.setState({isPopoverOpen: false}); + + closePopover() { + this.setState({ isPopoverOpen: false }); } - selectAll(status){ - const {selectedTactics, onChangeSelectedTactics} = this.props; - Object.keys(selectedTactics).map( item => { + selectAll(status) { + const { selectedTactics, onChangeSelectedTactics } = this.props; + Object.keys(selectedTactics).map((item) => { selectedTactics[item] = status; }); onChangeSelectedTactics(selectedTactics); @@ -273,11 +276,11 @@ export class Tactics extends Component { this.selectAll(false); }, }, - ] - } - ] + ], + }, + ]; return ( - <div style={{ backgroundColor: "#80808014", padding: "10px 10px 0 10px", height: "100%"}}> + <div style={{ backgroundColor: '#80808014', padding: '10px 10px 0 10px', height: '100%' }}> <EuiFlexGroup> <EuiFlexItem> <EuiTitle size="m"> @@ -285,23 +288,31 @@ export class Tactics extends Component { </EuiTitle> </EuiFlexItem> - <EuiFlexItem grow={false} style={{marginTop:'15px', marginRight:8}}> + <EuiFlexItem grow={false} style={{ marginTop: '15px', marginRight: 8 }}> <EuiPopover - button={(<EuiButtonIcon iconType="gear" onClick={() => this.onGearButtonClick()} aria-label={'tactics options'}></EuiButtonIcon>)} + button={ + <EuiButtonIcon + iconType="gear" + onClick={() => this.onGearButtonClick()} + aria-label={'tactics options'} + ></EuiButtonIcon> + } isOpen={this.state.isPopoverOpen} panelPaddingSize="none" - closePopover={() => this.closePopover()}> - <EuiContextMenu initialPanelId={0} panels={panels} /> + closePopover={() => this.closePopover()} + > + <EuiContextMenu initialPanelId={0} panels={panels} /> </EuiPopover> </EuiFlexItem> </EuiFlexGroup> - { this.props.isLoading - ? <EuiFlexItem style={{ alignItems: 'center', marginTop: 50 }} ><EuiLoadingSpinner size="xl" /></EuiFlexItem> - : <EuiFacetGroup style={{ }}> - {this.getTacticsList()} - </EuiFacetGroup> - } + {this.props.isLoading ? ( + <EuiFlexItem style={{ alignItems: 'center', marginTop: 50 }}> + <EuiLoadingSpinner size="xl" /> + </EuiFlexItem> + ) : ( + <EuiFacetGroup style={{}}>{this.getTacticsList()}</EuiFacetGroup> + )} </div> - ) + ); } } diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 99dda5b392..6fa1b41b1b 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -41,6 +41,9 @@ import { AppNavigate } from '../../../../../../../react-services/app-navigate'; import { Discover } from '../../../../../../common/modules/discover'; import { getUiSettings } from '../../../../../../../kibana-services'; import { FilterManager } from '../../../../../../../../../../src/plugins/data/public/'; +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 class FlyoutTechnique extends Component { _isMount = false; @@ -126,7 +129,20 @@ export class FlyoutTechnique extends Component { }); const rawData = (((result || {}).data || {}).data || {}).affected_items !!rawData && this.formatTechniqueData(rawData[0]); - }catch(err){ + }catch(error){ + const options = { + context: `${FlyoutTechnique.name}.getTechniqueData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error obtaining the requested technique`, + }, + }; + getErrorOrchestrator().handleError(options); this.setState({loading: false}); } } diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 8d32c76646..b7e0964c0d 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react' +import React, { Component, Fragment } from 'react'; import { EuiFacetButton, EuiFlexGroup, @@ -33,446 +33,542 @@ import { getElasticAlerts, IFilterParams } from '../../lib'; import { ITactic } from '../../'; import { withWindowSize } from '../../../../../components/common/hocs/withWindowSize'; import { WzRequest } from '../../../../../react-services/wz-request'; -import {WAZUH_ALERTS_PATTERN} from '../../../../../../common/constants'; +import { WAZUH_ALERTS_PATTERN } from '../../../../../../common/constants'; import { AppState } from '../../../../../react-services/app-state'; import { WzFieldSearchDelay } from '../../../../common/search'; import { getDataPlugin, getToasts } from '../../../../../kibana-services'; +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 Techniques = withWindowSize( + class Techniques extends Component { + _isMount = false; + + props!: { + tacticsObject: ITactic; + selectedTactics: any; + indexPattern: any; + filterParams: IFilterParams; + }; + + state: { + techniquesCount: { key: string; doc_count: number }[]; + isFlyoutVisible: Boolean; + currentTechniqueData: {}; + currentTechnique: string; + hideAlerts: boolean; + actionsOpen: string; + filteredTechniques: boolean | [string]; + mitreTechniques: []; + isSearching: boolean; + }; + + constructor(props) { + super(props); + + this.state = { + isFlyoutVisible: false, + currentTechniqueData: {}, + techniquesCount: [], + currentTechnique: '', + hideAlerts: false, + actionsOpen: '', + filteredTechniques: false, + mitreTechniques: [], + isSearching: false, + }; + this.onChangeFlyout.bind(this); + } -export const Techniques = withWindowSize(class Techniques extends Component { - _isMount = false; - - props!: { - tacticsObject: ITactic - selectedTactics: any - indexPattern: any - filterParams: IFilterParams - } - - state: { - techniquesCount: {key: string, doc_count: number}[] - isFlyoutVisible: Boolean, - currentTechniqueData: {}, - currentTechnique: string, - hideAlerts: boolean, - actionsOpen: string, - filteredTechniques: boolean | [string] - mitreTechniques: [], - isSearching: boolean - } + async componentDidMount() { + this._isMount = true; + await this.buildMitreTechniquesFromApi(); + } - constructor(props) { - super(props); - - this.state = { - isFlyoutVisible: false, - currentTechniqueData: {}, - techniquesCount: [], - currentTechnique: '', - hideAlerts: false, - actionsOpen: "", - filteredTechniques: false, - mitreTechniques: [], - isSearching: false + shouldComponentUpdate(nextProps, nextState) { + const { filterParams, indexPattern, selectedTactics, isLoading } = this.props; + if (nextProps.isLoading !== isLoading) return true; + if (JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams)) return true; + if (JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern)) return true; + if (JSON.stringify(nextState.selectedTactics) !== JSON.stringify(selectedTactics)) + return true; + return false; } - this.onChangeFlyout.bind(this); - } - - async componentDidMount(){ - this._isMount = true; - await this.buildMitreTechniquesFromApi(); - } - shouldComponentUpdate(nextProps, nextState) { - const { filterParams, indexPattern, selectedTactics, isLoading } = this.props; - if (nextProps.isLoading !== isLoading) - return true; - if (JSON.stringify(nextProps.filterParams) !== JSON.stringify(filterParams)) - return true; - if (JSON.stringify(nextProps.indexPattern) !== JSON.stringify(indexPattern)) - return true; - if (JSON.stringify(nextState.selectedTactics) !== JSON.stringify(selectedTactics)) - return true; - return false; - } + componentDidUpdate(prevProps) { + const { isLoading, tacticsObject, filters } = this.props; + if ( + JSON.stringify(prevProps.tacticsObject) !== JSON.stringify(tacticsObject) || + isLoading !== prevProps.isLoading + ) + this.getTechniquesCount(); + } - componentDidUpdate(prevProps) { - const { isLoading, tacticsObject, filters } = this.props; - if ( JSON.stringify(prevProps.tacticsObject) !== JSON.stringify(tacticsObject) || (isLoading !== prevProps.isLoading)) - this.getTechniquesCount(); - } + componentWillUnmount() { + this._isMount = false; + } - componentWillUnmount() { - this._isMount = false; - } + showToast(color: string, title: string = '', text: string = '', time: number = 3000) { + getToasts().add({ + color: color, + title: title, + text: text, + toastLifeTimeMs: time, + }); + } - showToast(color: string, title: string = '', text: string = '', time: number = 3000) { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - async getTechniquesCount() { - try{ - const {indexPattern, filters} = this.props; - if ( !indexPattern ) { return; } - const aggs = { - techniques: { - terms: { - field: "rule.mitre.id", - size: 1000, - } + async getTechniquesCount() { + try { + const { indexPattern, filters } = this.props; + if (!indexPattern) { + return; } + const aggs = { + techniques: { + terms: { + field: 'rule.mitre.id', + size: 1000, + }, + }, + }; + this._isMount && this.setState({ loadingAlerts: true }); + // TODO: use `status` and `statusText` to show errors + // @ts-ignore + const { data, status, statusText } = await getElasticAlerts(indexPattern, filters, aggs); + const { buckets } = data.aggregations.techniques; + this._isMount && this.setState({ techniquesCount: buckets, loadingAlerts: false }); + } catch (error) { + const options = { + context: `${Techniques.name}.getTechniquesCount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre alerts could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + this._isMount && this.setState({ loadingAlerts: false }); } - this._isMount && this.setState({loadingAlerts: true}); - // TODO: use `status` and `statusText` to show errors - // @ts-ignore - const {data, status, statusText, } = await getElasticAlerts(indexPattern, filters, aggs); - const { buckets } = data.aggregations.techniques; - this._isMount && this.setState({techniquesCount: buckets, loadingAlerts: false}); - - } catch(err){ - // this.showToast( - // 'danger', - // 'Error', - // `Mitre alerts could not be fetched: ${err}`, - // 3000 - // ); - this._isMount && this.setState({loadingAlerts: false}) } - } - buildPanel(techniqueID){ - return [ - { - id: 0, - title: 'Actions', - items: [ - { - name: 'Filter for value', - icon: <EuiIcon type="magnifyWithPlus" size="m" />, - onClick: () => { - this.closeActionsMenu(); - this.addFilter({key: 'rule.mitre.id', value: techniqueID, negate: false} ); + buildPanel(techniqueID) { + return [ + { + id: 0, + title: 'Actions', + items: [ + { + name: 'Filter for value', + icon: <EuiIcon type="magnifyWithPlus" size="m" />, + onClick: () => { + this.closeActionsMenu(); + this.addFilter({ key: 'rule.mitre.id', value: techniqueID, negate: false }); + }, }, - }, - { - name: 'Filter out value', - icon: <EuiIcon type="magnifyWithMinus" size="m" />, - onClick: () => { - this.closeActionsMenu(); - this.addFilter({key: 'rule.mitre.id', value: techniqueID, negate: true} ); + { + name: 'Filter out value', + icon: <EuiIcon type="magnifyWithMinus" size="m" />, + onClick: () => { + this.closeActionsMenu(); + this.addFilter({ key: 'rule.mitre.id', value: techniqueID, negate: true }); + }, }, - }, - { - name: 'View technique details', - icon: <EuiIcon type="filebeatApp" size="m" />, - onClick: () => { - this.closeActionsMenu(); - this.showFlyout(techniqueID) + { + name: 'View technique details', + icon: <EuiIcon type="filebeatApp" size="m" />, + onClick: () => { + this.closeActionsMenu(); + this.showFlyout(techniqueID); + }, }, - } - ], - } - ] - } - - techniqueColumnsResponsive(){ - if(this.props && this.props.windowSize){ - return this.props.windowSize.width < 930 ? 2 - : this.props.windowSize.width < 1200 ? 3 - : 4; - }else{ - return 4; + ], + }, + ]; } - } - async getMitreTechniques (params) { - try{ - return await WzRequest.apiReq("GET", "/mitre/techniques", { params }); - }catch(error){ - this.showToast( - 'danger', - 'Error', - `Mitre techniques could not be fetched: ${error}`, - 3000 - ); - return []; + techniqueColumnsResponsive() { + if (this.props && this.props.windowSize) { + return this.props.windowSize.width < 930 ? 2 : this.props.windowSize.width < 1200 ? 3 : 4; + } else { + return 4; + } } - } - async buildMitreTechniquesFromApi () { - const limitResults = 500; - const params = { limit: limitResults }; - this.setState({ isSearching: true }); - const output = await this.getMitreTechniques(params); - const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; - let mitreTechniques = []; - mitreTechniques.push(...output.data.data.affected_items); - if (totalItems && output.data && output.data.data && totalItems > limitResults) { - const extraResults = await Promise.all( - Array(Math.ceil((totalItems-params.limit)/params.limit)).fill() - .map(async (_,index) => { - const response = await this.getMitreTechniques({...params, offset: limitResults * (1+index)}); - return response.data.data.affected_items; - }) - ); - mitreTechniques.push(...extraResults.flat()); + async getMitreTechniques(params) { + try { + return await WzRequest.apiReq('GET', '/mitre/techniques', { params }); + } catch (error) { + const options = { + context: `${Techniques.name}.getMitreTechniques`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre techniques could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); + return []; + } } - this.setState({ mitreTechniques: mitreTechniques, isSearching: false }); - } - buildObjTechniques(techniques){ - const techniquesObj = []; - techniques.forEach(element => { - const mitreObj = this.state.mitreTechniques.find(item => item.id === element); - if(mitreObj){ - const mitreTechniqueName = mitreObj.name; - const mitreTechniqueID = mitreObj.references.find(item => item.source === "mitre-attack").external_id; - mitreTechniqueID ? techniquesObj.push({ id : mitreTechniqueID, name: mitreTechniqueName}) : ''; + async buildMitreTechniquesFromApi() { + const limitResults = 500; + const params = { limit: limitResults }; + this.setState({ isSearching: true }); + const output = await this.getMitreTechniques(params); + const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; + let mitreTechniques = []; + mitreTechniques.push(...output.data.data.affected_items); + if (totalItems && output.data && output.data.data && totalItems > limitResults) { + const extraResults = await Promise.all( + Array(Math.ceil((totalItems - params.limit) / params.limit)) + .fill() + .map(async (_, index) => { + const response = await this.getMitreTechniques({ + ...params, + offset: limitResults * (1 + index), + }); + return response.data.data.affected_items; + }) + ); + mitreTechniques.push(...extraResults.flat()); } - }); - return techniquesObj; - } + this.setState({ mitreTechniques: mitreTechniques, isSearching: false }); + } - renderFacet() { - const { tacticsObject } = this.props; - const { techniquesCount } = this.state; - let hash = {}; - let tacticsToRender: Array<any> = []; - const currentTechniques = Object.keys(tacticsObject).map(tacticsKey => ({tactic: tacticsKey, techniques: this.buildObjTechniques(tacticsObject[tacticsKey].techniques)})) - .filter(tactic => this.props.selectedTactics[tactic.tactic]) - .map(tactic => tactic.techniques) - .flat() - .filter((techniqueID, index, array) => array.indexOf(techniqueID) === index); - tacticsToRender = currentTechniques - .filter(technique => this.state.filteredTechniques ? this.state.filteredTechniques.includes(technique.id) : technique.id && hash[technique.id] ? false : hash[technique.id] = true) - .map(technique => { - return { - id: technique.id, - label: `${technique.id} - ${technique.name}`, - quantity: (techniquesCount.find(item => item.key === technique.id) || {}).doc_count || 0 + buildObjTechniques(techniques) { + const techniquesObj = []; + techniques.forEach((element) => { + const mitreObj = this.state.mitreTechniques.find((item) => item.id === element); + if (mitreObj) { + const mitreTechniqueName = mitreObj.name; + const mitreTechniqueID = mitreObj.references.find( + (item) => item.source === 'mitre-attack' + ).external_id; + mitreTechniqueID + ? techniquesObj.push({ id: mitreTechniqueID, name: mitreTechniqueName }) + : ''; } - }) - .filter(technique => this.state.hideAlerts ? technique.quantity !== 0 : true); - const tacticsToRenderOrdered = tacticsToRender.sort((a, b) => b.quantity - a.quantity).map( (item,idx) => { - const tooltipContent = `View details of ${item.label} (${item.id})`; - const toolTipAnchorClass = "wz-display-inline-grid" + (this.state.hover=== item.id ? " wz-mitre-width" : " "); - return( - <EuiFlexItem - onMouseEnter={() => this.setState({ hover: item.id })} - onMouseLeave={() => this.setState({ hover: "" })} - key={idx} style={{border: "1px solid #8080804a", maxWidth: 'calc(25% - 8px)', maxHeight: 41}}> - - <EuiPopover - id="techniqueActionsContextMenu" - anchorClassName="wz-width-100" - button={( - <EuiFacetButton - style={{width: "100%", padding: "0 5px 0 5px", lineHeight: "40px", maxHeight: 40}} - quantity={item.quantity} - className={"module-table"} - onClick={() => this.showFlyout(item.id)}> - <EuiToolTip position="top" content={tooltipContent} anchorClassName={toolTipAnchorClass}> - <span style={{ - display: "block", - overflow: "hidden", - whiteSpace: "nowrap", - textOverflow: "ellipsis"}}> - {item.label} - </span> - </EuiToolTip> - - {this.state.hover === item.id && - <span style={{float: "right", position: 'fixed'}}> - <EuiToolTip position="top" content={"Show " + item.id + " in Dashboard"} > - <EuiIcon onClick={(e) => {this.openDashboard(e,item.id);e.stopPropagation()}} color="primary" type="visualizeApp"></EuiIcon> - </EuiToolTip>   - <EuiToolTip position="top" content={"Inspect " + item.id + " in Events"} > - <EuiIcon onClick={(e) => {this.openDiscover(e,item.id);e.stopPropagation()}} color="primary" type="discoverApp"></EuiIcon> - </EuiToolTip> - - </span> - } - </EuiFacetButton> - ) - } - isOpen={this.state.actionsOpen === item.id} - closePopover={() => this.closeActionsMenu()} - panelPaddingSize="none" - style={{width: "100%"}} - anchorPosition="downLeft"> - <EuiContextMenu initialPanelId={0} panels={this.buildPanel(item.id)} /> - </EuiPopover> - </EuiFlexItem> - ); - - }) - if(this.state.isSearching || this.state.loadingAlerts || this.props.isLoading){ - return ( - <EuiFlexItem style={{ height: "calc(100vh - 450px)", alignItems: 'center' }} > - <EuiLoadingSpinner size='xl'/> - </EuiFlexItem> - ) + }); + return techniquesObj; } - if(tacticsToRender.length){ - return ( - <EuiFlexGrid columns={this.techniqueColumnsResponsive()} gutterSize="s" style={{ maxHeight: "calc(100vh - 420px)", overflow: "overlay", overflowX: "hidden", paddingRight: 10}}> - {tacticsToRenderOrdered} - </EuiFlexGrid> - ) - }else{ - return <EuiCallOut title='There are no results.' iconType='help' color='warning'></EuiCallOut> - } - } - - openDiscover(e,techniqueID){ - this.addFilter({key: 'rule.mitre.id', value: techniqueID, negate: false} ); - this.props.onSelectedTabChanged('events'); - } + renderFacet() { + const { tacticsObject } = this.props; + const { techniquesCount } = this.state; + let hash = {}; + let tacticsToRender: Array<any> = []; + const currentTechniques = Object.keys(tacticsObject) + .map((tacticsKey) => ({ + tactic: tacticsKey, + techniques: this.buildObjTechniques(tacticsObject[tacticsKey].techniques), + })) + .filter((tactic) => this.props.selectedTactics[tactic.tactic]) + .map((tactic) => tactic.techniques) + .flat() + .filter((techniqueID, index, array) => array.indexOf(techniqueID) === index); + tacticsToRender = currentTechniques + .filter((technique) => + this.state.filteredTechniques + ? this.state.filteredTechniques.includes(technique.id) + : technique.id && hash[technique.id] + ? false + : (hash[technique.id] = true) + ) + .map((technique) => { + return { + id: technique.id, + label: `${technique.id} - ${technique.name}`, + quantity: + (techniquesCount.find((item) => item.key === technique.id) || {}).doc_count || 0, + }; + }) + .filter((technique) => (this.state.hideAlerts ? technique.quantity !== 0 : true)); + const tacticsToRenderOrdered = tacticsToRender + .sort((a, b) => b.quantity - a.quantity) + .map((item, idx) => { + const tooltipContent = `View details of ${item.label} (${item.id})`; + const toolTipAnchorClass = + 'wz-display-inline-grid' + (this.state.hover === item.id ? ' wz-mitre-width' : ' '); + return ( + <EuiFlexItem + onMouseEnter={() => this.setState({ hover: item.id })} + onMouseLeave={() => this.setState({ hover: '' })} + key={idx} + style={{ border: '1px solid #8080804a', maxWidth: 'calc(25% - 8px)', maxHeight: 41 }} + > + <EuiPopover + id="techniqueActionsContextMenu" + anchorClassName="wz-width-100" + button={ + <EuiFacetButton + style={{ + width: '100%', + padding: '0 5px 0 5px', + lineHeight: '40px', + maxHeight: 40, + }} + quantity={item.quantity} + className={'module-table'} + onClick={() => this.showFlyout(item.id)} + > + <EuiToolTip + position="top" + content={tooltipContent} + anchorClassName={toolTipAnchorClass} + > + <span + style={{ + display: 'block', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {item.label} + </span> + </EuiToolTip> + + {this.state.hover === item.id && ( + <span style={{ float: 'right', position: 'fixed' }}> + <EuiToolTip position="top" content={'Show ' + item.id + ' in Dashboard'}> + <EuiIcon + onClick={(e) => { + this.openDashboard(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + ></EuiIcon> + </EuiToolTip>{' '} +   + <EuiToolTip position="top" content={'Inspect ' + item.id + ' in Events'}> + <EuiIcon + onClick={(e) => { + this.openDiscover(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + ></EuiIcon> + </EuiToolTip> + </span> + )} + </EuiFacetButton> + } + isOpen={this.state.actionsOpen === item.id} + closePopover={() => this.closeActionsMenu()} + panelPaddingSize="none" + style={{ width: '100%' }} + anchorPosition="downLeft" + > + <EuiContextMenu initialPanelId={0} panels={this.buildPanel(item.id)} /> + </EuiPopover> + </EuiFlexItem> + ); + }); + if (this.state.isSearching || this.state.loadingAlerts || this.props.isLoading) { + return ( + <EuiFlexItem style={{ height: 'calc(100vh - 450px)', alignItems: 'center' }}> + <EuiLoadingSpinner size="xl" /> + </EuiFlexItem> + ); + } + if (tacticsToRender.length) { + return ( + <EuiFlexGrid + columns={this.techniqueColumnsResponsive()} + gutterSize="s" + style={{ + maxHeight: 'calc(100vh - 420px)', + overflow: 'overlay', + overflowX: 'hidden', + paddingRight: 10, + }} + > + {tacticsToRenderOrdered} + </EuiFlexGrid> + ); + } else { + return ( + <EuiCallOut title="There are no results." iconType="help" color="warning"></EuiCallOut> + ); + } + } - openDashboard(e,techniqueID){ - this.addFilter({key: 'rule.mitre.id', value: techniqueID, negate: false} ); - this.props.onSelectedTabChanged('dashboard'); - } + openDiscover(e, techniqueID) { + this.addFilter({ key: 'rule.mitre.id', value: techniqueID, negate: false }); + this.props.onSelectedTabChanged('events'); + } - openIntelligence(e,redirectTo,itemId){ - this.props.onSelectedTabChanged('intelligence'); - window.location.href = window.location+`&tabRedirect=${redirectTo}&idToRedirect=${itemId}` - } + openDashboard(e, techniqueID) { + this.addFilter({ key: 'rule.mitre.id', value: techniqueID, negate: false }); + this.props.onSelectedTabChanged('dashboard'); + } - /** - * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} - * @param filter - */ - addFilter(filter) { - const { filterManager } = getDataPlugin().query; - const matchPhrase = {}; - matchPhrase[filter.key] = filter.value; - const newFilter = { - "meta": { - "disabled": false, - "key": filter.key, - "params": { "query": filter.value }, - "type": "phrase", - "negate": filter.negate || false, - "index": AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN - }, - "query": { "match_phrase": matchPhrase }, - "$state": { "store": "appState" } + openIntelligence(e, redirectTo, itemId) { + this.props.onSelectedTabChanged('intelligence'); + window.location.href = window.location + `&tabRedirect=${redirectTo}&idToRedirect=${itemId}`; } - filterManager.addFilters([newFilter]); - } - onChange = searchValue => { - if(!searchValue){ - this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); + /** + * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} + * @param filter + */ + addFilter(filter) { + const { filterManager } = getDataPlugin().query; + const matchPhrase = {}; + matchPhrase[filter.key] = filter.value; + const newFilter = { + meta: { + disabled: false, + key: filter.key, + params: { query: filter.value }, + type: 'phrase', + negate: filter.negate || false, + index: AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN, + }, + query: { match_phrase: matchPhrase }, + $state: { store: 'appState' }, + }; + filterManager.addFilters([newFilter]); } - } - onSearch = async searchValue => { - try{ - if(searchValue){ - this._isMount && this.setState({isSearching: true}); - const response = await WzRequest.apiReq('GET', '/mitre/techniques', { - params: { - search: searchValue, - limit: 500 - } - }); - const filteredTechniques = ((((response || {}).data || {}).data).affected_items || []).map(item => item.references.filter(reference => reference.source === "mitre-attack")[0].external_id); - this._isMount && this.setState({ filteredTechniques, isSearching: false }); - }else{ + onChange = (searchValue) => { + if (!searchValue) { this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); } - }catch(error){ - this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); + }; + + onSearch = async (searchValue) => { + try { + if (searchValue) { + this._isMount && this.setState({ isSearching: true }); + const response = await WzRequest.apiReq('GET', '/mitre/techniques', { + params: { + search: searchValue, + limit: 500, + }, + }); + const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( + (item) => + item.references.filter((reference) => reference.source === 'mitre-attack')[0] + .external_id + ); + this._isMount && this.setState({ filteredTechniques, isSearching: false }); + } else { + this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); + } + } catch (error) { + const options = { + context: `${Techniques.name}.onSearch`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + this._isMount && this.setState({ filteredTechniques: false, isSearching: false }); + } + }; + async closeActionsMenu() { + this.setState({ actionsOpen: false }); } - } - async closeActionsMenu() { - this.setState({actionsOpen: false}); - } - async showActionsMenu(techniqueData) { - this.setState({actionsOpen: techniqueData }); - } + async showActionsMenu(techniqueData) { + this.setState({ actionsOpen: techniqueData }); + } - async showFlyout(techniqueData) { - this.setState({isFlyoutVisible: true, currentTechnique: techniqueData }); - } + async showFlyout(techniqueData) { + this.setState({ isFlyoutVisible: true, currentTechnique: techniqueData }); + } - closeFlyout() { - this.setState({ isFlyoutVisible: false, currentTechniqueData: {}, }); - } + closeFlyout() { + this.setState({ isFlyoutVisible: false, currentTechniqueData: {} }); + } - onChangeFlyout = (isFlyoutVisible: boolean) => { + onChangeFlyout = (isFlyoutVisible: boolean) => { this.setState({ isFlyoutVisible }); - } + }; - hideAlerts(){ - this.setState({hideAlerts: !this.state.hideAlerts}) - } - - render() { - const { isFlyoutVisible, currentTechnique } = this.state; - return ( - <div style={{padding: 10}}> - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <EuiTitle size="m"> - <h1>Techniques</h1> - </EuiTitle> - </EuiFlexItem> + hideAlerts() { + this.setState({ hideAlerts: !this.state.hideAlerts }); + } - <EuiFlexItem grow={false}> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiText grow={false}> + render() { + const { isFlyoutVisible, currentTechnique } = this.state; + return ( + <div style={{ padding: 10 }}> + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <EuiTitle size="m"> + <h1>Techniques</h1> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiText grow={false}> <span>Hide techniques with no alerts </span>   - <EuiSwitch - label="" - checked={this.state.hideAlerts} - onChange={e => this.hideAlerts()} - /> + <EuiSwitch + label="" + checked={this.state.hideAlerts} + onChange={(e) => this.hideAlerts()} + /> </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xs" /> - - <WzFieldSearchDelay - fullWidth={true} - placeholder="Filter techniques of selected tactic/s" - onChange={this.onChange} - onSearch={this.onSearch} - isClearable={true} - aria-label="Use aria labels when no actual label is in use" - /> - <EuiSpacer size="s" /> - - <div> - {this.renderFacet()} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xs" /> + + <WzFieldSearchDelay + fullWidth={true} + placeholder="Filter techniques of selected tactic/s" + onChange={this.onChange} + onSearch={this.onSearch} + isClearable={true} + aria-label="Use aria labels when no actual label is in use" + /> + <EuiSpacer size="s" /> + + <div>{this.renderFacet()}</div> + {isFlyoutVisible && ( + <EuiOverlayMask + headerZindexLocation="below" + // @ts-ignore + onClick={() => this.onChangeFlyout(false)} + > + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + openIntelligence={(e, redirectTo, itemId) => + this.openIntelligence(e, redirectTo, itemId) + } + onChangeFlyout={this.onChangeFlyout} + currentTechniqueData={this.state.currentTechniqueData} + currentTechnique={currentTechnique} + tacticsObject={this.props.tacticsObject} + /> + </EuiOverlayMask> + )} </div> - { isFlyoutVisible && - <EuiOverlayMask - headerZindexLocation="below" - // @ts-ignore - onClick={() => this.onChangeFlyout(false) } > - <FlyoutTechnique - openDashboard={(e,itemId) => this.openDashboard(e,itemId)} - openDiscover={(e,itemId) => this.openDiscover(e,itemId)} - openIntelligence={(e,redirectTo,itemId) => this.openIntelligence(e,redirectTo,itemId)} - onChangeFlyout={this.onChangeFlyout} - currentTechniqueData={this.state.currentTechniqueData} - currentTechnique={currentTechnique} - tacticsObject={this.props.tacticsObject} /> - </EuiOverlayMask> - } - </div> - ) - } -}) + ); + } + } +); diff --git a/public/components/overview/mitre/mitre.tsx b/public/components/overview/mitre/mitre.tsx index 3264a55e4d..83eed4531a 100644 --- a/public/components/overview/mitre/mitre.tsx +++ b/public/components/overview/mitre/mitre.tsx @@ -25,7 +25,10 @@ import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { ModulesHelper } from '../../common/modules/modules-helper'; import { getDataPlugin, getToasts } from '../../../kibana-services'; -import { withErrorBoundary } from "../../common/hocs" +import { withErrorBoundary } from "../../common/hocs"; +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 interface ITactic { [key:string]: string[] @@ -122,14 +125,21 @@ export const Mitre = withErrorBoundary (class Mitre extends Component { tacticsObject[item.name] = item; }); this._isMount && this.setState({tacticsObject, isLoading: false}); - } catch(err) { + } catch(error) { this.setState({ isLoading: false }); - this.showToast( - 'danger', - 'Error', - `Mitre data could not be fetched: ${err}`, - 3000 - ); + const options = { + context: `${Mitre.name}.buildTacticsObject`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Mitre data could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index 8782b4be54..85ebe6deab 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -15,6 +15,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { TableWzAPI } from '../../../components/common/tables'; import { WzRequest } from '../../../react-services'; import { ModuleMitreAttackIntelligenceFlyout } from './resource_detail_flyout'; +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 ModuleMitreAttackIntelligenceResource = ({ label, @@ -50,8 +53,20 @@ export const ModuleMitreAttackIntelligenceResource = ({ )?.external_id, })); setDetails(data[0]); - } catch { - return {}; + } catch (error) { + const options = { + context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx index 6861e48a89..3a89f0b326 100644 --- a/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx @@ -18,6 +18,9 @@ import { SortDirection, EuiInMemoryTable, } from '@elastic/eui'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; type backToTopType = () => void; @@ -55,7 +58,21 @@ export const ReferencesTable = ({referencesName, referencesArray, columns, backT })); setData(data.flat()); } - catch (error){}; + catch (error){ + const options = { + context: `${ReferencesTable.name}.getValues`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + }; setIsLoading(false); }; diff --git a/public/components/overview/mitre_attack_intelligence/resources.tsx b/public/components/overview/mitre_attack_intelligence/resources.tsx index 320e4d3e9b..e3d9d25d30 100644 --- a/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -16,6 +16,9 @@ import { Markdown } from '../../common/util'; import { formatUIDate } from '../../../react-services'; import React from 'react'; import { EuiLink } from '@elastic/eui'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) => async (input: string) => { try{ @@ -30,6 +33,19 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) .sort() .slice(0,9) }catch(error){ + const options = { + context: `${ModuleMitreAttackIntelligenceResource.name}.getMitreItemToRedirect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error getting suggestions`, + }, + }; + getErrorOrchestrator().handleError(options); return []; }; }; From 03beeadfb0f6b62bbcdee5f123b533ec05116f24 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Tue, 13 Jul 2021 12:21:25 -0300 Subject: [PATCH 068/493] Refactor try catch in Management > Statistics (#3429) * Added error handling implementation * Changed wrong var err * Draft statistics test and snapshot * Updated CHANGELOG * Updating title and message error Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 3 ++ package.json | 1 + .../statistics/statistics-main.test.tsx | 43 +++++++++++++++++++ .../statistics/statistics-overview.js | 33 ++++++++++++-- test/jest/config.js | 2 + 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 public/controllers/management/components/management/statistics/statistics-main.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fc9119e21..40f0117542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Added implementation of `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on User section [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) - Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) +- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) +- Added try catch strategy with ErrorOrchestrator service on Management > Statistics [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) diff --git a/package.json b/package.json index a5e460df86..f1ecb926c5 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-codemirror": "^1.0.0", "react-cookie": "^4.0.3", "read-last-lines": "^1.7.2", + "redux-mock-store": "^1.5.4", "timsort": "^0.3.0", "winston": "3.0.0" }, diff --git a/public/controllers/management/components/management/statistics/statistics-main.test.tsx b/public/controllers/management/components/management/statistics/statistics-main.test.tsx new file mode 100644 index 0000000000..eaf2de0364 --- /dev/null +++ b/public/controllers/management/components/management/statistics/statistics-main.test.tsx @@ -0,0 +1,43 @@ +/* + * Wazuh app - React test for Statistics component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +import WzStatistics from './statistics-main'; + +jest.mock('../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +jest.mock('react'); +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Statistics component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = shallow( + <Provider store={store}> + <WzStatistics /> + </Provider> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/statistics/statistics-overview.js b/public/controllers/management/components/management/statistics/statistics-overview.js index a29a8c6b44..3e4e81b89c 100644 --- a/public/controllers/management/components/management/statistics/statistics-overview.js +++ b/public/controllers/management/components/management/statistics/statistics-overview.js @@ -38,7 +38,9 @@ import { PromptStatisticsDisabled } from './prompt-statistics-disabled'; import { PromptStatisticsNoIndices } from './prompt-statistics-no-indices'; import { WazuhConfig } from "../../../../../react-services/wazuh-config"; import { WzRequest } from '../../../../../react-services/wz-request'; - +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; const wzConfig = new WazuhConfig(); export class WzStatisticsOverview extends Component { @@ -85,11 +87,23 @@ export class WzStatisticsOverview extends Component { clusterNodes: nodes, clusterNodeSelected: nodes[0].value, }); - } catch (err) { + } catch (error) { this.setState({ clusterNodes: [], clusterNodeSelected: 'all', }); + + const options = { + context: `${WzStatisticsOverview.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -253,7 +267,20 @@ export default compose( setLoading(true); const data = await WzRequest.genericReq('GET', '/elastic/statistics'); setExistStatisticsIndices(data.data); - }catch(error){} + }catch(error){ + setLoading(false); + const options = { + context: `${WzStatisticsOverview.name}.fetchData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error when fetching data` + }, + }; + getErrorOrchestrator().handleError(options); + } setLoading(false); }; diff --git a/test/jest/config.js b/test/jest/config.js index c12025c204..87fcc1322a 100644 --- a/test/jest/config.js +++ b/test/jest/config.js @@ -43,6 +43,7 @@ export default { 'json', 'ts', 'tsx', + 'html' ], modulePathIgnorePatterns: [ '__fixtures__/', @@ -54,6 +55,7 @@ export default { transform: { '^.+\\.js$': `${kbnDir}/src/dev/jest/babel_transform.js`, '^.+\\.tsx?$': `${kbnDir}/src/dev/jest/babel_transform.js`, + '^.+\\.html?$': `${kbnDir}/src/dev/jest/babel_transform.js`, }, transformIgnorePatterns: [ '[/\\\\]node_modules[/\\\\].+\\.js$', From 01beca55f631c9289d791349f291cbefa0eae5ea Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Tue, 13 Jul 2021 12:25:15 -0300 Subject: [PATCH 069/493] Refactor try catch in Management > Configuration (#3451) * Added new try-catch strategy in Management > Configuration * Updated CHANGELOG * Requested changes * doc(changelog): update Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 3 +- .../configuration/configuration-switch.js | 41 +++++++++- .../edit-configuration/edit-configuration.js | 74 ++++++++++++------- .../refresh-cluster-info-button.js | 14 ++++ .../configuration/util-hocs/loading.js | 25 +++++++ .../configuration/utils/wz-fetch.js | 34 ++++----- 6 files changed, 143 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f0117542..28e9122d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) - Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) ### Changed @@ -2111,4 +2112,4 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Search bar across panels now support parenthesis grouping -- Several CSS fixes for IE browser \ No newline at end of file +- Several CSS fixes for IE browser diff --git a/public/controllers/management/components/management/configuration/configuration-switch.js b/public/controllers/management/components/management/configuration/configuration-switch.js index a568018230..8540fd0073 100644 --- a/public/controllers/management/components/management/configuration/configuration-switch.js +++ b/public/controllers/management/components/management/configuration/configuration-switch.js @@ -71,6 +71,9 @@ import { import { agentIsSynchronized } from './utils/wz-fetch'; import { WzRequest } from '../../../../../react-services/wz-request'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; class WzConfigurationSwitch extends Component { constructor(props) { @@ -102,8 +105,18 @@ class WzConfigurationSwitch extends Component { try { const agentSynchronized = await agentIsSynchronized(this.props.agent); this.setState({ agentSynchronized }); - } catch (error) { - // do nothing + }catch(error){ + const options = { + context: `${WzConfigurationSwitch.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } } else { try { @@ -126,19 +139,39 @@ class WzConfigurationSwitch extends Component { // do nothing if it isn't a cluster this.props.updateClusterNodes(false); this.props.updateClusterNodeSelected(false); + const options = { + context: `${WzConfigurationSwitch.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } // If manager/cluster require agent platform info to filter sections in overview. It isn't coming from props for Management/Configuration try{ this.setState({ loadingOverview: true }); - const masterNodeInfo = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=000'}}); - this.setState({ masterNodeInfo: masterNodeInfo.data.affected_items[0] }); this.setState({ loadingOverview: false }); }catch(error){ this.setState({ loadingOverview: false }); + const options = { + context: `${WzConfigurationSwitch.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } } } diff --git a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js index a3271873c6..b08032180c 100644 --- a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js +++ b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js @@ -53,6 +53,10 @@ import { compose } from 'redux'; import { AppState } from '../../../../../../react-services/app-state'; import { ApiCheck } from '../../../../../../react-services/wz-api-check'; +import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; + class WzEditConfiguration extends Component { constructor(props) { super(props); @@ -96,42 +100,45 @@ class WzEditConfiguration extends Component { color: 'success' }); } catch (error) { + let errorMessage; if (error.details) { - this.addToast({ - title: ( - <Fragment> - <EuiIcon type="alert" /> -   - <span> - File ossec.conf saved, but there were found several error while - validating the configuration. - </span> - </Fragment> - ), - color: 'warning', - text: error.details - }); + errorMessage = `File ossec.conf saved, but there were found several error while validating the configuration. ${error.details}`; } else { - this.addToast({ - title: ( - <Fragment> - <EuiIcon type="alert" /> -   - <span>Error saving configuration</span> - </Fragment> - ), - color: 'danger', - text: typeof error === 'string' ? error : error.message - }); + errorMessage = 'Error saving configuration'; } this.setState({ saving: false, infoChangesAfterRestart: false }); + const options = { + context: `${WzEditConfiguration.name}.editorSave`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: errorMessage || error, + title: `${error.name}: Mitre alerts could not be fetched`, + }, + }; + getErrorOrchestrator().handleError(options); } } editorCancel() { this.props.updateConfigurationSection(''); } refresh() { - this.checkIfClusterOrManager(); + try { + this.checkIfClusterOrManager(); + }catch(error){ + const options = { + context: `${WzEditConfiguration.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); + } } toggleRestart() { this.setState({ restart: !this.state.restart }); @@ -180,6 +187,17 @@ class WzEditConfiguration extends Component { } catch (error) { this.props.updateWazuhNotReadyYet(''); this.setState({ restart: false, saving: false, restarting: false }); + const options = { + context: `${WzEditConfiguration.name}.confirmRestart`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -222,6 +240,7 @@ class WzEditConfiguration extends Component { 'edit-configuration', 'Manager configuration' ); + throw error; } } render() { @@ -344,6 +363,7 @@ const WzEditorConfiguration = compose( ) )( class WzEditorConfiguration extends Component { + constructor(props) { super(props); } @@ -413,3 +433,5 @@ const WzEditorConfiguration = compose( } } ); + + diff --git a/public/controllers/management/components/management/configuration/util-components/refresh-cluster-info-button.js b/public/controllers/management/components/management/configuration/util-components/refresh-cluster-info-button.js index d2dcc3d21d..6b921b7931 100644 --- a/public/controllers/management/components/management/configuration/util-components/refresh-cluster-info-button.js +++ b/public/controllers/management/components/management/configuration/util-components/refresh-cluster-info-button.js @@ -22,6 +22,9 @@ import { updateRefreshTime } from '../../../../../../redux/actions/configurationActions'; import { clusterNodes, clusterReq } from '../utils/wz-fetch'; +import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; class WzRefreshClusterInfoButton extends Component { constructor(props) { @@ -58,6 +61,17 @@ class WzRefreshClusterInfoButton extends Component { // do nothing if it isn't a cluster this.props.updateClusterNodes(false); this.props.updateClusterNodeSelected(false); + const options = { + context: `${WzRefreshClusterInfoButton.name}.checkIfClusterOrManager`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } this.setState({isLoading: false}); this.props.updateRefreshTime(); diff --git a/public/controllers/management/components/management/configuration/util-hocs/loading.js b/public/controllers/management/components/management/configuration/util-hocs/loading.js index 64dcddb14a..07a947ac74 100644 --- a/public/controllers/management/components/management/configuration/util-hocs/loading.js +++ b/public/controllers/management/components/management/configuration/util-hocs/loading.js @@ -13,6 +13,9 @@ import React, { Component, Fragment } from 'react'; import WzLoading from '../util-components/loading'; +import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../../react-services/common-services'; const withLoading = ( load, @@ -36,6 +39,17 @@ const withLoading = ( this.setState({ isLoading: false, error: false, wrappedProps }); } catch (error) { this.setState({ isLoading: false, error, wrappedProps: undefined }); + const options = { + context: `${WithLoading.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } } async componentDidUpdate(prevProps){ @@ -47,6 +61,17 @@ const withLoading = ( this.setState({ isLoading: false, wrappedProps }); } catch (error) { this.setState({ isLoading: false, error, wrappedProps: undefined }); + const options = { + context: `${WithLoading.name}.componentDidUpdate`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error + }, + }; + getErrorOrchestrator().handleError(options); } } } diff --git a/public/controllers/management/components/management/configuration/utils/wz-fetch.js b/public/controllers/management/components/management/configuration/utils/wz-fetch.js index 8272ec22e6..72a9a619e3 100644 --- a/public/controllers/management/components/management/configuration/utils/wz-fetch.js +++ b/public/controllers/management/components/management/configuration/utils/wz-fetch.js @@ -79,7 +79,7 @@ export const getCurrentConfig = async ( } return result; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -146,7 +146,7 @@ export const handleError = async (error, location, updateWazuhNotReadyYet, isClu return text; } catch (error) { - console.error(error); + throw error; } }; @@ -181,7 +181,7 @@ export const checkDaemons = async (isCluster) => { console.warn('Wazuh not ready yet'); } } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -212,7 +212,7 @@ export const makePing = async (updateWazuhNotReadyYet, isCluster, tries = 30) => } return Promise.resolve('Wazuh is ready'); } catch (error) { - return Promise.reject('Wazuh could not be recovered.'); + throw new Error('Wazuh could not be recovered.'); } }; @@ -258,7 +258,7 @@ export const fetchFile = async selectedNode => { xml = xml.replace(/..xml.+\?>/, ''); return xml; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -282,7 +282,7 @@ export const restartNodeSelected = async ( isCluster ? await restartNode(selectedNode) : await restartManager(); return await makePing(updateWazuhNotReadyYet, isCluster); } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -304,7 +304,7 @@ export const restartManager = async () => { const result = await WzRequest.apiReq('PUT', `/manager/restart`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -335,7 +335,7 @@ export const restartCluster = async () => { } }; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -364,7 +364,7 @@ export const restartNode = async node => { return result; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -379,7 +379,7 @@ export const saveConfiguration = async (selectedNode, xml) => { await saveFileManager(xml); } } catch (error) { - return Promise.reject(error.message || error); + throw error; } }; @@ -399,7 +399,7 @@ export const saveNodeConfiguration = async (node, content) => { ); return result; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -420,7 +420,7 @@ export const saveFileCluster = async (text, node) => { ); await validateAfterSent(node); } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -440,7 +440,7 @@ export const saveFileManager = async text => { ); await validateAfterSent(false); } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -479,7 +479,7 @@ export const validateAfterSent = async (node = false) => { } return true; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -499,7 +499,7 @@ export const clusterNodes = async () => { const result = await WzRequest.apiReq('GET', `/cluster/nodes`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -518,7 +518,7 @@ export const checkCurrentSecurityPlatform = async () => { return platform; } catch (error) { - return Promise.reject(error); + throw error; } }; @@ -544,6 +544,6 @@ export const restartClusterOrManager = async (updateWazuhNotReadyYet) => { await makePing(updateWazuhNotReadyYet, isCluster); return { restarted: isCluster ? 'Cluster' : 'Manager'} }catch (error){ - return Promise.reject(error); + throw error; }; }; From a9f64451d289e34abbf51d546dc7577aa2d8c5ab Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Tue, 13 Jul 2021 20:35:29 +0200 Subject: [PATCH 070/493] Updating test. (#3470) * test(unittest): Updating test. * test(unittest): Updating test. --- package.json | 1 - .../reporting-main.test.tsx.snap | 961 +----------------- .../reporting/reporting-main.test.tsx | 4 +- .../statistics/statistics-main.test.tsx | 43 - 4 files changed, 5 insertions(+), 1004 deletions(-) delete mode 100644 public/controllers/management/components/management/statistics/statistics-main.test.tsx diff --git a/package.json b/package.json index f1ecb926c5..a5e460df86 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "react-codemirror": "^1.0.0", "react-cookie": "^4.0.3", "read-last-lines": "^1.7.2", - "redux-mock-store": "^1.5.4", "timsort": "^0.3.0", "winston": "3.0.0" }, diff --git a/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap b/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap index 7e4c4c48a2..b7a65fb1b1 100644 --- a/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap +++ b/public/controllers/management/components/management/reporting/__snapshots__/reporting-main.test.tsx.snap @@ -1,962 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Reporting component renders correctly to match the snapshot 1`] = ` -<WzReporting> - <WzReduxProvider> - <Provider - store={ - Object { - "dispatch": [Function], - "getState": [Function], - "replaceReducer": [Function], - "subscribe": [Function], - Symbol(observable): [Function], - } - } - > - <WzReportingOverview> - <EuiPage - style={ - Object { - "background": "transparent", - } - } - > - <div - className="euiPage" - style={ - Object { - "background": "transparent", - } - } - > - <EuiPanel> - <div - className="euiPanel euiPanel--paddingMedium" - > - <EuiFlexGroup> - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem> - <div - className="euiFlexItem" - > - <EuiFlexGroup> - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem> - <div - className="euiFlexItem" - > - <EuiTitle> - <h2 - className="euiTitle euiTitle--medium" - > - Reporting - </h2> - </EuiTitle> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </div> - </EuiFlexItem> - <Connect(WzReportingActionButtons)> - <WzReportingActionButtons - state={ - Object { - "isLoading": true, - "isProcessing": true, - "itemList": Array [], - "showModal": false, - } - } - updateIsProcessing={[Function]} - > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiButtonEmpty - iconType="refresh" - onClick={[Function]} - > - <button - className="euiButtonEmpty euiButtonEmpty--primary" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="left" - iconType="refresh" - textProps={ - Object { - "className": "euiButtonEmpty__text", - } - } - > - <span - className="euiButtonContent euiButtonEmpty__content" - > - <EuiIcon - className="euiButtonContent__icon" - size="m" - type="refresh" - > - <EuiIconEmpty - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" - focusable="false" - role="img" - style={null} - > - <svg - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" - focusable="false" - height={16} - role="img" - style={null} - viewBox="0 0 16 16" - width={16} - xmlns="http://www.w3.org/2000/svg" - /> - </EuiIconEmpty> - </EuiIcon> - <span - className="euiButtonEmpty__text" - > - Refresh - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </div> - </EuiFlexItem> - </WzReportingActionButtons> - </Connect(WzReportingActionButtons)> - </div> - </EuiFlexGroup> - <EuiFlexGroup> - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem> - <div - className="euiFlexItem" - > - <EuiText - color="subdued" - style={ - Object { - "paddingBottom": "15px", - } - } - > - <div - className="euiText euiText--medium" - style={ - Object { - "paddingBottom": "15px", - } - } - > - <EuiTextColor - color="subdued" - component="div" - > - <div - className="euiTextColor euiTextColor--subdued" - > - From here you can check all your reports. - </div> - </EuiTextColor> - </div> - </EuiText> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - <EuiFlexGroup> - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem> - <div - className="euiFlexItem" - > - <Connect(WzReportingTable)> - <WzReportingTable - state={ - Object { - "isLoading": true, - "isProcessing": true, - "itemList": Array [], - "showModal": false, - } - } - updateIsProcessing={[Function]} - updateListItemsForRemove={[Function]} - updateShowModal={[Function]} - > - <div> - <EuiInMemoryTable - columns={ - Array [ - Object { - "align": "left", - "field": "name", - "name": "File", - "sortable": true, - }, - Object { - "field": "size", - "name": "Size", - "render": [Function], - "sortable": true, - }, - Object { - "field": "date", - "name": "Created", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "name": "Actions", - "render": [Function], - }, - ] - } - items={Array []} - loading={true} - message={null} - pagination={true} - responsive={true} - search={ - Object { - "box": Object { - "incremental": true, - }, - } - } - sorting={ - Object { - "sort": Object { - "direction": "desc", - "field": "date", - }, - } - } - tableLayout="fixed" - > - <div> - <EuiSearchBar - box={ - Object { - "incremental": true, - } - } - onChange={[Function]} - > - <EuiFlexGroup - alignItems="center" - gutterSize="m" - wrap={true} - > - <div - className="euiFlexGroup euiFlexGroup--gutterMedium euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive euiFlexGroup--wrap" - > - <EuiFlexItem - className="euiSearchBar__searchHolder" - grow={true} - > - <div - className="euiFlexItem euiSearchBar__searchHolder" - > - <EuiSearchBox - incremental={true} - isInvalid={false} - onSearch={[Function]} - placeholder="Search..." - query="" - > - <EuiFieldSearch - aria-label="This is a search bar. As you type, the results lower in the page will automatically filter." - compressed={false} - defaultValue="" - fullWidth={true} - incremental={true} - inputRef={[Function]} - isClearable={true} - isInvalid={false} - isLoading={false} - onSearch={[Function]} - placeholder="Search..." - > - <EuiFormControlLayout - compressed={false} - fullWidth={true} - icon="search" - isLoading={false} - > - <div - className="euiFormControlLayout euiFormControlLayout--fullWidth" - > - <div - className="euiFormControlLayout__childrenWrapper" - > - <EuiValidatableControl - isInvalid={false} - > - <input - aria-label="This is a search bar. As you type, the results lower in the page will automatically filter." - className="euiFieldSearch euiFieldSearch--fullWidth euiFieldSearch-isClearable" - defaultValue="" - onKeyUp={[Function]} - placeholder="Search..." - type="search" - /> - </EuiValidatableControl> - <EuiFormControlLayoutIcons - icon="search" - isLoading={false} - > - <div - className="euiFormControlLayoutIcons" - > - <EuiFormControlLayoutCustomIcon - type="search" - > - <span - className="euiFormControlLayoutCustomIcon" - > - <EuiIcon - aria-hidden="true" - className="euiFormControlLayoutCustomIcon__icon" - type="search" - > - <EuiIconEmpty - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" - focusable="false" - role="img" - style={null} - > - <svg - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" - focusable="false" - height={16} - role="img" - style={null} - viewBox="0 0 16 16" - width={16} - xmlns="http://www.w3.org/2000/svg" - /> - </EuiIconEmpty> - </EuiIcon> - </span> - </EuiFormControlLayoutCustomIcon> - </div> - </EuiFormControlLayoutIcons> - </div> - </div> - </EuiFormControlLayout> - </EuiFieldSearch> - </EuiSearchBox> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </EuiSearchBar> - <EuiSpacer - size="l" - > - <div - className="euiSpacer euiSpacer--l" - /> - </EuiSpacer> - <EuiBasicTable - columns={ - Array [ - Object { - "align": "left", - "field": "name", - "name": "File", - "sortable": true, - }, - Object { - "field": "size", - "name": "Size", - "render": [Function], - "sortable": true, - }, - Object { - "field": "date", - "name": "Created", - "render": [Function], - "sortable": true, - }, - Object { - "align": "left", - "name": "Actions", - "render": [Function], - }, - ] - } - items={Array []} - loading={true} - noItemsMessage={null} - onChange={[Function]} - pagination={ - Object { - "hidePerPageOptions": undefined, - "pageIndex": 0, - "pageSize": 10, - "pageSizeOptions": Array [ - 10, - 25, - 50, - ], - "totalItemCount": 0, - } - } - responsive={true} - sorting={ - Object { - "allowNeutralSort": true, - "sort": Object { - "direction": "desc", - "field": "Created", - }, - } - } - tableLayout="fixed" - > - <div - className="euiBasicTable euiBasicTable-loading" - > - <div> - <EuiTableHeaderMobile> - <div - className="euiTableHeaderMobile" - > - <EuiFlexGroup - alignItems="baseline" - justifyContent="spaceBetween" - responsive={false} - > - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" - > - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - /> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiTableSortMobile - items={ - Array [ - Object { - "isSortAscending": undefined, - "isSorted": false, - "key": "_data_s_name_0", - "name": "File", - "onSort": [Function], - }, - Object { - "isSortAscending": undefined, - "isSorted": false, - "key": "_data_s_size_1", - "name": "Size", - "onSort": [Function], - }, - Object { - "isSortAscending": false, - "isSorted": true, - "key": "_data_s_date_2", - "name": "Created", - "onSort": [Function], - }, - ] - } - > - <div - className="euiTableSortMobile" - > - <EuiPopover - anchorPosition="downRight" - button={ - <EuiButtonEmpty - flush="right" - iconSide="right" - iconType="arrowDown" - onClick={[Function]} - size="xs" - > - <EuiI18n - default="Sorting" - token="euiTableSortMobile.sorting" - /> - </EuiButtonEmpty> - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - > - <EuiOutsideClickDetector - isDisabled={true} - onOutsideClick={[Function]} - > - <div - className="euiPopover euiPopover--anchorDownRight" - onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} - > - <div - className="euiPopover__anchor" - > - <EuiButtonEmpty - flush="right" - iconSide="right" - iconType="arrowDown" - onClick={[Function]} - size="xs" - > - <button - className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" - disabled={false} - onClick={[Function]} - type="button" - > - <EuiButtonContent - className="euiButtonEmpty__content" - iconSide="right" - iconType="arrowDown" - textProps={ - Object { - "className": "euiButtonEmpty__text", - } - } - > - <span - className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" - > - <EuiIcon - className="euiButtonContent__icon" - size="m" - type="arrowDown" - > - <EuiIconEmpty - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" - focusable="false" - role="img" - style={null} - > - <svg - aria-hidden={true} - className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" - focusable="false" - height={16} - role="img" - style={null} - viewBox="0 0 16 16" - width={16} - xmlns="http://www.w3.org/2000/svg" - /> - </EuiIconEmpty> - </EuiIcon> - <span - className="euiButtonEmpty__text" - > - <EuiI18n - default="Sorting" - token="euiTableSortMobile.sorting" - > - Sorting - </EuiI18n> - </span> - </span> - </EuiButtonContent> - </button> - </EuiButtonEmpty> - </div> - </div> - </EuiOutsideClickDetector> - </EuiPopover> - </div> - </EuiTableSortMobile> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </div> - </EuiTableHeaderMobile> - <EuiTable - id="__table_bd685711-da9b-11eb-80ce-ff701c08216e" - responsive={true} - tableLayout="fixed" - > - <table - className="euiTable euiTable--responsive" - id="__table_bd685711-da9b-11eb-80ce-ff701c08216e" - tabIndex={-1} - > - <EuiScreenReaderOnly> - <caption - className="euiScreenReaderOnly euiTableCaption" - > - <EuiDelayRender - delay={500} - /> - </caption> - </EuiScreenReaderOnly> - <EuiTableHeader> - <thead> - <tr> - <EuiTableHeaderCell - align="left" - allowNeutralSort={true} - data-test-subj="tableHeaderCell_name_0" - isSorted={false} - key="_data_h_name_0" - onSort={[Function]} - > - <th - aria-live="polite" - aria-sort="none" - className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_name_0" - role="columnheader" - scope="col" - style={ - Object { - "width": undefined, - } - } - > - <button - className="euiTableHeaderButton" - data-test-subj="tableHeaderSortButton" - onClick={[Function]} - type="button" - > - <span - className="euiTableCellContent" - > - <EuiInnerText> - <EuiI18n - default="{innerText}; Sorted in {ariaSortValue} order" - token="euiTableHeaderCell.titleTextWithSort" - values={ - Object { - "ariaSortValue": "none", - "innerText": "File", - } - } - > - <span - className="euiTableCellContent__text" - title="File" - > - File - </span> - </EuiI18n> - </EuiInnerText> - <EuiScreenReaderOnly> - <span - className="euiScreenReaderOnly" - > - <EuiI18n - default="Click to sort in ascending order" - token="euiTableHeaderCell.clickForAscending" - > - Click to sort in ascending order - </EuiI18n> - </span> - </EuiScreenReaderOnly> - </span> - </button> - </th> - </EuiTableHeaderCell> - <EuiTableHeaderCell - align="left" - allowNeutralSort={true} - data-test-subj="tableHeaderCell_size_1" - isSorted={false} - key="_data_h_size_1" - onSort={[Function]} - > - <th - aria-live="polite" - aria-sort="none" - className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_size_1" - role="columnheader" - scope="col" - style={ - Object { - "width": undefined, - } - } - > - <button - className="euiTableHeaderButton" - data-test-subj="tableHeaderSortButton" - onClick={[Function]} - type="button" - > - <span - className="euiTableCellContent" - > - <EuiInnerText> - <EuiI18n - default="{innerText}; Sorted in {ariaSortValue} order" - token="euiTableHeaderCell.titleTextWithSort" - values={ - Object { - "ariaSortValue": "none", - "innerText": "Size", - } - } - > - <span - className="euiTableCellContent__text" - title="Size" - > - Size - </span> - </EuiI18n> - </EuiInnerText> - <EuiScreenReaderOnly> - <span - className="euiScreenReaderOnly" - > - <EuiI18n - default="Click to sort in ascending order" - token="euiTableHeaderCell.clickForAscending" - > - Click to sort in ascending order - </EuiI18n> - </span> - </EuiScreenReaderOnly> - </span> - </button> - </th> - </EuiTableHeaderCell> - <EuiTableHeaderCell - align="left" - allowNeutralSort={true} - data-test-subj="tableHeaderCell_date_2" - isSortAscending={false} - isSorted={true} - key="_data_h_date_2" - onSort={[Function]} - > - <th - aria-live="polite" - aria-sort="descending" - className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_date_2" - role="columnheader" - scope="col" - style={ - Object { - "width": undefined, - } - } - > - <button - className="euiTableHeaderButton euiTableHeaderButton-isSorted" - data-test-subj="tableHeaderSortButton" - onClick={[Function]} - type="button" - > - <span - className="euiTableCellContent" - > - <EuiInnerText> - <EuiI18n - default="{innerText}; Sorted in {ariaSortValue} order" - token="euiTableHeaderCell.titleTextWithSort" - values={ - Object { - "ariaSortValue": "descending", - "innerText": "Created", - } - } - > - <span - className="euiTableCellContent__text" - title="Created; Sorted in descending order" - > - Created - </span> - </EuiI18n> - </EuiInnerText> - <EuiI18n - default="Sorted in {ariaSortValue} order" - token="euiTableHeaderCell.sortedAriaLabel" - values={ - Object { - "ariaSortValue": "descending", - } - } - > - <EuiIcon - aria-label="Sorted in descending order" - className="euiTableSortIcon" - size="m" - type="sortDown" - > - <EuiIconEmpty - aria-hidden={true} - aria-label="Sorted in descending order" - className="euiIcon euiIcon--medium euiIcon-isLoading euiTableSortIcon" - focusable="false" - role="img" - style={null} - > - <svg - aria-hidden={true} - aria-label="Sorted in descending order" - className="euiIcon euiIcon--medium euiIcon-isLoading euiTableSortIcon" - focusable="false" - height={16} - role="img" - style={null} - viewBox="0 0 16 16" - width={16} - xmlns="http://www.w3.org/2000/svg" - /> - </EuiIconEmpty> - </EuiIcon> - </EuiI18n> - <EuiScreenReaderOnly> - <span - className="euiScreenReaderOnly" - > - <EuiI18n - default="Click to unsort" - token="euiTableHeaderCell.clickForUnsort" - > - Click to unsort - </EuiI18n> - </span> - </EuiScreenReaderOnly> - </span> - </button> - </th> - </EuiTableHeaderCell> - <EuiTableHeaderCell - align="left" - data-test-subj="tableHeaderCell_Actions_3" - key="_computed_column_h_3" - > - <th - className="euiTableHeaderCell" - data-test-subj="tableHeaderCell_Actions_3" - role="columnheader" - scope="col" - style={ - Object { - "width": undefined, - } - } - > - <div - className="euiTableCellContent" - > - <EuiInnerText> - <span - className="euiTableCellContent__text" - title="Actions" - > - Actions - </span> - </EuiInnerText> - </div> - </th> - </EuiTableHeaderCell> - </tr> - </thead> - </EuiTableHeader> - <EuiTableBody> - <tbody> - <EuiTableRow> - <tr - className="euiTableRow" - > - <EuiTableRowCell - align="center" - colSpan={4} - isMobileFullWidth={true} - > - <td - className="euiTableRowCell euiTableRowCell--isMobileFullWidth" - colSpan={4} - style={ - Object { - "width": undefined, - } - } - > - <div - className="euiTableCellContent euiTableCellContent--alignCenter" - > - <span - className="euiTableCellContent__text" - /> - </div> - </td> - </EuiTableRowCell> - </tr> - </EuiTableRow> - </tbody> - </EuiTableBody> - </table> - </EuiTable> - </div> - </div> - </EuiBasicTable> - </div> - </EuiInMemoryTable> - </div> - </WzReportingTable> - </Connect(WzReportingTable)> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - </div> - </EuiPanel> - </div> - </EuiPage> - </WzReportingOverview> - </Provider> - </WzReduxProvider> -</WzReporting> +<WzReduxProvider> + <WzReportingOverview /> +</WzReduxProvider> `; diff --git a/public/controllers/management/components/management/reporting/reporting-main.test.tsx b/public/controllers/management/components/management/reporting/reporting-main.test.tsx index 9c097e8eb8..c5789212ad 100644 --- a/public/controllers/management/components/management/reporting/reporting-main.test.tsx +++ b/public/controllers/management/components/management/reporting/reporting-main.test.tsx @@ -13,7 +13,7 @@ */ import React from 'react'; -import { mount } from 'enzyme'; +import { shallow } from 'enzyme'; import WzReporting from './reporting-main'; jest.mock('../../../../../kibana-services', () => ({ @@ -27,7 +27,7 @@ jest.mock('../../../../../kibana-services', () => ({ describe('Reporting component', () => { it('renders correctly to match the snapshot', () => { - const wrapper = mount(<WzReporting />); + const wrapper = shallow(<WzReporting />); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/public/controllers/management/components/management/statistics/statistics-main.test.tsx b/public/controllers/management/components/management/statistics/statistics-main.test.tsx deleted file mode 100644 index eaf2de0364..0000000000 --- a/public/controllers/management/components/management/statistics/statistics-main.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Wazuh app - React test for Statistics component. - * - * Copyright (C) 2015-2021 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 from 'react'; -import { shallow } from 'enzyme'; -import { Provider } from 'react-redux'; -import configureMockStore from 'redux-mock-store'; -import WzStatistics from './statistics-main'; - -jest.mock('../../../../../kibana-services', () => ({ - getAngularModule: jest.fn(), - getHttp: () => ({ - basePath: { - prepend: (str) => str, - }, - }), -})); - -jest.mock('react'); -const mockStore = configureMockStore(); -const store = mockStore({}); - -describe('Statistics component', () => { - it('renders correctly to match the snapshot', () => { - const wrapper = shallow( - <Provider store={store}> - <WzStatistics /> - </Provider> - ); - expect(wrapper).toMatchSnapshot(); - }); -}); From 0b475863a5b973c71097c34d70878fbaa00f41c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 14 Jul 2021 11:59:38 +0200 Subject: [PATCH 071/493] Added unit tests for component --- .../visualize/components/sample-data.js | 15 +++-- .../visualize/components/sample-data.test.js | 60 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 public/components/visualize/components/sample-data.test.js diff --git a/public/components/visualize/components/sample-data.js b/public/components/visualize/components/sample-data.js index 9c95be1729..8a745168aa 100644 --- a/public/components/visualize/components/sample-data.js +++ b/public/components/visualize/components/sample-data.js @@ -12,18 +12,18 @@ import { EuiCallOut, EuiLink } from '@elastic/eui'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { WzRequest } from '../../../react-services'; import { getErrorOrchestrator } from '../../../react-services/common-services'; export const SampleData = ({ context = 'sample-data', ...props }) => { const [isSampleData, setIsSampleData] = useState(false); - useEffect(async () => { + const usesSampleData = async () => { try { - const thereAreSampleAlerts = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})) - .data.sampleAlertsInstalled; - setIsSampleData(thereAreSampleAlerts); + const result = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})).data + .sampleAlertsInstalled; + setIsSampleData(result); } catch (error) { const options = { context, @@ -37,6 +37,9 @@ export const SampleData = ({ context = 'sample-data', ...props }) => { }; getErrorOrchestrator().handleError(options); } + }; + useEffect(() => { + usesSampleData(); }, [ setIsSampleData, context, @@ -56,7 +59,7 @@ export const SampleData = ({ context = 'sample-data', ...props }) => { {...props} > <p> - The data displayed may contain sample alerts. Go ººººº + The data displayed may contain sample alerts. Go <EuiLink href="#/settings?tab=sample_data" aria-label="go to configure sample data"> here </EuiLink>{' '} diff --git a/public/components/visualize/components/sample-data.test.js b/public/components/visualize/components/sample-data.test.js new file mode 100644 index 0000000000..45c0119f14 --- /dev/null +++ b/public/components/visualize/components/sample-data.test.js @@ -0,0 +1,60 @@ +import { SampleData } from './sample-data'; +import { WzRequest } from '../../../react-services'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import React from 'react'; + +const awaitForMyComponent = async (wrapper) => { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + wrapper.update(); + }); +}; +jest.mock('../../../react-services'); +jest.mock('../../../react-services/common-services'); +describe('Check sample data component', () => { + it('should render if there is sample data', async () => { + WzRequest.genericReq.mockResolvedValue({ data: { sampleAlertsInstalled: true } }); + const wrapper = await mount(<SampleData />); + await awaitForMyComponent(wrapper); + expect(wrapper.find('EuiCallOut').exists()); + }); + + it('should not render if there is no sample data', async () => { + WzRequest.genericReq.mockResolvedValue({ data: { sampleAlertsInstalled: false } }); + const wrapper = await mount(<SampleData />); + await awaitForMyComponent(wrapper); + expect(wrapper.contains('EuiCallOut')).toBe(false); + }); + + it('should call the orchestrator upon error', async () => { + const context = 'testComponent'; + const error = { + message: 'This is a test', + name: 'This should not be the thing', + }; + const mockOptions = { + context, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator.mockImplementation(() => { + return { + handleError: (options) => { + expect(options).toEqual(mockOptions); + }, + }; + }); + WzRequest.genericReq.mockRejectedValue(error); + const wrapper = await mount(<SampleData context={context} />); + await awaitForMyComponent(wrapper); + }); +}); From fba308a75403c6ace6d444f14dbcdbff14b61ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 14 Jul 2021 12:12:13 +0200 Subject: [PATCH 072/493] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc557c2785..5dfe794dad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) ### Changed From 3fe9fc1ecc1af08f3eb21a8e2deccfaf2ffaf766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 14 Jul 2021 14:43:46 +0200 Subject: [PATCH 073/493] Removed context prop --- public/components/visualize/components/sample-data.js | 8 ++++---- public/components/visualize/wz-visualize.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/components/visualize/components/sample-data.js b/public/components/visualize/components/sample-data.js index 8a745168aa..fe25f7e6c8 100644 --- a/public/components/visualize/components/sample-data.js +++ b/public/components/visualize/components/sample-data.js @@ -1,5 +1,5 @@ /* - * Wazuh app - React component for Visualize. + * Wazuh app - React component for Visualize - Sample Data. * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -16,7 +16,7 @@ import React, { useState, useEffect } from 'react'; import { WzRequest } from '../../../react-services'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -export const SampleData = ({ context = 'sample-data', ...props }) => { +export const SampleData = ({...props }) => { const [isSampleData, setIsSampleData] = useState(false); const usesSampleData = async () => { @@ -26,7 +26,7 @@ export const SampleData = ({ context = 'sample-data', ...props }) => { setIsSampleData(result); } catch (error) { const options = { - context, + context: `${SampleData.name}.usesSampleData`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, error: { @@ -41,8 +41,8 @@ export const SampleData = ({ context = 'sample-data', ...props }) => { useEffect(() => { usesSampleData(); }, [ + SampleData, setIsSampleData, - context, UI_ERROR_SEVERITIES, UI_LOGGER_LEVELS, getErrorOrchestrator, diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index fd2dadf027..b8a0dd4fb6 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -257,7 +257,7 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class <Fragment> {/* Sample alerts Callout */} {this.props.resultState === 'ready' && ( - <SampleData context={`${WzVisualize.name}-sample-data`} /> + <SampleData/> )} {this.props.resultState === 'none' && ( From 0e442424c63d5bf4455e4b70f573589c2607aa91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 14 Jul 2021 14:43:59 +0200 Subject: [PATCH 074/493] Updated tests --- .../visualize/components/sample-data.test.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/public/components/visualize/components/sample-data.test.js b/public/components/visualize/components/sample-data.test.js index 45c0119f14..6d7e1840a9 100644 --- a/public/components/visualize/components/sample-data.test.js +++ b/public/components/visualize/components/sample-data.test.js @@ -1,3 +1,14 @@ +/* + * Wazuh app - Testing suite for Visualize - Sample Data. + * Copyright (C) 2015-2021 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 { SampleData } from './sample-data'; import { WzRequest } from '../../../react-services'; import { mount } from 'enzyme'; @@ -21,6 +32,7 @@ describe('Check sample data component', () => { const wrapper = await mount(<SampleData />); await awaitForMyComponent(wrapper); expect(wrapper.find('EuiCallOut').exists()); + expect(wrapper.find('EuiCallOut').props().title).toEqual("This dashboard contains sample data"); }); it('should not render if there is no sample data', async () => { @@ -31,19 +43,18 @@ describe('Check sample data component', () => { }); it('should call the orchestrator upon error', async () => { - const context = 'testComponent'; const error = { message: 'This is a test', name: 'This should not be the thing', }; const mockOptions = { - context, + context: `${SampleData.name}.usesSampleData`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, error: { error: error, - message: error.message || error, - title: error.name || error, + message: error.message, + title: error.name, }, }; getErrorOrchestrator.mockImplementation(() => { @@ -54,7 +65,7 @@ describe('Check sample data component', () => { }; }); WzRequest.genericReq.mockRejectedValue(error); - const wrapper = await mount(<SampleData context={context} />); + const wrapper = await mount(<SampleData />); await awaitForMyComponent(wrapper); }); }); From 7714fbf33e982944348fb65821f0d743c35c2435 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 15 Jul 2021 13:59:40 +0200 Subject: [PATCH 075/493] add panel base view --- .../common/modules/main-overview.tsx | 3 +- .../common/modules/modules-defaults.js | 5 + .../components/common/modules/panel/index.tsx | 16 + .../common/modules/panel/main-panel.tsx | 28 + .../common/modules/panel/module-body.tsx | 15 + .../modules/panel/module-side-panel.tsx | 28 + .../overview/office-panel/index.tsx | 14 + .../overview/office-panel/mockup-tables.tsx | 102 +++ .../overview/office-panel/module-stats.tsx | 28 + .../overview/office-panel/office-panel.tsx | 65 ++ public/services/common-data.js | 1 + .../visualizations/overview/index.ts | 2 + .../overview/overview-office.ts | 671 ++++++++++++++++++ 13 files changed, 977 insertions(+), 1 deletion(-) create mode 100644 public/components/common/modules/panel/index.tsx create mode 100644 public/components/common/modules/panel/main-panel.tsx create mode 100644 public/components/common/modules/panel/module-body.tsx create mode 100644 public/components/common/modules/panel/module-side-panel.tsx create mode 100644 public/components/overview/office-panel/index.tsx create mode 100644 public/components/overview/office-panel/mockup-tables.tsx create mode 100644 public/components/overview/office-panel/module-stats.tsx create mode 100644 public/components/overview/office-panel/office-panel.tsx create mode 100644 server/integration-files/visualizations/overview/overview-office.ts diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index 8cf7c4bf9b..f8651ab108 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -45,6 +45,7 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; +import { OfficePanel } from '../../overview/office-panel'; export class MainModuleOverview extends Component { constructor(props) { @@ -188,8 +189,8 @@ const ModuleTabViewer = compose( {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} {section === 'fim' && selectView === 'inventory' && <MainFim {...props} />} {section === 'sca' && selectView === 'inventory' && <MainSca {...props} />} - {section === 'vuls' && selectView === 'inventory' && <MainVuls {...props} />} + {section === 'office' && selectView === 'inventory' && <OfficePanel {...props} />} {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 0a7232f2c2..9b83235b71 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -30,6 +30,11 @@ export const ModulesDefaults = { tabs: [{ id: 'inventory', name: 'Inventory' }, { id: 'events', name: 'Events' }], buttons: ['settings'] }, + office: { + init: 'dashboard', + tabs: [{ id: 'inventory', name: 'Panel' }, { id: 'dashboard', name: 'Dashboards' }, { id: 'events', name: 'Events' }], + buttons: ['reporting'] + }, mitre: { init: 'dashboard', tabs: [{id: 'intelligence', name: 'Intelligence'}, { id: 'inventory', name: 'Framework' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], diff --git a/public/components/common/modules/panel/index.tsx b/public/components/common/modules/panel/index.tsx new file mode 100644 index 0000000000..5b6b8a8051 --- /dev/null +++ b/public/components/common/modules/panel/index.tsx @@ -0,0 +1,16 @@ +/* + * Wazuh app - Panel components + * Copyright (C) 2015-2021 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 { MainPanel } from './main-panel'; +export { ModuleSidePanel } from './module-side-panel'; +export { ModuleBody } from './module-body'; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx new file mode 100644 index 0000000000..298fa64133 --- /dev/null +++ b/public/components/common/modules/panel/main-panel.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageSideBar, + EuiPageBody, +} from '@elastic/eui'; +import { ModuleSidePanel, ModuleStats, ModuleBody } from './'; + + +export const MainPanel = ({ sidePanelChildren, children, moduleStatsList = [], ...props }) => { + + return ( + <EuiFlexGroup style={{ margin: '0 8px' }}> + <EuiFlexItem> + {sidePanelChildren && <ModuleSidePanel> + {sidePanelChildren} + </ModuleSidePanel > + } + <EuiPageBody> + <ModuleBody>{children}</ModuleBody> + </EuiPageBody> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/common/modules/panel/module-body.tsx new file mode 100644 index 0000000000..ea11f94589 --- /dev/null +++ b/public/components/common/modules/panel/module-body.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { EuiPageBody, EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; + + +export const ModuleBody = ({ children, ...props }) => { + + return ( + <EuiFlexGroup > + <EuiFlexItem> + {children} + + </EuiFlexItem> + </EuiFlexGroup> + ) +} \ No newline at end of file diff --git a/public/components/common/modules/panel/module-side-panel.tsx b/public/components/common/modules/panel/module-side-panel.tsx new file mode 100644 index 0000000000..01e9039830 --- /dev/null +++ b/public/components/common/modules/panel/module-side-panel.tsx @@ -0,0 +1,28 @@ +import { EuiCollapsibleNav, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; + +export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => { + const [navIsOpen, setNavIsOpen] = useState(false); + + const infoBtnStyle = { borderRadius: '0 5px 5px 0', background: '#0000000a', zIndex: 2001 }; + return ( + <EuiCollapsibleNav + isOpen={navIsOpen} + isDocked={navIsDocked} + showCloseButton={true} + maskProps={{ headerZindexLocation: 'below', className: 'wz-no-display' }} + button={ + <EuiButtonEmpty style={{ position: "fixed", left: 0, ...infoBtnStyle, }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'iInCircle'}> + </EuiButtonEmpty> + } + onClose={() => setNavIsOpen(false)}> + <div> + <EuiButtonEmpty style={{ float: 'right' }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'}> + </EuiButtonEmpty> + <div style={{ padding: 16 }}> + {children} + </div> + </div> + </EuiCollapsibleNav> + ); +}; \ No newline at end of file diff --git a/public/components/overview/office-panel/index.tsx b/public/components/overview/office-panel/index.tsx new file mode 100644 index 0000000000..2b4b39b6d7 --- /dev/null +++ b/public/components/overview/office-panel/index.tsx @@ -0,0 +1,14 @@ +/* + * Wazuh app - Office 365 Panel index. + * + * Copyright (C) 2015-2021 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 * from './office-panel'; \ No newline at end of file diff --git a/public/components/overview/office-panel/mockup-tables.tsx b/public/components/overview/office-panel/mockup-tables.tsx new file mode 100644 index 0000000000..f07051f51e --- /dev/null +++ b/public/components/overview/office-panel/mockup-tables.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; + +export const MockupTables = () => { + const tables = [ + { + title: 'Tenants', + minWidth: '400px', + columns: [ + { + field: 'tenant', + name: 'Tenant', + }, + { + field: 'count', + name: 'Count', + }, + ], + }, + { + title: 'Subscriptions', + minWidth: '400px', + columns: [ + { + field: 'subscription', + name: 'Subscription', + }, + { + field: 'count', + name: 'count', + }, + ], + }, + { + title: 'Events', + minWidth: '750px', + width: '750px', + columns: [ + { + field: 'rule', + name: 'Description', + render: (rule) => rule.description, + }, + { + field: 'rule', + name: 'Level', + render: (rule) => rule.level, + }, + ], + }, + ]; + const generateItems = () => { + const random = new Random(); + const rules = [ + { + level: 1, + description: 'A simple rule', + }, + { + level: 5, + description: 'A worrysome rule', + }, + { + level: 12, + description: 'Oh snap', + }, + ]; + const subscriptions = ['netflix', 'spotify', 'prime']; + const tenant = ['David', 'Doctor', 'Crowlie']; + const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map((id) => { + return { + id, + rule: random.oneOf(rules), + tenant: random.oneOf(tenant), + subscription: random.oneOf(subscriptions), + count: random.integer(0, 10), + }; + }); + return items; + }; + const TitlePanel = ({ title, children, panelProps, titleProps = { size: 's' } }) => { + return <EuiPanel {...panelProps}> + <EuiTitle {...titleProps}><h2>{title}</h2></EuiTitle> + {children} + </EuiPanel> + } + const renderTables = (items, tables) => { + const rendered = tables.map((table, key) => { + return ( + <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }} key={key}> + <TitlePanel title={table.title}> + <EuiBasicTable items={items} columns={table.columns} /> + </TitlePanel> + </EuiFlexItem> + ); + }); + return rendered; + }; + + + return <EuiFlexGroup wrap>{renderTables(generateItems(), tables)}</EuiFlexGroup> +} \ No newline at end of file diff --git a/public/components/overview/office-panel/module-stats.tsx b/public/components/overview/office-panel/module-stats.tsx new file mode 100644 index 0000000000..f293829867 --- /dev/null +++ b/public/components/overview/office-panel/module-stats.tsx @@ -0,0 +1,28 @@ +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import moduleLogo from '../../../assets/office365.svg'; +import React from 'react'; + + +export const ModuleStats = ({ listItems = [] }) => { + const logoStyle = { width: 30 }; + return ( + <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem> + <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size={"xs"}><h4>Office 365</h4></EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiDescriptionList + listItems={listItems} + compressed + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx new file mode 100644 index 0000000000..de8321fb4e --- /dev/null +++ b/public/components/overview/office-panel/office-panel.tsx @@ -0,0 +1,65 @@ +/* + * Wazuh app - Office 365 Panel react component. + * + * Copyright (C) 2015-2021 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, { useState } from 'react'; + +import { MainPanel } from '../../../components/common/modules/panel'; +import { FilterManager, Filter } from '../../../../../../src/plugins/data/public/'; +//@ts-ignore +import { getDataPlugin } from '../../../kibana-services'; +import { KbnSearchBar } from '../../kbn-search-bar'; +import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; +import { withErrorBoundary } from '../../common/hocs'; +import { MockupTables } from './mockup-tables'; +import { ModuleStats } from './module-stats'; + +export const OfficePanel = withErrorBoundary(({ ...props }) => { + + const KibanaServices = getDataPlugin().query; + const filterManager = KibanaServices.filterManager; + const timefilter = KibanaServices.timefilter.timefilter; + + const [moduleStatsList, setModuleStatsList] = useState([]); + const [filterParams, setFilterParams] = useState({ + filters: filterManager.getFilters() || [], + query: { language: 'kuery', query: '' }, + time: timefilter.getTime(), + }); + const [isLoading, setLoading] = useState(false); + + + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { + const { query, dateRange } = payload; + const filters = { query, time: dateRange, filters: filterParams.filters }; + setLoading(true); + setFilterParams(filters); + + } + + const onFiltersUpdated = (filters: Filter[]) => { + const { query, time } = filterParams; + const updatedFilterParams = { query, time, filters }; + setLoading(true); + setFilterParams(updatedFilterParams); + } + + return ( + <MainPanel sidePanelChildren={<ModuleStats listItems={moduleStatsList} />}> + <KbnSearchBar + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} /> + <MockupTables /> + </MainPanel> + ) +}); diff --git a/public/services/common-data.js b/public/services/common-data.js index f7e3f31523..53b272e940 100644 --- a/public/services/common-data.js +++ b/public/services/common-data.js @@ -143,6 +143,7 @@ export class CommonData { tsc: { group: 'tsc' }, aws: { group: 'amazon' }, gcp: { group: 'gcp' }, + office: { group: 'office365' }, virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, diff --git a/server/integration-files/visualizations/overview/index.ts b/server/integration-files/visualizations/overview/index.ts index 1a65fefbad..08b95a9298 100644 --- a/server/integration-files/visualizations/overview/index.ts +++ b/server/integration-files/visualizations/overview/index.ts @@ -25,6 +25,7 @@ import pm from './overview-pm'; import virustotal from './overview-virustotal'; import vuls from './overview-vuls'; import mitre from './overview-mitre'; +import office from './overview-office'; import osquery from './overview-osquery'; import docker from './overview-docker'; @@ -45,6 +46,7 @@ export { virustotal, vuls, mitre, + office, osquery, docker }; diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts new file mode 100644 index 0000000000..a869510acc --- /dev/null +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -0,0 +1,671 @@ +/* + * Wazuh app - Module for Overview Office visualizations + * Copyright (C) 2015-2021 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 default [ + { + _id: 'Wazuh-App-Overview-OFFICE', + _source: { + title: 'Mitre attack count', + visState: JSON.stringify({ + aggs: [ + { enabled: true, id: '1', params: {}, schema: 'metric', type: 'count' }, + { + enabled: true, + id: '2', + params: { + field: 'rule.mitre.id', + customLabel: 'Attack ID', + missingBucket: false, + missingBucketLabel: 'Missing', + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + size: 244, + }, + schema: 'bucket', + type: 'terms', + }, + ], + params: { + dimensions: { + buckets: [], + metrics: [{ accessor: 0, aggType: 'count', format: { id: 'number' }, params: {} }], + }, + perPage: 10, + percentageCol: '', + showMetricsAtAllLevels: false, + showPartialRows: false, + showTotal: false, + showToolbar: true, + sort: { columnIndex: null, direction: null }, + totalFunc: 'sum', + }, + title: 'mitre', + type: 'table', + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Alerts-Evolution', + _source: { + title: 'Mitre alerts evolution', + visState: JSON.stringify({ + title: 'Alert Evolution', + type: 'line', + params: { + type: 'line', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'line', + mode: 'normal', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + lineWidth: 2, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: { + accessor: 0, + format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, + params: { + date: true, + interval: 'PT3H', + format: 'YYYY-MM-DD HH:mm', + bounds: { min: '2019-11-07T15:45:45.770Z', max: '2019-11-14T15:45:45.770Z' }, + }, + aggType: 'date_histogram', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 1, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + timeRange: { from: 'now-7d', to: 'now' }, + useNormalizedEsInterval: true, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + customLabel: 'Attack ID', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Agent', + _source: { + title: 'Mitre techniques by agent', + visState: JSON.stringify({ + title: 'attack by agent', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 0, format: { id: 'number' }, params: {}, aggType: 'count' }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Technique', + _source: { + title: 'Attacks by technique', + visState: JSON.stringify({ + title: 'Attacks by tactic', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { show: false }, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, + dimensions: { + x: null, + y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.technique', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Top-Tactics-By-Agent', + _source: { + title: 'Top tactics by agent', + visState: JSON.stringify({ + title: 'Top tactics by agent - vertical', + type: 'area', + params: { + addLegend: true, + addTimeMarker: false, + addTooltip: true, + categoryAxes: [ + { + id: 'CategoryAxis-1', + labels: { filter: true, show: true, truncate: 10 }, + position: 'bottom', + scale: { type: 'linear' }, + show: true, + style: {}, + title: {}, + type: 'category', + }, + ], + dimensions: { + x: { + accessor: 1, + format: { + id: 'terms', + params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, + }, + params: {}, + aggType: 'terms', + }, + y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], + series: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, + labels: {}, + legendPosition: 'right', + seriesParams: [ + { + data: { id: '1', label: 'Count' }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + mode: 'normal', + show: 'true', + showCircles: true, + type: 'histogram', + valueAxis: 'ValueAxis-1', + }, + ], + thresholdLine: { color: '#34130C', show: false, style: 'full', value: 10, width: 1 }, + times: [], + type: 'area', + valueAxes: [ + { + id: 'ValueAxis-1', + labels: { filter: false, rotate: 0, show: true, truncate: 100 }, + name: 'LeftAxis-1', + position: 'left', + scale: { mode: 'normal', type: 'linear' }, + show: true, + style: {}, + title: { text: 'Count' }, + type: 'value', + }, + ], + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Top-Tactics', + _source: { + title: 'Top tactics', + visState: JSON.stringify({ + title: 'Top tactics PIE2', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + buckets: [ + { + accessor: 0, + format: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + }, + }, + params: {}, + aggType: 'terms', + }, + ], + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'rule.mitre.tactic', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + ], + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { language: 'lucene', query: '' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-MITRE-Alerts-summary', + _type: 'visualization', + _source: { + title: 'Alerts summary', + visState: JSON.stringify({ + title: 'Alerts summary', + type: 'table', + params: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { columnIndex: 3, direction: 'desc' }, + showTotal: false, + showToolbar: true, + totalFunc: 'sum', + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 50, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, + }, + { + id: '4', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', + }, + }, + ], + }), + uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, +]; From e0f7d10fc7915b321e9e02f16f39ce37e60b9124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 16 Jul 2021 10:11:23 +0200 Subject: [PATCH 076/493] Fixed issue with missing spaces --- public/components/visualize/components/sample-data.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/components/visualize/components/sample-data.js b/public/components/visualize/components/sample-data.js index fe25f7e6c8..fd6b4b4533 100644 --- a/public/components/visualize/components/sample-data.js +++ b/public/components/visualize/components/sample-data.js @@ -16,7 +16,7 @@ import React, { useState, useEffect } from 'react'; import { WzRequest } from '../../../react-services'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -export const SampleData = ({...props }) => { +export const SampleData = ({ ...props }) => { const [isSampleData, setIsSampleData] = useState(false); const usesSampleData = async () => { @@ -59,11 +59,11 @@ export const SampleData = ({...props }) => { {...props} > <p> - The data displayed may contain sample alerts. Go + {'The data displayed may contain sample alerts. Go '} <EuiLink href="#/settings?tab=sample_data" aria-label="go to configure sample data"> - here - </EuiLink>{' '} - to configure the sample data. + {'here '} + </EuiLink> + {'to configure the sample data.'} </p> </EuiCallOut> ); From cbf1c77c9971efbb98cb7eb3a417a86117ceec81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 16 Jul 2021 12:02:05 +0200 Subject: [PATCH 077/493] Created hook that performs a search It seems to be called 5 times every time for some reason --- public/components/common/hooks/index.ts | 1 + public/components/common/hooks/useEsSearch.js | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 public/components/common/hooks/useEsSearch.js diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 270031ca7f..4f03e95fe8 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,3 +24,4 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; +export { useEsSearch } from './useEsSearch'; diff --git a/public/components/common/hooks/useEsSearch.js b/public/components/common/hooks/useEsSearch.js new file mode 100644 index 0000000000..a984b80e68 --- /dev/null +++ b/public/components/common/hooks/useEsSearch.js @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react'; +import { getDataPlugin } from '../../../kibana-services'; +import { useQuery, useIndexPattern } from '../hooks'; +import { AppState } from '../../../react-services/app-state'; +import _ from 'lodash'; + +const useEsSearch = (preAppliedQuery = {}, preAppliedAggs = {}, size = 10) => { + const data = getDataPlugin(); + const indexPattern = useIndexPattern(); + const [query] = useQuery(); + const [esQuery, setEsQuery] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [searchResults, setSearchResults] = useState({}); + + useEffect(() => { + getEsQuery().then((result) => { + if (!_.isEqual(result, query)) { + setEsQuery(result); + } + }); + }, [query]); + const getEsQuery = async () => { + const esQuery = await data.query.getEsQuery(indexPattern); + return esQuery; + }; + + //useEffect(() => { + // setIsLoading(true); + // search() + // .then((result) => { + // if(!_.isEqual(result,searchResults)){ + // setSearchResults(result); + // } + // }) + // .finally(() => { + // setIsLoading(false); + // }); + //}, [esQuery, preAppliedQuery, preAppliedAggs, size, indexPattern]); + const search = async () => { + console.log("SEARCHING") + if (indexPattern) { + const searchSource = await data.search.searchSource.create(); + const combined = _.merge({ ...preAppliedQuery }, { ...esQuery } || {}); + const results = searchSource + .setParent(undefined) + .setField('query', combined) + .setField('aggs', preAppliedAggs) + .setField('size', size) + .setField('index', indexPattern) + .fetch(); + return results; + } else { + return {}; + } + }; + return { + searchResults, + isLoading, + search, + }; +}; + +export { useEsSearch }; From 3115ffd32407e9fe748e45a5eae2c07137c66096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 16 Jul 2021 14:42:21 +0200 Subject: [PATCH 078/493] Created hook that gets raw data based on the global search Need to test the preApplied parammeters and add pagination functions --- public/components/common/hooks/useEsSearch.js | 49 ++++++------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/public/components/common/hooks/useEsSearch.js b/public/components/common/hooks/useEsSearch.js index a984b80e68..88b08d3c47 100644 --- a/public/components/common/hooks/useEsSearch.js +++ b/public/components/common/hooks/useEsSearch.js @@ -1,48 +1,29 @@ import { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useQuery, useIndexPattern } from '../hooks'; -import { AppState } from '../../../react-services/app-state'; +import { useQuery, useIndexPattern, useFilterManager } from '../hooks'; import _ from 'lodash'; const useEsSearch = (preAppliedQuery = {}, preAppliedAggs = {}, size = 10) => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const [query] = useQuery(); - const [esQuery, setEsQuery] = useState({}); - const [isLoading, setIsLoading] = useState(true); - const [searchResults, setSearchResults] = useState({}); + const filterManager = useFilterManager(); + const [esResults, setEsResults] = useState({}) + useEffect(()=>{ + search() + .then((result)=>{ + setEsResults(result) + }) + },[indexPattern]) - useEffect(() => { - getEsQuery().then((result) => { - if (!_.isEqual(result, query)) { - setEsQuery(result); - } - }); - }, [query]); - const getEsQuery = async () => { - const esQuery = await data.query.getEsQuery(indexPattern); - return esQuery; - }; - - //useEffect(() => { - // setIsLoading(true); - // search() - // .then((result) => { - // if(!_.isEqual(result,searchResults)){ - // setSearchResults(result); - // } - // }) - // .finally(() => { - // setIsLoading(false); - // }); - //}, [esQuery, preAppliedQuery, preAppliedAggs, size, indexPattern]); const search = async () => { - console.log("SEARCHING") if (indexPattern) { + const esQuery = await data.query.getEsQuery(indexPattern); const searchSource = await data.search.searchSource.create(); const combined = _.merge({ ...preAppliedQuery }, { ...esQuery } || {}); - const results = searchSource + + const results = await searchSource .setParent(undefined) + .setField('filter', combined.bool.filter) .setField('query', combined) .setField('aggs', preAppliedAggs) .setField('size', size) @@ -54,9 +35,7 @@ const useEsSearch = (preAppliedQuery = {}, preAppliedAggs = {}, size = 10) => { } }; return { - searchResults, - isLoading, - search, + esResults }; }; From aa7a26b84046f986f57f31042c261a7de982da6c Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Fri, 16 Jul 2021 17:56:10 +0200 Subject: [PATCH 079/493] Refactor try catch in Agent Syscollector/Vuls/Stats (#3462) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(syscollector): Added try catch strategy. * Implement try catch strategy in Groups (#3415) * Implement try catch groups * Add test * test(groups): Added simple snapshot test. * Add changelog * Change context * Change title Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> * Create Cypress Workflow for 4.3-7.10 (#3444) * Fixed dispatch for updateCurrentAgentData (#3453) * fix(syscollector): Fixed dispatch for updateCurrentAgentData * fix(syscollector): Refactor for agents-sections. * doc(changelog): Updated * Updating test. (#3470) * test(unittest): Updating test. * test(unittest): Updating test. * test(agent-table): Added unit test & fix collectCoverage Co-authored-by: Pablo Martínez <pablo.martinez@wazuh.com> Co-authored-by: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> --- CHANGELOG.md | 1 + .../stats/__snapshots__/table.test.tsx.snap | 448 ++++++++++++++++++ .../components/agents/stats/agent-stats.tsx | 29 +- public/components/agents/stats/table.test.tsx | 104 ++++ public/components/agents/stats/table.tsx | 68 +-- .../agents/vuls/inventory/flyout.tsx | 23 +- .../common/hooks/useGenericRequest.ts | 31 +- test/jest/config.js | 4 +- 8 files changed, 663 insertions(+), 45 deletions(-) create mode 100644 public/components/agents/stats/__snapshots__/table.test.tsx.snap create mode 100644 public/components/agents/stats/table.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 043a02612d..6e5e48a42d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Added try catch strategy with ErrorOrchestrator service on Syscollector/Vuls/Stats sections [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) - Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) diff --git a/public/components/agents/stats/__snapshots__/table.test.tsx.snap b/public/components/agents/stats/__snapshots__/table.test.tsx.snap new file mode 100644 index 0000000000..b925dcd79d --- /dev/null +++ b/public/components/agents/stats/__snapshots__/table.test.tsx.snap @@ -0,0 +1,448 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AgentStatTable component Renders correctly to match the snapshot 1`] = ` +<AgentStatTable + columns={ + Array [ + Object { + "field": "location", + "name": "Location", + "sortable": true, + }, + Object { + "field": "events", + "name": "Events", + "sortable": true, + }, + Object { + "field": "bytes", + "name": "Bytes", + "sortable": true, + }, + ] + } + end="" + exportCSVFilename="agent-stats-10101-logcollector-global" + items={Array []} + loading={false} + start="" + title="Test" +> + <EuiPanel> + <div + className="euiPanel euiPanel--paddingMedium" + > + <EuiFlexGroup + justifyContent="spaceBetween" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiText> + <div + className="euiText euiText--medium" + > + Test + </div> + </EuiText> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiText> + <div + className="euiText euiText--medium" + > + <EuiIcon + type="calendar" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + Start: + + - + - End: + + - + </div> + </EuiText> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiHorizontalRule + margin="xs" + > + <hr + className="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginXSmall" + /> + </EuiHorizontalRule> + <EuiInMemoryTable + columns={ + Array [ + Object { + "field": "location", + "name": "Location", + "sortable": true, + }, + Object { + "field": "events", + "name": "Events", + "sortable": true, + }, + Object { + "field": "bytes", + "name": "Bytes", + "sortable": true, + }, + ] + } + items={Array []} + loading={false} + pagination={true} + responsive={true} + tableLayout="fixed" + > + <EuiBasicTable + columns={ + Array [ + Object { + "field": "location", + "name": "Location", + "sortable": true, + }, + Object { + "field": "events", + "name": "Events", + "sortable": true, + }, + Object { + "field": "bytes", + "name": "Bytes", + "sortable": true, + }, + ] + } + items={Array []} + loading={false} + noItemsMessage="No items found" + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], + "totalItemCount": 0, + } + } + responsive={true} + tableLayout="fixed" + > + <div + className="euiBasicTable" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="htmlId" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="htmlId" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_location_0" + key="_data_h_location_0" + > + <th + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_location_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent" + > + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Location" + > + Location + </span> + </EuiInnerText> + </div> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_events_1" + key="_data_h_events_1" + > + <th + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_events_1" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent" + > + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Events" + > + Events + </span> + </EuiInnerText> + </div> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_bytes_2" + key="_data_h_bytes_2" + > + <th + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_bytes_2" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent" + > + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Bytes" + > + Bytes + </span> + </EuiInnerText> + </div> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody> + <tbody> + <EuiTableRow> + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="center" + colSpan={3} + isMobileFullWidth={true} + > + <td + className="euiTableRowCell euiTableRowCell--isMobileFullWidth" + colSpan={3} + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + className="euiTableCellContent__text" + > + No items found + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> + </EuiInMemoryTable> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + <EuiFlexGroup + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiButtonEmpty + iconType="importAction" + isDisabled={false} + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="importAction" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="importAction" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + Download CSV + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiPanel> +</AgentStatTable> +`; diff --git a/public/components/agents/stats/agent-stats.tsx b/public/components/agents/stats/agent-stats.tsx index 3f0f7059f8..16aae07042 100644 --- a/public/components/agents/stats/agent-stats.tsx +++ b/public/components/agents/stats/agent-stats.tsx @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { useState, useEffect } from 'react'; -import { +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, @@ -26,6 +26,14 @@ import { compose } from 'redux'; import { WzRequest, formatUIDate } from '../../../react-services'; import { AgentStatTable } from './table'; import { PromptNoActiveAgentWithoutSelect, PromptAgentFeatureVersion } from '../prompts'; +import { + UIErrorLog, + UI_ERROR_SEVERITIES, + UILogLevel, + UIErrorSeverity, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; const tableColumns = [ { @@ -96,7 +104,7 @@ export const MainAgentStats = compose( }, ]), withUserAuthorizationPrompt(({agent}) => [[ - {action: 'agent:read', resource: `agent:id:${agent.id}`}, + {action: 'agent:read', resource: `agent:id:${agent.id}`}, ...(agent.group || []).map(group => ({ action: 'agent:read', resource: `agent:group:${group}` })) ]]), withGuard(({agent}) => agent.status !== 'active', PromptNoActiveAgentWithoutSelect), @@ -118,10 +126,21 @@ function AgentStats({agent}){ const responseDataStatAgent = await WzRequest.apiReq('GET', `/agents/${agent.id}/stats/agent`, {}); setDataStatLogcollector(responseDataStatLogcollector?.data?.data?.affected_items?.[0] || {}); setDataStatAgent(responseDataStatAgent?.data?.data?.affected_items?.[0] || undefined); - }catch(error){ - + } catch(error) { + const options: UIErrorLog = { + context: `${AgentStats.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } finally { + setLoading(false); } - setLoading(false); })() }, []); return ( diff --git a/public/components/agents/stats/table.test.tsx b/public/components/agents/stats/table.test.tsx new file mode 100644 index 0000000000..4e8de0853f --- /dev/null +++ b/public/components/agents/stats/table.test.tsx @@ -0,0 +1,104 @@ +/* + * Wazuh app - React test for Ruleset component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { AgentStatTable } from './table'; + +jest.mock('../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); +jest.mock( + '../../../../../../../kibana/node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const tableColumns = [ + { + field: 'location', + name: 'Location', + sortable: true, + }, + { + field: 'events', + name: 'Events', + sortable: true, + }, + { + field: 'bytes', + name: 'Bytes', + sortable: true, + }, +]; + +describe('AgentStatTable component', () => { + it('Renders correctly to match the snapshot', () => { + const wrapper = mount( + <AgentStatTable + columns={tableColumns} + loading={false} + start={''} + end={''} + title="Test" + items={[]} + exportCSVFilename={`agent-stats-10101-logcollector-global`} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('Should return loading in process', () => { + const wrapper = mount( + <AgentStatTable + columns={tableColumns} + loading={true} + start={''} + end={''} + title="Test" + items={[]} + exportCSVFilename={`agent-stats-10101-logcollector-global`} + /> + ); + + expect(wrapper.find('EuiLoadingSpinner').exists()).toBeTruthy(); + expect(wrapper.find('EuiLoadingSpinner').first().prop('size')).toBe('s'); + }); + + it('Checking onClick event', () => { + const wrapper = mount( + <AgentStatTable + columns={tableColumns} + loading={true} + start={''} + end={''} + title="Test" + items={[]} + exportCSVFilename={`agent-stats-10101-logcollector-global`} + /> + ); + + const mockOnClick = jest.fn(); + wrapper.find('EuiButtonEmpty').props().onClick = mockOnClick; + // @ts-ignore + wrapper.find('EuiButtonEmpty').props().onClick(); + expect(mockOnClick).toHaveBeenCalled(); + }); +}); diff --git a/public/components/agents/stats/table.tsx b/public/components/agents/stats/table.tsx index 35c299a071..8f84e9c389 100644 --- a/public/components/agents/stats/table.tsx +++ b/public/components/agents/stats/table.tsx @@ -10,42 +10,48 @@ * Find more information about this on the LICENSE file. */ import React from 'react'; -import { +import { EuiButtonEmpty, - EuiInMemoryTable, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiIcon, + EuiInMemoryTable, EuiLoadingSpinner, EuiPanel, EuiSpacer, - EuiText + EuiText, } from '@elastic/eui'; import * as FileSaver from '../../../services/file-saver'; -import { getToasts } from '../../../kibana-services'; import { formatUIDate } from '../../../react-services'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; -export function AgentStatTable({columns, title, start, end, loading, items, exportCSVFilename}){ +export function AgentStatTable({ columns, title, start, end, loading, items, exportCSVFilename }) { return ( <EuiPanel> - <EuiFlexGroup justifyContent='spaceBetween'> + <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <EuiText>{title}</EuiText> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiText><EuiIcon type='calendar'/> Start: {loading ? <EuiLoadingSpinner size="s" /> : (start ? formatUIDate(start) : '-') } - End: {loading ? <EuiLoadingSpinner size="s" /> : (end ? formatUIDate(end) : '-') }</EuiText> + <EuiText> + <EuiIcon type="calendar" /> Start:{' '} + {loading ? <EuiLoadingSpinner size="s" /> : start ? formatUIDate(start) : '-'} - End:{' '} + {loading ? <EuiLoadingSpinner size="s" /> : end ? formatUIDate(end) : '-'} + </EuiText> </EuiFlexItem> </EuiFlexGroup> - <EuiHorizontalRule margin="xs"/> - <EuiInMemoryTable - columns={columns} - items={items || []} - loading={loading} - pagination={true} - /> - <EuiSpacer size='xs'/> - <EuiFlexGroup justifyContent='flexEnd'> + <EuiHorizontalRule margin="xs" /> + <EuiInMemoryTable columns={columns} items={items || []} loading={loading} pagination={true} /> + <EuiSpacer size="xs" /> + <EuiFlexGroup justifyContent="flexEnd"> <EuiFlexItem grow={false}> <EuiButtonEmpty onClick={() => downloadCsv(columns, items, exportCSVFilename)} @@ -57,25 +63,27 @@ export function AgentStatTable({columns, title, start, end, loading, items, expo </EuiFlexItem> </EuiFlexGroup> </EuiPanel> - ) + ); } - function downloadCsv(columns: any[], data: any[], filename: string) { try { - const header = columns.map(column => column.name).join(','); - const body = data - .map(row => columns.map(column => row[column.field]).join(',')) - .join('\n'); - const result = [header,body].join('\n'); + const header = columns.map((column) => column.name).join(','); + const body = data.map((row) => columns.map((column) => row[column.field]).join(',')).join('\n'); + const result = [header, body].join('\n'); const blob = new Blob([result], { type: 'text/csv' }); // eslint-disable-line FileSaver.saveAs(blob, `${filename}.csv`); } catch (error) { - getToasts().add({ - color: 'danger', - title: 'CSV', - text: 'Error generating CSV', - toastLifeTimeMs: 4000, - }); + const options: UIErrorLog = { + context: 'downloadCsv', + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: `Error generating CSV: ${error.message}`, + title: `CSV: ${error.name}`, + }, + }; + getErrorOrchestrator().handleError(options); } -} \ No newline at end of file +} diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index a8374e093d..10410cd5a7 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -21,6 +21,14 @@ import { } from '@elastic/eui'; import { Details } from './detail'; import { AppState } from '../../../../react-services/app-state'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export class FlyoutDetail extends Component { state: { @@ -60,10 +68,23 @@ export class FlyoutDetail extends Component { throw false; } this.setState({ currentItem, isLoading: false }); - } catch (err) { + } catch (error) { + const options: UIErrorLog = { + context: `${FlyoutDetail.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: `Data could not be fetched for ${this.props.vulName}`, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); this.setState({ error: `Data could not be fetched for ${this.props.vulName}`, }); + } finally { + this.setState({ isLoading: false }); } } diff --git a/public/components/common/hooks/useGenericRequest.ts b/public/components/common/hooks/useGenericRequest.ts index 7c4c6b71f5..d8fd733cff 100644 --- a/public/components/common/hooks/useGenericRequest.ts +++ b/public/components/common/hooks/useGenericRequest.ts @@ -1,10 +1,18 @@ import { useState, useEffect } from 'react'; import { GenericRequest } from '../../../react-services/generic-request'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; export function useGenericRequest(method, path, params, formatFunction) { - const [items, setItems] = useState({}); - const [isLoading, setisLoading] = useState(true); - const [error, setError] = useState(""); + const [items, setItems] = useState({}); + const [isLoading, setisLoading] = useState(true); + const [error, setError] = useState(""); useEffect( () => { try{ @@ -13,13 +21,24 @@ export function useGenericRequest(method, path, params, formatFunction) { const response = await GenericRequest.request(method, path, params); setItems(response); setisLoading(false); - } + } fetchData(); - }catch(err){ + } catch(error) { setError(error); setisLoading(false); + const options: UIErrorLog = { + context: `${useGenericRequest.name}.fetchData`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }, [params]); return {isLoading, data: formatFunction(items), error}; -} \ No newline at end of file +} diff --git a/test/jest/config.js b/test/jest/config.js index 87fcc1322a..68da094706 100644 --- a/test/jest/config.js +++ b/test/jest/config.js @@ -12,9 +12,7 @@ export default { `${kbnDir}/node_modules` ], collectCoverageFrom: [ - "./common/**/*.{js,jsx,ts,tsx}", - "./public/**/*.{js,jsx,ts,tsx}", - "./server/**/*.{js,jsx,ts,tsx}", + "**/*.{js,jsx,ts,tsx}", "./!**/node_modules/**", ], moduleNameMapper: { From a70e46165e1c47553e333bf041422e1ee070c292 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Fri, 16 Jul 2021 12:56:59 -0300 Subject: [PATCH 080/493] Refactor try catch in Management > Status (#3434) * Added try-catc new strategy * Added test file and component snapshot * Updated CHANGELOG * Updating error title and message * Refactor try catch in Management > Statistics (#3429) * Added error handling implementation * Changed wrong var err * Draft statistics test and snapshot * Updated CHANGELOG * Updating title and message error Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> * Merged conflicts * Updating test. (#3470) * test(unittest): Updating test. * test(unittest): Updating test. * merged CHANGELOG Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 1 + .../__snapshots__/status-main.test.tsx.snap | 65 +++++++++++++++++++ .../management/status/actions-buttons-main.js | 41 ++++++++++-- .../management/status/status-main.test.tsx | 33 ++++++++++ .../management/status/status-overview.js | 17 ++++- .../management/status/utils/status-handler.js | 22 +++---- 6 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 public/controllers/management/components/management/status/__snapshots__/status-main.test.tsx.snap create mode 100644 public/controllers/management/components/management/status/status-main.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5e48a42d..e3fb334cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Syscollector/Vuls/Stats sections [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) - Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) +- Added try catch strategy with ErrorOrchestrator service on Management > Status [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) ### Changed diff --git a/public/controllers/management/components/management/status/__snapshots__/status-main.test.tsx.snap b/public/controllers/management/components/management/status/__snapshots__/status-main.test.tsx.snap new file mode 100644 index 0000000000..4960df7a0f --- /dev/null +++ b/public/controllers/management/components/management/status/__snapshots__/status-main.test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status component renders correctly to match the snapshot 1`] = ` +<WzStatus> + <WzReduxProvider> + <Provider + store={ + Object { + "dispatch": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + Symbol(observable): [Function], + } + } + > + <Component> + <Component> + <div + className="withUserLogged" + > + <img + alt="" + className="withUserLogged-logo" + src="/plugins/wazuh/assets/icon_blue.svg" + /> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiText + className="subdued-color" + > + <div + className="euiText euiText--medium subdued-color" + > + Loading ... + </div> + </EuiText> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiProgress + className="withUserLogged-loader" + color="primary" + size="xs" + > + <div + className="euiProgress euiProgress--indeterminate euiProgress--xs euiProgress--primary withUserLogged-loader" + /> + </EuiProgress> + </div> + </Component> + </Component> + </Provider> + </WzReduxProvider> +</WzStatus> +`; diff --git a/public/controllers/management/components/management/status/actions-buttons-main.js b/public/controllers/management/components/management/status/actions-buttons-main.js index 8d5188a824..bafc1b0734 100644 --- a/public/controllers/management/components/management/status/actions-buttons-main.js +++ b/public/controllers/management/components/management/status/actions-buttons-main.js @@ -34,6 +34,10 @@ import StatusHandler from './utils/status-handler'; import { getToasts } from '../../../../../kibana-services'; import { WzButtonPermissions } from '../../../../../components/common/permissions/button'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; + class WzStatusActionButtons extends Component { _isMounted = false; @@ -73,7 +77,17 @@ class WzStatusActionButtons extends Component { ); } catch (error) { this.setState({ isRestarting: false }); - this.showToast('danger', `Error restarting cluster: ${error.message || error}`, 3000); + const options = { + context: `${WzStatusActionButtons.name}.restartCluster`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error restarting cluster`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -88,7 +102,17 @@ class WzStatusActionButtons extends Component { this.showToast('success', 'Restarting manager.', 3000); } catch (error) { this.setState({ isRestarting: false }); - this.showToast('danger', `Error restarting manager: ${error.message || error}`, 3000); + const options = { + context: `${WzStatusActionButtons.name}.restartManager`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error restarting manager`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -136,9 +160,18 @@ class WzStatusActionButtons extends Component { this.props.updateLoadingStatus(false); } catch (error) { this.props.updateLoadingStatus(false); - this.showToast('danger', `Node ${node} is down`, 3000); + const options = { + context: `${WzStatusActionButtons.name}.changeNode`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Node ${node} is down` + }, + }; + getErrorOrchestrator().handleError(options); } - return; }; diff --git a/public/controllers/management/components/management/status/status-main.test.tsx b/public/controllers/management/components/management/status/status-main.test.tsx new file mode 100644 index 0000000000..b8db75d7e5 --- /dev/null +++ b/public/controllers/management/components/management/status/status-main.test.tsx @@ -0,0 +1,33 @@ +/* + * Wazuh app - React test for Status component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import WzStatus from './status-main'; + +jest.mock('../../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +describe('Status component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(<WzStatus />); + expect(wrapper).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/public/controllers/management/components/management/status/status-overview.js b/public/controllers/management/components/management/status/status-overview.js index 4c649d70cc..edf551429f 100644 --- a/public/controllers/management/components/management/status/status-overview.js +++ b/public/controllers/management/components/management/status/status-overview.js @@ -48,7 +48,10 @@ import { getToasts } from '../../../../../kibana-services'; import { withUserAuthorizationPrompt, withGlobalBreadcrumb } from '../../../../../components/common/hocs'; import { compose } from 'redux'; -import { ToastNotifications } from '../../../../../react-services/toast-notifications'; + +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; export class WzStatusOverview extends Component { _isMounted = false; @@ -159,7 +162,17 @@ export class WzStatusOverview extends Component { this.props.updateAgentInfo(lastAgent); } catch (error) { - getToasts().error('management:status:overview.fetchData', error); + const options = { + context: `${WzStatusOverview.name}.fetchData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: management:status:overview` + }, + }; + getErrorOrchestrator().handleError(options); } this.props.updateLoadingStatus(false); } diff --git a/public/controllers/management/components/management/status/utils/status-handler.js b/public/controllers/management/components/management/status/utils/status-handler.js index 0752b1f1e6..f3a657ae05 100644 --- a/public/controllers/management/components/management/status/utils/status-handler.js +++ b/public/controllers/management/components/management/status/utils/status-handler.js @@ -21,7 +21,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('GET', `/agents/summary/status`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -33,7 +33,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('GET', `/cluster/status`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -45,7 +45,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('GET', `/cluster/nodes`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -61,7 +61,7 @@ export default class StatusHandler { ); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -77,7 +77,7 @@ export default class StatusHandler { ); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -93,7 +93,7 @@ export default class StatusHandler { ); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -105,7 +105,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('GET', `/manager/info`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -117,7 +117,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('GET', `/manager/status`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -135,7 +135,7 @@ export default class StatusHandler { }); return result; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -159,7 +159,7 @@ export default class StatusHandler { await WzRequest.apiReq('PUT', `/cluster/restart`, { delay: 15000 }); return { data: { data: 'Restarting cluster' } }; } catch (error) { - return Promise.reject(error); + throw error; } } @@ -184,7 +184,7 @@ export default class StatusHandler { const result = await WzRequest.apiReq('PUT', `/manager/restart`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } } From e91d3c7cdacb85a356d3c20c7d721b02532f55c6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Sat, 17 Jul 2021 12:51:12 +0200 Subject: [PATCH 081/493] restructure components --- .../common/modules/panel/main-panel.tsx | 48 ++++++++- .../common/modules/panel/module-body.tsx | 11 +- .../overview/office-panel/mockup-tables.tsx | 102 ------------------ .../overview/office-panel/office-panel.tsx | 72 +++++-------- 4 files changed, 77 insertions(+), 156 deletions(-) delete mode 100644 public/components/overview/office-panel/mockup-tables.tsx diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 298fa64133..3b5d82282f 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -6,10 +6,44 @@ import { EuiPageSideBar, EuiPageBody, } from '@elastic/eui'; -import { ModuleSidePanel, ModuleStats, ModuleBody } from './'; +import { ModuleSidePanel, ModuleBody } from './'; +import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; +//@ts-ignore +import { getDataPlugin } from '../../../../kibana-services'; +import { KbnSearchBar } from '../../../kbn-search-bar'; +import { TimeRange, Query } from '../../../../../../../src/plugins/data/common'; +import { MockupTables } from './mockup-tables'; +import { WzRequest } from '../../../../react-services/wz-request'; -export const MainPanel = ({ sidePanelChildren, children, moduleStatsList = [], ...props }) => { +export const MainPanel = ({ sidePanelChildren, moduleStatsList = [], ...props }) => { + + const KibanaServices = getDataPlugin().query; + const filterManager = KibanaServices.filterManager; + const timefilter = KibanaServices.timefilter.timefilter; + + const [filterParams, setFilterParams] = useState({ + filters: filterManager.getFilters() || [], + query: { language: 'kuery', query: '' }, + time: timefilter.getTime(), + }); + const [isLoading, setLoading] = useState(false); + + + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { + const { query, dateRange } = payload; + const filters = { query, time: dateRange, filters: filterParams.filters }; + setLoading(true); + setFilterParams(filters); + + } + + const onFiltersUpdated = (filters: Filter[]) => { + const { query, time } = filterParams; + const updatedFilterParams = { query, time, filters }; + setLoading(true); + setFilterParams(updatedFilterParams); + } return ( <EuiFlexGroup style={{ margin: '0 8px' }}> @@ -19,7 +53,13 @@ export const MainPanel = ({ sidePanelChildren, children, moduleStatsList = [], . </ModuleSidePanel > } <EuiPageBody> - <ModuleBody>{children}</ModuleBody> + <ModuleBody> + <KbnSearchBar + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} /> + <MockupTables /> + </ModuleBody> </EuiPageBody> </EuiFlexItem> </EuiFlexGroup> diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/common/modules/panel/module-body.tsx index ea11f94589..522167eb73 100644 --- a/public/components/common/modules/panel/module-body.tsx +++ b/public/components/common/modules/panel/module-body.tsx @@ -2,14 +2,13 @@ import React from 'react'; import { EuiPageBody, EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; -export const ModuleBody = ({ children, ...props }) => { - +export const ModuleBody = ({ vizList = [], ...props }) => { + return ( <EuiFlexGroup > - <EuiFlexItem> - {children} - - </EuiFlexItem> + {vizList.map((Component, key) => <EuiFlexItem key={key}> + <Component /> + </EuiFlexItem>)} </EuiFlexGroup> ) } \ No newline at end of file diff --git a/public/components/overview/office-panel/mockup-tables.tsx b/public/components/overview/office-panel/mockup-tables.tsx deleted file mode 100644 index f07051f51e..0000000000 --- a/public/components/overview/office-panel/mockup-tables.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; - -export const MockupTables = () => { - const tables = [ - { - title: 'Tenants', - minWidth: '400px', - columns: [ - { - field: 'tenant', - name: 'Tenant', - }, - { - field: 'count', - name: 'Count', - }, - ], - }, - { - title: 'Subscriptions', - minWidth: '400px', - columns: [ - { - field: 'subscription', - name: 'Subscription', - }, - { - field: 'count', - name: 'count', - }, - ], - }, - { - title: 'Events', - minWidth: '750px', - width: '750px', - columns: [ - { - field: 'rule', - name: 'Description', - render: (rule) => rule.description, - }, - { - field: 'rule', - name: 'Level', - render: (rule) => rule.level, - }, - ], - }, - ]; - const generateItems = () => { - const random = new Random(); - const rules = [ - { - level: 1, - description: 'A simple rule', - }, - { - level: 5, - description: 'A worrysome rule', - }, - { - level: 12, - description: 'Oh snap', - }, - ]; - const subscriptions = ['netflix', 'spotify', 'prime']; - const tenant = ['David', 'Doctor', 'Crowlie']; - const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map((id) => { - return { - id, - rule: random.oneOf(rules), - tenant: random.oneOf(tenant), - subscription: random.oneOf(subscriptions), - count: random.integer(0, 10), - }; - }); - return items; - }; - const TitlePanel = ({ title, children, panelProps, titleProps = { size: 's' } }) => { - return <EuiPanel {...panelProps}> - <EuiTitle {...titleProps}><h2>{title}</h2></EuiTitle> - {children} - </EuiPanel> - } - const renderTables = (items, tables) => { - const rendered = tables.map((table, key) => { - return ( - <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }} key={key}> - <TitlePanel title={table.title}> - <EuiBasicTable items={items} columns={table.columns} /> - </TitlePanel> - </EuiFlexItem> - ); - }); - return rendered; - }; - - - return <EuiFlexGroup wrap>{renderTables(generateItems(), tables)}</EuiFlexGroup> -} \ No newline at end of file diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index de8321fb4e..172d402c1b 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -11,55 +11,39 @@ * Find more information about this on the LICENSE file. */ -import React, { useState } from 'react'; - +import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../../components/common/modules/panel'; -import { FilterManager, Filter } from '../../../../../../src/plugins/data/public/'; -//@ts-ignore -import { getDataPlugin } from '../../../kibana-services'; -import { KbnSearchBar } from '../../kbn-search-bar'; -import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { withErrorBoundary } from '../../common/hocs'; -import { MockupTables } from './mockup-tables'; import { ModuleStats } from './module-stats'; - +import { WzRequest } from '../../../react-services/wz-request'; +import { queryConfig } from '../../../services/query-config'; +import { WazuhConfig } from '../../../react-services/wazuh-config'; export const OfficePanel = withErrorBoundary(({ ...props }) => { - - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; - const timefilter = KibanaServices.timefilter.timefilter; - + const wazuhConfig = new WazuhConfig(); + const extraFilters = []; + const conf = wazuhConfig.getConfig(); const [moduleStatsList, setModuleStatsList] = useState([]); - const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters() || [], - query: { language: 'kuery', query: '' }, - time: timefilter.getTime(), - }); - const [isLoading, setLoading] = useState(false); - - - const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { - const { query, dateRange } = payload; - const filters = { query, time: dateRange, filters: filterParams.filters }; - setLoading(true); - setFilterParams(filters); - - } - - const onFiltersUpdated = (filters: Filter[]) => { - const { query, time } = filterParams; - const updatedFilterParams = { query, time, filters }; - setLoading(true); - setFilterParams(updatedFilterParams); - } - + useEffect(() => { + (async () => { + try { + const modulesConfig = await queryConfig( + '000', + [{ component: 'wmodules', configuration: 'wmodules' }], + WzRequest.apiReq + ); + const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules + .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { + const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; + return { title: configProp[0], description } + }) + setModuleStatsList(config); + } catch (error) { + setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); + } + } + )(); + }, []) return ( - <MainPanel sidePanelChildren={<ModuleStats listItems={moduleStatsList} />}> - <KbnSearchBar - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> - <MockupTables /> - </MainPanel> + <MainPanel sidePanelChildren={<ModuleStats listItems={moduleStatsList} />}></MainPanel> ) }); From fc1a4cb53817fe51ff3d5c64a9f30f0d34ae9271 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Sat, 17 Jul 2021 12:51:43 +0200 Subject: [PATCH 082/493] restructure components --- .../common/modules/panel/mockup-tables.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 public/components/common/modules/panel/mockup-tables.tsx diff --git a/public/components/common/modules/panel/mockup-tables.tsx b/public/components/common/modules/panel/mockup-tables.tsx new file mode 100644 index 0000000000..f07051f51e --- /dev/null +++ b/public/components/common/modules/panel/mockup-tables.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; + +export const MockupTables = () => { + const tables = [ + { + title: 'Tenants', + minWidth: '400px', + columns: [ + { + field: 'tenant', + name: 'Tenant', + }, + { + field: 'count', + name: 'Count', + }, + ], + }, + { + title: 'Subscriptions', + minWidth: '400px', + columns: [ + { + field: 'subscription', + name: 'Subscription', + }, + { + field: 'count', + name: 'count', + }, + ], + }, + { + title: 'Events', + minWidth: '750px', + width: '750px', + columns: [ + { + field: 'rule', + name: 'Description', + render: (rule) => rule.description, + }, + { + field: 'rule', + name: 'Level', + render: (rule) => rule.level, + }, + ], + }, + ]; + const generateItems = () => { + const random = new Random(); + const rules = [ + { + level: 1, + description: 'A simple rule', + }, + { + level: 5, + description: 'A worrysome rule', + }, + { + level: 12, + description: 'Oh snap', + }, + ]; + const subscriptions = ['netflix', 'spotify', 'prime']; + const tenant = ['David', 'Doctor', 'Crowlie']; + const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map((id) => { + return { + id, + rule: random.oneOf(rules), + tenant: random.oneOf(tenant), + subscription: random.oneOf(subscriptions), + count: random.integer(0, 10), + }; + }); + return items; + }; + const TitlePanel = ({ title, children, panelProps, titleProps = { size: 's' } }) => { + return <EuiPanel {...panelProps}> + <EuiTitle {...titleProps}><h2>{title}</h2></EuiTitle> + {children} + </EuiPanel> + } + const renderTables = (items, tables) => { + const rendered = tables.map((table, key) => { + return ( + <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }} key={key}> + <TitlePanel title={table.title}> + <EuiBasicTable items={items} columns={table.columns} /> + </TitlePanel> + </EuiFlexItem> + ); + }); + return rendered; + }; + + + return <EuiFlexGroup wrap>{renderTables(generateItems(), tables)}</EuiFlexGroup> +} \ No newline at end of file From 51505a57fe9ad4fb49981009e83372266774c8f1 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 19 Jul 2021 10:50:19 +0200 Subject: [PATCH 083/493] Added visualizations config file --- .../common/modules/panel/main-panel.tsx | 8 ++--- .../common/modules/panel/module-body.tsx | 26 ++++++++++----- .../overview/office-panel/module-config.tsx | 33 +++++++++++++++++++ .../overview/office-panel/office-panel.tsx | 17 +++++++--- .../overview/overview-office.ts | 6 ++-- 5 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 public/components/overview/office-panel/module-config.tsx diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 3b5d82282f..40e8bf4aaa 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -13,10 +13,10 @@ import { getDataPlugin } from '../../../../kibana-services'; import { KbnSearchBar } from '../../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../../src/plugins/data/common'; import { MockupTables } from './mockup-tables'; -import { WzRequest } from '../../../../react-services/wz-request'; -export const MainPanel = ({ sidePanelChildren, moduleStatsList = [], ...props }) => { + +export const MainPanel = ({ sidePanelChildren, visualizations = [], ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; @@ -53,12 +53,12 @@ export const MainPanel = ({ sidePanelChildren, moduleStatsList = [], ...props }) </ModuleSidePanel > } <EuiPageBody> - <ModuleBody> + <ModuleBody visualizations={visualizations}> <KbnSearchBar onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} isLoading={isLoading} /> - <MockupTables /> + </ModuleBody> </EuiPageBody> </EuiFlexItem> diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/common/modules/panel/module-body.tsx index 522167eb73..1b83507d18 100644 --- a/public/components/common/modules/panel/module-body.tsx +++ b/public/components/common/modules/panel/module-body.tsx @@ -1,14 +1,24 @@ import React from 'react'; import { EuiPageBody, EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; +import WzReduxProvider from '../../../../redux/wz-redux-provider'; -export const ModuleBody = ({ vizList = [], ...props }) => { +export const ModuleBody = ({ visualizations = [], ...props }) => { - return ( - <EuiFlexGroup > - {vizList.map((Component, key) => <EuiFlexItem key={key}> - <Component /> - </EuiFlexItem>)} - </EuiFlexGroup> - ) + return <> + <WzReduxProvider> + {visualizations?.map((row, key) => { + return <EuiFlexGroup key={key} style={{ + height: row.height || 150 + 'px' + }}>{ + row.columns.map((column, key) => { + return <EuiFlexItem key={key}> + {column.component} + </EuiFlexItem> + }) + }</EuiFlexGroup> + }) + } + </WzReduxProvider> + </> } \ No newline at end of file diff --git a/public/components/overview/office-panel/module-config.tsx b/public/components/overview/office-panel/module-config.tsx new file mode 100644 index 0000000000..5069a25ba5 --- /dev/null +++ b/public/components/overview/office-panel/module-config.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import KibanaVis from '../../../kibana-integrations/kibana-vis'; + + +const rows = [ + { + height: 300, + columns: [ + { + width: 3, + component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE'} tab={'office'} /> + }, + { + width: 3, + component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Alerts-Evolution'} tab={'office'} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 3, + component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Attacks-By-Agent'} tab={'office'} /> + }, + { + width: 3, + component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Attacks-By-Technique'} tab={'office'} /> + }, + ] + }, +] +export default rows; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 172d402c1b..c94d1c3138 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -17,15 +17,23 @@ import { withErrorBoundary } from '../../common/hocs'; import { ModuleStats } from './module-stats'; import { WzRequest } from '../../../react-services/wz-request'; import { queryConfig } from '../../../services/query-config'; -import { WazuhConfig } from '../../../react-services/wazuh-config'; +import { getCurrentConfig } from '../../../controllers/management/components/management/configuration/utils/wz-fetch'; +// import { getCurrentConfig } from '../../../react-services'; +import moduleVisualizations from './module-config'; + export const OfficePanel = withErrorBoundary(({ ...props }) => { - const wazuhConfig = new WazuhConfig(); + const extraFilters = []; - const conf = wazuhConfig.getConfig(); const [moduleStatsList, setModuleStatsList] = useState([]); useEffect(() => { (async () => { try { + // const testResult = await getCurrentConfig( + // '000', + // [{ component: 'wmodules', configuration: 'wmodules' }], + // props.clusterNodeSelected, + // props.updateWazuhNotReadyYet + // ); const modulesConfig = await queryConfig( '000', [{ component: 'wmodules', configuration: 'wmodules' }], @@ -44,6 +52,7 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { )(); }, []) return ( - <MainPanel sidePanelChildren={<ModuleStats listItems={moduleStatsList} />}></MainPanel> + <MainPanel visualizations={moduleVisualizations} + sidePanelChildren={<ModuleStats listItems={moduleStatsList} />} /> ) }); diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index a869510acc..25109ddcc1 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -66,7 +66,7 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Alerts-Evolution', + _id: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', _source: { title: 'Mitre alerts evolution', visState: JSON.stringify({ @@ -199,7 +199,7 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Agent', + _id: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', _source: { title: 'Mitre techniques by agent', visState: JSON.stringify({ @@ -266,7 +266,7 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Attacks-By-Technique', + _id: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', _source: { title: 'Attacks by technique', visState: JSON.stringify({ From 40e002cd4e4e8502f6d81978010707ec719b4f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Mon, 19 Jul 2021 11:04:51 +0200 Subject: [PATCH 084/493] Changed the name of the component to SampleDataWarning Applied Prettier to wz-visualize --- .../components/visualize/components/index.ts | 2 +- ...{sample-data.js => sample-data-warning.js} | 43 +- ...ta.test.js => sample-data-warning.test.js} | 10 +- public/components/visualize/wz-visualize.js | 505 +++++++++--------- 4 files changed, 279 insertions(+), 281 deletions(-) rename public/components/visualize/components/{sample-data.js => sample-data-warning.js} (70%) rename public/components/visualize/components/{sample-data.test.js => sample-data-warning.test.js} (89%) diff --git a/public/components/visualize/components/index.ts b/public/components/visualize/components/index.ts index f4cd522aa5..63c5dafb49 100644 --- a/public/components/visualize/components/index.ts +++ b/public/components/visualize/components/index.ts @@ -11,4 +11,4 @@ */ export { SecurityAlerts } from './security-alerts'; -export { SampleData } from './sample-data'; \ No newline at end of file +export { SampleDataWarning } from './sample-data-warning'; \ No newline at end of file diff --git a/public/components/visualize/components/sample-data.js b/public/components/visualize/components/sample-data-warning.js similarity index 70% rename from public/components/visualize/components/sample-data.js rename to public/components/visualize/components/sample-data-warning.js index fd6b4b4533..f538173b91 100644 --- a/public/components/visualize/components/sample-data.js +++ b/public/components/visualize/components/sample-data-warning.js @@ -16,32 +16,31 @@ import React, { useState, useEffect } from 'react'; import { WzRequest } from '../../../react-services'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -export const SampleData = ({ ...props }) => { +export const SampleDataWarning = ({ ...props }) => { const [isSampleData, setIsSampleData] = useState(false); - const usesSampleData = async () => { - try { - const result = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})).data - .sampleAlertsInstalled; - setIsSampleData(result); - } catch (error) { - const options = { - context: `${SampleData.name}.usesSampleData`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - }; useEffect(() => { - usesSampleData(); + (async () => { + try { + const result = (await WzRequest.genericReq('GET', '/elastic/samplealerts', {})).data + .sampleAlertsInstalled; + setIsSampleData(result); + } catch (error) { + const options = { + context: `${SampleDataWarning.name}.usesSampleData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + })(); }, [ - SampleData, + SampleDataWarning, setIsSampleData, UI_ERROR_SEVERITIES, UI_LOGGER_LEVELS, diff --git a/public/components/visualize/components/sample-data.test.js b/public/components/visualize/components/sample-data-warning.test.js similarity index 89% rename from public/components/visualize/components/sample-data.test.js rename to public/components/visualize/components/sample-data-warning.test.js index 6d7e1840a9..09fdcc835f 100644 --- a/public/components/visualize/components/sample-data.test.js +++ b/public/components/visualize/components/sample-data-warning.test.js @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -import { SampleData } from './sample-data'; +import { SampleDataWarning } from './sample-data-warning'; import { WzRequest } from '../../../react-services'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; @@ -29,7 +29,7 @@ jest.mock('../../../react-services/common-services'); describe('Check sample data component', () => { it('should render if there is sample data', async () => { WzRequest.genericReq.mockResolvedValue({ data: { sampleAlertsInstalled: true } }); - const wrapper = await mount(<SampleData />); + const wrapper = await mount(<SampleDataWarning />); await awaitForMyComponent(wrapper); expect(wrapper.find('EuiCallOut').exists()); expect(wrapper.find('EuiCallOut').props().title).toEqual("This dashboard contains sample data"); @@ -37,7 +37,7 @@ describe('Check sample data component', () => { it('should not render if there is no sample data', async () => { WzRequest.genericReq.mockResolvedValue({ data: { sampleAlertsInstalled: false } }); - const wrapper = await mount(<SampleData />); + const wrapper = await mount(<SampleDataWarning />); await awaitForMyComponent(wrapper); expect(wrapper.contains('EuiCallOut')).toBe(false); }); @@ -48,7 +48,7 @@ describe('Check sample data component', () => { name: 'This should not be the thing', }; const mockOptions = { - context: `${SampleData.name}.usesSampleData`, + context: `${SampleDataWarning.name}.usesSampleData`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.UI, error: { @@ -65,7 +65,7 @@ describe('Check sample data component', () => { }; }); WzRequest.genericReq.mockRejectedValue(error); - const wrapper = await mount(<SampleData />); + const wrapper = await mount(<SampleDataWarning />); await awaitForMyComponent(wrapper); }); }); diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index b8a0dd4fb6..83e1c3f3a2 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -23,7 +23,7 @@ import { EuiButtonIcon, EuiDescriptionList, EuiCallOut, - EuiLink + EuiLink, } from '@elastic/eui'; import WzReduxProvider from '../../redux/wz-redux-provider'; import { WazuhConfig } from '../../react-services/wazuh-config'; @@ -34,173 +34,173 @@ import { RawVisualizations } from '../../factories/raw-visualizations'; import { Metrics } from '../overview/metrics/metrics'; import { PatternHandler } from '../../react-services/pattern-handler'; import { getToasts } from '../../kibana-services'; -import { SecurityAlerts } from './components'; +import { SampleDataWarning, SecurityAlerts } from './components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { withReduxProvider,withErrorBoundary } from '../common/hocs'; +import { withReduxProvider, withErrorBoundary } from '../common/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'; -import { SampleData } from './components'; - const visHandler = new VisHandlers(); -export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class WzVisualize extends Component { - _isMount = false; - constructor(props) { - super(props); - this.state = { - visualizations: !!props.isAgent ? agentVisualizations : visualizations, - expandedVis: false, - hasRefreshedKnownFields: false, - refreshingKnownFields: [], - refreshingIndex: true - }; - this.metricValues = false; - this.rawVisualizations = new RawVisualizations(); - this.wzReq = WzRequest; - const wazuhConfig = new WazuhConfig(); - this.commonData = new CommonData(); - const configuration = wazuhConfig.getConfig(); - this.monitoringEnabled = !!(configuration || {})[ - 'wazuh.monitoring.enabled' - ]; - this.newFields={}; - } +export const WzVisualize = compose( + withErrorBoundary, + withReduxProvider +)( + class WzVisualize extends Component { + _isMount = false; + constructor(props) { + super(props); + this.state = { + visualizations: !!props.isAgent ? agentVisualizations : visualizations, + expandedVis: false, + hasRefreshedKnownFields: false, + refreshingKnownFields: [], + refreshingIndex: true, + }; + this.metricValues = false; + this.rawVisualizations = new RawVisualizations(); + this.wzReq = WzRequest; + const wazuhConfig = new WazuhConfig(); + this.commonData = new CommonData(); + const configuration = wazuhConfig.getConfig(); + this.monitoringEnabled = !!(configuration || {})['wazuh.monitoring.enabled']; + this.newFields = {}; + } - async componentDidMount() { - this._isMount = true; - visHandler.removeAll(); - this.agentsStatus = false; - if (!this.monitoringEnabled) { - const data = await this.wzReq.apiReq('GET', '/agents/summary/status', {}); - const result = ((data || {}).data || {}).data || false; - if (result) { - this.agentsStatus = [ - { - title: 'Total', - description: result.total, - }, - { - title: 'Active', - description: result.active, - }, - { - title: 'Disconnected', - description: result.disconnected, - }, - { - title: 'Never Connected', - description: result['never_connected'], - }, - { - title: 'Agents coverage', - description: ((result.total) ? ((result.active) / (result.total)) * 100 : 0) + '%', - }, - ]; + async componentDidMount() { + this._isMount = true; + visHandler.removeAll(); + this.agentsStatus = false; + if (!this.monitoringEnabled) { + const data = await this.wzReq.apiReq('GET', '/agents/summary/status', {}); + const result = ((data || {}).data || {}).data || false; + if (result) { + this.agentsStatus = [ + { + title: 'Total', + description: result.total, + }, + { + title: 'Active', + description: result.active, + }, + { + title: 'Disconnected', + description: result.disconnected, + }, + { + title: 'Never Connected', + description: result['never_connected'], + }, + { + title: 'Agents coverage', + description: (result.total ? (result.active / result.total) * 100 : 0) + '%', + }, + ]; + } } } - } - async componentDidUpdate(prevProps) { - if (prevProps.isAgent !== this.props.isAgent) { - this._isMount && - this.setState({ visualizations: !!this.props.isAgent ? agentVisualizations : visualizations }); - typeof prevProps.isAgent !== 'undefined' && visHandler.removeAll(); + async componentDidUpdate(prevProps) { + if (prevProps.isAgent !== this.props.isAgent) { + this._isMount && + this.setState({ + visualizations: !!this.props.isAgent ? agentVisualizations : visualizations, + }); + typeof prevProps.isAgent !== 'undefined' && visHandler.removeAll(); + } } - } - componentWillUnmount() { - this._isMount = false; - } + componentWillUnmount() { + this._isMount = false; + } - expand = id => { - this.setState({ expandedVis: this.state.expandedVis === id ? false : id }); - }; + expand = (id) => { + this.setState({ expandedVis: this.state.expandedVis === id ? false : id }); + }; - refreshKnownFields = async ( newField = null ) => { - if(newField && newField.name){ - this.newFields[newField.name] = newField; - } - if (!this.state.hasRefreshedKnownFields) { // Known fields are refreshed only once per dashboard loading - try { - this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); - await PatternHandler.refreshIndexPattern(this.newFields); - this.setState({ isRefreshing: false }); - this.reloadToast(); - this.newFields={}; - } catch (error) { - this.setState({ isRefreshing: false }); - const options = { - context: `${WzVisualize.name}.refreshKnownFields`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: 'The index pattern could not be refreshed' || error.message || error, - title: error.name || error, - }, - }; + refreshKnownFields = async (newField = null) => { + if (newField && newField.name) { + this.newFields[newField.name] = newField; + } + if (!this.state.hasRefreshedKnownFields) { + // Known fields are refreshed only once per dashboard loading + try { + this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); + await PatternHandler.refreshIndexPattern(this.newFields); + this.setState({ isRefreshing: false }); + this.reloadToast(); + this.newFields = {}; + } catch (error) { + this.setState({ isRefreshing: false }); + const options = { + context: `${WzVisualize.name}.refreshKnownFields`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: 'The index pattern could not be refreshed' || error.message || error, + title: error.name || error, + }, + }; - getErrorOrchestrator().handleError(options); + getErrorOrchestrator().handleError(options); + } + } else if (this.state.isRefreshing) { + await new Promise((r) => setTimeout(r, 150)); + await this.refreshKnownFields(); } - } else if (this.state.isRefreshing) { - await new Promise(r => setTimeout(r, 150)); - await this.refreshKnownFields(); - } - } - reloadToast = () => { - getToasts().add({ - color: 'success', - title: 'The index pattern was refreshed successfully.', - text: toMountPoint(<EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> - <EuiFlexItem grow={false}> - There were some unknown fields for the current index pattern. - You need to refresh the page to apply the changes. - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton onClick={() => window.location.reload()} size="s">Reload page</EuiButton> - </EuiFlexItem> - </EuiFlexGroup>) - }) - } - render() { - const { visualizations } = this.state; - const { selectedTab } = this.props; - const renderVisualizations = vis => { - return ( - <EuiFlexItem - grow={parseInt((vis.width || 10) / 10)} - key={vis.id} - style={{ maxWidth: vis.width + '%', margin: 0, padding: 12 }} - > - <EuiPanel - paddingSize="none" - className={ - this.state.expandedVis === vis.id ? 'fullscreen h-100' : 'h-100' - } + }; + reloadToast = () => { + getToasts().add({ + color: 'success', + title: 'The index pattern was refreshed successfully.', + text: toMountPoint( + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + There were some unknown fields for the current index pattern. You need to refresh the + page to apply the changes. + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => window.location.reload()} size="s"> + Reload page + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ), + }); + }; + render() { + const { visualizations } = this.state; + const { selectedTab } = this.props; + const renderVisualizations = (vis) => { + return ( + <EuiFlexItem + grow={parseInt((vis.width || 10) / 10)} + key={vis.id} + style={{ maxWidth: vis.width + '%', margin: 0, padding: 12 }} > - <EuiFlexItem className="h-100"> - <EuiFlexGroup - style={{ padding: '12px 12px 0px' }} - className="embPanel__header" - > - <h2 className="embPanel__title wz-headline-title"> - {vis.title} - </h2> - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => this.expand(vis.id)} - iconType="expand" - aria-label="Expand" - /> - </EuiFlexGroup> - <div style={{ height: '100%' }}> - {(vis.id !== 'Wazuh-App-Overview-General-Agents-status' || - (vis.id === 'Wazuh-App-Overview-General-Agents-status' && - this.monitoringEnabled)) && ( + <EuiPanel + paddingSize="none" + className={this.state.expandedVis === vis.id ? 'fullscreen h-100' : 'h-100'} + > + <EuiFlexItem className="h-100"> + <EuiFlexGroup style={{ padding: '12px 12px 0px' }} className="embPanel__header"> + <h2 className="embPanel__title wz-headline-title">{vis.title}</h2> + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => this.expand(vis.id)} + iconType="expand" + aria-label="Expand" + /> + </EuiFlexGroup> + <div style={{ height: '100%' }}> + {(vis.id !== 'Wazuh-App-Overview-General-Agents-status' || + (vis.id === 'Wazuh-App-Overview-General-Agents-status' && + this.monitoringEnabled)) && ( <WzReduxProvider> <KibanaVis refreshKnownFields={this.refreshKnownFields} @@ -210,123 +210,122 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class ></KibanaVis> </WzReduxProvider> )} - {vis.id === 'Wazuh-App-Overview-General-Agents-status' && - !this.monitoringEnabled && ( - <EuiPage style={{ background: 'transparent' }}> - <EuiDescriptionList - type="column" - listItems={this.agentsStatus} - style={{ maxWidth: '400px' }} - /> - </EuiPage> - )} - </div> - </EuiFlexItem> - </EuiPanel> - </EuiFlexItem> - ); - }; - - const renderVisualizationRow = (rows, width, idx) => { - return ( - <EuiFlexItem - grow={(width || 10) / 10} - key={idx} - style={{ maxWidth: width + '%', margin: 0, padding: 12 }} - > - {rows.map((visRow, j) => { - return ( - <EuiFlexGroup - key={j} - style={{ - height: visRow.height || 0 + 'px', - marginBottom: visRow.noMargin ? '' : '4px' - }} - > - {visRow.vis.map(visualizeRow => { - return renderVisualizations(visualizeRow); - })} - </EuiFlexGroup> - ); - })} - </EuiFlexItem> - ); - }; - - return ( - <Fragment> - {/* Sample alerts Callout */} - {this.props.resultState === 'ready' && ( - <SampleData/> - )} - - {this.props.resultState === 'none' && ( - <div className="wz-margin-top-10 wz-margin-right-8 wz-margin-left-8"> - <EuiCallOut title="There are no results for selected time range. Try another - one." color="warning" iconType='help'></EuiCallOut> - </div> - )} - <EuiFlexItem className={this.props.resultState === 'none' && 'no-opacity' || ''}> - {this.props.resultState === 'ready' && - < Metrics section={selectedTab} resultState={this.props.resultState} />} + {vis.id === 'Wazuh-App-Overview-General-Agents-status' && + !this.monitoringEnabled && ( + <EuiPage style={{ background: 'transparent' }}> + <EuiDescriptionList + type="column" + listItems={this.agentsStatus} + style={{ maxWidth: '400px' }} + /> + </EuiPage> + )} + </div> + </EuiFlexItem> + </EuiPanel> + </EuiFlexItem> + ); + }; - {selectedTab && - selectedTab !== 'welcome' && - visualizations[selectedTab] && - visualizations[selectedTab].rows.map((row, i) => { + const renderVisualizationRow = (rows, width, idx) => { + return ( + <EuiFlexItem + grow={(width || 10) / 10} + key={idx} + style={{ maxWidth: width + '%', margin: 0, padding: 12 }} + > + {rows.map((visRow, j) => { return ( <EuiFlexGroup - key={i} + key={j} style={{ - display: row.hide && 'none', - height: row.height || 0 + 'px', - margin: 0, - maxWidth: '100%' + height: visRow.height || 0 + 'px', + marginBottom: visRow.noMargin ? '' : '4px', }} > - {row.vis.map((vis, n) => { - return !vis.hasRows - ? renderVisualizations(vis) - : renderVisualizationRow(vis.rows, vis.width, n); + {visRow.vis.map((visualizeRow) => { + return renderVisualizations(visualizeRow); })} </EuiFlexGroup> ); })} - </EuiFlexItem> - <EuiFlexGroup style={{ margin: 0 }}> - <EuiFlexItem> - {this.props.selectedTab === "general" && this.props.resultState !== "none" && + </EuiFlexItem> + ); + }; + + return ( + <Fragment> + {/* Sample alerts Callout */} + {this.props.resultState === 'ready' && <SampleDataWarning />} + + {this.props.resultState === 'none' && ( + <div className="wz-margin-top-10 wz-margin-right-8 wz-margin-left-8"> + <EuiCallOut + title="There are no results for selected time range. Try another + one." + color="warning" + iconType="help" + ></EuiCallOut> + </div> + )} + <EuiFlexItem className={(this.props.resultState === 'none' && 'no-opacity') || ''}> + {this.props.resultState === 'ready' && ( + <Metrics section={selectedTab} resultState={this.props.resultState} /> + )} - <EuiPanel - paddingSize="none" - className={ - this.state.expandedVis === 'security-alerts' ? 'fullscreen h-100 wz-overflow-y-auto wz-overflow-x-hidden' : 'h-100' - } - > - <EuiFlexItem className="h-100" style={{ marginBottom: 12 }}> + {selectedTab && + selectedTab !== 'welcome' && + visualizations[selectedTab] && + visualizations[selectedTab].rows.map((row, i) => { + return ( <EuiFlexGroup - style={{ padding: '12px 12px 0px' }} - className="embPanel__header" + key={i} + style={{ + display: row.hide && 'none', + height: row.height || 0 + 'px', + margin: 0, + maxWidth: '100%', + }} > - <h2 className="embPanel__title wz-headline-title"> - Security Alerts - </h2> - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => this.expand('security-alerts')} - iconType="expand" - aria-label="Expand" - /> + {row.vis.map((vis, n) => { + return !vis.hasRows + ? renderVisualizations(vis) + : renderVisualizationRow(vis.rows, vis.width, n); + })} </EuiFlexGroup> - <SecurityAlerts /> - - </EuiFlexItem> - </EuiPanel> - } + ); + })} </EuiFlexItem> - </EuiFlexGroup> - </Fragment> - ); + <EuiFlexGroup style={{ margin: 0 }}> + <EuiFlexItem> + {this.props.selectedTab === 'general' && this.props.resultState !== 'none' && ( + <EuiPanel + paddingSize="none" + className={ + this.state.expandedVis === 'security-alerts' + ? 'fullscreen h-100 wz-overflow-y-auto wz-overflow-x-hidden' + : 'h-100' + } + > + <EuiFlexItem className="h-100" style={{ marginBottom: 12 }}> + <EuiFlexGroup style={{ padding: '12px 12px 0px' }} className="embPanel__header"> + <h2 className="embPanel__title wz-headline-title">Security Alerts</h2> + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => this.expand('security-alerts')} + iconType="expand" + aria-label="Expand" + /> + </EuiFlexGroup> + <SecurityAlerts /> + </EuiFlexItem> + </EuiPanel> + )} + </EuiFlexItem> + </EuiFlexGroup> + </Fragment> + ); + } } -}) +); From 0d4aac4827dff043b7d4b210fe72d9b7961ee982 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 19 Jul 2021 11:59:28 +0200 Subject: [PATCH 085/493] queryConfig from angular to react --- .../overview/office-panel/office-panel.tsx | 3 +- public/react-services/index.ts | 1 + public/react-services/query-config.js | 84 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 public/react-services/query-config.js diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index c94d1c3138..79298c80b0 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -16,7 +16,8 @@ import { MainPanel } from '../../../components/common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { ModuleStats } from './module-stats'; import { WzRequest } from '../../../react-services/wz-request'; -import { queryConfig } from '../../../services/query-config'; +//import { queryConfig } from '../../../services/query-config'; +import { queryConfig } from '../../../react-services/query-config'; import { getCurrentConfig } from '../../../controllers/management/components/management/configuration/utils/wz-fetch'; // import { getCurrentConfig } from '../../../react-services'; import moduleVisualizations from './module-config'; diff --git a/public/react-services/index.ts b/public/react-services/index.ts index 8363f3664f..ba403f2533 100644 --- a/public/react-services/index.ts +++ b/public/react-services/index.ts @@ -22,3 +22,4 @@ export * from './wz-request'; export * from './wz-security-opendistro'; export * from './wz-security-xpack'; export * from './wz-user-permissions'; +export * from './query-config' \ No newline at end of file diff --git a/public/react-services/query-config.js b/public/react-services/query-config.js new file mode 100644 index 0000000000..e63ff75a5c --- /dev/null +++ b/public/react-services/query-config.js @@ -0,0 +1,84 @@ +/* + * Wazuh app - Query config on-demand + * Copyright (C) 2015-2021 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 { WzRequest } from './wz-request'; +import { getErrorOrchestrator } from './common-services'; +import { AppState } from './app-state'; + +export const queryConfig = async (agentId, sections, node = false) => { + try { + if ( + !agentId || + typeof agentId !== 'string' || + !sections || + !sections.length || + typeof sections !== 'object' || + !Array.isArray(sections) + ) { + throw new Error('Invalid parameters'); + } + + const result = {}; + sections.forEach((section) => { async () => { + const { component, configuration } = section; + if ( + !component || + typeof component !== 'string' || + !configuration || + typeof configuration !== 'string' + ) { + throw new Error('Invalid section'); + } + try { + const url = node + ? `/cluster/${node}/configuration/${component}/${configuration}` + : !node + && agentId === '000' + ? `/manager/configuration/${component}/${configuration}` + : `/agents/${agentId}/configuration/${component}/${configuration}`; + + const partialResult = await WzRequest.apiReq('GET', url, {}); + result[`${component}-${configuration}`] = partialResult.data.data; + } catch (error) { + const options = { + context: `${AppState.name}.queryConfig`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: false, + error: { + error: error, + message: error.message || error, + title: `Fetch Configuration`, + }, + }; + getErrorOrchestrator().handleError(options); + } + }}); + return result; + } catch (error) { + const options = { + context: `${AppState.name}.queryConfig`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: false, + error: { + error: error, + message: error.message || error, + title: `Error getting the query config`, + }, + }; + + getErrorOrchestrator().handleError(options); + } +} From 250bcdad6a33e9ffd6ccf33856b486bc8bc5ab68 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 19 Jul 2021 12:52:47 +0200 Subject: [PATCH 086/493] queryConfig bug fixing --- public/components/overview/office-panel/office-panel.tsx | 3 +-- public/react-services/query-config.js | 7 +++++-- public/utils/config-handler.js | 8 +------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 79298c80b0..06910c5b97 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -37,8 +37,7 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { // ); const modulesConfig = await queryConfig( '000', - [{ component: 'wmodules', configuration: 'wmodules' }], - WzRequest.apiReq + [{ component: 'wmodules', configuration: 'wmodules' }] ); const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { diff --git a/public/react-services/query-config.js b/public/react-services/query-config.js index e63ff75a5c..3c82962f54 100644 --- a/public/react-services/query-config.js +++ b/public/react-services/query-config.js @@ -13,6 +13,7 @@ import { WzRequest } from './wz-request'; import { getErrorOrchestrator } from './common-services'; import { AppState } from './app-state'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; export const queryConfig = async (agentId, sections, node = false) => { try { @@ -28,7 +29,7 @@ export const queryConfig = async (agentId, sections, node = false) => { } const result = {}; - sections.forEach((section) => { async () => { + sections.forEach((section) => { (async () => { const { component, configuration } = section; if ( !component || @@ -48,6 +49,7 @@ export const queryConfig = async (agentId, sections, node = false) => { const partialResult = await WzRequest.apiReq('GET', url, {}); result[`${component}-${configuration}`] = partialResult.data.data; + return result; } catch (error) { const options = { context: `${AppState.name}.queryConfig`, @@ -63,7 +65,8 @@ export const queryConfig = async (agentId, sections, node = false) => { }; getErrorOrchestrator().handleError(options); } - }}); + })(); + }); return result; } catch (error) { const options = { diff --git a/public/utils/config-handler.js b/public/utils/config-handler.js index fd3bf72c52..f28bf776a4 100644 --- a/public/utils/config-handler.js +++ b/public/utils/config-handler.js @@ -11,7 +11,7 @@ */ import js2xmlparser from 'js2xmlparser'; import XMLBeautifier from './xml-beautifier'; -import { queryConfig } from '../services/query-config'; +import { queryConfig } from '../react-services/query-config'; import { objectWithoutProperties } from './remove-hash-key.js'; import { WzRequest } from '../react-services/wz-request'; import { ErrorHandler } from '../react-services/error-handler'; @@ -62,8 +62,6 @@ export class ConfigurationHandler { $scope.currentConfig = await queryConfig( agentId || '000', sections, - WzRequest.apiReq, - this.errorHandler, node ); @@ -133,8 +131,6 @@ export class ConfigurationHandler { $scope.currentConfig = await queryConfig( agentId || '000', [{ component: 'wmodules', configuration: 'wmodules' }], - WzRequest.apiReq, - this.errorHandler, node ); @@ -176,8 +172,6 @@ export class ConfigurationHandler { const wodlesConfig = await queryConfig( agentId || '000', [{ component: 'wmodules', configuration: 'wmodules' }], - WzRequest.apiReq, - this.errorHandler ); // Filter by provided wodleName From bdd046f6b917c1a8f64aeaf87d64a30d2b74188c Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 19 Jul 2021 12:55:56 +0200 Subject: [PATCH 087/493] Adding Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc557c2785..c0d84f4074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,8 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed -- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Query config refactor [#3490](https://github.com/wazuh/wazuh-kibana-app/pull/3490) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From d161e45a38587caa3b39ca4b83086b7dc0a95125 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Mon, 19 Jul 2021 08:48:10 -0300 Subject: [PATCH 088/493] Refactor try-catch components/add-moludes-data (#3472) * Implemented error-handling * Updated CHANGELOG Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 1 + .../add-modules-data/sample-data.tsx | 266 +++++++++++------- 2 files changed, 168 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fb334cd5..72175c6e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Syscollector/Vuls/Stats sections [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) - Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) +- Added try catch strategy with ErrorOrchestrator service on Components > Add Modules Data [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) - Added try catch strategy with ErrorOrchestrator service on Management > Status [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) ### Changed diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index f4457d7be0..dd7a85c108 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -16,14 +16,8 @@ import { WzButtonPermissions } from '../../components/common/permissions/button' import { EuiFlexItem, EuiCard, - EuiSpacer, EuiFlexGrid, EuiFlexGroup, - EuiButton, - EuiButtonEmpty, - EuiTitle, - EuiToolTip, - EuiButtonIcon } from '@elastic/eui'; import { getToasts } from '../../kibana-services'; @@ -31,80 +25,119 @@ import { WzRequest } from '../../react-services/wz-request'; import { AppState } from '../../react-services/app-state'; import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { getErrorOrchestrator } from '../../react-services/common-services'; + export default class WzSampleData extends Component { - categories: { title: string, description: string, image: string, categorySampleAlertsIndex: string }[] - generateAlertsParams: any + categories: { + title: string; + description: string; + image: string; + categorySampleAlertsIndex: string; + }[]; + generateAlertsParams: any; state: { [name: string]: { - exists: boolean - addDataLoading: boolean - removeDataLoading: boolean - } - } + exists: boolean; + addDataLoading: boolean; + removeDataLoading: boolean; + }; + }; constructor(props) { super(props); this.generateAlertsParams = {}; // extra params to add to generateAlerts function in server this.categories = [ { title: 'Sample security information', - description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, authorization, ssh, web).', + description: + 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, authorization, ssh, web).', image: '', - categorySampleAlertsIndex: 'security' + categorySampleAlertsIndex: 'security', }, { title: 'Sample auditing and policy monitoring', - description: 'Sample data, visualizations and dashboards for events of auditing and policy monitoring (policy monitoring, system auditing, OpenSCAP, CIS-CAT).', + description: + 'Sample data, visualizations and dashboards for events of auditing and policy monitoring (policy monitoring, system auditing, OpenSCAP, CIS-CAT).', image: '', - categorySampleAlertsIndex: 'auditing-policy-monitoring' + categorySampleAlertsIndex: 'auditing-policy-monitoring', }, { title: 'Sample threat detection and response', - description: 'Sample data, visualizations and dashboards for threat events of detection and response (vulnerabilities, VirustTotal, Osquery, Docker listener, MITRE).', + description: + 'Sample data, visualizations and dashboards for threat events of detection and response (vulnerabilities, VirustTotal, Osquery, Docker listener, MITRE).', image: '', - categorySampleAlertsIndex: 'threat-detection' - } + categorySampleAlertsIndex: 'threat-detection', + }, ]; this.state = {}; - this.categories.forEach(category => { + this.categories.forEach((category) => { this.state[category.categorySampleAlertsIndex] = { exists: false, addDataLoading: false, - removeDataLoading: false - } + removeDataLoading: false, + }; }); } async componentDidMount() { - // Check if sample data for each category was added try { - const results = await PromiseAllRecursiveObject(this.categories.reduce((accum, cur) => { - accum[cur.categorySampleAlertsIndex] = WzRequest.genericReq('GET', `/elastic/samplealerts/${cur.categorySampleAlertsIndex}`) - return accum - }, {})); + // Check if sample data for each category was added + try { + const results = await PromiseAllRecursiveObject( + this.categories.reduce((accum, cur) => { + accum[cur.categorySampleAlertsIndex] = WzRequest.genericReq( + 'GET', + `/elastic/samplealerts/${cur.categorySampleAlertsIndex}` + ); + return accum; + }, {}) + ); - this.setState(Object.keys(results).reduce((accum, cur) => { - accum[cur] = { - ...this.state[cur], - exists: results[cur].data.exists - } - return accum - }, { ...this.state })); - } catch (error) { } + this.setState( + Object.keys(results).reduce( + (accum, cur) => { + accum[cur] = { + ...this.state[cur], + exists: results[cur].data.exists, + }; + return accum; + }, + { ...this.state } + ) + ); + } catch (error) { + throw error; + } - // Get information about cluster/manager - try { - const clusterName = AppState.getClusterInfo().cluster; - const managerName = AppState.getClusterInfo().manager; - this.generateAlertsParams.manager = { - name: managerName - }; - if (clusterName && clusterName !== 'Disabled') { - this.generateAlertsParams.cluster = { - name: clusterName, - node: clusterName + // Get information about cluster/manager + try { + const clusterName = AppState.getClusterInfo().cluster; + const managerName = AppState.getClusterInfo().manager; + this.generateAlertsParams.manager = { + name: managerName, }; + if (clusterName && clusterName !== 'Disabled') { + this.generateAlertsParams.cluster = { + name: clusterName, + node: clusterName, + }; + } + } catch (error) { + throw error; + } + } catch (error) { + const options = { + context: `${WzSampleData.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, }; - - } catch (error) { } + getErrorOrchestrator().handleError(options); + } } showToast(color: string, title: string = '', text: string = '', time: number = 3000) { getToasts().add({ @@ -113,31 +146,50 @@ export default class WzSampleData extends Component { text: text, toastLifeTimeMs: time, }); - }; + } async addSampleData(category) { try { this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], - addDataLoading: true - } + addDataLoading: true, + }, }); - await WzRequest.genericReq('POST', `/elastic/samplealerts/${category.categorySampleAlertsIndex}`, { params: this.generateAlertsParams }); - this.showToast('success', `${category.title} alerts added`, 'Date range for sample data is now-7 days ago', 5000); + await WzRequest.genericReq( + 'POST', + `/elastic/samplealerts/${category.categorySampleAlertsIndex}`, + { params: this.generateAlertsParams } + ); + this.showToast( + 'success', + `${category.title} alerts added`, + 'Date range for sample data is now-7 days ago', + 5000 + ); this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], exists: true, - addDataLoading: false - } + addDataLoading: false, + }, }); } catch (error) { - this.showToast('danger', 'Error', `Error trying to add sample data: ${error.message || error}`); + const options = { + context: `${WzSampleData.name}.addSampleData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error trying to add sample data`, + }, + }; + getErrorOrchestrator().handleError(options); this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], - addDataLoading: false - } + addDataLoading: false, + }, }); } } @@ -146,90 +198,106 @@ export default class WzSampleData extends Component { this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], - removeDataLoading: true - } + removeDataLoading: true, + }, }); - await WzRequest.genericReq('DELETE', `/elastic/samplealerts/${category.categorySampleAlertsIndex}`); + await WzRequest.genericReq( + 'DELETE', + `/elastic/samplealerts/${category.categorySampleAlertsIndex}` + ); this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], exists: false, - removeDataLoading: false - } + removeDataLoading: false, + }, }); this.showToast('success', `${category.title} alerts removed`); } catch (error) { + const options = { + context: `${WzSampleData.name}.removeSampleData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error trying to delete sample data`, + }, + }; + getErrorOrchestrator().handleError(options); this.setState({ [category.categorySampleAlertsIndex]: { ...this.state[category.categorySampleAlertsIndex], - removeDataLoading: false - } + removeDataLoading: false, + }, }); - this.showToast('danger', 'Error', `Error trying to delete sample data: ${error.message || error}`); } } renderCard(category) { - const { addDataLoading, exists, removeDataLoading } = this.state[category.categorySampleAlertsIndex]; + const { addDataLoading, exists, removeDataLoading } = this.state[ + category.categorySampleAlertsIndex + ]; return ( <EuiFlexItem key={`sample-data-${category.title}`}> <EuiCard - textAlign='left' + textAlign="left" title={category.title} description={category.description} image={category.image} betaBadgeLabel={exists ? 'Installed' : undefined} - footer={( + footer={ <EuiFlexGroup justifyContent="flexEnd"> <EuiFlexItem grow={false}> - {exists && ( + {(exists && ( <WzButtonPermissions - color='danger' + color="danger" roles={[WAZUH_ROLE_ADMINISTRATOR_NAME]} onClick={() => this.removeSampleData(category)} > - {removeDataLoading && 'Removing data' || 'Remove data'} + {(removeDataLoading && 'Removing data') || 'Remove data'} + </WzButtonPermissions> + )) || ( + <WzButtonPermissions + isLoading={addDataLoading} + roles={[WAZUH_ROLE_ADMINISTRATOR_NAME]} + onClick={() => this.addSampleData(category)} + > + {(addDataLoading && 'Adding data') || 'Add data'} </WzButtonPermissions> - ) || ( - <WzButtonPermissions - isLoading={addDataLoading} - roles={[WAZUH_ROLE_ADMINISTRATOR_NAME]} - onClick={() => this.addSampleData(category)} - > - {addDataLoading && 'Adding data' || 'Add data'} - </WzButtonPermissions> - )} + )} </EuiFlexItem> </EuiFlexGroup> - )} + } /> </EuiFlexItem> - ) + ); } render() { return ( <EuiFlexGrid columns={3}> - {this.categories.map(category => this.renderCard(category))} + {this.categories.map((category) => this.renderCard(category))} </EuiFlexGrid> - ) + ); } } const zipObject = (keys = [], values = []) => { return keys.reduce((accumulator, key, index) => { - accumulator[key] = values[index] - return accumulator - }, {}) -} + accumulator[key] = values[index]; + return accumulator; + }, {}); +}; const PromiseAllRecursiveObject = function (obj) { const keys = Object.keys(obj); - return Promise.all(keys.map(key => { - const value = obj[key]; - // Promise.resolve(value) !== value should work, but !value.then always works - if (typeof value === 'object' && !value.then) { - return PromiseAllRecursiveObject(value); - } - return value; - })) - .then(result => zipObject(keys, result)); + return Promise.all( + keys.map((key) => { + const value = obj[key]; + // Promise.resolve(value) !== value should work, but !value.then always works + if (typeof value === 'object' && !value.then) { + return PromiseAllRecursiveObject(value); + } + return value; + }) + ).then((result) => zipObject(keys, result)); }; From 42ee2ebebf131827f90976294d215913d9fa3929 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 19 Jul 2021 14:00:03 +0200 Subject: [PATCH 089/493] Removing comments --- public/components/overview/office-panel/office-panel.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 06910c5b97..e908882bbd 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -16,10 +16,8 @@ import { MainPanel } from '../../../components/common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { ModuleStats } from './module-stats'; import { WzRequest } from '../../../react-services/wz-request'; -//import { queryConfig } from '../../../services/query-config'; import { queryConfig } from '../../../react-services/query-config'; import { getCurrentConfig } from '../../../controllers/management/components/management/configuration/utils/wz-fetch'; -// import { getCurrentConfig } from '../../../react-services'; import moduleVisualizations from './module-config'; export const OfficePanel = withErrorBoundary(({ ...props }) => { @@ -29,12 +27,6 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { useEffect(() => { (async () => { try { - // const testResult = await getCurrentConfig( - // '000', - // [{ component: 'wmodules', configuration: 'wmodules' }], - // props.clusterNodeSelected, - // props.updateWazuhNotReadyYet - // ); const modulesConfig = await queryConfig( '000', [{ component: 'wmodules', configuration: 'wmodules' }] From 39ae0aae9994da8f650f6b80ca9473c8cd4b7dc6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 19 Jul 2021 15:50:13 +0200 Subject: [PATCH 090/493] added drilldown base view --- .../components/common/modules/panel/index.tsx | 1 + .../common/modules/panel/main-panel.tsx | 13 ++++--- .../common/modules/panel/module-body.tsx | 39 ++++++++++--------- .../common/modules/panel/module-drilldown.tsx | 27 +++++++++++++ .../office-panel/config/drilldown-config.ts | 37 ++++++++++++++++++ .../overview/office-panel/config/index.ts | 2 + .../office-panel/config/main-view-config.ts | 37 ++++++++++++++++++ .../overview/office-panel/module-config.tsx | 33 ---------------- .../overview/office-panel/office-panel.tsx | 13 ++----- 9 files changed, 135 insertions(+), 67 deletions(-) create mode 100644 public/components/common/modules/panel/module-drilldown.tsx create mode 100644 public/components/overview/office-panel/config/drilldown-config.ts create mode 100644 public/components/overview/office-panel/config/index.ts create mode 100644 public/components/overview/office-panel/config/main-view-config.ts delete mode 100644 public/components/overview/office-panel/module-config.tsx diff --git a/public/components/common/modules/panel/index.tsx b/public/components/common/modules/panel/index.tsx index 5b6b8a8051..385d5bc1a1 100644 --- a/public/components/common/modules/panel/index.tsx +++ b/public/components/common/modules/panel/index.tsx @@ -14,3 +14,4 @@ export { MainPanel } from './main-panel'; export { ModuleSidePanel } from './module-side-panel'; export { ModuleBody } from './module-body'; +export { ModuleDrilldown } from './module-drilldown'; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 40e8bf4aaa..d84fbbd813 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -6,7 +6,7 @@ import { EuiPageSideBar, EuiPageBody, } from '@elastic/eui'; -import { ModuleSidePanel, ModuleBody } from './'; +import { ModuleSidePanel, ModuleBody, ModuleDrilldown } from './'; import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; //@ts-ignore import { getDataPlugin } from '../../../../kibana-services'; @@ -16,7 +16,7 @@ import { MockupTables } from './mockup-tables'; -export const MainPanel = ({ sidePanelChildren, visualizations = [], ...props }) => { +export const MainPanel = ({ sidePanelChildren, moduleConfig = {}, drilldownConfig = {}, ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; @@ -53,13 +53,14 @@ export const MainPanel = ({ sidePanelChildren, visualizations = [], ...props }) </ModuleSidePanel > } <EuiPageBody> - <ModuleBody visualizations={visualizations}> + {<ModuleBody {...moduleConfig}> <KbnSearchBar onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} isLoading={isLoading} /> - - </ModuleBody> + + </ModuleBody>} + {drilldownConfig && <ModuleDrilldown {...drilldownConfig} />} </EuiPageBody> </EuiFlexItem> </EuiFlexGroup> diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/common/modules/panel/module-body.tsx index 1b83507d18..b507f79861 100644 --- a/public/components/common/modules/panel/module-body.tsx +++ b/public/components/common/modules/panel/module-body.tsx @@ -1,24 +1,27 @@ import React from 'react'; -import { EuiPageBody, EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; -export const ModuleBody = ({ visualizations = [], ...props }) => { +export const ModuleBody = ({ rows = [], ...props }) => { - return <> - <WzReduxProvider> - {visualizations?.map((row, key) => { - return <EuiFlexGroup key={key} style={{ - height: row.height || 150 + 'px' - }}>{ - row.columns.map((column, key) => { - return <EuiFlexItem key={key}> - {column.component} - </EuiFlexItem> - }) - }</EuiFlexGroup> - }) - } - </WzReduxProvider> - </> + return <> + <WzReduxProvider> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} style={{ + height: row.height || (150 + 'px') + }}>{ + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + const visProps = { ...(column.props || {}) }; + return <EuiFlexItem key={key} grow={growthFactor}> + <column.component {...visProps} /> + </EuiFlexItem> + }) + }</EuiFlexGroup> + }) + } + </WzReduxProvider> + </> } \ No newline at end of file diff --git a/public/components/common/modules/panel/module-drilldown.tsx b/public/components/common/modules/panel/module-drilldown.tsx new file mode 100644 index 0000000000..28ec9a9501 --- /dev/null +++ b/public/components/common/modules/panel/module-drilldown.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import WzReduxProvider from '../../../../redux/wz-redux-provider'; + + +export const ModuleDrilldown = ({ rows = [], ...props }) => { + + return <> + <WzReduxProvider> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} style={{ + height: row.height || (150 + 'px') + }}>{ + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + const visProps = { ...(column.props || {}) }; + return <EuiFlexItem key={key} grow={growthFactor}> + <column.component {...visProps} /> + </EuiFlexItem> + }) + }</EuiFlexGroup> + }) + } + </WzReduxProvider> + </> +} \ No newline at end of file diff --git a/public/components/overview/office-panel/config/drilldown-config.ts b/public/components/overview/office-panel/config/drilldown-config.ts new file mode 100644 index 0000000000..21b4a7232c --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-config.ts @@ -0,0 +1,37 @@ +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; + + +export const DrilldownConfig = { + rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } + }, + { + width: 50, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } + }, + ] + }, + { + height: 300, + columns: [ + { + width: 70, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', tab: 'office' } + }, + { + width: 30, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', tab: 'office' } + }, + ] + }, + ] +}; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts new file mode 100644 index 0000000000..4a739e937e --- /dev/null +++ b/public/components/overview/office-panel/config/index.ts @@ -0,0 +1,2 @@ +export { DrilldownConfig } from './drilldown-config'; +export { MainViewConfig } from './main-view-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.ts b/public/components/overview/office-panel/config/main-view-config.ts new file mode 100644 index 0000000000..cb2471173c --- /dev/null +++ b/public/components/overview/office-panel/config/main-view-config.ts @@ -0,0 +1,37 @@ +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; + + +export const MainViewConfig = { + rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } + }, + { + width: 50, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } + }, + ] + }, + { + height: 300, + columns: [ + { + width: 70, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', tab: 'office' } + }, + { + width: 30, + component: KibanaVis, + props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', tab: 'office' } + }, + ] + }, + ] +}; diff --git a/public/components/overview/office-panel/module-config.tsx b/public/components/overview/office-panel/module-config.tsx deleted file mode 100644 index 5069a25ba5..0000000000 --- a/public/components/overview/office-panel/module-config.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import KibanaVis from '../../../kibana-integrations/kibana-vis'; - - -const rows = [ - { - height: 300, - columns: [ - { - width: 3, - component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE'} tab={'office'} /> - }, - { - width: 3, - component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Alerts-Evolution'} tab={'office'} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 3, - component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Attacks-By-Agent'} tab={'office'} /> - }, - { - width: 3, - component: <KibanaVis visID={'Wazuh-App-Overview-OFFICE-Attacks-By-Technique'} tab={'office'} /> - }, - ] - }, -] -export default rows; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index c94d1c3138..9b0129dae7 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -17,9 +17,8 @@ import { withErrorBoundary } from '../../common/hocs'; import { ModuleStats } from './module-stats'; import { WzRequest } from '../../../react-services/wz-request'; import { queryConfig } from '../../../services/query-config'; -import { getCurrentConfig } from '../../../controllers/management/components/management/configuration/utils/wz-fetch'; -// import { getCurrentConfig } from '../../../react-services'; -import moduleVisualizations from './module-config'; +// import { queryConfig } from '../../../react-services/query-config'; +import { DrilldownConfig, MainViewConfig } from './config/'; export const OfficePanel = withErrorBoundary(({ ...props }) => { @@ -28,12 +27,6 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { useEffect(() => { (async () => { try { - // const testResult = await getCurrentConfig( - // '000', - // [{ component: 'wmodules', configuration: 'wmodules' }], - // props.clusterNodeSelected, - // props.updateWazuhNotReadyYet - // ); const modulesConfig = await queryConfig( '000', [{ component: 'wmodules', configuration: 'wmodules' }], @@ -52,7 +45,7 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { )(); }, []) return ( - <MainPanel visualizations={moduleVisualizations} + <MainPanel moduleConfig={MainViewConfig} drillDownConfig={DrilldownConfig} sidePanelChildren={<ModuleStats listItems={moduleStatsList} />} /> ) }); From 4974eceb18f0d863a0f9c45275abb613c55ec9a5 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 19 Jul 2021 19:41:13 +0200 Subject: [PATCH 091/493] Add drilldown action --- .../common/modules/panel/main-panel.tsx | 26 ++-- .../common/modules/panel/module-body.tsx | 6 +- .../common/modules/panel/module-drilldown.tsx | 20 +-- .../office-panel/config/main-view-config.ts | 18 +++ .../office-panel}/mockup-tables.tsx | 58 ++------- .../overview/office-panel/office-panel.tsx | 3 - public/react-services/query-config.js | 120 +++++++++--------- 7 files changed, 113 insertions(+), 138 deletions(-) rename public/components/{common/modules/panel => overview/office-panel}/mockup-tables.tsx (53%) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index d84fbbd813..65e2b8d967 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -12,22 +12,21 @@ import { FilterManager, Filter } from '../../../../../../../src/plugins/data/pub import { getDataPlugin } from '../../../../kibana-services'; import { KbnSearchBar } from '../../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../../src/plugins/data/common'; -import { MockupTables } from './mockup-tables'; - export const MainPanel = ({ sidePanelChildren, moduleConfig = {}, drilldownConfig = {}, ...props }) => { + const [isDrilldownOpen, setIsDrilldownOpen] = useState(false); const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; const timefilter = KibanaServices.timefilter.timefilter; + const [isLoading, setLoading] = useState(false); const [filterParams, setFilterParams] = useState({ filters: filterManager.getFilters() || [], query: { language: 'kuery', query: '' }, time: timefilter.getTime(), }); - const [isLoading, setLoading] = useState(false); const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { @@ -45,6 +44,13 @@ export const MainPanel = ({ sidePanelChildren, moduleConfig = {}, drilldownConfi setFilterParams(updatedFilterParams); } + const toggleDrilldown = () => { + setIsDrilldownOpen(!isDrilldownOpen); + } + + const bodyProps = { ...moduleConfig, toggleDrilldown }; + const drilldownProps = { ...drilldownConfig, toggleDrilldown }; + return ( <EuiFlexGroup style={{ margin: '0 8px' }}> <EuiFlexItem> @@ -53,14 +59,12 @@ export const MainPanel = ({ sidePanelChildren, moduleConfig = {}, drilldownConfi </ModuleSidePanel > } <EuiPageBody> - {<ModuleBody {...moduleConfig}> - <KbnSearchBar - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> - - </ModuleBody>} - {drilldownConfig && <ModuleDrilldown {...drilldownConfig} />} + <KbnSearchBar + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} /> + {!isDrilldownOpen && <ModuleBody {...bodyProps} />} + {drilldownConfig && isDrilldownOpen && <ModuleDrilldown {...drilldownProps} />} </EuiPageBody> </EuiFlexItem> </EuiFlexGroup> diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/common/modules/panel/module-body.tsx index b507f79861..64203264cb 100644 --- a/public/components/common/modules/panel/module-body.tsx +++ b/public/components/common/modules/panel/module-body.tsx @@ -3,7 +3,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; -export const ModuleBody = ({ rows = [], ...props }) => { +export const ModuleBody = ({ toggleDrilldown, rows = [], ...props }) => { return <> <WzReduxProvider> @@ -14,7 +14,9 @@ export const ModuleBody = ({ rows = [], ...props }) => { }}>{ row.columns.map((column, key) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - const visProps = { ...(column.props || {}) }; + const drilldownHandler = column.enableToggleDrilldown ? toggleDrilldown : undefined; + const visProps = { ...(column.props || {}), toggleDrilldown: drilldownHandler }; + return <EuiFlexItem key={key} grow={growthFactor}> <column.component {...visProps} /> </EuiFlexItem> diff --git a/public/components/common/modules/panel/module-drilldown.tsx b/public/components/common/modules/panel/module-drilldown.tsx index 28ec9a9501..05cea5732c 100644 --- a/public/components/common/modules/panel/module-drilldown.tsx +++ b/public/components/common/modules/panel/module-drilldown.tsx @@ -1,27 +1,13 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; -export const ModuleDrilldown = ({ rows = [], ...props }) => { +export const ModuleDrilldown = ({ toggleDrilldown, rows = [], ...props }) => { return <> <WzReduxProvider> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} style={{ - height: row.height || (150 + 'px') - }}>{ - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - const visProps = { ...(column.props || {}) }; - return <EuiFlexItem key={key} grow={growthFactor}> - <column.component {...visProps} /> - </EuiFlexItem> - }) - }</EuiFlexGroup> - }) - } + <EuiFlexGroup><EuiFlexItem><EuiButtonEmpty onClick={toggleDrilldown} iconType={"sortLeft"}></EuiButtonEmpty></EuiFlexItem></EuiFlexGroup> </WzReduxProvider> </> } \ No newline at end of file diff --git a/public/components/overview/office-panel/config/main-view-config.ts b/public/components/overview/office-panel/config/main-view-config.ts index cb2471173c..622ac3299e 100644 --- a/public/components/overview/office-panel/config/main-view-config.ts +++ b/public/components/overview/office-panel/config/main-view-config.ts @@ -1,8 +1,26 @@ import KibanaVis from '../../../../kibana-integrations/kibana-vis'; +import { MockupTables } from '../mockup-tables'; export const MainViewConfig = { rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: MockupTables, + enableToggleDrilldown: true, + props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } + }, + { + width: 50, + component: MockupTables, + enableToggleDrilldown: true, + props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } + }, + ] + }, { height: 300, columns: [ diff --git a/public/components/common/modules/panel/mockup-tables.tsx b/public/components/overview/office-panel/mockup-tables.tsx similarity index 53% rename from public/components/common/modules/panel/mockup-tables.tsx rename to public/components/overview/office-panel/mockup-tables.tsx index f07051f51e..1c2d3b5758 100644 --- a/public/components/common/modules/panel/mockup-tables.tsx +++ b/public/components/overview/office-panel/mockup-tables.tsx @@ -1,36 +1,9 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; -export const MockupTables = () => { - const tables = [ - { - title: 'Tenants', - minWidth: '400px', - columns: [ - { - field: 'tenant', - name: 'Tenant', - }, - { - field: 'count', - name: 'Count', - }, - ], - }, - { - title: 'Subscriptions', - minWidth: '400px', - columns: [ - { - field: 'subscription', - name: 'Subscription', - }, - { - field: 'count', - name: 'count', - }, - ], - }, +export const MockupTables = ({ toggleDrilldown, ...props }) => { + const random = new Random(); + const tables = [ { title: 'Events', minWidth: '750px', @@ -39,18 +12,18 @@ export const MockupTables = () => { { field: 'rule', name: 'Description', - render: (rule) => rule.description, + render: (rule) => (<span onClick={toggleDrilldown}>{rule.description}</span>), }, { field: 'rule', name: 'Level', - render: (rule) => rule.level, + render: (rule) => (<span onClick={toggleDrilldown}>{rule.level}</span>), }, ], }, ]; const generateItems = () => { - const random = new Random(); + const rules = [ { level: 1, @@ -85,16 +58,13 @@ export const MockupTables = () => { </EuiPanel> } const renderTables = (items, tables) => { - const rendered = tables.map((table, key) => { - return ( - <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }} key={key}> - <TitlePanel title={table.title}> - <EuiBasicTable items={items} columns={table.columns} /> - </TitlePanel> - </EuiFlexItem> - ); - }); - return rendered; + const table = tables[0]; + return <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }}> + <TitlePanel title={table.title}> + <EuiBasicTable items={items} columns={table.columns} /> + </TitlePanel> + </EuiFlexItem> + }; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 6a64437a4c..3fb7a5d3a6 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -15,14 +15,11 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../../components/common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { ModuleStats } from './module-stats'; -import { WzRequest } from '../../../react-services/wz-request'; import { queryConfig } from '../../../react-services/query-config'; -import { getCurrentConfig } from '../../../controllers/management/components/management/configuration/utils/wz-fetch'; import { DrilldownConfig, MainViewConfig } from './config/'; export const OfficePanel = withErrorBoundary(({ ...props }) => { - const extraFilters = []; const [moduleStatsList, setModuleStatsList] = useState([]); useEffect(() => { (async () => { diff --git a/public/react-services/query-config.js b/public/react-services/query-config.js index 3c82962f54..ae9a5944aa 100644 --- a/public/react-services/query-config.js +++ b/public/react-services/query-config.js @@ -16,72 +16,70 @@ import { AppState } from './app-state'; import { UI_LOGGER_LEVELS } from '../../common/constants'; export const queryConfig = async (agentId, sections, node = false) => { - try { - if ( - !agentId || - typeof agentId !== 'string' || - !sections || - !sections.length || - typeof sections !== 'object' || - !Array.isArray(sections) - ) { - throw new Error('Invalid parameters'); - } - - const result = {}; - sections.forEach((section) => { (async () => { - const { component, configuration } = section; - if ( - !component || - typeof component !== 'string' || - !configuration || - typeof configuration !== 'string' - ) { - throw new Error('Invalid section'); - } - try { - const url = node - ? `/cluster/${node}/configuration/${component}/${configuration}` - : !node - && agentId === '000' - ? `/manager/configuration/${component}/${configuration}` - : `/agents/${agentId}/configuration/${component}/${configuration}`; - - const partialResult = await WzRequest.apiReq('GET', url, {}); - result[`${component}-${configuration}`] = partialResult.data.data; - return result; - } catch (error) { - const options = { - context: `${AppState.name}.queryConfig`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: false, - error: { - error: error, - message: error.message || error, - title: `Fetch Configuration`, - }, - }; - getErrorOrchestrator().handleError(options); - } - })(); - }); - return result; + try { + if ( + !agentId || + typeof agentId !== 'string' || + !sections || + !sections.length || + typeof sections !== 'object' || + !Array.isArray(sections) + ) { + throw new Error('Invalid parameters'); + } + + const result = {}; + for (const section in sections) { + const { component, configuration } = section; + if ( + !component || + typeof component !== 'string' || + !configuration || + typeof configuration !== 'string' + ) { + throw new Error('Invalid section'); + } + try { + const url = node + ? `/cluster/${node}/configuration/${component}/${configuration}` + : !node + && agentId === '000' + ? `/manager/configuration/${component}/${configuration}` + : `/agents/${agentId}/configuration/${component}/${configuration}`; + + const partialResult = await WzRequest.apiReq('GET', url, {}); + result[`${component}-${configuration}`] = partialResult.data.data; } catch (error) { const options = { - context: `${AppState.name}.queryConfig`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: false, - error: { + context: `${AppState.name}.queryConfig`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: false, + error: { error: error, message: error.message || error, - title: `Error getting the query config`, - }, + title: `Fetch Configuration`, + }, }; - getErrorOrchestrator().handleError(options); + } } + return result; + } catch (error) { + const options = { + context: `${AppState.name}.queryConfig`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: false, + error: { + error: error, + message: error.message || error, + title: `Error getting the query config`, + }, + }; + + getErrorOrchestrator().handleError(options); + } } From f062103e401a19726769f98e9dd98fc2bcdd0949 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 19 Jul 2021 20:29:32 +0200 Subject: [PATCH 092/493] [Fix] Code overflows over the line numbers in API Console editor (#3439) --- CHANGELOG.md | 1 + public/utils/codemirror/codemirror.scss | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e008a668e8..9e90e1eac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) - Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) +- Fixed the code overflows over the line numbers in the API Console editor [#3439](https://github.com/wazuh/wazuh-kibana-app/pull/3439) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/public/utils/codemirror/codemirror.scss b/public/utils/codemirror/codemirror.scss index 06eaccc5cd..a31bf26801 100644 --- a/public/utils/codemirror/codemirror.scss +++ b/public/utils/codemirror/codemirror.scss @@ -389,4 +389,9 @@ span.CodeMirror-selectedtext { background: none; } .dev-tools-max-height { height: auto; -} \ No newline at end of file +} + +/* Fix the overflow code over the line numbers */ +.CodeMirror pre{ + z-index: 0; +} From 4c092da4e191a2cb38d7d394e3e80b88202538ea Mon Sep 17 00:00:00 2001 From: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Date: Mon, 19 Jul 2021 20:32:27 +0200 Subject: [PATCH 093/493] Fix error in save before exit prompt in configuration edit (#3460) * Changed the hasChanges state to false after the file is saved succesfully --- CHANGELOG.md | 1 + .../edit-configuration/edit-configuration.js | 164 +++++++++--------- 2 files changed, 81 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e90e1eac7..f692b6dc04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Fixed issue where configuration still asked you to save changes before exiting even after saving [#3460](https://github.com/wazuh/wazuh-kibana-app/pull/3460) - Added try catch strategy with ErrorOrchestrator service on Syscollector/Vuls/Stats sections [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) - Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) diff --git a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js index b08032180c..53f4a8de6b 100644 --- a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js +++ b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js @@ -35,7 +35,7 @@ import withLoading from '../util-hocs/loading'; import { updateWazuhNotReadyYet } from '../../../../../../redux/actions/appStateActions'; import { updateClusterNodes, - updateClusterNodeSelected + updateClusterNodeSelected, } from '../../../../../../redux/actions/configurationActions'; import { fetchFile, @@ -43,10 +43,10 @@ import { saveFileManager, saveFileCluster, clusterNodes, - clusterReq + clusterReq, } from '../utils/wz-fetch'; import { validateXML } from '../utils/xml'; -import { getToasts } from '../../../../../..//kibana-services'; +import { getToasts } from '../../../../../..//kibana-services'; import { connect } from 'react-redux'; import { compose } from 'redux'; @@ -69,35 +69,31 @@ class WzEditConfiguration extends Component { saving: false, hasChanges: false, infoChangesAfterRestart: false, - disableSaveRestartButtons: false + disableSaveRestartButtons: false, }; } - - addToast(toast){ + + addToast(toast) { getToasts().add(toast); } async editorSave() { try { this.setState({ saving: true }); this.props.clusterNodeSelected - ? await saveFileCluster( - this.state.editorValue, - this.props.clusterNodeSelected - ) + ? await saveFileCluster(this.state.editorValue, this.props.clusterNodeSelected) : await saveFileManager(this.state.editorValue); - this.setState({ saving: false, infoChangesAfterRestart: true }); + this.setState({ saving: false, infoChangesAfterRestart: true, hasChanges: false }); this.addToast({ title: ( <Fragment> <EuiIcon type="check" />   <span> - <b>{this.props.clusterNodeSelected || 'Manager'}</b> configuration - has been updated + <b>{this.props.clusterNodeSelected || 'Manager'}</b> configuration has been updated </span> </Fragment> ), - color: 'success' + color: 'success', }); } catch (error) { let errorMessage; @@ -147,7 +143,11 @@ class WzEditConfiguration extends Component { this.setState({ editorValue }); } onDidMount(xmlFetched, errorXMLFetched) { - this.setState({ editorValue: xmlFetched, disableSaveRestartButtons: errorXMLFetched,initialValue: xmlFetched}); + this.setState({ + editorValue: xmlFetched, + disableSaveRestartButtons: errorXMLFetched, + initialValue: xmlFetched, + }); } onLoadingConfiguration(disableSaveRestartButtons) { this.setState({ disableSaveRestartButtons }); @@ -162,10 +162,7 @@ class WzEditConfiguration extends Component { async confirmRestart() { try { this.setState({ restarting: true, saving: true, infoChangesAfterRestart: false }); - await restartNodeSelected( - this.props.clusterNodeSelected, - this.props.updateWazuhNotReadyYet - ); + await restartNodeSelected(this.props.clusterNodeSelected, this.props.updateWazuhNotReadyYet); this.props.updateWazuhNotReadyYet(''); this.setState({ restart: false, saving: false, restarting: false }); await this.checkIfClusterOrManager(); @@ -176,12 +173,12 @@ class WzEditConfiguration extends Component { <EuiIcon type="iInCircle" />   <span> - Nodes could take some time to restart, it may be necessary to - perform a refresh to see them all. + Nodes could take some time to restart, it may be necessary to perform a refresh to + see them all. </span> </Fragment> ), - color: 'success' + color: 'success', }); } } catch (error) { @@ -205,32 +202,26 @@ class WzEditConfiguration extends Component { try { // in case which enable/disable cluster configuration, update Redux Store const clusterStatus = await clusterReq(); - if(clusterStatus.data.data.enabled === 'yes' && clusterStatus.data.data.running === 'yes'){ + if (clusterStatus.data.data.enabled === 'yes' && clusterStatus.data.data.running === 'yes') { // try if it is a cluster const nodes = await clusterNodes(); // set cluster nodes in Redux Store this.props.updateClusterNodes(nodes.data.data.affected_items); // set cluster node selected in Redux Store const existsClusterCurrentNodeSelected = nodes.data.data.affected_items.find( - node => node.name === this.props.clusterNodeSelected + (node) => node.name === this.props.clusterNodeSelected ); this.props.updateClusterNodeSelected( existsClusterCurrentNodeSelected ? existsClusterCurrentNodeSelected.name - : nodes.data.data.affected_items.find(node => node.type === 'master').name + : nodes.data.data.affected_items.find((node) => node.type === 'master').name ); - this.props.updateConfigurationSection( - 'edit-configuration', - 'Cluster configuration' - ); - }else{ + this.props.updateConfigurationSection('edit-configuration', 'Cluster configuration'); + } else { // do nothing if it isn't a cluster this.props.updateClusterNodes(false); this.props.updateClusterNodeSelected(false); - this.props.updateConfigurationSection( - 'edit-configuration', - 'Manager configuration' - ); + this.props.updateConfigurationSection('edit-configuration', 'Manager configuration'); } } catch (error) { // do nothing if it isn't a cluster @@ -265,7 +256,14 @@ class WzEditConfiguration extends Component { </EuiButton> ) : ( <WzButtonPermissions - permissions={[this.props.clusterNodeSelected ? {action: 'cluster:update_config', resource: `node:id:${this.props.clusterNodeSelected}`} : {action: 'manager:update_config', resource: "*:*:*"}]} + permissions={[ + this.props.clusterNodeSelected + ? { + action: 'cluster:update_config', + resource: `node:id:${this.props.clusterNodeSelected}`, + } + : { action: 'manager:update_config', resource: '*:*:*' }, + ]} isDisabled={saving || disableSaveRestartButtons} iconType="save" onClick={() => this.editorSave()} @@ -276,23 +274,30 @@ class WzEditConfiguration extends Component { </EuiFlexItem> <EuiFlexItem grow={false}> <WzButtonPermissions - permissions={[this.props.clusterNodeSelected ? {action: 'cluster:restart', resource: `node:id:${this.props.clusterNodeSelected}`} : {action: 'manager:restart', resource: '*:*:*'}]} + permissions={[ + this.props.clusterNodeSelected + ? { + action: 'cluster:restart', + resource: `node:id:${this.props.clusterNodeSelected}`, + } + : { action: 'manager:restart', resource: '*:*:*' }, + ]} fill iconType="refresh" onClick={() => this.toggleRestart()} isDisabled={disableSaveRestartButtons || restarting} isLoading={restarting} > - {restarting ? 'Restarting' : 'Restart' } {clusterNodeSelected || 'Manager'} + {restarting ? 'Restarting' : 'Restart'} {clusterNodeSelected || 'Manager'} </WzButtonPermissions> </EuiFlexItem> </WzConfigurationPath> <WzEditorConfiguration - onChange={value => this.onChange(value)} + onChange={(value) => this.onChange(value)} onDidMount={(xmlFetched, errorXMLFetched) => this.onDidMount(xmlFetched, errorXMLFetched)} toggleRestart={() => this.toggleRestart()} confirmRestart={() => this.confirmRestart()} - onLoadingConfiguration={value => this.onLoadingConfiguration(value)} + onLoadingConfiguration={(value) => this.onLoadingConfiguration(value)} {...this.state} agent={agent} xmlError={xmlError} @@ -300,8 +305,7 @@ class WzEditConfiguration extends Component { {restart && !restarting && ( <EuiOverlayMask> <EuiConfirmModal - title={`${clusterNodeSelected || - 'Manager'} will be restarted`} + title={`${clusterNodeSelected || 'Manager'} will be restarted`} onCancel={() => this.toggleRestart()} onConfirm={() => this.confirmRestart()} cancelButtonText="Cancel" @@ -315,51 +319,53 @@ class WzEditConfiguration extends Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state) => ({ wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, clusterNodes: state.configurationReducers.clusterNodes, - clusterNodeSelected: state.configurationReducers.clusterNodeSelected + clusterNodeSelected: state.configurationReducers.clusterNodeSelected, }); -const mapDispatchToProps = dispatch => ({ - updateClusterNodes: clusterNodes => - dispatch(updateClusterNodes(clusterNodes)), - updateClusterNodeSelected: clusterNodeSelected => +const mapDispatchToProps = (dispatch) => ({ + updateClusterNodes: (clusterNodes) => dispatch(updateClusterNodes(clusterNodes)), + updateClusterNodeSelected: (clusterNodeSelected) => dispatch(updateClusterNodeSelected(clusterNodeSelected)), - updateWazuhNotReadyYet: value => dispatch(updateWazuhNotReadyYet(value)) + updateWazuhNotReadyYet: (value) => dispatch(updateWazuhNotReadyYet(value)), }); WzEditConfiguration.propTypes = { wazuhNotReadyYet: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - updateWazuhNotReadyYet: PropTypes.func + updateWazuhNotReadyYet: PropTypes.func, }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzEditConfiguration); +export default connect(mapStateToProps, mapDispatchToProps)(WzEditConfiguration); -const mapStateToPropsEditor = state => ({ +const mapStateToPropsEditor = (state) => ({ clusterNodeSelected: state.configurationReducers.clusterNodeSelected, clusterNodes: state.configurationReducers.clusterNodes, refreshTime: state.configurationReducers.refreshTime, - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); const WzEditorConfiguration = compose( connect(mapStateToPropsEditor), - withLoading(async props => { - try { - props.onLoadingConfiguration(true); - const xmlFetched = await fetchFile(props.clusterNodeSelected); - props.onLoadingConfiguration(false); - return { xmlFetched }; - } catch (error) { - props.onLoadingConfiguration(false); - return { xmlFetched: null, errorXMLFetched: error }; - } - }, - (props, prevProps) => (props.agent.id === '000' && props.clusterNodeSelected && prevProps.clusterNodeSelected && props.clusterNodeSelected !== prevProps.clusterNodeSelected) || (props.refreshTime !== prevProps.refreshTime) + withLoading( + async (props) => { + try { + props.onLoadingConfiguration(true); + const xmlFetched = await fetchFile(props.clusterNodeSelected); + props.onLoadingConfiguration(false); + return { xmlFetched }; + } catch (error) { + props.onLoadingConfiguration(false); + return { xmlFetched: null, errorXMLFetched: error }; + } + }, + (props, prevProps) => + (props.agent.id === '000' && + props.clusterNodeSelected && + prevProps.clusterNodeSelected && + props.clusterNodeSelected !== prevProps.clusterNodeSelected) || + props.refreshTime !== prevProps.refreshTime ) )( class WzEditorConfiguration extends Component { @@ -378,13 +384,11 @@ const WzEditorConfiguration = compose( infoChangesAfterRestart, editorValue, onChange, - wazuhNotReadyYet + wazuhNotReadyYet, } = this.props; const existsClusterCurrentNodeSelected = this.props.clusterNodes && - this.props.clusterNodes.find( - node => node.name === this.props.clusterNodeSelected - ); + this.props.clusterNodes.find((node) => node.name === this.props.clusterNodeSelected); return ( <Fragment> {!this.props.errorXMLFetched ? ( @@ -392,15 +396,10 @@ const WzEditorConfiguration = compose( <EuiText> Edit <span style={{ fontWeight: 'bold' }}>ossec.conf</span> of{' '} <span style={{ fontWeight: 'bold' }}> - {(existsClusterCurrentNodeSelected && clusterNodeSelected) || - 'Manager'} - {existsClusterCurrentNodeSelected && - clusterNodeSelected && - clusterNodes + {(existsClusterCurrentNodeSelected && clusterNodeSelected) || 'Manager'} + {existsClusterCurrentNodeSelected && clusterNodeSelected && clusterNodes ? ' (' + - clusterNodes.find( - node => node.name === clusterNodeSelected - ).type + + clusterNodes.find((node) => node.name === clusterNodeSelected).type + ')' : ''} </span> @@ -417,13 +416,10 @@ const WzEditorConfiguration = compose( <WzCodeEditor mode="xml" value={editorValue} - onChange={value => onChange(value)} - minusHeight={ - wazuhNotReadyYet || infoChangesAfterRestart ? 320 : 270 - } + onChange={(value) => onChange(value)} + minusHeight={wazuhNotReadyYet || infoChangesAfterRestart ? 320 : 270} /> )} - </Fragment> ) : ( <WzWazuhAPINotReachable error={this.props.errorXMLFetched} /> From 46b91073aa08edd3e052925caba1ca3b70fc3360 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Mon, 19 Jul 2021 20:38:10 +0200 Subject: [PATCH 094/493] feat(orchestrator): Applied try-catch strategy on controllers & factories & directives. (#3474) --- public/controllers/dev-tools/dev-tools.ts | 104 ++++++++++++++++- .../misc/blank-screen-controller.js | 18 ++- public/controllers/tools/index.ts | 1 + public/controllers/tools/tools.ts | 22 +++- .../wz-logtest/components/logtest.tsx | 106 ++++++++++-------- public/factories/vis2png.js | 29 ++++- 6 files changed, 231 insertions(+), 49 deletions(-) diff --git a/public/controllers/dev-tools/dev-tools.ts b/public/controllers/dev-tools/dev-tools.ts index c08908a41d..0a37edeeab 100644 --- a/public/controllers/dev-tools/dev-tools.ts +++ b/public/controllers/dev-tools/dev-tools.ts @@ -22,6 +22,14 @@ import store from '../../redux/store'; import { WzRequest } from '../../react-services/wz-request'; import { ErrorHandler } from '../../react-services/error-handler'; import { getUiSettings } from '../../kibana-services'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services'; export class DevToolsController { /** @@ -231,6 +239,17 @@ export class DevToolsController { starts = []; return tmpgroups; } catch (error) { + const options: UIErrorLog = { + context: `${DevToolsController.name}.analyzeGroups`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); return []; } } @@ -315,6 +334,18 @@ export class DevToolsController { noHScroll: true }) }); + + const options: UIErrorLog = { + context: `${DevToolsController.name}.checkJsonParseError`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error parsing query`, + }, + }; + getErrorOrchestrator().handleError(options); } } } @@ -330,6 +361,18 @@ export class DevToolsController { this.apiInputBox.model = !response.error ? response.data : []; } catch (error) { this.apiInputBox.model = []; + + const options: UIErrorLog = { + context: `${DevToolsController.name}.getAvailableMethods`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -519,7 +562,18 @@ export class DevToolsController { inputBodyPreviousKeys = Object.keys((requestBodyCursorKeys || []).reduce((acumm, key) => acumm[key], JSON.parse(bodySanitizedBodyParam))); } catch (error) { inputBodyPreviousKeys = []; - }; + const options: UIErrorLog = { + context: `${DevToolsController.name}.getDictionary`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } hints = paramsBody .filter(bodyParam => !inputBodyPreviousKeys.includes(bodyParam.name) && bodyParam.name && (inputKeyBodyParam ? bodyParam.name.includes(inputKeyBodyParam) : true)) @@ -657,6 +711,18 @@ export class DevToolsController { } catch (error) { $('#play_button').hide(); $('#wazuh_dev_tools_documentation').hide(); + const options: UIErrorLog = { + context: `${DevToolsController.name}.calculateWhichGroup`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + return null; } } @@ -731,6 +797,17 @@ export class DevToolsController { JSONraw = JSON.parse(paramsInline || desiredGroup.requestTextJson); } catch (error) { JSONraw = {}; + const options: UIErrorLog = { + context: `${DevToolsController.name}.send`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } if (typeof extra.pretty !== 'undefined') delete extra.pretty; @@ -759,6 +836,19 @@ export class DevToolsController { (firstTime || !desiredGroup) && this.apiOutputBox.setValue('Welcome!'); } catch (error) { + //TODO: for the moment we will only add the new orchestrator to leave a message of this error in UI, but we have to deprecate the old ErrorHandler :) + const options: UIErrorLog = { + context: `${DevToolsController.name}.send`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + if ((error || {}).status === -1) { return this.apiOutputBox.setValue( 'Wazuh API is not reachable. Reason: timeout.' @@ -784,7 +874,17 @@ export class DevToolsController { }); FileSaver.saveAs(blob, 'export.json'); } catch (error) { - ErrorHandler.handle(error, 'Export JSON'); + const options: UIErrorLog = { + context: `${DevToolsController.name}.exportOutput`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Export JSON`, + }, + }; + getErrorOrchestrator().handleError(options); } } } diff --git a/public/controllers/misc/blank-screen-controller.js b/public/controllers/misc/blank-screen-controller.js index 50c9ab281e..8155508678 100644 --- a/public/controllers/misc/blank-screen-controller.js +++ b/public/controllers/misc/blank-screen-controller.js @@ -13,6 +13,9 @@ import { AppState } from '../../react-services/app-state'; import { ErrorHandler } from '../../react-services/error-handler'; import { WzMisc } from '../../factories/misc'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { getErrorOrchestrator } from '../../react-services/common-services'; export class BlankScreenController { /** @@ -39,7 +42,20 @@ export class BlankScreenController { let parsed = null; try { parsed = ErrorHandler.handle(catchedError, '', { silent: true }); - } catch (error) {} // eslint-disable-line + } catch (error) { + const options = { + context: `${BlankScreenController.name}.$onInit`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } + this.errorToShow = parsed || catchedError; this.$scope.$applyAsync(); this.wzMisc.setBlankScr(false); diff --git a/public/controllers/tools/index.ts b/public/controllers/tools/index.ts index 87484a89ef..5db036e595 100644 --- a/public/controllers/tools/index.ts +++ b/public/controllers/tools/index.ts @@ -16,6 +16,7 @@ import { DevToolsController } from '../dev-tools/dev-tools'; const app = getAngularModule(); +Logtest.displayName = 'Logtest'; app .controller('devToolsController', DevToolsController) .controller('toolsController', ToolsController) diff --git a/public/controllers/tools/tools.ts b/public/controllers/tools/tools.ts index 015419253e..738f234704 100644 --- a/public/controllers/tools/tools.ts +++ b/public/controllers/tools/tools.ts @@ -13,6 +13,14 @@ import { TabNames } from '../../utils/tab-names'; import store from '../../redux/store'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; import { updateSelectedToolsSection } from '../../redux/actions/appStateActions'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { getErrorOrchestrator } from '../../react-services/common-services'; export class ToolsController { /** @@ -53,7 +61,19 @@ export class ToolsController { { text: this.tab === 'devTools' ? 'API Console' : 'Ruleset Test' }, ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); - } catch (error) {} + } catch (error) { + const options: UIErrorLog = { + context: `${ToolsController.name}.$onInit`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index 9ebc5b26d5..ee3ee9b95b 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -30,9 +30,17 @@ import { import { WzRequest } from '../../../react-services'; import { withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; import { compose } from 'redux'; -import { useSelector, useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { updateLogtestToken } from '../../../redux/actions/appStateActions'; import { WzButtonPermissionsModalConfirm } from '../../../components/common/buttons'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; type LogstestProps = { openCloseFlyout: () => {}; @@ -50,7 +58,7 @@ export const Logtest = compose( const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(''); const dispatch = useDispatch(); - const sessionToken = useSelector((state)=> state.appStateReducers.logtestToken); + const sessionToken = useSelector((state) => state.appStateReducers.logtestToken); const onChange = (e) => { setEvents(e.target.value.split('\n').filter((item) => item)); @@ -101,14 +109,14 @@ export const Logtest = compose( log_format: 'syslog', location: 'logtest', event, - ...(token ? { token }: {}) + ...(token ? { token } : {}), }); token = response.data.data.token; !sessionToken && !gotToken && token && dispatch(updateLogtestToken(token)); token && (gotToken = true); responses.push(response); - }; - + } + const testResults = responses.map((response) => response.data.data.output.rule || '' ? formatResult(response.data.data.output, response.data.data.alert) @@ -126,16 +134,25 @@ export const Logtest = compose( } }; - const deleteToken = async() =>{ + const deleteToken = async () => { try { - const response = await WzRequest.apiReq('DELETE', `/logtest/sessions/${sessionToken}`, {}); + await WzRequest.apiReq('DELETE', `/logtest/sessions/${sessionToken}`, {}); dispatch(updateLogtestToken('')); setTestResult(''); + } catch (error) { + const options: UIErrorLog = { + context: `${Logtest.name}.deleteToken`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: `Error trying to delete logtest token due to: ${error.message || error}`, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); } - catch(error) { - this.showToast('danger', 'Error', `Error trying to delete logtest token due to: ${error.message || error}`); - } - } + }; const buildLogtest = () => { return ( @@ -150,39 +167,40 @@ export const Logtest = compose( /> <EuiSpacer size="m" /> <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButton - style={{ maxWidth: '100px' }} - isLoading={testing} - isDisabled={testing || events.length === 0} - iconType="play" - fill - onClick={runAllTests} - > - Test - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <WzButtonPermissionsModalConfirm - style={{ maxWidth: '150px' }} - tooltip={{position: 'top', content: 'Clear current session'}} - fill - isDisabled={sessionToken === '' ? true : false} - aria-label="Clear current session" - iconType="broom" - onConfirm={async () => { - deleteToken(); - }} - color="danger" - modalTitle={`Do you want to clear current session?`} - modalProps={{ - buttonColor: 'danger', - children: 'Clearing the session means the logs execution history is removed. This affects to rules that fire an alert when similar logs are executed in a specific range of time.' - }} - > - Clear session - </WzButtonPermissionsModalConfirm> - </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + style={{ maxWidth: '100px' }} + isLoading={testing} + isDisabled={testing || events.length === 0} + iconType="play" + fill + onClick={runAllTests} + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <WzButtonPermissionsModalConfirm + style={{ maxWidth: '150px' }} + tooltip={{ position: 'top', content: 'Clear current session' }} + fill + isDisabled={sessionToken === '' ? true : false} + aria-label="Clear current session" + iconType="broom" + onConfirm={async () => { + deleteToken(); + }} + color="danger" + modalTitle={`Do you want to clear current session?`} + modalProps={{ + buttonColor: 'danger', + children: + 'Clearing the session means the logs execution history is removed. This affects to rules that fire an alert when similar logs are executed in a specific range of time.', + }} + > + Clear session + </WzButtonPermissionsModalConfirm> + </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="m" /> <EuiCodeBlock diff --git a/public/factories/vis2png.js b/public/factories/vis2png.js index bb1b735db4..d632082fd1 100644 --- a/public/factories/vis2png.js +++ b/public/factories/vis2png.js @@ -12,6 +12,10 @@ import domtoimage from '../utils/dom-to-image'; import { getAngularModule } from '../kibana-services'; +import { UI_ERROR_SEVERITIES } from '../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { getErrorOrchestrator } from '../react-services/common-services'; + const app = getAngularModule(); export class Vis2PNG { @@ -47,7 +51,19 @@ export class Vis2PNG { height: tmpNode.height(), id: currentValue }); - } catch (error) {} // eslint-disable-line + } catch (error) { + const options = { + context: `${Vis2PNG.name}.tmpResult`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } currentCompleted++; this.$rootScope.reportStatus = `Generating report...${Math.round( (currentCompleted / len) * 100 @@ -60,6 +76,17 @@ export class Vis2PNG { this.$rootScope.reportStatus = `Generating PDF document...`; return this.rawArray; } catch (error) { + const options = { + context: `${Vis2PNG.name}.checkArray`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); this.working = false; return Promise.reject(error); } From a090aefe736e3ed49a715b0560067f8db0c0ebcc Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 19 Jul 2021 15:49:10 -0300 Subject: [PATCH 095/493] bugfix(techniques): Added validation to check if mitre object is mitre source. --- .../mitre/components/techniques/techniques.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 7f05985fb6..406b88b3b1 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -42,6 +42,8 @@ import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +const MITRE_ATTACK = 'mitre-attack' + export const Techniques = withWindowSize( class Techniques extends Component { _isMount = false; @@ -252,9 +254,10 @@ export const Techniques = withWindowSize( const mitreObj = this.state.mitreTechniques.find((item) => item.id === element); if (mitreObj) { const mitreTechniqueName = mitreObj.name; - const mitreTechniqueID = mitreObj.references.find( - (item) => item.source === 'mitre-attack' - ).external_id; + const mitreTechniqueID = + mitreObj.source === MITRE_ATTACK + ? mitreObj.external_id + : mitreObj.references.find((item) => item.source === MITRE_ATTACK).external_id; mitreTechniqueID ? techniquesObj.push({ id: mitreTechniqueID, name: mitreTechniqueName }) : ''; @@ -462,7 +465,7 @@ export const Techniques = withWindowSize( }); const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( (item) => - item.references.filter((reference) => reference.source === 'mitre-attack')[0] + item.references.filter((reference) => reference.source === MITRE_ATTACK)[0] .external_id ); this._isMount && this.setState({ filteredTechniques, isSearching: false }); From 7aeb43de4509fd305eb10c80d0590cd131fb2192 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 20 Jul 2021 09:29:48 +0200 Subject: [PATCH 096/493] changed drilldown layout --- public/components/common/modules/panel/module-drilldown.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/components/common/modules/panel/module-drilldown.tsx b/public/components/common/modules/panel/module-drilldown.tsx index 05cea5732c..8b1ff3b0ba 100644 --- a/public/components/common/modules/panel/module-drilldown.tsx +++ b/public/components/common/modules/panel/module-drilldown.tsx @@ -3,11 +3,12 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; -export const ModuleDrilldown = ({ toggleDrilldown, rows = [], ...props }) => { +export const ModuleDrilldown = ({ toggleDrilldown, rows = [], children, ...props }) => { return <> <WzReduxProvider> - <EuiFlexGroup><EuiFlexItem><EuiButtonEmpty onClick={toggleDrilldown} iconType={"sortLeft"}></EuiButtonEmpty></EuiFlexItem></EuiFlexGroup> + <EuiFlexGroup><EuiFlexItem grow={false}><div><EuiButtonEmpty onClick={toggleDrilldown} iconType={"sortLeft"}></EuiButtonEmpty></div></EuiFlexItem></EuiFlexGroup> + {children} </WzReduxProvider> </> } \ No newline at end of file From defc79139e663cc8f34bd3b2329e720207060088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 20 Jul 2021 09:47:20 +0200 Subject: [PATCH 097/493] Finished useEsSearch hook --- public/components/common/hooks/use-query.ts | 42 +++++++++------ public/components/common/hooks/useEsSearch.js | 54 ++++++++++++++----- 2 files changed, 66 insertions(+), 30 deletions(-) diff --git a/public/components/common/hooks/use-query.ts b/public/components/common/hooks/use-query.ts index 2a6e4d9984..4c760af02c 100644 --- a/public/components/common/hooks/use-query.ts +++ b/public/components/common/hooks/use-query.ts @@ -12,31 +12,39 @@ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect } from 'react'; import { ModulesHelper } from '../modules/modules-helper'; +import _ from 'lodash'; -export function useQuery(): [{ - language: 'kuery' | 'lucene'; - query: string; -}, ((query: any) => void)] { +export function useQuery(): [ + { + language: 'kuery' | 'lucene'; + query: string; + }, + (query: any) => void +] { const [query, setQuery] = useState({ language: 'kuery', query: '' }); useEffect(() => { let subscription; - ModulesHelper.getDiscoverScope() - .then(scope => { - setQuery(scope.state.query); - subscription = scope.$watchCollection('fetchStatus', - () => {setQuery(scope.state.query)}); + ModulesHelper.getDiscoverScope().then((scope) => { + setQuery(scope.state.query); + subscription = scope.$watchCollection('fetchStatus', () => { + if (!_.isEqual(query, scope.state.query)) { + console.log("QUERY CHANGED") + setQuery(scope.state.query); + } }); - return () => { subscription && subscription(); } + }); + return () => { + subscription && subscription(); + }; }, []); const updateQuery = (query) => { - ModulesHelper.getDiscoverScope() - .then(scope => { - scope.state.query = query; - }) - } - return [ query, updateQuery ]; + ModulesHelper.getDiscoverScope().then((scope) => { + scope.state.query = query; + }); + }; + return [query, updateQuery]; } export const useQueryManager = () => { return useState(getDataPlugin().query.queryString.getQuery()); -} +}; diff --git a/public/components/common/hooks/useEsSearch.js b/public/components/common/hooks/useEsSearch.js index 88b08d3c47..5a7dde392a 100644 --- a/public/components/common/hooks/useEsSearch.js +++ b/public/components/common/hooks/useEsSearch.js @@ -3,30 +3,47 @@ import { getDataPlugin } from '../../../kibana-services'; import { useQuery, useIndexPattern, useFilterManager } from '../hooks'; import _ from 'lodash'; -const useEsSearch = (preAppliedQuery = {}, preAppliedAggs = {}, size = 10) => { +/* +You can find more info on how to use the preAppliedAggs object at https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html +You can find more info on how to construct a filter object at https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html +*/ +const useEsSearch = ({ preAppliedFilters = {}, preAppliedAggs = {}, size = 10 }) => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); const filterManager = useFilterManager(); - const [esResults, setEsResults] = useState({}) - useEffect(()=>{ - search() - .then((result)=>{ - setEsResults(result) - }) - },[indexPattern]) + const [esResults, setEsResults] = useState({}); + const [managedFilters, setManagedFilters] = useState([]); + const [page, setPage] = useState(0); + const [query] = useQuery(); + useEffect(() => { + search().then((result) => { + setEsResults(result); + }); + }, [indexPattern, query, managedFilters, page]); + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const newFilters = filterManager.getFilters(); + if (!_.isEqual(managedFilters, newFilters)) { + setManagedFilters(newFilters); + } + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }); const search = async () => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern); const searchSource = await data.search.searchSource.create(); - const combined = _.merge({ ...preAppliedQuery }, { ...esQuery } || {}); - + const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; const results = await searchSource .setParent(undefined) - .setField('filter', combined.bool.filter) - .setField('query', combined) + .setField('filter', combined) + .setField('query', esQuery) .setField('aggs', preAppliedAggs) .setField('size', size) + .setField('from', page * size) .setField('index', indexPattern) .fetch(); return results; @@ -34,8 +51,19 @@ const useEsSearch = (preAppliedQuery = {}, preAppliedAggs = {}, size = 10) => { return {}; } }; + + const nextPage = () => { + setPage(page + 1); + }; + const prevPage = () => { + setPage(page - 1 < 0 ? 0 : page - 1); + }; + return { - esResults + esResults, + setPage, + nextPage, + prevPage, }; }; From ae5cc187de053e1fdf5830e9c2db154a2c3b245e Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 20 Jul 2021 10:13:19 -0300 Subject: [PATCH 098/493] doc(changelog): Fixed changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f692b6dc04..f79654154a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,9 +35,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) - Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) -- Refactored all try catch strategy on Controller/Agent section [#3398](https://github.com/wazuh/wazuh-kibana-app/issues/3398) -- Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/issues/3432) -- Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/issues/3415) +- Refactored all try catch strategy on Controller/Agent section [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) +- Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) +- Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) ### Fixed From d367ec46d657a19d6263d633756a071542fa1deb Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Tue, 20 Jul 2021 13:57:37 -0300 Subject: [PATCH 099/493] Added try-catch stratategy in common/tables (#3480) * Implemented error-handling * Updated CHANGELOG * Added test files and snapshots * doc(changelog): Update * doc(changelog): Update Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 44 +- .../__snapshots__/table-default.test.tsx.snap | 603 +++++++++++++ .../table-with-search-bar.test.tsx.snap | 829 ++++++++++++++++++ .../__snapshots__/table-wz-api.test.tsx.snap | 660 ++++++++++++++ .../export-table-csv.test.tsx.snap | 77 ++ .../components/export-table-csv.test.tsx | 39 + .../tables/components/export-table-csv.tsx | 15 +- .../common/tables/table-default.test.tsx | 81 ++ .../common/tables/table-default.tsx | 16 +- .../tables/table-with-search-bar.test.tsx | 83 ++ .../common/tables/table-with-search-bar.tsx | 92 +- .../common/tables/table-wz-api.test.tsx | 79 ++ .../components/common/tables/table-wz-api.tsx | 19 +- 13 files changed, 2571 insertions(+), 66 deletions(-) create mode 100644 public/components/common/tables/__snapshots__/table-default.test.tsx.snap create mode 100644 public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap create mode 100644 public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap create mode 100644 public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap create mode 100644 public/components/common/tables/components/export-table-csv.test.tsx create mode 100644 public/components/common/tables/table-default.test.tsx create mode 100644 public/components/common/tables/table-with-search-bar.test.tsx create mode 100644 public/components/common/tables/table-wz-api.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f6d5bb19..71355eb7e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,37 +7,37 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) -- Added new error handler to be responsible for the error orchestration [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) -- Added `Error Boundary` HOC and Component to handle render errors. [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) -- Added implementation of `Error Boundary` HOC in each main react-component. [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) +- Improved the frontend handle errors strategy: UI, Toasts, console log and log in file + [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) + [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) + [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) + [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) + [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) + [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) + [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) + [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) + [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) + [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) + [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) + [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) + [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) + [#3480](https://github.com/wazuh/wazuh-kibana-app/pull/3480) + [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) + [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) + [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) + [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) + [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) + [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) + - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) -- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) -- Added try catch strategy with ErrorOrchestrator service on User section [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) -- Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) -- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) -- Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) -- Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) -- Added try catch strategy with ErrorOrchestrator service on Management > Statistics [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) -- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) -- Added try catch strategy with ErrorOrchestrator service on Management > Reporting [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) -- Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) - Fixed issue where configuration still asked you to save changes before exiting even after saving [#3460](https://github.com/wazuh/wazuh-kibana-app/pull/3460) -- Added try catch strategy with ErrorOrchestrator service on Syscollector/Vuls/Stats sections [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) -- Added try-catch strategy in Configuration section [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) -- Added try catch strategy with ErrorOrchestrator service on Components > Overview [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) -- Added try catch strategy with ErrorOrchestrator service on Components > Add Modules Data [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) -- Added try catch strategy with ErrorOrchestrator service on Management > Status [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) -- Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) -- Refactored all try catch strategy on Controller/Agent section [#3398](https://github.com/wazuh/wazuh-kibana-app/issues/3398) -- Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/issues/3432) -- Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/issues/3415) ### Fixed diff --git a/public/components/common/tables/__snapshots__/table-default.test.tsx.snap b/public/components/common/tables/__snapshots__/table-default.test.tsx.snap new file mode 100644 index 0000000000..e023d4b871 --- /dev/null +++ b/public/components/common/tables/__snapshots__/table-default.test.tsx.snap @@ -0,0 +1,603 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Default component renders correctly to match the snapshot 1`] = ` +<TableDefault + onSearch={[Function]} + reload={[Function]} + tableColumns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + tableInitialSortingDirection="asc" + tableInitialSortingField="" + tablePageSizeOptions={ + Array [ + 15, + 25, + 50, + 100, + ] + } + tableProps={Object {}} +> + <EuiBasicTable + columns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + items={Array []} + loading={true} + noItemsMessage="No items found" + onChange={[Function]} + pagination={ + Object { + "pageIndex": 0, + "pageSize": 15, + "pageSizeOptions": Array [ + 15, + 25, + 50, + 100, + ], + "totalItemCount": 0, + } + } + responsive={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "", + }, + } + } + tableLayout="fixed" + > + <div + className="euiBasicTable euiBasicTable-loading" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiTableSortMobile + items={ + Array [ + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_version_0", + "name": "Version", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_architecture_1", + "name": "Architecture", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_cve_2", + "name": "CVE", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_name_3", + "name": "Name", + "onSort": [Function], + }, + ] + } + > + <div + className="euiTableSortMobile" + > + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + /> + </EuiButtonEmpty> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownRight" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <button + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="right" + iconType="arrowDown" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="arrowDown" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + > + Sorting + </EuiI18n> + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </div> + </EuiTableSortMobile> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="htmlId" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="htmlId" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_version_0" + isSorted={false} + key="_data_h_version_0" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_version_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Version", + } + } + > + <span + className="euiTableCellContent__text" + title="Version" + > + Version + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_architecture_1" + isSorted={false} + key="_data_h_architecture_1" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_architecture_1" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Architecture", + } + } + > + <span + className="euiTableCellContent__text" + title="Architecture" + > + Architecture + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_cve_2" + isSorted={false} + key="_data_h_cve_2" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_cve_2" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "CVE", + } + } + > + <span + className="euiTableCellContent__text" + title="CVE" + > + CVE + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_name_3" + isSorted={false} + key="_data_h_name_3" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_name_3" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Name", + } + } + > + <span + className="euiTableCellContent__text" + title="Name" + > + Name + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody> + <tbody> + <EuiTableRow> + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="center" + colSpan={4} + isMobileFullWidth={true} + > + <td + className="euiTableRowCell euiTableRowCell--isMobileFullWidth" + colSpan={4} + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + className="euiTableCellContent__text" + > + No items found + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> +</TableDefault> +`; diff --git a/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap new file mode 100644 index 0000000000..9be43733c8 --- /dev/null +++ b/public/components/common/tables/__snapshots__/table-with-search-bar.test.tsx.snap @@ -0,0 +1,829 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table With Search Bar component renders correctly to match the snapshot 1`] = ` +<TableWithSearchBar + onSearch={[Function]} + reload={[Function]} + rowProps={[Function]} + searchBarSuggestions={Array []} + tableColumns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + tableInitialSortingDirection="asc" + tableInitialSortingField="" + tablePageSizeOptions={ + Array [ + 15, + 25, + 50, + 100, + ] + } + tableProps={Object {}} +> + <WzSearchBar + filters={Array []} + noDeleteFiltersOnUpdateSuggests={true} + onFiltersChange={[Function]} + placeholder="Filter or search" + suggestions={Array []} + > + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiSuggest + inputRef={[Function]} + isInvalid={false} + isPopoverOpen={false} + onClosePopover={[Function]} + onInputChange={[Function]} + onItemClick={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Filter or search" + prepend={ + <WzSearchBadges + filters={Array []} + onFiltersChange={[Function]} + /> + } + status="unchanged" + suggestions={Array []} + value="" + > + <div + onChange={[Function]} + > + <EuiSuggestInput + inputRef={[Function]} + isInvalid={false} + isPopoverOpen={false} + onClosePopover={[Function]} + onKeyPress={[Function]} + onPopoverFocus={[Function]} + placeholder="Filter or search" + prepend={ + <WzSearchBadges + filters={Array []} + onFiltersChange={[Function]} + /> + } + sendValue={[Function]} + status="unchanged" + suggestions={Array []} + value="" + > + <div + className="euiSuggestInput" + > + <EuiInputPopover + anchorPosition="downLeft" + attachToAnchor={true} + closePopover={[Function]} + display="block" + fullWidth={true} + id="popover" + input={ + <EuiFieldText + append={Array []} + fullWidth={true} + inputRef={[Function]} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Filter or search" + prepend={ + <WzSearchBadges + filters={Array []} + onFiltersChange={[Function]} + /> + } + value="" + /> + } + isOpen={false} + panelPaddingSize="none" + > + <EuiPopover + anchorPosition="downLeft" + attachToAnchor={true} + button={ + <EuiResizeObserver + onResize={[Function]} + > + [Function] + </EuiResizeObserver> + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelRef={[Function]} + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownLeft euiPopover--displayBlock euiInputPopover euiInputPopover--fullWidth" + id="popover" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiResizeObserver + onResize={[Function]} + > + <div> + <EuiFieldText + append={Array []} + fullWidth={true} + inputRef={[Function]} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Filter or search" + prepend={ + <WzSearchBadges + filters={Array []} + onFiltersChange={[Function]} + /> + } + value="" + > + <EuiFormControlLayout + append={Array []} + fullWidth={true} + isLoading={false} + prepend={ + <WzSearchBadges + filters={Array []} + onFiltersChange={[Function]} + /> + } + > + <div + className="euiFormControlLayout euiFormControlLayout--fullWidth euiFormControlLayout--group" + > + <WzSearchBadges + className="euiFormControlLayout__prepend" + filters={Array []} + key="0/.0" + onFiltersChange={[Function]} + > + <div + style={ + Object { + "display": "flex", + } + } + /> + </WzSearchBadges> + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl + isInvalid={false} + > + <input + className="euiFieldText euiFieldText--fullWidth euiFieldText--inGroup" + onChange={[Function]} + onFocus={[Function]} + onKeyPress={[Function]} + placeholder="Filter or search" + type="text" + value="" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons + isLoading={false} + /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </EuiResizeObserver> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </EuiInputPopover> + </div> + </EuiSuggestInput> + </div> + </EuiSuggest> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </WzSearchBar> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiBasicTable + columns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + items={Array []} + loading={true} + noItemsMessage="No items found" + onChange={[Function]} + pagination={ + Object { + "pageIndex": 0, + "pageSize": 15, + "pageSizeOptions": Array [ + 15, + 25, + 50, + 100, + ], + "totalItemCount": 0, + } + } + responsive={true} + rowProps={[Function]} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "", + }, + } + } + tableLayout="fixed" + > + <div + className="euiBasicTable euiBasicTable-loading" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiTableSortMobile + items={ + Array [ + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_version_0", + "name": "Version", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_architecture_1", + "name": "Architecture", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_cve_2", + "name": "CVE", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_name_3", + "name": "Name", + "onSort": [Function], + }, + ] + } + > + <div + className="euiTableSortMobile" + > + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + /> + </EuiButtonEmpty> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownRight" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <button + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="right" + iconType="arrowDown" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="arrowDown" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + > + Sorting + </EuiI18n> + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </div> + </EuiTableSortMobile> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="htmlId" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="htmlId" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_version_0" + isSorted={false} + key="_data_h_version_0" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_version_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Version", + } + } + > + <span + className="euiTableCellContent__text" + title="Version" + > + Version + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_architecture_1" + isSorted={false} + key="_data_h_architecture_1" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_architecture_1" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Architecture", + } + } + > + <span + className="euiTableCellContent__text" + title="Architecture" + > + Architecture + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_cve_2" + isSorted={false} + key="_data_h_cve_2" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_cve_2" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "CVE", + } + } + > + <span + className="euiTableCellContent__text" + title="CVE" + > + CVE + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_name_3" + isSorted={false} + key="_data_h_name_3" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_name_3" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Name", + } + } + > + <span + className="euiTableCellContent__text" + title="Name" + > + Name + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody> + <tbody> + <EuiTableRow> + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="center" + colSpan={4} + isMobileFullWidth={true} + > + <td + className="euiTableRowCell euiTableRowCell--isMobileFullWidth" + colSpan={4} + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + className="euiTableCellContent__text" + > + No items found + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> +</TableWithSearchBar> +`; diff --git a/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap new file mode 100644 index 0000000000..ae152d0c0b --- /dev/null +++ b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap @@ -0,0 +1,660 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table WZ API component renders correctly to match the snapshot 1`] = ` +<TableWzAPI + downloadCsv={false} + endpoint="/" + error={false} + searchBar={false} + searchTable={false} + tableColumns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + title="Table" +> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h1 + className="euiTitle euiTitle--small" + > + Table + + <EuiLoadingSpinner + size="s" + > + <span + className="euiLoadingSpinner euiLoadingSpinner--small" + /> + </EuiLoadingSpinner> + </h1> + </EuiTitle> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <TableDefault + downloadCsv={false} + error={false} + onSearch={[Function]} + searchBar={false} + searchTable={false} + tableColumns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + title="Table" + > + <EuiBasicTable + columns={ + Array [ + Object { + "field": "version", + "name": "Version", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "architecture", + "name": "Architecture", + "sortable": true, + }, + Object { + "field": "cve", + "name": "CVE", + "sortable": true, + "truncateText": true, + }, + Object { + "field": "name", + "name": "Name", + "sortable": true, + }, + ] + } + items={Array []} + loading={true} + noItemsMessage="No items found" + onChange={[Function]} + pagination={ + Object { + "pageIndex": 0, + "pageSize": 15, + "pageSizeOptions": Array [ + 15, + 25, + 50, + 100, + ], + "totalItemCount": 0, + } + } + responsive={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "", + }, + } + } + tableLayout="fixed" + > + <div + className="euiBasicTable euiBasicTable-loading" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiTableSortMobile + items={ + Array [ + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_version_0", + "name": "Version", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_architecture_1", + "name": "Architecture", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_cve_2", + "name": "CVE", + "onSort": [Function], + }, + Object { + "isSortAscending": undefined, + "isSorted": false, + "key": "_data_s_name_3", + "name": "Name", + "onSort": [Function], + }, + ] + } + > + <div + className="euiTableSortMobile" + > + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + /> + </EuiButtonEmpty> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownRight" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + flush="right" + iconSide="right" + iconType="arrowDown" + onClick={[Function]} + size="xs" + > + <button + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall euiButtonEmpty--flushRight" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="right" + iconType="arrowDown" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonContent--iconRight euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="arrowDown" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + <EuiI18n + default="Sorting" + token="euiTableSortMobile.sorting" + > + Sorting + </EuiI18n> + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </div> + </EuiTableSortMobile> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="htmlId" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="htmlId" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_version_0" + isSorted={false} + key="_data_h_version_0" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_version_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Version", + } + } + > + <span + className="euiTableCellContent__text" + title="Version" + > + Version + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_architecture_1" + isSorted={false} + key="_data_h_architecture_1" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_architecture_1" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Architecture", + } + } + > + <span + className="euiTableCellContent__text" + title="Architecture" + > + Architecture + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_cve_2" + isSorted={false} + key="_data_h_cve_2" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_cve_2" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "CVE", + } + } + > + <span + className="euiTableCellContent__text" + title="CVE" + > + CVE + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_name_3" + isSorted={false} + key="_data_h_name_3" + onSort={[Function]} + > + <th + aria-live="polite" + aria-sort="none" + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_name_3" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <button + className="euiTableHeaderButton" + data-test-subj="tableHeaderSortButton" + onClick={[Function]} + type="button" + > + <span + className="euiTableCellContent" + > + <EuiInnerText> + <EuiI18n + default="{innerText}; Sorted in {ariaSortValue} order" + token="euiTableHeaderCell.titleTextWithSort" + values={ + Object { + "ariaSortValue": "none", + "innerText": "Name", + } + } + > + <span + className="euiTableCellContent__text" + title="Name" + > + Name + </span> + </EuiI18n> + </EuiInnerText> + <EuiScreenReaderOnly> + <span + className="euiScreenReaderOnly" + > + <EuiI18n + default="Click to sort in ascending order" + token="euiTableHeaderCell.clickForAscending" + > + Click to sort in ascending order + </EuiI18n> + </span> + </EuiScreenReaderOnly> + </span> + </button> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody> + <tbody> + <EuiTableRow> + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="center" + colSpan={4} + isMobileFullWidth={true} + > + <td + className="euiTableRowCell euiTableRowCell--isMobileFullWidth" + colSpan={4} + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + className="euiTableCellContent__text" + > + No items found + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> + </TableDefault> +</TableWzAPI> +`; diff --git a/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap b/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap new file mode 100644 index 0000000000..5e82bb0298 --- /dev/null +++ b/public/components/common/tables/components/__snapshots__/export-table-csv.test.tsx.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Export Table Csv component renders correctly to match the snapshot 1`] = ` +<ExportTableCsv + endpoint="/" + filters={Array []} + title="" + totalItems={0} +> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiButtonEmpty + iconType="importAction" + isDisabled={true} + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty-isDisabled" + disabled={true} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="importAction" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="importAction" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + > + Export formatted + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </EuiFlexItem> +</ExportTableCsv> +`; diff --git a/public/components/common/tables/components/export-table-csv.test.tsx b/public/components/common/tables/components/export-table-csv.test.tsx new file mode 100644 index 0000000000..fa33587ba9 --- /dev/null +++ b/public/components/common/tables/components/export-table-csv.test.tsx @@ -0,0 +1,39 @@ +/* + * Wazuh app - React test for Export Table Csv component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { ExportTableCsv } from './export-table-csv'; + +jest.mock('../../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +jest.mock('../../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); + +describe('Export Table Csv component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(<ExportTableCsv />); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/tables/components/export-table-csv.tsx b/public/components/common/tables/components/export-table-csv.tsx index b0f968321b..1f8b8fc3ac 100644 --- a/public/components/common/tables/components/export-table-csv.tsx +++ b/public/components/common/tables/components/export-table-csv.tsx @@ -18,6 +18,9 @@ import { import { filtersToObject } from '../../../wz-search-bar/'; import exportCsv from '../../../../react-services/wz-csv'; import { getToasts } from '../../../../kibana-services'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export function ExportTableCsv({endpoint,totalItems,filters,title}){ @@ -42,7 +45,17 @@ export function ExportTableCsv({endpoint,totalItems,filters,title}){ `vuls-${(title).toLowerCase()}` ); } catch (error) { - showToast('danger', error, 3000); + const options = { + context: `${ExportTableCsv.name}.downloadCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error downloading csv`, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/common/tables/table-default.test.tsx b/public/components/common/tables/table-default.test.tsx new file mode 100644 index 0000000000..c7482d3f8a --- /dev/null +++ b/public/components/common/tables/table-default.test.tsx @@ -0,0 +1,81 @@ +/* + * Wazuh app - React test for table default component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { TableDefault } from './table-default'; + +jest.mock('../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); + +jest.mock( + '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const columns = [ + { + field: 'version', + name: 'Version', + sortable: true, + truncateText: true, + }, + { + field: 'architecture', + name: 'Architecture', + sortable: true, + }, + { + field: 'cve', + name: 'CVE', + sortable: true, + truncateText: true, + }, + { + field: 'name', + name: 'Name', + sortable: true, + }, +]; + +const tableProps = { + onSearch: () => {}, + tableColumns: columns, + tablePageSizeOptions: [15, 25, 50, 100], + tableInitialSortingDirection: 'asc', + tableInitialSortingField: '', + tableProps: {}, + reload: () => {}, +}; + +describe('Table Default component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(<TableDefault {...tableProps} />); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx index ec1e7c27a6..8f6d9deb65 100644 --- a/public/components/common/tables/table-default.tsx +++ b/public/components/common/tables/table-default.tsx @@ -12,8 +12,11 @@ import React, { useState, useEffect } from 'react'; import { EuiBasicTable } from '@elastic/eui'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; -export function TableDeafult({ +export function TableDefault({ onSearch, tableColumns, tablePageSizeOptions = [15, 25, 50, 100], @@ -65,6 +68,17 @@ export function TableDeafult({ }catch(error){ setItems([]); setTotalItems(0); + const options = { + context: `${TableDefault.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); } setLoading(false); })() diff --git a/public/components/common/tables/table-with-search-bar.test.tsx b/public/components/common/tables/table-with-search-bar.test.tsx new file mode 100644 index 0000000000..cc62fb83ac --- /dev/null +++ b/public/components/common/tables/table-with-search-bar.test.tsx @@ -0,0 +1,83 @@ +/* + * Wazuh app - React test for Table With Search Bar component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { TableWithSearchBar } from './table-with-search-bar'; + +jest.mock('../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); + +jest.mock( + '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const columns = [ + { + field: 'version', + name: 'Version', + sortable: true, + truncateText: true, + }, + { + field: 'architecture', + name: 'Architecture', + sortable: true, + }, + { + field: 'cve', + name: 'CVE', + sortable: true, + truncateText: true, + }, + { + field: 'name', + name: 'Name', + sortable: true, + }, +]; + +const tableProps = { + onSearch: () => {}, + tableColumns: columns, + tablePageSizeOptions: [15, 25, 50, 100], + tableInitialSortingDirection: 'asc', + tableInitialSortingField: '', + tableProps: {}, + reload: () => {}, + searchBarSuggestions: [], + rowProps: () => {}, +}; + +describe('Table With Search Bar component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(<TableWithSearchBar {...tableProps} />); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index 363728f425..07e3cbd351 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -12,7 +12,10 @@ import React, { useState, useEffect } from 'react'; import { EuiBasicTable, EuiSpacer } from '@elastic/eui'; -import { WzSearchBar } from '../../wz-search-bar/' +import { WzSearchBar } from '../../wz-search-bar/'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; export function TableWithSearchBar({ onSearch, @@ -27,31 +30,29 @@ export function TableWithSearchBar({ tableProps = {}, reload, ...rest -}) - { - +}) { const [loading, setLoading] = useState(false); const [items, setItems] = useState([]); const [totalItems, setTotalItems] = useState(0); const [filters, setFilters] = useState(rest.filters || []); const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: tablePageSizeOptions[0] + pageSize: tablePageSizeOptions[0], }); const [sorting, setSorting] = useState({ sort: { field: tableInitialSortingField, direction: tableInitialSortingDirection, - } + }, }); - - function tableOnChange({ page = {}, sort = {} }){ + + function tableOnChange({ page = {}, sort = {} }) { const { index: pageIndex, size: pageSize } = page; const { field, direction } = sort; setPagination({ pageIndex, - pageSize + pageSize, }); setSorting({ sort: { @@ -59,51 +60,64 @@ export function TableWithSearchBar({ direction, }, }); - }; - + } + useEffect(() => { - (async function(){ - try{ + (async function () { + try { setLoading(true); const { items, totalItems } = await onSearch(filters, pagination, sorting); setItems(items); setTotalItems(totalItems); - }catch(error){ + } catch (error) { setItems([]); setTotalItems(0); + const options = { + context: `${TableWithSearchBar.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); } setLoading(false); - })() + })(); }, [filters, pagination, sorting, reload]); useEffect(() => { - setFilters(rest.filters || []) + setFilters(rest.filters || []); }, [rest.filters]); const tablePagination = { ...pagination, totalItemCount: totalItems, - pageSizeOptions: tablePageSizeOptions - } - return <> - <WzSearchBar - noDeleteFiltersOnUpdateSuggests - filters={filters} - onFiltersChange={setFilters} - suggestions={searchBarSuggestions} - placeholder={searchBarPlaceholder} - {...searchBarProps} - /> - <EuiSpacer size='s'/> - <EuiBasicTable - columns={tableColumns} - items={items} - loading={loading} - pagination={tablePagination} - sorting={sorting} - onChange={tableOnChange} - rowProps={rowProps} - {...tableProps} - /> - </> + pageSizeOptions: tablePageSizeOptions, + }; + return ( + <> + <WzSearchBar + noDeleteFiltersOnUpdateSuggests + filters={filters} + onFiltersChange={setFilters} + suggestions={searchBarSuggestions} + placeholder={searchBarPlaceholder} + {...searchBarProps} + /> + <EuiSpacer size="s" /> + <EuiBasicTable + columns={tableColumns} + items={items} + loading={loading} + pagination={tablePagination} + sorting={sorting} + onChange={tableOnChange} + rowProps={rowProps} + {...tableProps} + /> + </> + ); } diff --git a/public/components/common/tables/table-wz-api.test.tsx b/public/components/common/tables/table-wz-api.test.tsx new file mode 100644 index 0000000000..2280756263 --- /dev/null +++ b/public/components/common/tables/table-wz-api.test.tsx @@ -0,0 +1,79 @@ +/* + * Wazuh app - React test for table wz api component. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { mount } from 'enzyme'; +import { TableWzAPI } from './table-wz-api'; + +jest.mock('../../../kibana-services', () => ({ + getAngularModule: jest.fn(), + getHttp: () => ({ + basePath: { + prepend: (str) => str, + }, + }), +})); + +jest.mock('../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: (options) => {}, + }), +})); + +jest.mock( + '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const columns = [ + { + field: 'version', + name: 'Version', + sortable: true, + truncateText: true, + }, + { + field: 'architecture', + name: 'Architecture', + sortable: true, + }, + { + field: 'cve', + name: 'CVE', + sortable: true, + truncateText: true, + }, + { + field: 'name', + name: 'Name', + sortable: true, + }, +]; + +describe('Table WZ API component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount( + <TableWzAPI + title="Table" + tableColumns={columns} + endpoint={'/'} + searchTable={false} + error={false} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx index 2ce607f3d7..ec91da0b1c 100644 --- a/public/components/common/tables/table-wz-api.tsx +++ b/public/components/common/tables/table-wz-api.tsx @@ -19,9 +19,12 @@ import { } from '@elastic/eui'; import { filtersToObject } from '../../wz-search-bar'; import { TableWithSearchBar } from './table-with-search-bar'; -import { TableDeafult } from './table-default' +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'; export function TableWzAPI({endpoint, ...rest}){ @@ -51,7 +54,17 @@ export function TableWzAPI({endpoint, ...rest}){ } catch (error) { setIsLoading(false); setTotalItems(0); - return Promise.reject(error); + const options = { + context: `${TableWithSearchBar.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error searching items`, + }, + }; + getErrorOrchestrator().handleError(options); }; },[]); @@ -73,7 +86,7 @@ export function TableWzAPI({endpoint, ...rest}){ onSearch={onSearch} {...rest} /> : - <TableDeafult + <TableDefault onSearch={onSearch} {...rest} /> From 6225d1ac172848aafa7b35c7bbc8c9854b6e8bdd Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Tue, 20 Jul 2021 19:01:06 +0200 Subject: [PATCH 100/493] Refactor try catch strategy on controller/settings (#3469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(settings): Applied try catch strategy on controller/settings * feat(test): Updated snapshot test of reporting * doc(changelog): Updated * test(workflow): fixed branch * test(workflow): fixed branch + update snapshot * test(workflow): update snapshot * Updating test. (#3470) * test(unittest): Updating test. * test(unittest): Updating test. * log error Co-authored-by: pablomarga <pablomarga@hotmail.com> Co-authored-by: Pablo Martínez <pablo.martinez@wazuh.com> --- CHANGELOG.md | 3 +- public/controllers/settings/settings.js | 134 ++++++++++++++++++++---- 2 files changed, 118 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71355eb7e6..7f2601ae09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,8 @@ All notable changes to the Wazuh app project will be documented in this file. [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) + [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) + [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) diff --git a/public/controllers/settings/settings.js b/public/controllers/settings/settings.js index ca614c72ba..0fa6d68016 100644 --- a/public/controllers/settings/settings.js +++ b/public/controllers/settings/settings.js @@ -22,6 +22,10 @@ import { formatUIDate } from '../../react-services/time-service'; import store from '../../redux/store'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; import { updateSelectedSettingsSection } from '../../redux/actions/appStateActions'; +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 class SettingsController { /** * Class constructor @@ -79,7 +83,18 @@ export class SettingsController { await this.getAppInfo(); } catch (error) { - ErrorHandler.handle('Cannot initialize Settings'); + const options = { + context: `${SettingsController.name}.$onInit`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Cannot initialize Settings`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -188,18 +203,35 @@ export class SettingsController { this.apiEntries[idx].status = 'online'; } catch (error) { const code = ((error || {}).data || {}).code; - const downReason = typeof error === 'string' ? error : - (error || {}).message || ((error || {}).data || {}).message || 'Wazuh is not reachable'; + const downReason = + typeof error === 'string' + ? error + : (error || {}).message || + ((error || {}).data || {}).message || + 'Wazuh is not reachable'; const status = code === 3099 ? 'down' : 'unknown'; this.apiEntries[idx].status = { status, downReason }; numError = numError + 1; - if(this.apiEntries[idx].id === this.currentDefault){ // if the selected API is down, we remove it so a new one will selected + if (this.apiEntries[idx].id === this.currentDefault) { + // if the selected API is down, we remove it so a new one will selected AppState.removeCurrentAPI(); } } } return numError; - } catch (error) {} + } catch (error) { + const options = { + context: `${SettingsController.name}.checkApisStatus`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } // Set default API @@ -241,16 +273,24 @@ export class SettingsController { this.$scope.$applyAsync(); return this.currentDefault; } catch (error) { - ErrorHandler.handle(error); + const options = { + context: `${SettingsController.name}.setDefault`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } // Get settings function async getSettings() { try { - const patternList = await SavedObject.getListOfWazuhValidIndexPatterns(); - - this.indexPatterns = patternList; + this.indexPatterns = await SavedObject.getListOfWazuhValidIndexPatterns(); if (!this.indexPatterns.length) { this.wzMisc.setBlankScr('Sorry but no valid index patterns were found'); @@ -286,7 +326,17 @@ export class SettingsController { this.$scope.$applyAsync(); } catch (error) { - ErrorHandler.handle('Error getting API entries', 'Settings'); + const options = { + context: `${SettingsController.name}.getSettings`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error getting API entries`, + }, + }; + getErrorOrchestrator().handleError(options); } // Every time that the API entries are required in the settings the registry will be checked in order to remove orphan host entries await this.genericReq.request('POST', '/hosts/remove-orphan-entries', { @@ -342,12 +392,21 @@ export class SettingsController { this.apiIsDown = false; !silent && ErrorHandler.info('Connection success', 'Settings'); this.$scope.$applyAsync(); - return; } catch (error) { this.load = false; this.$scope.$applyAsync(); if (!silent) { - ErrorHandler.handle(error); + const options = { + context: `${SettingsController.name}.checkManager`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } return Promise.reject(error); } @@ -373,6 +432,18 @@ export class SettingsController { this.$scope.$applyAsync(); return logs.data.lastLogs.map(item => JSON.parse(item)); } catch (error) { + const options = { + context: `${SettingsController.name}.getAppLogs`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + return [ { date: new Date(), @@ -416,12 +487,18 @@ export class SettingsController { this.$scope.$applyAsync(); } catch (error) { AppState.removeNavigation(); - ErrorHandler.handle( - error, - 'Settings' - ); + const options = { + context: `${SettingsController.name}.getAppInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } - return; } /** @@ -463,8 +540,18 @@ export class SettingsController { closedEnabled: true }; } - return; } catch (error) { + const options = { + context: `${SettingsController.name}.checkForNewApis`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); return Promise.reject(error); } } @@ -545,6 +632,17 @@ export class SettingsController { return this.apiEntries; } catch (error) { this.showAddApiWithInitialError(error); + const options = { + context: `${SettingsController.name}.refreshApiEntries`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); return Promise.reject(error); } } From 7f672af08287e49a68141b6dc8038897d360ffd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Tue, 20 Jul 2021 19:03:54 +0200 Subject: [PATCH 101/493] Implement try catch in Components > Wz files (#3448) * Implement try catch wz files in components * Fix useEffect * Update changelog * Solve comments * Change some error display options Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> Co-authored-by: Maximiliano Ibarra <maximilianoaibarra@gmail.com> --- CHANGELOG.md | 3 +- .../components/wz-filter-bar/wz-filter-bar.js | 486 +++++++++--------- public/components/wz-menu/wz-menu-agent.js | 18 +- public/components/wz-menu/wz-menu.js | 62 ++- .../wz-search-bar/lib/suggest-handler.ts | 177 ++++--- .../wz-search-bar/wz-search-bar.tsx | 103 ++-- 6 files changed, 496 insertions(+), 353 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2601ae09..ca80fc4e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ All notable changes to the Wazuh app project will be documented in this file. [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) + [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) + [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) diff --git a/public/components/wz-filter-bar/wz-filter-bar.js b/public/components/wz-filter-bar/wz-filter-bar.js index ada604ae3c..098d7cf061 100644 --- a/public/components/wz-filter-bar/wz-filter-bar.js +++ b/public/components/wz-filter-bar/wz-filter-bar.js @@ -14,292 +14,300 @@ import { EuiComboBox } from '@elastic/eui'; import PropTypes from 'prop-types'; import './wz-filter-bar.scss'; import { withErrorBoundary } from '../common/hocs'; +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 WzFilterBar = withErrorBoundary (class WzFilterBar extends Component { - constructor(props) { - super(props); - const { model, selectedOptions } = this.props; - this.state = { - selectedOptions: selectedOptions || [], - toggleIdSelected: 'AND', - isProcessing: true, - options: model +export const WzFilterBar = withErrorBoundary( + class WzFilterBar extends Component { + constructor(props) { + super(props); + const { model, selectedOptions } = this.props; + this.state = { + selectedOptions: selectedOptions || [], + toggleIdSelected: 'AND', + isProcessing: true, + options: model, + }; + this.toggleButtons = [ + { + id: 'AND', + label: 'AND', + }, + { + id: 'OR', + label: 'OR', + }, + ]; + this.refComboBox = React.createRef(); + } + + onOperatorClick = (ev, option) => { + ev.preventDefault(); + const selectedOptions = this.state.selectedOptions; + if (selectedOptions[option].type === 'search') return; + selectedOptions[option].type = selectedOptions[option].type === 'AND' ? 'OR' : 'AND'; + this.setState({ + isProcessing: true, + selectedOptions, + }); }; - this.toggleButtons = [ - { - id: 'AND', - label: 'AND' - }, - { - id: 'OR', - label: 'OR' - } - ]; - this.refComboBox = React.createRef(); - } - onOperatorClick = (ev, option) => { - ev.preventDefault(); - const selectedOptions = this.state.selectedOptions; - if (selectedOptions[option].type === 'search') return; - selectedOptions[option].type = - selectedOptions[option].type === 'AND' ? 'OR' : 'AND'; - this.setState({ - isProcessing: true, - selectedOptions - }); - }; + componentDidUpdate(prevProps) { + if (JSON.stringify(this.state.selectedOptions) !== JSON.stringify(this.props.selectedOptions)) + this.setState({ + isProcessing: true, + selectedOptions: this.props.selectedOptions, + }); - componentDidUpdate(prevProps) { - if(JSON.stringify(this.state.selectedOptions) !== JSON.stringify(this.props.selectedOptions)) - this.setState({ - isProcessing: true, - selectedOptions: this.props.selectedOptions - }); - - const { model } = this.props; - if (JSON.stringify(prevProps.model) !== JSON.stringify(model)) { - const { selectedOptions } = this.state; - this.clearSeletedOptions(model, selectedOptions); + const { model } = this.props; + if (JSON.stringify(prevProps.model) !== JSON.stringify(model)) { + const { selectedOptions } = this.state; + this.clearSeletedOptions(model, selectedOptions); - for (const selectedOption of selectedOptions) { - for (const group of model) { - let flag = false; - for (const option of group.options) { - if (selectedOption.group === option.group) { - flag = true; + for (const selectedOption of selectedOptions) { + for (const group of model) { + let flag = false; + for (const option of group.options) { + if (selectedOption.group === option.group) { + flag = true; + break; + } + } + if (flag) { + group.options.push(selectedOption); break; } } - if (flag) { - group.options.push(selectedOption); - break; - } } - } - this.setState({ options: model }); - } - if (this.state.isProcessing) { - for (const i in this.state.selectedOptions) { - if (this.state.selectedOptions.hasOwnProperty(i)) { - const selectedOptions = this.state.selectedOptions[i]; - const el = $('.wzFilterBarOperator .euiBadge__content')[i]; - const hasBtn = $(el).find('.wzFilterBarOperatorBtn'); - if (hasBtn.length) { - $(hasBtn[0]).remove(); - } - if (i != 0) { - if (selectedOptions.type != 'search') { - const button = $( - `<button class="wzFilterBarOperatorBtn euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"><b>${selectedOptions.type}<b></button>` - ); - button[0].addEventListener('click', ev => { - this.onOperatorClick(ev, i); - }); - $(el).prepend(button); - } else { - const button = $( - `<span class="wzFilterBarOperatorBtn"><b>AND<b></button>` - ); - $(el).prepend(button); + this.setState({ options: model }); + } + if (this.state.isProcessing) { + for (const i in this.state.selectedOptions) { + if (this.state.selectedOptions.hasOwnProperty(i)) { + const selectedOptions = this.state.selectedOptions[i]; + const el = $('.wzFilterBarOperator .euiBadge__content')[i]; + const hasBtn = $(el).find('.wzFilterBarOperatorBtn'); + if (hasBtn.length) { + $(hasBtn[0]).remove(); + } + if (i != 0) { + if (selectedOptions.type != 'search') { + const button = $( + `<button class="wzFilterBarOperatorBtn euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall"><b>${selectedOptions.type}<b></button>` + ); + button[0].addEventListener('click', (ev) => { + this.onOperatorClick(ev, i); + }); + $(el).prepend(button); + } else { + const button = $(`<span class="wzFilterBarOperatorBtn"><b>AND<b></button>`); + $(el).prepend(button); + } } } } + this.buildQuery(); + this.setState({ isProcessing: false }); + this.refComboBox.current.closeList(); } - this.buildQuery(); - this.setState({ isProcessing: false }); - this.refComboBox.current.closeList(); } - } - checkIfExistsOrIsSearch(selectedOptions, last) { - const { group, label, type } = selectedOptions[last]; - const lastOption = `${group - .trim() - .toLowerCase()}:${label.trim().toLowerCase()}`; + checkIfExistsOrIsSearch(selectedOptions, last) { + const { group, label, type } = selectedOptions[last]; + const lastOption = `${group.trim().toLowerCase()}:${label.trim().toLowerCase()}`; - for (const option of selectedOptions) { - const isSelected = option.label.trim().toLowerCase() === lastOption; - const isSearch = type === 'search' && option.type === 'search'; - if (isSelected || isSearch) { - return true; + for (const option of selectedOptions) { + const isSelected = option.label.trim().toLowerCase() === lastOption; + const isSearch = type === 'search' && option.type === 'search'; + if (isSelected || isSearch) { + return true; + } } } - } - encodeFilter(option) { - const newFilter = option; + encodeFilter(option) { + const newFilter = option; - newFilter.type = this.state.toggleIdSelected; - if (!newFilter.label.includes(':')) { - newFilter.label_ = newFilter.label; - } + newFilter.type = this.state.toggleIdSelected; + if (!newFilter.label.includes(':')) { + newFilter.label_ = newFilter.label; + } - newFilter.label = option.group + ':' + newFilter.label_; - newFilter.className = 'wzFilterBarOperator'; - } + newFilter.label = option.group + ':' + newFilter.label_; + newFilter.className = 'wzFilterBarOperator'; + } - decodeFilters(options, selectedOptions) { - const labels = selectedOptions.map(item => { - return item.label_; - }); - for (const groups of options) { - for (const option of groups.options) { - if (option.label.includes(':') && !labels.includes(option.label_)) { - option.label = option.label_; - delete option.type; - delete option.label_; - delete option.className; + decodeFilters(options, selectedOptions) { + const labels = selectedOptions.map((item) => { + return item.label_; + }); + for (const groups of options) { + for (const option of groups.options) { + if (option.label.includes(':') && !labels.includes(option.label_)) { + option.label = option.label_; + delete option.type; + delete option.label_; + delete option.className; + } } } } - } - clearSeletedOptions(options, selectedOptions) { - selectedOptions - .filter(x => { - return x.type != 'search'; - }) - .forEach(x => { - const group = options.findIndex(m => { - const g1 = x.group.toLowerCase(); - const g2 = ((m.options[0] || []).group || '').toLowerCase(); - return g1 === g2; - }); - if (group != undefined && group != -1) { - const idx = options[group].options.findIndex(l => { - return ( - l.label.trim().toLowerCase() === x.label_.trim().toLowerCase() - ); + clearSeletedOptions(options, selectedOptions) { + selectedOptions + .filter((x) => { + return x.type != 'search'; + }) + .forEach((x) => { + const group = options.findIndex((m) => { + const g1 = x.group.toLowerCase(); + const g2 = ((m.options[0] || []).group || '').toLowerCase(); + return g1 === g2; }); - if (idx !== -1) options[group].options.splice(idx, 1); - } + if (group != undefined && group != -1) { + const idx = options[group].options.findIndex((l) => { + return l.label.trim().toLowerCase() === x.label_.trim().toLowerCase(); + }); + if (idx !== -1) options[group].options.splice(idx, 1); + } + }); + } + + onChange = (selectedOptions) => { + const last = selectedOptions.findIndex((x) => { + return !x.type; }); - } - onChange = selectedOptions => { - const last = selectedOptions.findIndex(x => { - return !x.type; - }); + if (last !== -1) { + if (this.checkIfExistsOrIsSearch(selectedOptions, last)) { + return; + } - if (last !== -1) { - if (this.checkIfExistsOrIsSearch(selectedOptions, last)) { - return; + this.encodeFilter(selectedOptions[last]); } - this.encodeFilter(selectedOptions[last]); - } - - const options = this.state.options; - this.clearSeletedOptions(options, selectedOptions); - this.decodeFilters(options, selectedOptions); + const options = this.state.options; + this.clearSeletedOptions(options, selectedOptions); + this.decodeFilters(options, selectedOptions); - this.setState({ - isProcessing: true, - selectedOptions, - options - }); - }; + this.setState({ + isProcessing: true, + selectedOptions, + options, + }); + }; - onCreateOption = searchValue => { - if (!searchValue) { - return; - } + onCreateOption = (searchValue) => { + if (!searchValue) { + return; + } - const normalizedSearchValue = searchValue.trim().toLowerCase(); + const normalizedSearchValue = searchValue.trim().toLowerCase(); - if (!normalizedSearchValue) { - return; - } + if (!normalizedSearchValue) { + return; + } - let newOption = {}; + let newOption = {}; - if (normalizedSearchValue.includes(':')) { - newOption = { - label: normalizedSearchValue.split(':')[1] || '', - group: normalizedSearchValue.split(':')[0] || '' - }; - } else { - newOption = { - label: searchValue, - type: 'search', - className: 'wzFilterBarOperator' - }; - } + if (normalizedSearchValue.includes(':')) { + newOption = { + label: normalizedSearchValue.split(':')[1] || '', + group: normalizedSearchValue.split(':')[0] || '', + }; + } else { + newOption = { + label: searchValue, + type: 'search', + className: 'wzFilterBarOperator', + }; + } - // Select the option. - this.setState( - prevState => ({ - selectedOptions: prevState.selectedOptions.concat(newOption) - }), - () => { - if (searchValue.includes(':')) { - this.onChange(this.state.selectedOptions); + // Select the option. + this.setState( + (prevState) => ({ + selectedOptions: prevState.selectedOptions.concat(newOption), + }), + () => { + if (searchValue.includes(':')) { + this.onChange(this.state.selectedOptions); + } } - } - ); - this.setState({ isProcessing: true }); - }; + ); + this.setState({ isProcessing: true }); + }; - buildQuery = () => { - try { - const selectedOptions = this.state.selectedOptions; - const queryObj = { - query: '', - search: '' - }; - const queryElements = selectedOptions.filter(x => { - return x.type !== 'search'; - }); - const searchElement = selectedOptions.filter(x => { - return x.type === 'search'; - }); - if (searchElement.length) { - queryObj.search = searchElement[0].label; - } - const twoOrMoreElements = queryElements.length > 1; - if (twoOrMoreElements) { - queryObj.query += '('; - } - queryElements.forEach((option, idx) => { - if (idx != 0) { - queryObj.query += option.type === 'AND' ? ';' : ','; + buildQuery = () => { + try { + const selectedOptions = this.state.selectedOptions; + const queryObj = { + query: '', + search: '', + }; + const queryElements = selectedOptions.filter((x) => { + return x.type !== 'search'; + }); + const searchElement = selectedOptions.filter((x) => { + return x.type === 'search'; + }); + if (searchElement.length) { + queryObj.search = searchElement[0].label; } - queryObj.query += option.query - ? option.query - : option.label.replace(':', '='); - }); - if (twoOrMoreElements) { - queryObj.query += ')'; - } - this.props.clickAction({ - q: queryObj.query, - search: queryObj.search, - selectedOptions: selectedOptions - }); - } catch (error) {} // eslint-disable-line - }; + const twoOrMoreElements = queryElements.length > 1; + if (twoOrMoreElements) { + queryObj.query += '('; + } + queryElements.forEach((option, idx) => { + if (idx != 0) { + queryObj.query += option.type === 'AND' ? ';' : ','; + } + queryObj.query += option.query ? option.query : option.label.replace(':', '='); + }); + if (twoOrMoreElements) { + queryObj.query += ')'; + } + this.props.clickAction({ + q: queryObj.query, + search: queryObj.search, + selectedOptions: selectedOptions, + }); + } catch (error) { + const options = { + context: `${WzFilterBar.name}.buildQuery`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } // eslint-disable-line + }; - render() { - const { options, selectedOptions } = this.state; - return ( - <EuiComboBox - className="wz-search-bar" - placeholder="Add filter or search" - options={options} - selectedOptions={selectedOptions} - onChange={this.onChange} - fullWidth={true} - onCreateOption={this.onCreateOption} - ref={this.refComboBox} - /> - ); + render() { + const { options, selectedOptions } = this.state; + return ( + <EuiComboBox + className="wz-search-bar" + placeholder="Add filter or search" + options={options} + selectedOptions={selectedOptions} + onChange={this.onChange} + fullWidth={true} + onCreateOption={this.onCreateOption} + ref={this.refComboBox} + /> + ); + } } -}); +); WzFilterBar.propTypes = { clickAction: PropTypes.func, model: PropTypes.array, - selectedOptions: PropTypes.array + selectedOptions: PropTypes.array, }; diff --git a/public/components/wz-menu/wz-menu-agent.js b/public/components/wz-menu/wz-menu-agent.js index b318767d79..85ebf3c45f 100644 --- a/public/components/wz-menu/wz-menu-agent.js +++ b/public/components/wz-menu/wz-menu-agent.js @@ -18,7 +18,9 @@ import { AppState } from '../../react-services/app-state'; import { hasAgentSupportModule } from '../../react-services/wz-agents'; import { AgentInfo } from './../common/welcome/agents-info'; import { getAngularModule } from '../../kibana-services'; -import { WAZUH_MODULES_ID } from '../../../common/constants'; +import { WAZUH_MODULES_ID, UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services' class WzMenuAgent extends Component { constructor(props) { @@ -111,7 +113,19 @@ class WzMenuAgent extends Component { const result = await WzRequest.apiReq('GET', '/agents/' + agentId, {}); return result; } catch (error) { - return Promise.reject(error); + const options = { + context: `${WzMenuAgent.name}.getAgentData`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 8a0a7c5fec..dbd020dcb9 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -46,6 +46,9 @@ import { AppNavigate } from '../../react-services/app-navigate'; import WzTextWithTooltipIfTruncated from '../../components/common/wz-text-with-tooltip-if-truncated'; import { getDataPlugin } from '../../kibana-services'; import { withWindowSize } from '../../components/common/hocs/withWindowSize'; +import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../react-services/common-services' const sections = { @@ -103,8 +106,21 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { } } } - } catch (err) { } - + } catch (error) { + const options = { + context: `${WzMenu.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } } showToast = (color, title, text, time) => { @@ -167,7 +183,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { }); } } catch (error) { - this.showToast('danger', 'Error', error, 4000); + throw error; } } @@ -253,7 +269,19 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { }); } } catch (error) { - this.showToast('danger', 'Error', error.message || error, 4000); + const options = { + context: `${WzMenu.name}.load`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + display: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } this.isLoading = false; } @@ -272,7 +300,19 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { this.updatePatternAndApi(); } } catch (error) { - this.showToast('danger', 'Error', error, 4000); + const options = { + context: `${WzMenu.name}.changePattern`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: false, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error changing the Index Pattern`, + }, + }; + getErrorOrchestrator().handleError(options); } }; @@ -323,7 +363,17 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { this.router.reload(); } } catch (error) { - this.showToast('danger', 'Error', error, 4000); + const options = { + context: `${WzMenu.name}.changePattern`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error changing the selected API`, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/components/wz-search-bar/lib/suggest-handler.ts b/public/components/wz-search-bar/lib/suggest-handler.ts index 12cf9e9fa2..af46b15493 100644 --- a/public/components/wz-search-bar/lib/suggest-handler.ts +++ b/public/components/wz-search-bar/lib/suggest-handler.ts @@ -13,20 +13,23 @@ import { BaseHandler, IWzSuggestItem, QInterpreter } from './'; import { suggestItem, IWzSearchBarProps } from '../wz-search-bar'; import { queryObject } from './q-interpreter'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; interface ISuggestHandlerProps extends IWzSearchBarProps { - setStatus: Function - setIsOpen: Function - setInvalid: Function + setStatus: Function; + setIsOpen: Function; + setInvalid: Function; } export class SuggestHandler extends BaseHandler { suggestItems: IWzSuggestItem[]; inputStage: 'field' | 'operator' | 'value' | 'conjuntion'; searchType: 'search' | 'q' | 'params'; - filters: { field: string, value: string | number }[]; + filters: { field: string; value: string | number }[]; props: ISuggestHandlerProps; setInputValue: Function; - inputRef!: HTMLElement + inputRef!: HTMLElement; lastCall: number; status: 'unchanged' | 'loading' = 'unchanged'; operators = { @@ -36,7 +39,7 @@ export class SuggestHandler extends BaseHandler { '<': 'Smaller', '~': 'Like', ':': 'Equals', - } + }; constructor(props, setInputValue) { super(); @@ -49,16 +52,14 @@ export class SuggestHandler extends BaseHandler { this.lastCall = 0; } - combine = (...args) => (input) => args.reduceRight((acc, arg) => acc = arg(acc), input) + combine = (...args) => (input) => args.reduceRight((acc, arg) => (acc = arg(acc)), input); - someItem = (queryElement, key) => this.suggestItems.some(item => item[key] === queryElement); - findItem = (queryElement, key) => this.suggestItems.find(item => item[key] === queryElement); + someItem = (queryElement, key) => this.suggestItems.some((item) => item[key] === queryElement); + findItem = (queryElement, key) => this.suggestItems.find((item) => item[key] === queryElement); checkType = (input: string) => { - const operator = /:|=|!=|~|<|>/.exec(input) - this.searchType = operator - ? operator[0] === ':' ? 'params' : 'q' - : 'search' + const operator = /:|=|!=|~|<|>/.exec(input); + this.searchType = operator ? (operator[0] === ':' ? 'params' : 'q') : 'search'; return input; }; @@ -66,10 +67,10 @@ export class SuggestHandler extends BaseHandler { const { searchType } = this; if (searchType === 'search') { this.inputStage = 'field'; - return { field: input } + return { field: input }; } else if (searchType === 'params') { this.inputStage = 'value'; - const { 0: field, 1: value, 2: operator=':' } = input.split(':'); + const { 0: field, 1: value, 2: operator = ':' } = input.split(':'); return { field, value, operator }; } else if (searchType === 'q') { const qInterpreter = new QInterpreter(input); @@ -84,95 +85,122 @@ export class SuggestHandler extends BaseHandler { return { conjuntion, field, operator, value }; } return input; - } + }; checkQuery = async (query: queryObject) => { try { - const { inputStage, lastCall, searchType } = this; const { field = '', value = '', operator } = query; - if ((operator && !this.someItem(field, 'label'))) throw {error: 'Invalid field', message: `The field '${field}' is not valid`}; + if (operator && !this.someItem(field, 'label')) + throw { error: 'Invalid field', message: `The field '${field}' is not valid` }; //@ts-ignore - if (operator && ((searchType === 'params' && operator !== ':') || (searchType === 'q' && operator === ':'))) - throw {error: 'Invalid operator', message: `The operator '${operator}' is not valid`}; + if ( + operator && + ((searchType === 'params' && operator !== ':') || (searchType === 'q' && operator === ':')) + ) + throw { error: 'Invalid operator', message: `The operator '${operator}' is not valid` }; const suggestions = [ ...this.buildSuggestSearch(field), ...(value && inputStage === 'value' ? this.buildSuggestApply() : []), ...(value && inputStage === 'value' ? this.buildSuggestConjuntions(field) : []), - ...((this.someItem(field, 'label') && inputStage !== 'value') ? this.buildSuggestOperator(field) : []), + ...(this.someItem(field, 'label') && inputStage !== 'value' + ? this.buildSuggestOperator(field) + : []), ...(inputStage === 'field' ? this.buildSuggestFields(field) : []), - ...(inputStage === 'value' ? await this.buildSuggestValues(field, value) : []) - ] + ...(inputStage === 'value' ? await this.buildSuggestValues(field, value) : []), + ]; if (lastCall === this.lastCall) { this.lastCall++; - return {suggestions}; + return { suggestions }; } } catch (error) { - return {error} + const options = { + context: `${SuggestHandler.name}.checkQuery`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + return { error }; } - throw "New request in progress"; - } + throw 'New request in progress'; + }; - checkErrors = async (promise)=> { - const {suggestions=[], error} = await promise; + checkErrors = async (promise) => { + const { suggestions = [], error } = await promise; if (!error) return suggestions; - return [{ - label: error.error, - type: { iconType: 'alert', color: 'tint2' }, - ...(!!error.message && {description: error.message}) - }] - } + return [ + { + label: error.error, + type: { iconType: 'alert', color: 'tint2' }, + ...(!!error.message && { description: error.message }), + }, + ]; + }; - public buildSuggestItems = this.combine(this.checkErrors, this.checkQuery, this.checkStage, this.checkType); + public buildSuggestItems = this.combine( + this.checkErrors, + this.checkQuery, + this.checkStage, + this.checkType + ); buildSuggestSearch(inputValue): suggestItem[] { const { searchDisable } = this.props; if (!searchDisable && this.searchType === 'search') { - return [{ - label: inputValue, - type: { iconType: 'search', color: 'tint8' }, - description: 'Search', - }] + return [ + { + label: inputValue, + type: { iconType: 'search', color: 'tint8' }, + description: 'Search', + }, + ]; } - return [] + return []; } buildSuggestApply(): suggestItem[] { - return [{ - label: 'Apply filter', - type: { iconType: 'kqlFunction', color: 'tint5' }, - description: 'Click here or press "Enter" to apply the filter', - }] + return [ + { + label: 'Apply filter', + type: { iconType: 'kqlFunction', color: 'tint5' }, + description: 'Click here or press "Enter" to apply the filter', + }, + ]; } buildSuggestFields(inputValue: string) { return this.suggestItems - .filter(item => item.label.includes(inputValue)) + .filter((item) => item.label.includes(inputValue)) .map(this.mapSuggestFields); } buildSuggestOperator(inputValue: string) { const { operators } = this; - const operatorSuggest = op => ({ + const operatorSuggest = (op) => ({ label: op, description: operators[op], - type: { iconType: 'kqlOperand', color: 'tint1' } - }) + type: { iconType: 'kqlOperand', color: 'tint1' }, + }); const item = this.findItem(inputValue, 'label'); if (item && item.type === 'params') { return [operatorSuggest(':')]; } else { - const ops = (item || {}).operators || Object.keys(operators) - return ops - .filter(op => op !== ':').map(operatorSuggest) + const ops = (item || {}).operators || Object.keys(operators); + return ops.filter((op) => op !== ':').map(operatorSuggest); } } async buildSuggestValues(field: string, value: string) { const item = this.findItem(field, 'label'); - const rawSuggestions: string[] = (item && typeof item.values === 'function') - ? await this.getFunctionValues(item, value) - : (item || {}).values; + const rawSuggestions: string[] = + item && typeof item.values === 'function' + ? await this.getFunctionValues(item, value) + : (item || {}).values; const suggestions = rawSuggestions.map(this.buildSuggestValue); return suggestions; } @@ -180,33 +208,33 @@ export class SuggestHandler extends BaseHandler { buildSuggestConjuntions(inputValue: string): suggestItem[] { if (this.searchType !== 'q') return []; const suggestions = [ - { 'label': 'AND ', 'description': 'Requires `both arguments` to be true' }, - { 'label': 'OR ', 'description': 'Requires `one or more arguments` to be true' } + { label: 'AND ', description: 'Requires `both arguments` to be true' }, + { label: 'OR ', description: 'Requires `one or more arguments` to be true' }, ].map((item) => { return { type: { iconType: 'kqlSelector', color: 'tint3' }, label: item.label, - description: item.description - } - }) + description: item.description, + }; + }); return suggestions; } async getFunctionValues(item, value) { const { setStatus } = this.props; - this.status = 'loading' + this.status = 'loading'; setStatus('loading'); - const values = await item.values(value) - this.status = 'unchanged' + const values = await item.values(value); + this.status = 'unchanged'; setStatus('unchanged'); return values; } createParamFilter(field, value) { - const filters = [...this.filters.map(filter => ({...filter}))]; // "Clone" the filter elements to new objects - const idx = filters.findIndex(filter => filter.field === field); + const filters = [...this.filters.map((filter) => ({ ...filter }))]; // "Clone" the filter elements to new objects + const idx = filters.findIndex((filter) => filter.field === field); idx !== -1 - ? filters[idx].value = value // This change in filters produces a bug in ReactJS method componentDidUpdate if you try to modify the filter objects because prevState/prevPros and nextState/nextProps are same object and not cloning filter elements before the change of filter property + ? (filters[idx].value = value) // This change in filters produces a bug in ReactJS method componentDidUpdate if you try to modify the filter objects because prevState/prevPros and nextState/nextProps are same object and not cloning filter elements before the change of filter property : filters.push({ field: field, value }); this.props.onFiltersChange(filters); this.setInputValue(''); @@ -216,12 +244,9 @@ export class SuggestHandler extends BaseHandler { createQFilter(inputValue) { const qInterpreter = new QInterpreter(inputValue); - if (qInterpreter.queryObjects.some(q => !q.value)) return; + if (qInterpreter.queryObjects.some((q) => !q.value)) return; const value = qInterpreter.toString(); - const filters = [ - ...this.filters, - { field: 'q', value } - ]; + const filters = [...this.filters, { field: 'q', value }]; this.props.onFiltersChange(filters); this.setInputValue(''); this.props.setIsOpen(false); @@ -235,7 +260,9 @@ export class SuggestHandler extends BaseHandler { inputValue && this.createParamFilter('search', item.label); return; case 'kqlField': - this.searchType = (this.suggestItems.find(e => e.label === item.label) || { type: 'params' }).type; + this.searchType = ( + this.suggestItems.find((e) => e.label === item.label) || { type: 'params' } + ).type; if (this.searchType === 'params') { this.setInputValue(`${item.label}:`); } else { @@ -292,4 +319,4 @@ export class SuggestHandler extends BaseHandler { this.createQFilter(inputValue); } } -} \ No newline at end of file +} diff --git a/public/components/wz-search-bar/wz-search-bar.tsx b/public/components/wz-search-bar/wz-search-bar.tsx index 9b7c8c08e3..1287fa8c2a 100644 --- a/public/components/wz-search-bar/wz-search-bar.tsx +++ b/public/components/wz-search-bar/wz-search-bar.tsx @@ -11,48 +11,59 @@ */ import React, { useEffect, useState } from 'react'; import { EuiSuggest } from '../eui-suggest'; -import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { qSuggests } from './lib/q-handler'; import { apiSuggests } from './lib/api-handler'; import { WzSearchButtons, filterButton } from './wz-search-buttons'; import { WzSearchBadges } from './components'; import { SuggestHandler } from './lib'; import { IFilter } from './'; +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 interface suggestItem { - type: {iconType: string, color: string } - label: string - description?: string + type: { iconType: string; color: string }; + label: string; + description?: string; } export interface IWzSuggestItem extends apiSuggests, qSuggests { - type: 'q' | 'params' + type: 'q' | 'params'; } export interface IWzSearchBarProps { - suggestions: IWzSuggestItem[] - buttonOptions?: filterButton[] - searchDisable?: boolean - noDeleteFiltersOnUpdateSuggests?: boolean - placeholder?: string - filters: IFilter[] - onFiltersChange(filters:{}[]): void + suggestions: IWzSuggestItem[]; + buttonOptions?: filterButton[]; + searchDisable?: boolean; + noDeleteFiltersOnUpdateSuggests?: boolean; + placeholder?: string; + filters: IFilter[]; + onFiltersChange(filters: {}[]): void; } export function WzSearchBar(props: IWzSearchBarProps) { const [inputRef, setInputRef] = useState(); const [inputValue, setInputValue] = useState(''); const [isOpen, setIsOpen] = useState(false); - const [suggestsItems, handler, status, isInvalid] = useSuggestHandler(props, inputValue, setInputValue, inputRef, setIsOpen); + const [suggestsItems, handler, status, isInvalid] = useSuggestHandler( + props, + inputValue, + setInputValue, + inputRef, + setIsOpen + ); return ( <EuiFlexGroup> <EuiFlexItem> <EuiSuggest status={status} inputRef={setInputRef} - prepend={<WzSearchBadges filters={props.filters} onFiltersChange={props.onFiltersChange} />} + prepend={ + <WzSearchBadges filters={props.filters} onFiltersChange={props.onFiltersChange} /> + } value={inputValue} - onKeyPress={event => handler && handler.onKeyPress(inputValue, event)} + onKeyPress={(event) => handler && handler.onKeyPress(inputValue, event)} onItemClick={(item) => handler && handler.onItemClick(item, inputValue)} isPopoverOpen={isOpen} onClosePopover={() => setIsOpen(false)} @@ -60,41 +71,73 @@ export function WzSearchBar(props: IWzSearchBarProps) { suggestions={suggestsItems} onInputChange={setInputValue} isInvalid={isInvalid} - placeholder={props.placeholder} /> + placeholder={props.placeholder} + /> </EuiFlexItem> - {!!props.buttonOptions && + {!!props.buttonOptions && ( <EuiFlexItem grow={false}> - <WzSearchButtons filters={props.filters} options={props.buttonOptions} onChange={(filters) => props.onFiltersChange(filters)} /> + <WzSearchButtons + filters={props.filters} + options={props.buttonOptions} + onChange={(filters) => props.onFiltersChange(filters)} + /> </EuiFlexItem> - } + )} </EuiFlexGroup> - ) + ); } -function useSuggestHandler(props: IWzSearchBarProps, inputValue, setInputValue, inputRef, setIsOpen): -[suggestItem[], SuggestHandler | undefined, string, boolean] { +function useSuggestHandler( + props: IWzSearchBarProps, + inputValue, + setInputValue, + inputRef, + setIsOpen +): [suggestItem[], SuggestHandler | undefined, string, boolean] { const [handler, setHandler] = useState<undefined | SuggestHandler>(); const [suggestsItems, setSuggestItems] = useState<suggestItem[]>([]); - const [status, setStatus] = useState<'unchanged'|'loading'>('unchanged'); + const [status, setStatus] = useState<'unchanged' | 'loading'>('unchanged'); const [isInvalid, setInvalid] = useState(false); useEffect(() => { - setHandler(new SuggestHandler({...props, status, setStatus, setInvalid, setIsOpen}, setInputValue)) + setHandler( + new SuggestHandler({ ...props, status, setStatus, setInvalid, setIsOpen }, setInputValue) + ); !props.noDeleteFiltersOnUpdateSuggests && props.onFiltersChange([]); }, [props.suggestions]); - useEffect(() => {handler && (handler.inputRef = inputRef)}, [inputRef]) + useEffect(() => { + handler && (handler.inputRef = inputRef); + }, [inputRef]); useEffect(() => { - handler && handler.buildSuggestItems(inputValue) - .then(setSuggestItems) - .catch((e)=>{e !== 'New request in progress' && console.log(e)}); + if (handler) { + (async () => { + try { + setSuggestItems(await handler.buildSuggestItems(inputValue)); + } catch (error) { + if (error !== 'New request in progress') { + const options = { + context: `${useSuggestHandler.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + })(); + } }, [inputValue, handler]); useEffect(() => { handler && (handler.filters = props.filters); - }, [props.filters]) + }, [props.filters]); return [suggestsItems, handler, status, isInvalid]; } - From 63bb06dcd217ce9a68aedbdc51ae94c5695bb385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Tue, 20 Jul 2021 20:52:15 +0200 Subject: [PATCH 102/493] Implement try catch strategy in common/modules and management/groups (#3465) * Implement try catch strategy * Replace throw * Fix changelog * Fix errors * Remove store * doc(changelog): Update Co-authored-by: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 1 + .../common/modules/discover/discover.tsx | 1121 ++++++++++------- public/components/common/modules/events.tsx | 22 +- public/components/common/modules/main.tsx | 16 +- .../common/search/fieldsearch-delay.tsx | 14 + .../groups/multiple-agent-selector.tsx | 67 +- 6 files changed, 738 insertions(+), 503 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca80fc4e59..39d24f7298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to the Wazuh app project will be documented in this file. [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) + [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 9bb74970f2..ab7e564f69 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -8,10 +8,10 @@ * (at your option) any later version. * * Find more information about this on the LICENSE file. -*/ -import React, { Component, } from 'react'; + */ +import React, { Component } from 'react'; import './discover.scss'; -import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/' +import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; import { GenericRequest } from '../../../../react-services/generic-request'; import { AppState } from '../../../../react-services/app-state'; import { AppNavigate } from '../../../../react-services/app-navigate'; @@ -25,6 +25,9 @@ import { withErrorBoundary, withReduxProvider } from '../../../common/hocs'; import { connect } from 'react-redux'; import { compose } from 'redux'; import _ from 'lodash'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; import { EuiBasicTable, @@ -43,547 +46,697 @@ import { EuiToolTip } from '@elastic/eui'; import { - IIndexPattern, - TimeRange, - Query, - buildPhraseFilter, - getEsQueryConfig, - buildEsQuery, - IFieldType + IIndexPattern, + TimeRange, + Query, + buildPhraseFilter, + getEsQueryConfig, + buildEsQuery, + IFieldType, } from '../../../../../../../src/plugins/data/common'; import { getDataPlugin, getToasts, getUiSettings } from '../../../../kibana-services'; - -const mapStateToProps = state => ({ - currentAgentData: state.appStateReducers.currentAgentData + +const mapStateToProps = (state) => ({ + currentAgentData: state.appStateReducers.currentAgentData, }); - + export const Discover = compose( - withErrorBoundary, - withReduxProvider, - connect(mapStateToProps) -)(class Discover extends Component { - _isMount!: boolean; - timefilter: { - getTime(): TimeRange - setTime(time: TimeRange): void - _history: { history: { items: { from: string, to: string }[] } } - }; - - KibanaServices: { [key: string]: any }; - state: { - sort: object - selectedTechnique: string, - showMitreFlyout: boolean, - alerts: { _source: {}, _id: string }[] - total: number - pageIndex: number - pageSize: number - sortField: string - sortDirection: Direction - isLoading: boolean - requestFilters: object - requestSize: number - requestOffset: number - query: { language: "kuery" | "lucene", query: string } - itemIdToExpandedRowMap: any - dateRange: TimeRange - elasticQuery: object - columns: string[] - hover: string - }; - indexPattern!: IIndexPattern - props!: { - implicitFilters: object[], - initialFilters: object[], - query?: { language: "kuery" | "lucene", query: string } - type?: any, - updateTotalHits: Function, - openIntelligence: Function, - includeFilters?: string, - initialColumns: string[], - shareFilterManager: FilterManager, - shareFilterManagerWithUserAuthorized: Filter[], - refreshAngularDiscover?: number - } - constructor(props) { - super(props); - this.KibanaServices = getDataPlugin(); - this.timefilter = this.KibanaServices.query.timefilter.timefilter; - this.state = { - sort: {}, - selectedTechnique: "", - showMitreFlyout: false, - alerts: [], - total: 0, - pageIndex: 0, - pageSize: 10, - sortField: 'timestamp', - sortDirection: 'desc', - isLoading: false, - requestFilters: {}, - requestSize: 500, - requestOffset: 0, - itemIdToExpandedRowMap: {}, - dateRange: this.timefilter.getTime(), - dateRangeHistory: this.timefilter._history, - query: props.query || { language: "kuery", query: "" }, - elasticQuery: {}, - columns: [], - hover: "" - } + withErrorBoundary, + withReduxProvider, + connect(mapStateToProps) +)( + class Discover extends Component { + _isMount!: boolean; + timefilter: { + getTime(): TimeRange; + setTime(time: TimeRange): void; + _history: { history: { items: { from: string; to: string }[] } }; + }; - this.wazuhConfig = new WazuhConfig(); - this.nameEquivalences = { - "agent.id": "Agent", - "agent.name": "Agent name", - "syscheck.event": "Action", - "rule.id": "Rule ID", - "rule.description": "Description", - "rule.level": "Level", - "rule.mitre.id": "Technique(s)", - "rule.mitre.tactic": "Tactic(s)", - "rule.pci_dss": "PCI DSS", - "rule.gdpr": "GDPR", - "rule.nist_800_53": "NIST 800-53", - "rule.tsc": "TSC", - "rule.hipaa": "HIPAA", - } + KibanaServices: { [key: string]: any }; + state: { + sort: object; + selectedTechnique: string; + showMitreFlyout: boolean; + alerts: { _source: {}; _id: string }[]; + total: number; + pageIndex: number; + pageSize: number; + sortField: string; + sortDirection: Direction; + isLoading: boolean; + requestFilters: object; + requestSize: number; + requestOffset: number; + query: { language: 'kuery' | 'lucene'; query: string }; + itemIdToExpandedRowMap: any; + dateRange: TimeRange; + elasticQuery: object; + columns: string[]; + hover: string; + }; + indexPattern!: IIndexPattern; + props!: { + implicitFilters: object[]; + initialFilters: object[]; + query?: { language: 'kuery' | 'lucene'; query: string }; + type?: any; + updateTotalHits: Function; + openIntelligence: Function; + includeFilters?: string; + initialColumns: string[]; + shareFilterManager: FilterManager; + shareFilterManagerWithUserAuthorized: Filter[]; + refreshAngularDiscover?: number; + }; + constructor(props) { + super(props); + this.KibanaServices = getDataPlugin(); + this.timefilter = this.KibanaServices.query.timefilter.timefilter; + this.state = { + sort: {}, + selectedTechnique: '', + showMitreFlyout: false, + alerts: [], + total: 0, + pageIndex: 0, + pageSize: 10, + sortField: 'timestamp', + sortDirection: 'desc', + isLoading: false, + requestFilters: {}, + requestSize: 500, + requestOffset: 0, + itemIdToExpandedRowMap: {}, + dateRange: this.timefilter.getTime(), + dateRangeHistory: this.timefilter._history, + query: props.query || { language: 'kuery', query: '' }, + elasticQuery: {}, + columns: [], + hover: '', + }; - this.hideCreateCustomLabel.bind(this); - this.onQuerySubmit.bind(this); - this.onFiltersUpdated.bind(this); - this.hideCreateCustomLabel() - } + this.wazuhConfig = new WazuhConfig(); + this.nameEquivalences = { + 'agent.id': 'Agent', + 'agent.name': 'Agent name', + 'syscheck.event': 'Action', + 'rule.id': 'Rule ID', + 'rule.description': 'Description', + 'rule.level': 'Level', + 'rule.mitre.id': 'Technique(s)', + 'rule.mitre.tactic': 'Tactic(s)', + 'rule.pci_dss': 'PCI DSS', + 'rule.gdpr': 'GDPR', + 'rule.nist_800_53': 'NIST 800-53', + 'rule.tsc': 'TSC', + 'rule.hipaa': 'HIPAA', + }; - showToast = (color, title, time) => { - getToasts().add({ - color: color, - title: title, - toastLifeTimeMs: time, - }); - }; - - async componentDidMount() { - this._isMount = true; - try { - this.setState({columns: this.getColumns()}) //initial columns - await this.getIndexPattern(); - await this.getAlerts(); - } catch (err) { - console.log(err); + this.hideCreateCustomLabel.bind(this); + this.onQuerySubmit.bind(this); + this.onFiltersUpdated.bind(this); + this.hideCreateCustomLabel(); } - } - componentWillUnmount() { - this._isMount = false; - } - - async componentDidUpdate(prevProps, prevState) { - if (!this._isMount) { return; } - if((!prevProps.currentAgentData.id && this.props.currentAgentData.id) || (prevProps.currentAgentData.id && !this.props.currentAgentData.id) || prevProps.currentAgentData.id !== this.props.currentAgentData.id){ - this.setState({ columns: this.getColumns() }); // Updates the columns to be rendered if you change the selected agent to none or vice versa - return; - } - if(!_.isEqual(this.props.query,prevProps.query)){ - this.setState({ query: {...this.props.query}}); - return; - }; - if((this.props.currentAgentData.id !== prevProps.currentAgentData.id) - || (!_.isEqual(this.state.query, prevState.query)) - || (!_.isEqual(this.state.dateRange, prevState.dateRange)) - || (this.props.refreshAngularDiscover !== prevProps.refreshAngularDiscover) - ){ - this.setState({ pageIndex: 0 , tsUpdated: Date.now()}); - if(!_.isEqual(this.props.shareFilterManager, this.state.searchBarFilters)){ - this.setState({columns: this.getColumns(), searchBarFilters: this.props.shareFilterManager || []}); //initial columns - } - return; + showToast = (color, title, time) => { + getToasts().add({ + color: color, + title: title, + toastLifeTimeMs: time, + }); }; - if(['pageIndex', 'pageSize', 'sortField', 'sortDirection'].some(field => this.state[field] !== prevState[field]) || (this.state.tsUpdated !== prevState.tsUpdated)){ + + async componentDidMount() { + this._isMount = true; try { + this.setState({ columns: this.getColumns() }); //initial columns + await this.getIndexPattern(); await this.getAlerts(); - } catch (err) { - console.log(err); - }; - }; - } + } catch (error) { + const options = { + context: `${Discover.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } - getColumns () { - if(this.props.currentAgentData.id){ - return this.props.initialColumns.filter(column => !['agent.id', 'agent.name'].includes(column)); - }else{ - const columns = [...this.props.initialColumns]; - columns.splice(2, 0, 'agent.id'); - columns.splice(3, 0, 'agent.name'); - return columns; + componentWillUnmount() { + this._isMount = false; } - } - async getIndexPattern () { - this.indexPattern = {...await this.KibanaServices.indexPatterns.get(AppState.getCurrentPattern())}; - const fields:IFieldType[] = []; - Object.keys(this.indexPattern.fields).forEach(item => { - if (isNaN(item)) { - fields.push(this.indexPattern.fields[item]); - } else if (this.props.includeFilters && this.indexPattern.fields[item].name.includes(this.props.includeFilters)) { - fields.unshift(this.indexPattern.fields[item]); - } else { - fields.push(this.indexPattern.fields[item]); + async componentDidUpdate(prevProps, prevState) { + if (!this._isMount) { + return; } - }) - this.indexPattern.fields = fields; - } + if ( + (!prevProps.currentAgentData.id && this.props.currentAgentData.id) || + (prevProps.currentAgentData.id && !this.props.currentAgentData.id) || + prevProps.currentAgentData.id !== this.props.currentAgentData.id + ) { + this.setState({ columns: this.getColumns() }); // Updates the columns to be rendered if you change the selected agent to none or vice versa + return; + } + if (!_.isEqual(this.props.query, prevProps.query)) { + this.setState({ query: { ...this.props.query } }); + return; + } + if ( + this.props.currentAgentData.id !== prevProps.currentAgentData.id || + !_.isEqual(this.state.query, prevState.query) || + !_.isEqual(this.state.dateRange, prevState.dateRange) || + this.props.refreshAngularDiscover !== prevProps.refreshAngularDiscover + ) { + this.setState({ pageIndex: 0, tsUpdated: Date.now() }); + if (!_.isEqual(this.props.shareFilterManager, this.state.searchBarFilters)) { + this.setState({ + columns: this.getColumns(), + searchBarFilters: this.props.shareFilterManager || [], + }); //initial columns + } + return; + } + if ( + ['pageIndex', 'pageSize', 'sortField', 'sortDirection'].some( + (field) => this.state[field] !== prevState[field] + ) || + this.state.tsUpdated !== prevState.tsUpdated + ) { + try { + await this.getAlerts(); + } catch (error) { + const options = { + context: `${Discover.name}.componentDidUpdate`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + } - hideCreateCustomLabel = () => { - try { - const button = document.querySelector(".wz-discover #addFilterPopover > div > button > span > span"); - if (!button) return setTimeout(this.hideCreateCustomLabel, 100); - const findAndHide = () => { - const switcher = document.querySelector("#filterEditorCustomLabelSwitch") - if (!switcher) return setTimeout(findAndHide, 100); - switcher.parentElement.style.display = "none" + getColumns() { + if (this.props.currentAgentData.id) { + return this.props.initialColumns.filter( + (column) => !['agent.id', 'agent.name'].includes(column) + ); + } else { + const columns = [...this.props.initialColumns]; + columns.splice(2, 0, 'agent.id'); + columns.splice(3, 0, 'agent.name'); + return columns; } - button.onclick = findAndHide; - } catch (error) { } - } + } - filtersAsArray(filters) { - const keys = Object.keys(filters); - const result: {}[] = []; - for (var i = 0; i < keys.length; i++) { - const item = {}; - item[keys[i]] = filters[keys[i]]; - result.push(item); + async getIndexPattern() { + this.indexPattern = { + ...(await this.KibanaServices.indexPatterns.get(AppState.getCurrentPattern())), + }; + const fields: IFieldType[] = []; + Object.keys(this.indexPattern.fields).forEach((item) => { + if (isNaN(item)) { + fields.push(this.indexPattern.fields[item]); + } else if ( + this.props.includeFilters && + this.indexPattern.fields[item].name.includes(this.props.includeFilters) + ) { + fields.unshift(this.indexPattern.fields[item]); + } else { + fields.push(this.indexPattern.fields[item]); + } + }); + this.indexPattern.fields = fields; } - return result; - } - toggleDetails = item => { - const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMap[item._id]) { - delete itemIdToExpandedRowMap[item._id]; - this.setState({ itemIdToExpandedRowMap }); - } else { - const newItemIdToExpandedRowMap = {}; - newItemIdToExpandedRowMap[item._id] = ( - (<div style={{ width: "100%" }}> <RowDetails item={item} addFilter={(filter) => this.addFilter(filter)} addFilterOut={(filter) => this.addFilterOut(filter)} toggleColumn={(id) => this.addColumn(id)} /></div>) - ); - this.setState({ itemIdToExpandedRowMap: newItemIdToExpandedRowMap }); + hideCreateCustomLabel = () => { + try { + const button = document.querySelector( + '.wz-discover #addFilterPopover > div > button > span > span' + ); + if (!button) return setTimeout(this.hideCreateCustomLabel, 100); + const findAndHide = () => { + const switcher = document.querySelector('#filterEditorCustomLabelSwitch'); + if (!switcher) return setTimeout(findAndHide, 100); + switcher.parentElement.style.display = 'none'; + }; + button.onclick = findAndHide; + } catch (error) { + const options = { + context: `${Discover.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); + } + }; + + filtersAsArray(filters) { + const keys = Object.keys(filters); + const result: {}[] = []; + for (var i = 0; i < keys.length; i++) { + const item = {}; + item[keys[i]] = filters[keys[i]]; + result.push(item); + } + return result; } - }; - - buildFilter() { - const dateParse = ds => /\d+-\d+-\d+T\d+:\d+:\d+.\d+Z/.test(ds) ? DateMatch.parse(ds).toDate().getTime() : ds; - const { query } = this.state; - const { hideManagerAlerts } = this.wazuhConfig.getConfig(); - const extraFilters = []; - if (hideManagerAlerts) extraFilters.push({ - meta: { - alias: null, - disabled: false, - key: 'agent.id', - negate: true, - params: { query: '000' }, - type: 'phrase', - index: this.indexPattern.title - }, - query: { match_phrase: { 'agent.id': '000' } }, - $state: { store: 'appState' } - }); - - const filters = this.props.shareFilterManager ? this.props.shareFilterManager.getFilters() : []; - const previousFilters = this.KibanaServices && this.KibanaServices.query.filterManager.getFilters() || []; - const elasticQuery = - buildEsQuery( + + toggleDetails = (item) => { + const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; + if (itemIdToExpandedRowMap[item._id]) { + delete itemIdToExpandedRowMap[item._id]; + this.setState({ itemIdToExpandedRowMap }); + } else { + const newItemIdToExpandedRowMap = {}; + newItemIdToExpandedRowMap[item._id] = ( + <div style={{ width: '100%' }}> + {' '} + <RowDetails + item={item} + addFilter={(filter) => this.addFilter(filter)} + addFilterOut={(filter) => this.addFilterOut(filter)} + toggleColumn={(id) => this.addColumn(id)} + /> + </div> + ); + this.setState({ itemIdToExpandedRowMap: newItemIdToExpandedRowMap }); + } + }; + + buildFilter() { + const dateParse = (ds) => + /\d+-\d+-\d+T\d+:\d+:\d+.\d+Z/.test(ds) ? DateMatch.parse(ds).toDate().getTime() : ds; + const { query } = this.state; + const { hideManagerAlerts } = this.wazuhConfig.getConfig(); + const extraFilters = []; + if (hideManagerAlerts) + extraFilters.push({ + meta: { + alias: null, + disabled: false, + key: 'agent.id', + negate: true, + params: { query: '000' }, + type: 'phrase', + index: this.indexPattern.title, + }, + query: { match_phrase: { 'agent.id': '000' } }, + $state: { store: 'appState' }, + }); + + const filters = this.props.shareFilterManager + ? this.props.shareFilterManager.getFilters() + : []; + const previousFilters = + (this.KibanaServices && this.KibanaServices.query.filterManager.getFilters()) || []; + const elasticQuery = buildEsQuery( undefined, query, - _.union(previousFilters, filters,extraFilters, this.props.shareFilterManagerWithUserAuthorized || []), + _.union( + previousFilters, + filters, + extraFilters, + this.props.shareFilterManagerWithUserAuthorized || [] + ), getEsQueryConfig(getUiSettings()) ); - const { sortField, sortDirection } = this.state; + const { sortField, sortDirection } = this.state; - const range = { - range: { - timestamp: { - gte: dateParse(this.state.dateRange.from), - lte: dateParse(this.state.dateRange.to), - format: 'epoch_millis' - } + const range = { + range: { + timestamp: { + gte: dateParse(this.state.dateRange.from), + lte: dateParse(this.state.dateRange.to), + format: 'epoch_millis', + }, + }, + }; + elasticQuery.bool.must.push(range); + + if (this.props.implicitFilters) { + this.props.implicitFilters.map((impicitFilter) => + elasticQuery.bool.must.push({ + match: impicitFilter, + }) + ); } + if (this.props.currentAgentData.id) { + elasticQuery.bool.must.push({ + match: { 'agent.id': this.props.currentAgentData.id }, + }); + } + return { + query: elasticQuery, + size: this.state.pageSize, + from: this.state.pageIndex * this.state.pageSize, + ...(sortField ? { sort: { [sortField]: { order: sortDirection } } } : {}), + }; } - elasticQuery.bool.must.push(range); - if(this.props.implicitFilters){ - this.props.implicitFilters.map(impicitFilter => elasticQuery.bool.must.push({ - match: impicitFilter - })); - }; - if(this.props.currentAgentData.id){ - elasticQuery.bool.must.push({ - match: {"agent.id": this.props.currentAgentData.id} - }); - }; - return { - query: elasticQuery, - size: this.state.pageSize, - from: this.state.pageIndex*this.state.pageSize, - ...(sortField ? {sort: { [sortField]: { "order": sortDirection } }}: {}) - }; - } - - async getAlerts() { - if (!this.indexPattern || this.state.isLoading) return; - //compare filters so we only make a request into Elasticsearch if needed - const newFilters = this.buildFilter(); - try { - this.setState({ isLoading: true}); - const alerts = await GenericRequest.request( - 'POST', - `/elastic/alerts`, - { - index: this.indexPattern.title, - body: newFilters - } - ); + async getAlerts() { + if (!this.indexPattern || this.state.isLoading) return; + //compare filters so we only make a request into Elasticsearch if needed + const newFilters = this.buildFilter(); + try { + this.setState({ isLoading: true }); + const alerts = await GenericRequest.request('POST', `/elastic/alerts`, { + index: this.indexPattern.title, + body: newFilters, + }); if (this._isMount) { - this.setState({ alerts: alerts.data.hits.hits, total: alerts.data.hits.total.value, isLoading: false, requestFilters: newFilters}); + this.setState({ + alerts: alerts.data.hits.hits, + total: alerts.data.hits.total.value, + isLoading: false, + requestFilters: newFilters, + }); this.props.updateTotalHits(alerts.data.hits.total.value); } - } catch (err) { - if (this._isMount) { - this.setState({ alerts: [], total: 0, isLoading: false, requestFilters: newFilters}); - this.props.updateTotalHits(0); + } catch (error) { + if (this._isMount) { + this.setState({ alerts: [], total: 0, isLoading: false, requestFilters: newFilters }); + this.props.updateTotalHits(0); + } + throw error; } } - } - removeColumn(id) { - if (this.state.columns.length < 2) { - this.showToast('warning', "At least one column must be selected", 3000); - return; + removeColumn(id) { + if (this.state.columns.length < 2) { + this.showToast('warning', 'At least one column must be selected', 3000); + return; + } + const columns = this.state.columns; + columns.splice( + columns.findIndex((v) => v === id), + 1 + ); + this.setState(columns); } - const columns = this.state.columns; - columns.splice(columns.findIndex(v => v === id), 1); - this.setState(columns) - } - addColumn(id) { - if (this.state.columns.length > 11) { - this.showToast('warning', 'The maximum number of columns is 10', 3000); - return; - } - if (this.state.columns.find(element => element === id)) { - this.removeColumn(id); - return; + addColumn(id) { + if (this.state.columns.length > 11) { + this.showToast('warning', 'The maximum number of columns is 10', 3000); + return; + } + if (this.state.columns.find((element) => element === id)) { + this.removeColumn(id); + return; + } + const columns = this.state.columns; + columns.push(id); + this.setState(columns); } - const columns = this.state.columns; - columns.push(id); - this.setState(columns) - } - - columns = () => { - var columnsList = [...this.state.columns]; - const columns = columnsList.map((item) => { - if (item === "icon") { - return { - width: "25px", - isExpander: true, - render: item => { - return ( - <EuiIcon size="s" type={this.state.itemIdToExpandedRowMap[item._id] ? "arrowDown" : "arrowRight"} /> - ) - }, + columns = () => { + var columnsList = [...this.state.columns]; + const columns = columnsList.map((item) => { + if (item === 'icon') { + return { + width: '25px', + isExpander: true, + render: (item) => { + return ( + <EuiIcon + size="s" + type={this.state.itemIdToExpandedRowMap[item._id] ? 'arrowDown' : 'arrowRight'} + /> + ); + }, + }; } - } - if (item === "timestamp") { - return { - field: 'timestamp', - name: 'Time', - width: '10%', + if (item === 'timestamp') { + return { + field: 'timestamp', + name: 'Time', + width: '10%', + sortable: true, + render: (time) => { + return <span>{formatUIDate(time)}</span>; + }, + }; + } + let width = false; + let link = false; + const arrayCompilance = [ + 'rule.pci_dss', + 'rule.gdpr', + 'rule.nist_800_53', + 'rule.tsc', + 'rule.hipaa', + ]; + + if (item === 'agent.id') { + link = (ev, x) => { + AppNavigate.navigateToModule(ev, 'agents', { tab: 'welcome', agent: x }); + }; + width = '8%'; + } + if (item === 'agent.name') { + width = '12%'; + } + if (item === 'rule.level') { + width = '7%'; + } + if (item === 'rule.id') { + link = (ev, x) => + AppNavigate.navigateToModule(ev, 'manager', { tab: 'rules', redirectRule: x }); + width = '9%'; + } + if (item === 'rule.description' && columnsList.indexOf('syscheck.event') === -1) { + width = '30%'; + } + if (item === 'syscheck.event') { + width = '15%'; + } + if (item === 'rule.mitre.id') { + link = (ev, x, e) => this.props.openIntelligence(e, 'techniques', x); + } + if (arrayCompilance.indexOf(item) !== -1) { + width = '30%'; + } + + let column = { + field: item, + name: ( + <span + onMouseEnter={() => { + this.setState({ hover: item }); + }} + onMouseLeave={() => { + this.setState({ hover: '' }); + }} + style={{ display: 'inline-flex' }} + > + {this.nameEquivalences[item] || item}{' '} + {this.state.hover === item && ( + <EuiToolTip position="top" content={`Remove column`}> + <EuiButtonIcon + style={{ paddingBottom: 12, marginBottom: '-10px', paddingTop: 0 }} + onClick={(e) => { + this.removeColumn(item); + e.stopPropagation(); + }} + iconType="cross" + aria-label="Filter" + iconSize="s" + /> + </EuiToolTip> + )} + </span> + ), sortable: true, - render: time => { - return <span>{formatUIDate(time)}</span> - }, + }; + + if (width) { + column.width = width; + } + if ( + (link && item !== 'rule.mitre.id') || + (item === 'rule.mitre.id' && this.props.shareFilterManager) + ) { + column.render = (itemValue) => { + return ( + <span> + {(item === 'agent.id' && itemValue === '000' && ( + <span style={{ fontSize: 14, marginLeft: 8 }}>{itemValue}</span> + )) || + (item === 'rule.mitre.id' && + Array.isArray(itemValue) && + itemValue.map((currentItem) => ( + <EuiButtonEmpty + onClick={(ev) => { + ev.stopPropagation(); + }} + onMouseDown={(ev) => { + ev.stopPropagation(); + link(ev, currentItem); + }} + > + {currentItem} + </EuiButtonEmpty> + ))) || ( + <EuiButtonEmpty + onClick={(ev) => { + ev.stopPropagation(); + }} + onMouseDown={(ev) => { + ev.stopPropagation(); + link(ev, itemValue); + }} + > + {itemValue} + </EuiButtonEmpty> + )} + </span> + ); + }; } - } - let width = false; - let link = false; - const arrayCompilance = ["rule.pci_dss", "rule.gdpr", "rule.nist_800_53", "rule.tsc", "rule.hipaa"]; - if(item === 'agent.id') { - link = (ev,x) => {AppNavigate.navigateToModule(ev,'agents', {"tab": "welcome", "agent": x } )}; - width = '8%'; - } - if(item === 'agent.name') { - width = '12%'; - } - if(item === 'rule.level') { - width = '7%'; - } - if(item === 'rule.id') { - link = (ev,x) => AppNavigate.navigateToModule(ev,'manager', {tab:'rules', redirectRule: x}); - width = '9%'; - } - if (item === 'rule.description' && columnsList.indexOf('syscheck.event') === -1) { - width = '30%'; - } - if(item === 'syscheck.event') { - width = '15%'; - } - if (item === 'rule.mitre.id') { - link = (ev, x, e) => this.props.openIntelligence(e,'techniques',x); - } - if(arrayCompilance.indexOf(item) !== -1) { - width = '30%'; - } + return column; + }); + return columns; + }; - let column = { - field: item, - name: (<span - onMouseEnter={() => { this.setState({ hover: item }) }} - onMouseLeave={() => { this.setState({ hover: "" }) }} - style={{ display: "inline-flex" }}>{this.nameEquivalences[item] || item} {this.state.hover === item && - <EuiToolTip position="top" content={`Remove column`}> - <EuiButtonIcon - style={{ paddingBottom: 12, marginBottom: "-10px", paddingTop: 0 }} - onClick={(e) => { this.removeColumn(item); e.stopPropagation(); }} - iconType="cross" - aria-label="Filter" - iconSize="s" - /> - </EuiToolTip>} - </span>), - sortable: true - } + onTableChange = ({ page = {}, sort = {} }) => { + const { index: pageIndex, size: pageSize } = page; + const { field: sortField, direction: sortDirection } = sort; - if (width) { - column.width = width; - } - if (link && item !== 'rule.mitre.id' || (item === 'rule.mitre.id' && this.props.shareFilterManager)) { - column.render = itemValue => { - return <span> - {(item === 'agent.id' && itemValue === '000') && - <span style={{ fontSize: 14, marginLeft: 8 }}>{itemValue}</span> - || item === 'rule.mitre.id' && Array.isArray(itemValue) && - itemValue.map(currentItem => <EuiButtonEmpty - onClick={(ev) => { ev.stopPropagation(); }} - onMouseDown={(ev) => { ev.stopPropagation(); link(ev, currentItem) }}> - {currentItem} - </EuiButtonEmpty>) - || - <EuiButtonEmpty - onClick={(ev) => { ev.stopPropagation(); }} - onMouseDown={(ev) => { ev.stopPropagation(); link(ev, itemValue) }}> - {itemValue} - </EuiButtonEmpty> - } - </span> - } + this.setState({ + pageIndex, + pageSize, + sortField, + sortDirection, + }); + }; + + getFiltersAsObject(filters) { + var result = {}; + for (var i = 0; i < filters.length; i++) { + result = { ...result, ...filters[i] }; } + return result; + } - return column; - }) - return columns; - } + /** + * Adds a new negated filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} + * @param filter + */ + addFilterOut(filter) { + const filterManager = this.props.shareFilterManager; + const key = Object.keys(filter)[0]; + const value = filter[key]; + const valuesArray = Array.isArray(value) ? [...value] : [value]; + valuesArray.map((item) => { + const formattedFilter = buildPhraseFilter( + { name: key, type: 'string' }, + item, + this.indexPattern + ); + formattedFilter.meta.negate = true; - onTableChange = ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - - this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection, - }); - }; - - getFiltersAsObject(filters) { - var result = {}; - for (var i = 0; i < filters.length; i++) { - result = { ...result, ...filters[i] } + filterManager.addFilters(formattedFilter); + }); + this.setState({ pageIndex: 0, tsUpdated: Date.now() }); } - return result; - } - - /** - * Adds a new negated filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} - * @param filter - */ - addFilterOut(filter) { - const filterManager = this.props.shareFilterManager; - const key = Object.keys(filter)[0]; - const value = filter[key]; - const valuesArray = Array.isArray(value) ? [...value] : [value]; - valuesArray.map((item) => { - const formattedFilter = buildPhraseFilter({ name: key, type: "string" }, item, this.indexPattern); - formattedFilter.meta.negate = true; - - filterManager.addFilters(formattedFilter); - }) - this.setState({ pageIndex: 0 , tsUpdated: Date.now()}); - } - /** - * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} - * @param filter - */ - addFilter(filter) { - const filterManager = this.props.shareFilterManager; - const key = Object.keys(filter)[0]; - const value = filter[key]; - const valuesArray = Array.isArray(value) ? [...value] : [value]; - valuesArray.map((item) => { - const formattedFilter = buildPhraseFilter({ name: key, type: "string" }, item, this.indexPattern); - if (formattedFilter.meta.key === 'manager.name' || formattedFilter.meta.key === 'cluster.name') { - formattedFilter.meta["removable"] = false; - } - filterManager.addFilters(formattedFilter); - }) - this.setState({ pageIndex: 0 , tsUpdated: Date.now()}); - } + /** + * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} + * @param filter + */ + addFilter(filter) { + const filterManager = this.props.shareFilterManager; + const key = Object.keys(filter)[0]; + const value = filter[key]; + const valuesArray = Array.isArray(value) ? [...value] : [value]; + valuesArray.map((item) => { + const formattedFilter = buildPhraseFilter( + { name: key, type: 'string' }, + item, + this.indexPattern + ); + if ( + formattedFilter.meta.key === 'manager.name' || + formattedFilter.meta.key === 'cluster.name' + ) { + formattedFilter.meta['removable'] = false; + } + filterManager.addFilters(formattedFilter); + }); + this.setState({ pageIndex: 0, tsUpdated: Date.now() }); + } - onQuerySubmit = (payload: { dateRange: TimeRange, query: Query | undefined }) => { - this.setState({...payload, tsUpdated: Date.now()}); - } + onQuerySubmit = (payload: { dateRange: TimeRange; query: Query | undefined }) => { + this.setState({ ...payload, tsUpdated: Date.now() }); + }; - onFiltersUpdated = (filters: Filter[]) => { - this.setState({ pageIndex: 0 , tsUpdated: Date.now()}); - } + onFiltersUpdated = (filters: Filter[]) => { + this.setState({ pageIndex: 0, tsUpdated: Date.now() }); + }; - closeMitreFlyout = () => { - this.setState({showMitreFlyout: false}); - } + closeMitreFlyout = () => { + this.setState({ showMitreFlyout: false }); + }; - onMitreChangeFlyout = (showMitreFlyout: boolean) => { - this.setState({ showMitreFlyout }); - } + onMitreChangeFlyout = (showMitreFlyout: boolean) => { + this.setState({ showMitreFlyout }); + }; - openDiscover(e, techniqueID) { - AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "discover", filters: { 'rule.mitre.id': techniqueID } }) - } + 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 } }) - } + openDashboard(e, techniqueID) { + AppNavigate.navigateToModule(e, 'overview', { + tab: 'mitre', + tabView: 'dashboard', + filters: { 'rule.mitre.id': techniqueID }, + }); + } - render() { - if (this.state.isLoading) - return (<div style={{ alignSelf: "center", minHeight: 400 }}><EuiLoadingContent lines={3} /> </div>) - const { total, itemIdToExpandedRowMap, } = this.state; - const { query = this.state.query } = this.props; - const getRowProps = item => { - const { _id } = item; - return { - 'data-test-subj': `row-${_id}`, - className: 'customRowClass', - onClick: () => this.toggleDetails(item), + render() { + if (this.state.isLoading) + return ( + <div style={{ alignSelf: 'center', minHeight: 400 }}> + <EuiLoadingContent lines={3} />{' '} + </div> + ); + const { total, itemIdToExpandedRowMap } = this.state; + const { query = this.state.query } = this.props; + const getRowProps = (item) => { + const { _id } = item; + return { + 'data-test-subj': `row-${_id}`, + className: 'customRowClass', + onClick: () => this.toggleDetails(item), + }; }; - }; - const columns = this.columns(); + const columns = this.columns(); const sorting: EuiTableSortingType<{}> = { sort: { @@ -652,4 +805,4 @@ export const Discover = compose( {flyout} </div>); } -}) +); diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index c6ffc7d5ed..55eef4103a 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -17,9 +17,11 @@ 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/kibana_react/public'; +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 class Events extends Component { intervalCheckExistsDiscoverTableTime: number = 200; @@ -175,9 +177,9 @@ export class Events extends Component { this.setState({ isRefreshing: false }); this.reloadToast() - } catch (err) { + } catch (error) { this.setState({ isRefreshing: false }); - this.errorToast(err); + throw error; } } else if (this.state.isRefreshing) { await new Promise(r => setTimeout(r, 150)); @@ -221,7 +223,19 @@ export class Events extends Component { // It is a details table row this.enhanceDiscoverTableRowDetailsAddObserver(mutationElement, discoverRowsData, options); } - }catch(error){}; + }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) => { diff --git a/public/components/common/modules/main.tsx b/public/components/common/modules/main.tsx index 275abffa09..9f0e853d2d 100644 --- a/public/components/common/modules/main.tsx +++ b/public/components/common/modules/main.tsx @@ -29,6 +29,9 @@ import { MainModuleOverview } from './main-overview'; import store from '../../../redux/store'; import { compose } from 'redux'; import { withReduxProvider,withErrorBoundary } from '../hocs'; +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 MainModule = compose( withErrorBoundary, @@ -130,9 +133,20 @@ export const MainModule = compose( await this.startVis2PngByAgent(); $vizBackground.css('background-color', defaultVizBackground); $labels.css('color', defaultTextColor); - } catch (e) { + } catch (error) { $labels.css('color', defaultTextColor); $vizBackground.css('background-color', defaultVizBackground); + const options = { + context: `${MainModule.name}.startReport`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error generating the report', + }, + }; + getErrorOrchestrator().handleError(options); this.setState({ loadingReport: false }); } } else { diff --git a/public/components/common/search/fieldsearch-delay.tsx b/public/components/common/search/fieldsearch-delay.tsx index 999fd53565..8bef78ae68 100644 --- a/public/components/common/search/fieldsearch-delay.tsx +++ b/public/components/common/search/fieldsearch-delay.tsx @@ -11,6 +11,9 @@ */ import React, { useEffect, useRef, useState } from 'react'; import { EuiFieldSearch } from '@elastic/eui'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; type WzFieldSearchProps = { delay?: number @@ -52,6 +55,17 @@ export const WzFieldSearchDelay = ({ delay = 400, onChange, onSearch, onError, . await onSearch(searchValue); }catch(error){ onError && onError(error); + const options = { + context: `${WzFieldSearchDelay.name}.onSearchInput`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } setLoading(false); } diff --git a/public/components/management/groups/multiple-agent-selector.tsx b/public/components/management/groups/multiple-agent-selector.tsx index aab5fde185..1e0274d0dc 100644 --- a/public/components/management/groups/multiple-agent-selector.tsx +++ b/public/components/management/groups/multiple-agent-selector.tsx @@ -20,6 +20,9 @@ import './multiple-agent-selector.scss' import $ from 'jquery'; import { WzFieldSearchDelay } from '../../common/search'; import { withErrorBoundary } from '../../common/hocs'; +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 MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelector extends Component { constructor(props) { @@ -68,10 +71,19 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec load: false }); } catch (error) { - ErrorHandler.handle(error, 'Error loading agents'); - this.setState({ - load: false - }); + const options = { + context: `${MultipleAgentSelector.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error loading agents', + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ load: false}); + } } @@ -140,7 +152,7 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec } } } catch (error) { - ErrorHandler.handle(error, 'Error fetching all available agents'); + throw new Error('Error fetching all available agents'); } } @@ -193,8 +205,7 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec }) } } catch (error) { - ErrorHandler.handle(error, 'Error fetching group agents'); - throw error; + throw new Error('Error fetching group agents'); } this.setState({ selectedAgents: { @@ -297,9 +308,19 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec } this.setState({ savingChanges: false }); this.props.cancelButton(); - } catch (err) { + } catch (error) { this.setState({ savingChanges: false }); - ErrorHandler.handle(err, 'Error applying changes'); + const options = { + context: `${MultipleAgentSelector.name}.saveAddAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error applying changes`, + }, + }; + getErrorOrchestrator().handleError(options); } return; } @@ -363,7 +384,17 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec try { await this.loadAllAgents(searchTerm, start); } catch (error) { - ErrorHandler.handle(error, 'Error fetching all available agents'); + const options = { + context: `${MultipleAgentSelector.name}.reload`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error fetching all available agents', + }, + }; + getErrorOrchestrator().handleError(options); }; }; if (!this.state.availableAgents.loadedAll) { @@ -401,7 +432,17 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec try { await this.loadSelectedAgents(searchTerm); } catch (error) { - ErrorHandler.handle(error, 'Error fetching all selected agents'); + const options = { + context: `${MultipleAgentSelector.name}.reload`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error fetching group agents', + }, + }; + getErrorOrchestrator().handleError(options); } } } @@ -497,9 +538,7 @@ export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelec this.setState({ availableFilter: searchValue, availableItem: [] }); }} onSearch={async searchValue => { - try { - await this.reload("left", searchValue, true); - } catch (error) { } + await this.reload("left", searchValue, true); }} isClearable={true} fullWidth={true} From d6f5ae1b0edac25e720bc4489e20515cd2a8a8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Tue, 20 Jul 2021 22:41:16 +0200 Subject: [PATCH 103/493] Implement try catch react-services (#3436) * Implement try catch react-services * Changes requested * Change severity to show the toast * Add changelog * Solve comments * Changed all context value of all try-catch implementations (#3432) * refactor(error-orchestrator): Changed all context value of all try-catch implementations * docs(error-orchestrator): Updated changelog. * Implement try catch strategy in Groups (#3415) * Implement try catch groups * Add test * test(groups): Added simple snapshot test. * Add changelog * Change context * Change title Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> * Create Cypress Workflow for 4.3-7.10 (#3444) * Fixed dispatch for updateCurrentAgentData (#3453) * fix(syscollector): Fixed dispatch for updateCurrentAgentData * fix(syscollector): Refactor for agents-sections. * Fixing and updating unit-tests. (#3413) * test(unit-test): Fixing and updating unit-tests. * test(scheduler-job): Fixing suite scheduler-job tests. * test(unit-test): Fixing updating unit-tests. * test(check-result): Fixing uTest of check-result, added error case. * test(unit-test): Skipped uTest with dependency of API. * test(prettier): Applied prettier, redux-mock-store dependency * test(jest): Jest config without dependencies. * test(git-actions): Added workflow for uTest. * test(check-result): Update workflow. * test(git-actions): Updated workflow * test(gitactions): Dependencies for unit test. * test(gitactions): Added coverage-comment * test(gitactions): Changed text to text-summary * test(gitactions): Show summary * test(gitactions): Show summary + added dependencies * test(gitactions): Typo * test(gitactions): Porcents * test(gitactions): Typo * test(gitactions): Testing node version with yarn * test(gitactions): Update * test(gitactions): Update * test(gitactions): testing coverage summary * test(gitactions): testing coverage summary * test(gitactions): fix param github-token * test(gitactions): update * test(gitactions): testing report.json * test(gitactions): final test. * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing kibana dependencies * test(actions): testing * test(actions): testing * test(actions): test with old config jest. * test(actions): set node version for jest * test(actions): testing with bootstrap * test(actions): rollback dependencies * test(actions): rollback * test(groups-main): fixing snapshot test * test(groups-main): added coverage * test(groups-main): fix path * test(groups-main): fix path * test(git-actions): fix script * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * test(git-actions): fix checkout * fix(syscollector): Refactor for agents-sections. * fix(syscollector): Clean files. * fix(syscollector): Clean files. * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test * fix(syscollector): test comment coverage * fix(syscollector): test comment coverage * fix(syscollector): test comment coverage * fix(actions): test comment coverage * Create Cypress Workflow for 4.3-7.10 (#3444) * Fixed dispatch for updateCurrentAgentData (#3453) * fix(syscollector): Fixed dispatch for updateCurrentAgentData * fix(syscollector): Refactor for agents-sections. * fix(actions): add const env Co-authored-by: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> * Added try-catch strategy in Reporting section (#3427) * Implemented new try-catch strategy * Updated context * Added test file and snapshot * Updated CHANGELOG * Requested changes * Removed blank space in message * Updated error title and message Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> * Implement/try catch in Components > Overview (#3442) * Techniques and mitre * Resources * Update changelog * Refactor try catch in Management > Statistics (#3429) * Added error handling implementation * Changed wrong var err * Draft statistics test and snapshot * Updated CHANGELOG * Updating title and message error Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> * Refactor try catch in Management > Configuration (#3451) * Added new try-catch strategy in Management > Configuration * Updated CHANGELOG * Requested changes * doc(changelog): update Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> * Updating test. (#3470) * test(unittest): Updating test. * test(unittest): Updating test. * feat(orchestratorError): Fixed some options for orchestrator and added on app-state * Xpack and odfe tests * Fix test * Add copyright * fix(discover): fix conflict Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> Co-authored-by: Matias Ezequiel Moreno <49887871+matiasmoreno876@users.noreply.github.com> Co-authored-by: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> --- CHANGELOG.md | 1 + .../common/modules/discover/discover.tsx | 2 +- public/controllers/agent/agents.js | 1 - public/react-services/action-agents.js | 252 ++++++++++-------- public/react-services/app-state.js | 138 +++++++--- public/react-services/check-daemons-status.js | 1 + public/react-services/group-handler.js | 45 ++-- .../react-services/load-app-config.service.ts | 20 +- public/react-services/reporting.js | 78 +++--- public/react-services/saved-objects.js | 113 ++++---- public/react-services/vis-factory-handler.js | 112 ++++---- public/react-services/wz-agents.ts | 41 ++- public/react-services/wz-authentication.ts | 87 +++--- .../wz-security-opendistro.test.js | 46 ++++ .../react-services/wz-security-xpack.test.js | 88 ++++++ 15 files changed, 662 insertions(+), 363 deletions(-) create mode 100644 public/react-services/wz-security-opendistro.test.js create mode 100644 public/react-services/wz-security-xpack.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 39d24f7298..52056fdb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to the Wazuh app project will be documented in this file. [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) + [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index ab7e564f69..4f7daec002 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -805,4 +805,4 @@ export const Discover = compose( {flyout} </div>); } -); +}); diff --git a/public/controllers/agent/agents.js b/public/controllers/agent/agents.js index 3ef8a9d62b..deb3f32a9d 100644 --- a/public/controllers/agent/agents.js +++ b/public/controllers/agent/agents.js @@ -461,7 +461,6 @@ export class AgentsController { }); } this.$scope.tabView = subtab; - return; } catch (error) { throw new Error(error); } diff --git a/public/react-services/action-agents.js b/public/react-services/action-agents.js index d3f4baebe7..18f40eb3b9 100644 --- a/public/react-services/action-agents.js +++ b/public/react-services/action-agents.js @@ -10,7 +10,10 @@ * Find more information about this on the LICENSE file. */ import { WzRequest } from './wz-request'; -import { getToasts } from '../kibana-services'; +import { getToasts } from '../kibana-services'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; +import { getErrorOrchestrator } from './common-services'; export class ActionAgents { static showToast = (color, title, text, time) => { @@ -18,7 +21,7 @@ export class ActionAgents { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; @@ -26,34 +29,54 @@ export class ActionAgents { * Upgrade unique agent to the latest version avaible. * @param {Number} agentId */ - static upgradeAgent(agentId) { - WzRequest.apiReq('PUT', `/agents/${agentId}/upgrade`, { - force: 1 - }) - .then(() => { - console.log('Upgrading'); - }) - .catch(error => { - error !== 'Wazuh API error: 3021 - Timeout executing API request' - ? this.showToast('danger', 'Error upgrading agent', error, 5000) - : false; + static async upgradeAgent(agentId) { + try { + await WzRequest.apiReq('PUT', `/agents/${agentId}/upgrade`, { + force: 1, }); - this.showToast('success', 'Upgrading agent...', '', 5000); + this.showToast('success', 'Upgrading agent...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.upgradeAgent`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error upgrading the agent`, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** * Upgrade list of agents to the latest version avaible. * @param {Array} selectedItems */ - static upgradeAgents(selectedItems) { + static async upgradeAgents(selectedItems) { for (let item of selectedItems.filter( - item => item.outdated && item.status !== 'Disconnected' + (item) => item.outdated && item.status !== 'Disconnected' )) { - WzRequest.apiReq('PUT', `/agents/${item.id}/upgrade`, '1') - .then(() => {}) - .catch(error => {}); + try { + await WzRequest.apiReq('PUT', `/agents/${item.id}/upgrade`, '1'); + this.showToast('success', 'Upgrading selected agents...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.upgradeAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error upgrading selected agents`, + }, + }; + getErrorOrchestrator().handleError(options); + } } - this.showToast('success', 'Upgrading selected agents...', '', 5000); } /** @@ -61,125 +84,144 @@ export class ActionAgents { * @param {Array} selectedItems * @param {String} managerVersion */ - static upgradeAllAgents(selectedItems, managerVersion) { - selectedItems.forEach(agent => { + static async upgradeAllAgents(selectedItems, managerVersion) { + selectedItems.forEach(async (agent) => { if ( agent.id !== '000' && this.compareVersions(agent.version, managerVersion) === true && agent.status === 'active' ) { - WzRequest.apiReq('PUT', `/agents/${agent.id}/upgrade`, '1') - .then(() => {}) - .catch(error => {}); + try { + await WzRequest.apiReq('PUT', `/agents/${agent.id}/upgrade`, '1'); + this.showToast('success', 'Upgrading all agents...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.upgradeAllAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error upgrading all the agents`, + }, + }; + getErrorOrchestrator().handleError(options); + } } }); - this.showToast('success', 'Upgrading all agents...', '', 5000); } /** * Restart an agent * @param {Number} agentId */ - static restartAgent(agentId) { - WzRequest.apiReq('PUT', `/agents/restart`, { ids: agentId }) - .then(value => { - value.status === 200 - ? this.showToast('success', 'Restarting agent...', '', 5000) - : this.showToast('warning', 'Error restarting agent', '', 5000); - }) - .catch(error => { - this.showToast('danger', 'Error restarting agent', error, 5000); - }); + static async restartAgent(agentId) { + try { + await WzRequest.apiReq('PUT', `/agents/restart`, { ids: agentId }); + this.showToast('success', 'Restarting agent...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.restartAgent`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error restarting agent`, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** * Restart a list of agents * @param {Array} selectedItems */ - static restartAgents(selectedItems) { - const agentsId = selectedItems.map(item => item.id); - - WzRequest.apiReq('PUT', `/agents/restart`, { ids: [...agentsId] }) - .then(value => { - value.status === 200 - ? this.showToast('success', 'Restarting selected agents...', '', 5000) - : this.showToast( - 'warning', - 'Error restarting selected agents', - '', - 5000 - ); - }) - .catch(error => { - this.showToast( - 'danger', - 'Error restarting selected agents', - error, - 5000 - ); - }); + static async restartAgents(selectedItems) { + const agentsId = selectedItems.map((item) => item.id); + try { + await WzRequest.apiReq('PUT', `/agents/restart`, { ids: [...agentsId] }); + this.showToast('success', 'Restarting selected agents...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.restartAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error restarting selected agents`, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** * Restart a list of agents * @param {Array} selectedItems */ - static restartAllAgents(selectedItems) { + static async restartAllAgents(selectedItems) { let idAvaibleAgents = []; - selectedItems.forEach(agent => { + selectedItems.forEach((agent) => { if (agent.id !== '000' && agent.status === 'active') { idAvaibleAgents.push(agent.id); } }); - WzRequest.apiReq('PUT', `/agents/restart`, { ids: [...idAvaibleAgents] }) - .then(value => { - value.status === 200 - ? this.showToast('success', 'Restarting all agents...', '', 5000) - : this.showToast('warning', 'Error restarting all agents.', '', 5000); - }) - .catch(error => { - this.showToast('danger', 'Error restarting all agents.', error, 5000); - }); + try { + await WzRequest.apiReq('PUT', `/agents/restart`, { ids: [...idAvaibleAgents] }); + this.showToast('success', 'Restarting all agents...', '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.restartAllAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error restarting all agents`, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** * Delete an agent * @param {Number} selectedItems */ - static deleteAgents(selectedItems) { + static async deleteAgents(selectedItems) { const auxAgents = selectedItems - .map(agent => { + .map((agent) => { return agent.id !== '000' ? agent.id : null; }) - .filter(agent => agent !== null); - WzRequest.apiReq('DELETE', `/agents`, { - purge: true, - ids: auxAgents, - older_than: '1s' - }) - .then(value => { - value.status === 200 - ? this.showToast( - 'success', - `Selected agents were successfully deleted`, - '', - 5000 - ) - : this.showToast( - 'warning', - `Failed to delete selected agents`, - '', - 5000 - ); - }) - .catch(error => { - this.showToast( - 'danger', - `Failed to delete selected agents`, - error, - 5000 - ); + .filter((agent) => agent !== null); + try { + await WzRequest.apiReq('DELETE', `/agents`, { + purge: true, + ids: auxAgents, + older_than: '1s', }); + this.showToast('success', `Selected agents were successfully deleted`, '', 5000); + } catch (error) { + const options = { + context: `${ActionAgents.name}.deleteAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Failed to delete selected agents`, + }, + }; + getErrorOrchestrator().handleError(options); + } } /** @@ -190,12 +232,12 @@ export class ActionAgents { * @returns {Boolean} */ static compareVersions(managerVersion, agentVersion) { - let agentMatch = new RegExp( - /[.+]?v(?<version>\d+)\.(?<minor>\d+)\.(?<path>\d+)/ - ).exec(agentVersion); - let managerMatch = new RegExp( - /[.+]?v(?<version>\d+)\.(?<minor>\d+)\.(?<path>\d+)/ - ).exec(managerVersion); + let agentMatch = new RegExp(/[.+]?v(?<version>\d+)\.(?<minor>\d+)\.(?<path>\d+)/).exec( + agentVersion + ); + let managerMatch = new RegExp(/[.+]?v(?<version>\d+)\.(?<minor>\d+)\.(?<path>\d+)/).exec( + managerVersion + ); if (agentMatch === null || managerMatch === null) return; return managerMatch[1] <= agentMatch[1] ? managerMatch[2] <= agentMatch[2] diff --git a/public/react-services/app-state.js b/public/react-services/app-state.js index 1fa87b1761..a4a5fc9b00 100644 --- a/public/react-services/app-state.js +++ b/public/react-services/app-state.js @@ -22,6 +22,9 @@ import { CSVRequest } from '../services/csv-request'; import { getToasts, getCookies, getAngularModule } from '../kibana-services'; import * as FileSaver from '../services/file-saver'; import { WzAuthentication } from './wz-authentication'; +import { UI_ERROR_SEVERITIES, UIErrorLog, UIErrorSeverity, UILogLevel } from './error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { getErrorOrchestrator } from './common-services'; export class AppState { @@ -74,6 +77,17 @@ export class AppState { } } } catch (error) { + const options = { + context: `${AppState.name}.getExtensions`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); throw error; } }; @@ -91,10 +105,19 @@ export class AppState { }); const updateExtension = updateExtensions(id,extensions); store.dispatch(updateExtension); - } catch (err) { - console.log('Error set extensions'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.setExtensions`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error set extensions`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } }; @@ -107,10 +130,19 @@ export class AppState { ? decodeURI(getCookies().get('clusterInfo')) : false; return clusterInfo ? JSON.parse(clusterInfo) : {}; - } catch (err) { - console.log('Error get cluster info'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.getClusterInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error get cluster info`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } } @@ -129,10 +161,19 @@ export class AppState { path: window.location.pathname }); } - } catch (err) { - console.log('Error set cluster info'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.setClusterInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error set cluster info`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } } @@ -149,10 +190,19 @@ export class AppState { expires: exp, path: window.location.pathname }); - } catch (err) { - console.log('Error set createdAt date'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.setCreatedAt`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error set createdAt date`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } } @@ -165,10 +215,19 @@ export class AppState { ? decodeURI(getCookies().get('createdAt')) : false; return createdAt ? createdAt : false; - } catch (err) { - console.log('Error get createdAt date'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.getCreatedAt`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error get createdAt date`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } } @@ -211,12 +270,23 @@ export class AppState { const updateApiMenu = updateCurrentApi(JSON.parse(API).id); store.dispatch(updateApiMenu); WzAuthentication.refresh(); - } catch (err) {} + } catch (error) { + throw error; + } } - } catch (err) { - console.log('Error set current API'); - console.log(err); - throw err; + } catch (error) { + const options = { + context: `${AppState.name}.setCurrentAPI`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.UI, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error set current API`, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } } @@ -393,14 +463,18 @@ export class AppState { FileSaver.saveAs(blob, fileName); } catch (error) { - getToasts().add({ - color: 'success', - title: 'CSV', - text: 'Error generating CSV', - toastLifeTimeMs: 4000, - }); + const options = { + context: `${AppState.name}.downloadCsv`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error generating CSV`, + }, + }; + getErrorOrchestrator().handleError(options); } - return; } static checkCookies() { diff --git a/public/react-services/check-daemons-status.js b/public/react-services/check-daemons-status.js index 3727eb0839..19c4302842 100644 --- a/public/react-services/check-daemons-status.js +++ b/public/react-services/check-daemons-status.js @@ -40,6 +40,7 @@ export class CheckDaemonsStatus { } } catch (error) { store.dispatch(updateWazuhNotReadyYet('Wazuh could not be recovered.')); + throw error; } busy = false; } diff --git a/public/react-services/group-handler.js b/public/react-services/group-handler.js index 6f014aa594..b73542ec55 100644 --- a/public/react-services/group-handler.js +++ b/public/react-services/group-handler.js @@ -14,66 +14,51 @@ import { WzRequest } from './wz-request'; export class GroupHandler { static async removeGroup(group) { try { - const result = await WzRequest.apiReq( - 'DELETE', - `/agents/groups/${group}`, - {} - ); + const result = await WzRequest.apiReq('DELETE', `/agents/groups/${group}`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; //TODO remove } } static async removeAgentFromGroup(group, agentId) { try { - const result = await WzRequest.apiReq( - 'DELETE', - `/agents/${agentId}/group/${group}`, - {} - ); + const result = await WzRequest.apiReq('DELETE', `/agents/${agentId}/group/${group}`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; //TODO remove } } static async addAgentToGroup(group, agentId) { try { - const result = await WzRequest.apiReq( - 'PUT', - `/agents/${agentId}/group/${group}`, - {} - ); + const result = await WzRequest.apiReq('PUT', `/agents/${agentId}/group/${group}`, {}); return result; } catch (error) { - return Promise.reject(error); + throw error; } } static async sendConfiguration(group, content) { try { - const result = await WzRequest.apiReq( - 'POST', - `/agents/groups/${group}/files/agent.conf`, - { content, origin: 'xmleditor' } - ); + const result = await WzRequest.apiReq('POST', `/agents/groups/${group}/files/agent.conf`, { + content, + origin: 'xmleditor', + }); return result; } catch (error) { - return Promise.reject(error); + throw error //TODO remove } } static async createGroup(name) { try { - const result = await WzRequest.apiReq( - 'PUT', - `/agents/groups`, - {content: {group_id: name}} - ); + const result = await WzRequest.apiReq('PUT', `/agents/groups`, { + content: { group_id: name }, + }); return result; } catch (error) { - return Promise.reject(error); + throw error; } } } diff --git a/public/react-services/load-app-config.service.ts b/public/react-services/load-app-config.service.ts index 8e09d9b6e1..4fbdceb8c4 100644 --- a/public/react-services/load-app-config.service.ts +++ b/public/react-services/load-app-config.service.ts @@ -17,7 +17,9 @@ import { setAppConfigHasError, updateAppConfig, } from '../redux/actions/appConfigActions'; - +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; +import { getErrorOrchestrator } from './common-services'; /** * Retunrs the wazuh app config @@ -32,10 +34,20 @@ export const loadAppConfig = async () => { } const ymlContent = config.data.data; - store.dispatch(updateAppConfig(ymlContent)) + store.dispatch(updateAppConfig(ymlContent)); } catch (error) { store.dispatch(setAppConfigHasError()); - console.error('Error parsing wazuh.yml, using default values.'); // eslint-disable-line - console.error(error.message || error); // eslint-disable-line + const options = { + context: 'loadAppConfig', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error parsing wazuh.yml, using default values.`, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/react-services/reporting.js b/public/react-services/reporting.js index ed2326f1e5..2508a7a7db 100644 --- a/public/react-services/reporting.js +++ b/public/react-services/reporting.js @@ -17,8 +17,11 @@ import { GenericRequest } from '../react-services/generic-request'; import { Vis2PNG } from '../factories/vis2png'; import { RawVisualizations } from '../factories/raw-visualizations'; import { VisHandlers } from '../factories/vis-handlers'; -import { getToasts } from '../kibana-services'; +import { getToasts } from '../kibana-services'; import { getAngularModule } from '../kibana-services'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; +import { getErrorOrchestrator } from './common-services'; const app = getAngularModule(); export class ReportingService { @@ -35,7 +38,7 @@ export class ReportingService { color: color, title: title, text: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; @@ -45,11 +48,9 @@ export class ReportingService { } removeAgentStatusVis(idArray) { - const monitoringEnabled = this.wazuhConfig.getConfig()[ - 'wazuh.monitoring.enabled' - ]; + const monitoringEnabled = this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']; if (!monitoringEnabled) { - const visArray = idArray.filter(vis => { + const visArray = idArray.filter((vis) => { return vis !== 'Wazuh-App-Overview-General-Agents-status'; }); return visArray; @@ -69,17 +70,13 @@ export class ReportingService { this.vis2png.clear(); - const rawVisualizations = this.rawVisualizations - .getList() - .filter(this.removeTableVis); + const rawVisualizations = this.rawVisualizations.getList().filter(this.removeTableVis); let idArray = []; if (tab === 'general') { - idArray = this.removeAgentStatusVis( - rawVisualizations.map(item => item.id) - ); + idArray = this.removeAgentStatusVis(rawVisualizations.map((item) => item.id)); } else { - idArray = rawVisualizations.map(item => item.id); + idArray = rawVisualizations.map((item) => item.id); } for (const item of idArray) { @@ -87,14 +84,12 @@ export class ReportingService { this.vis2png.assignHTMLItem(item, tmpHTMLElement); } - const appliedFilters = await this.visHandlers.getAppliedFilters( - syscollectorFilters - ); + const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); const array = await this.vis2png.checkArray(idArray); - const name = `wazuh-${ - agents ? `agent-${agents}` : 'overview' - }-${tab}-${(Date.now() / 1000) | 0}.pdf`; + const name = `wazuh-${agents ? `agent-${agents}` : 'overview'}-${tab}-${ + (Date.now() / 1000) | 0 + }.pdf`; const browserTimezone = moment.tz.guess(true); @@ -109,10 +104,11 @@ export class ReportingService { tab, section: agents ? 'agents' : 'overview', agents, - browserTimezone + browserTimezone, }; - const apiEndpoint = tab === 'syscollector' ? `/reports/agents/${agents}/inventory` : `/reports/modules/${tab}`; + const apiEndpoint = + tab === 'syscollector' ? `/reports/agents/${agents}/inventory` : `/reports/modules/${tab}`; await GenericRequest.request('POST', apiEndpoint, data); this.$rootScope.reportBusy = false; @@ -128,7 +124,18 @@ export class ReportingService { } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; - this.showToast('danger', 'Error', error.message || error, 4000); + const options = { + context: `${ReportingService.name}.startVis2Png`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error creating the report`, + }, + }; + getErrorOrchestrator().handleError(options); } } @@ -138,24 +145,20 @@ export class ReportingService { this.$rootScope.reportStatus = 'Generating PDF document...'; this.$rootScope.$applyAsync(); - const docType = - type === 'agentConfig' - ? `wazuh-agent-${obj.id}` - : `wazuh-group-${obj.name}`; + const docType = type === 'agentConfig' ? `wazuh-agent-${obj.id}` : `wazuh-group-${obj.name}`; const name = `${docType}-configuration-${(Date.now() / 1000) | 0}.pdf`; const browserTimezone = moment.tz.guess(true); const data = { name, - filters: [ - type === 'agentConfig' ? { agent: obj.id } : { group: obj.name } - ], + filters: [type === 'agentConfig' ? { agent: obj.id } : { group: obj.name }], tab: type, browserTimezone, - components + components, }; - const apiEndpoint = type === 'agentConfig' ? `/reports/agents/${obj.id}` : `/reports/groups/${obj.name}`; + const apiEndpoint = + type === 'agentConfig' ? `/reports/agents/${obj.id}` : `/reports/groups/${obj.name}`; await GenericRequest.request('POST', apiEndpoint, data); this.$rootScope.reportBusy = false; @@ -171,8 +174,19 @@ export class ReportingService { } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; - this.showToast('danger', 'Error configuring report', error.message || error, 4000); + const options = { + context: `${ReportingService.name}.startConfigReport`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: `Error configuring report`, + }, + }; this.$rootScope.$applyAsync(); + getErrorOrchestrator().handleError(options); } } } diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index acf286650f..00811ee448 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -10,15 +10,15 @@ * Find more information about this on the LICENSE file. */ -import {GenericRequest} from './generic-request'; -import {KnownFields} from '../utils/known-fields'; -import {FieldsStatistics} from '../utils/statistics-fields'; -import {FieldsMonitoring} from '../utils/monitoring-fields'; +import { GenericRequest } from './generic-request'; +import { KnownFields } from '../utils/known-fields'; +import { FieldsStatistics } from '../utils/statistics-fields'; +import { FieldsMonitoring } from '../utils/monitoring-fields'; import { HEALTH_CHECK, WAZUH_INDEX_TYPE_ALERTS, WAZUH_INDEX_TYPE_MONITORING, - WAZUH_INDEX_TYPE_STATISTICS + WAZUH_INDEX_TYPE_STATISTICS, } from '../../common/constants'; export class SavedObject { @@ -34,7 +34,7 @@ export class SavedObject { ); return ((result || {}).data || {}).saved_objects || []; } catch (error) { - return ((error || {}).data || {}).message || false + throw ((error || {}).data || {}).message || false ? error.data.message : error.message || error; } @@ -73,19 +73,14 @@ export class SavedObject { } static validateIndexPatterns(list) { - const requiredFields = [ - 'timestamp', - 'rule.groups', - 'manager.name', - 'agent.id', - ]; + const requiredFields = ['timestamp', 'rule.groups', 'manager.name', 'agent.id']; - return list.filter(item => { + return list.filter((item) => { if (item.attributes && item.attributes.fields) { const fields = JSON.parse(item.attributes.fields); - return requiredFields.every((reqField => { - return fields.find(field => field.name === reqField); - })); + return requiredFields.every((reqField) => { + return fields.find((field) => field.name === reqField); + }); } return false; }); @@ -101,8 +96,8 @@ export class SavedObject { { attributes: { title: patternID, - timeFieldName: 'timestamp' - } + timeFieldName: 'timestamp', + }, }, fields ); @@ -123,7 +118,9 @@ export class SavedObject { return result.data; } catch (error) { if (error && error.response && error.response.status == 404) return false; - return ((error || {}).data || {}).message || false ? error.data.message : error.message || false; + return ((error || {}).data || {}).message || false + ? error.data.message + : error.message || false; } } @@ -146,7 +143,7 @@ export class SavedObject { status: true, statusCode: 200, title, - fields + fields, }; } } catch (error) { @@ -179,21 +176,17 @@ export class SavedObject { try { // same logic as Kibana when a new index is created, you need to refresh it to see its fields // we force the refresh of the index by requesting its fields and the assign these fields - await GenericRequest.request( - 'PUT', - `/api/saved_objects/index-pattern/${id}`, - { - attributes: { - fields: JSON.stringify(fields), - timeFieldName: 'timestamp', - title: title, - retry_on_conflict: 4, - }, - } - ); + await GenericRequest.request('PUT', `/api/saved_objects/index-pattern/${id}`, { + attributes: { + fields: JSON.stringify(fields), + timeFieldName: 'timestamp', + title: title, + retry_on_conflict: 4, + }, + }); return; } catch (error) { - return ((error || {}).data || {}).message || false + throw ((error || {}).data || {}).message || false ? error.data.message : error.message || error; } @@ -207,7 +200,7 @@ export class SavedObject { try { const fields = await SavedObject.getIndicesFields(pattern.title, WAZUH_INDEX_TYPE_ALERTS); - if(newFields && typeof newFields=="object") + if (newFields && typeof newFields == 'object') Object.keys(newFields).forEach((fieldName) => { if (this.isValidField(newFields[fieldName])) fields.push(newFields[fieldName]); }); @@ -216,7 +209,6 @@ export class SavedObject { return; } catch (error) { - console.log(error) return ((error || {}).data || {}).message || false ? error.data.message : error.message || error; @@ -228,12 +220,18 @@ export class SavedObject { * @param {index-pattern-field} field */ static isValidField(field) { + if (field == null || typeof field != 'object') return false; - if (field == null || typeof field != "object") return false; - - const isValid = ["name", "type", "esTypes", "searchable", "aggregatable", "readFromDocValues"].reduce((ok, prop) => { + const isValid = [ + 'name', + 'type', + 'esTypes', + 'searchable', + 'aggregatable', + 'readFromDocValues', + ].reduce((ok, prop) => { return ok && Object.keys(field).includes(prop); - }, true) + }, true); return isValid; } @@ -255,8 +253,8 @@ export class SavedObject { "data.vulnerability.reference":{"id":"url"}, "data.url":{"id":"url"} }`, - sourceFilters: '[{"value":"@timestamp"}]' - } + sourceFilters: '[{"value":"@timestamp"}]', + }, }, fields ); @@ -268,19 +266,24 @@ export class SavedObject { } } - static getIndicesFields = async (pattern, indexType) => GenericRequest.request( - //we check if indices exist before creating the index pattern - 'GET', - `/api/index_patterns/_fields_for_wildcard?pattern=${pattern}&meta_fields=_source&meta_fields=_id&meta_fields=_type&meta_fields=_index&meta_fields=_score`, - {} - ).then(response => response.data.fields).catch(() => { - switch (indexType) { - case WAZUH_INDEX_TYPE_MONITORING: - return FieldsMonitoring; - case WAZUH_INDEX_TYPE_STATISTICS: - return FieldsStatistics; - case WAZUH_INDEX_TYPE_ALERTS: - return KnownFields + static getIndicesFields = async (pattern, indexType) => { + try { + const response = await GenericRequest.request( + //we check if indices exist before creating the index pattern + 'GET', + `/api/index_patterns/_fields_for_wildcard?pattern=${pattern}&meta_fields=_source&meta_fields=_id&meta_fields=_type&meta_fields=_index&meta_fields=_score`, + {} + ); + return response.data.fields; + } catch { + switch (indexType) { + case WAZUH_INDEX_TYPE_MONITORING: + return FieldsMonitoring; + case WAZUH_INDEX_TYPE_STATISTICS: + return FieldsStatistics; + case WAZUH_INDEX_TYPE_ALERTS: + return KnownFields; + } } - }) + }; } diff --git a/public/react-services/vis-factory-handler.js b/public/react-services/vis-factory-handler.js index ba2ce740bb..3f4a190ea1 100644 --- a/public/react-services/vis-factory-handler.js +++ b/public/react-services/vis-factory-handler.js @@ -1,4 +1,3 @@ - /* * Wazuh app - Vis factory handler service * Copyright (C) 2015-2021 Wazuh, Inc. @@ -21,6 +20,9 @@ import { GenericRequest } from './generic-request'; import store from '../redux/store'; import { updateVis } from '../redux/actions/visualizationsActions'; import { getAngularModule } from '../kibana-services'; +import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; +import { getErrorOrchestrator } from './common-services'; export class VisFactoryHandler { /** @@ -59,29 +61,38 @@ export class VisFactoryHandler { static async buildOverviewVisualizations(filterHandler, tab, subtab, fromDiscover = false) { const rawVisualizations = new RawVisualizations(); //if(rawVisualizations.getType() !== 'general'){ - rawVisualizations.setType('general'); - const $injector = getAngularModule().$injector; - const commonData = $injector.get('commonData'); - - try { - const currentPattern = AppState.getCurrentPattern(); - const data = - tab !== 'sca' - ? await GenericRequest.request( - 'GET', - `/elastic/visualizations/overview-${tab}/${currentPattern}` - ) - : false; - data && rawVisualizations.assignItems(data.data.raw); - if(!fromDiscover){ - commonData.assignFilters(filterHandler, tab); - } - store.dispatch(updateVis({ update: true, raw: rawVisualizations.getList() })); - return; - } catch (error) { - return Promise.reject(error); + rawVisualizations.setType('general'); + const $injector = getAngularModule().$injector; + const commonData = $injector.get('commonData'); + + try { + const currentPattern = AppState.getCurrentPattern(); + const data = + tab !== 'sca' + ? await GenericRequest.request( + 'GET', + `/elastic/visualizations/overview-${tab}/${currentPattern}` + ) + : false; + data && rawVisualizations.assignItems(data.data.raw); + if (!fromDiscover) { + commonData.assignFilters(filterHandler, tab); } - //} + store.dispatch(updateVis({ update: true, raw: rawVisualizations.getList() })); + } catch (error) { + const options = { + context: `${VisFactoryHandler.name}.buildOverviewVisualizations`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; + } } /** @@ -92,30 +103,39 @@ export class VisFactoryHandler { * @param {*} localChange * @param {*} id */ - static async buildAgentsVisualizations(filterHandler, tab, subtab, id, fromDiscover = false){ + static async buildAgentsVisualizations(filterHandler, tab, subtab, id, fromDiscover = false) { const rawVisualizations = new RawVisualizations(); - // if (rawVisualizations.getType() !== 'agents') { - rawVisualizations.setType('agents'); - const $injector = getAngularModule().$injector; - const commonData = $injector.get('commonData'); + // if (rawVisualizations.getType() !== 'agents') { + rawVisualizations.setType('agents'); + const $injector = getAngularModule().$injector; + const commonData = $injector.get('commonData'); - try { - const data = - tab !== 'sca' - ? await GenericRequest.request( - 'GET', - `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` - ) - : false; - data && rawVisualizations.assignItems(data.data.raw); - if(!fromDiscover){ - commonData.assignFilters(filterHandler, tab, id); - } - store.dispatch(updateVis({ update: true })); - return; - } catch (error) { - return Promise.reject(error); + try { + const data = + tab !== 'sca' + ? await GenericRequest.request( + 'GET', + `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` + ) + : false; + data && rawVisualizations.assignItems(data.data.raw); + if (!fromDiscover) { + commonData.assignFilters(filterHandler, tab, id); } + store.dispatch(updateVis({ update: true })); + } catch (error) { + const options = { + context: `${VisFactoryHandler.name}.buildAgentsVisualizations`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } -// } -} \ No newline at end of file + } +} diff --git a/public/react-services/wz-agents.ts b/public/react-services/wz-agents.ts index 8f00eb6c27..4f3f6f0050 100644 --- a/public/react-services/wz-agents.ts +++ b/public/react-services/wz-agents.ts @@ -16,29 +16,29 @@ import { UnsupportedComponents } from '../utils/components-os-support'; import IApiResponse from './interfaces/api-response.interface'; import { WzRequest } from './wz-request'; -export function getAgentOSType(agent){ - if(agent?.os?.uname?.toLowerCase().includes(WAZUH_AGENTS_OS_TYPE.LINUX)){ +export function getAgentOSType(agent) { + if (agent?.os?.uname?.toLowerCase().includes(WAZUH_AGENTS_OS_TYPE.LINUX)) { return WAZUH_AGENTS_OS_TYPE.LINUX; - }else if (agent?.os?.platform === WAZUH_AGENTS_OS_TYPE.WINDOWS) { + } else if (agent?.os?.platform === WAZUH_AGENTS_OS_TYPE.WINDOWS) { return WAZUH_AGENTS_OS_TYPE.WINDOWS; } else if (agent?.os?.platform === WAZUH_AGENTS_OS_TYPE.SUNOS) { return WAZUH_AGENTS_OS_TYPE.SUNOS; } else if (agent?.os?.platform === WAZUH_AGENTS_OS_TYPE.DARWIN) { return WAZUH_AGENTS_OS_TYPE.DARWIN; - }else { + } else { return WAZUH_AGENTS_OS_TYPE.OTHERS; } -}; +} -export function hasAgentSupportModule(agent, component){ +export function hasAgentSupportModule(agent, component) { const agentOSType = getAgentOSType(agent); - return !(UnsupportedComponents[agentOSType].includes(component)); -}; + return !UnsupportedComponents[agentOSType].includes(component); +} export async function getAuthorizedAgents() { - try{ + try { const params = { limit: 500 }; - const output: IApiResponse<{id: string}> = await WzRequest.apiReq('GET', `/agents`, {}); + const output: IApiResponse<{ id: string }> = await WzRequest.apiReq('GET', `/agents`, {}); const totalItems = (((output || {}).data || {}).data || {}).total_affected_items; let itemsArray = []; if (totalItems && output.data && output.data.data && totalItems > 500) { @@ -46,23 +46,18 @@ export async function getAuthorizedAgents() { itemsArray.push(...output.data.data.affected_items); while (itemsArray.length < totalItems && params.offset < totalItems) { params.offset += params.limit; - const tmpData: IApiResponse<{id: string}> = await WzRequest.apiReq( - 'GET', - `/agents`, - { params: params }, - ); + const tmpData: IApiResponse<{ id: string }> = await WzRequest.apiReq('GET', `/agents`, { + params: params, + }); itemsArray.push(...tmpData.data.data.affected_items); } const allowedAgents = itemsArray ? itemsArray.map((agent) => agent.id) : []; return allowedAgents; - } - else{ - const allowedAgents = output ? output.data.data.affected_items.map((agent) => agent.id) : [] + } else { + const allowedAgents = output ? output.data.data.affected_items.map((agent) => agent.id) : []; return allowedAgents; } - }catch(error) { - getToasts().addError(error, {title: `Error getting user authorized agents`} as ErrorToastOptions); - return Promise.reject(); - }; + } catch (error) { + throw error; + } } - diff --git a/public/react-services/wz-authentication.ts b/public/react-services/wz-authentication.ts index 549753d5b2..f50512164e 100644 --- a/public/react-services/wz-authentication.ts +++ b/public/react-services/wz-authentication.ts @@ -14,17 +14,24 @@ import { WzRequest } from './wz-request'; import { AppState } from './app-state'; import jwtDecode from 'jwt-decode'; import store from '../redux/store'; -import { updateUserPermissions, updateUserRoles, updateWithUserLogged, updateAllowedAgents } from '../redux/actions/appStateActions'; -import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../common/constants'; +import { + updateUserPermissions, + updateUserRoles, + updateWithUserLogged, + updateAllowedAgents, +} from '../redux/actions/appStateActions'; +import { UI_LOGGER_LEVELS, WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../common/constants'; import { getToasts } from '../kibana-services'; import { getAuthorizedAgents } from '../react-services/wz-agents'; +import { UI_ERROR_SEVERITIES, UIErrorLog, UIErrorSeverity, UILogLevel } from './error-orchestrator/types'; +import { getErrorOrchestrator } from './common-services'; -export class WzAuthentication{ - private static async login(force=false){ - try{ +export class WzAuthentication { + private static async login(force = false) { + try { var idHost = JSON.parse(AppState.getCurrentAPI()).id; - while(!idHost){ - await new Promise(r => setTimeout(r, 500)); + while (!idHost) { + await new Promise((r) => setTimeout(r, 500)); idHost = JSON.parse(AppState.getCurrentAPI()).id; } @@ -32,15 +39,15 @@ export class WzAuthentication{ const token = ((response || {}).data || {}).token; return token as string; - }catch(error){ - return Promise.reject(error); + } catch (error) { + throw error; } } - static async refresh(force = false){ + static async refresh(force = false) { try { // Get user token const token: string = await WzAuthentication.login(force); - if(!token){ + if (!token) { // Remove old existent token await WzAuthentication.deleteExistentToken(); return; @@ -51,53 +58,63 @@ export class WzAuthentication{ // Get user Policies const userPolicies = await WzAuthentication.getUserPolicies(); - + //Get allowed agents for the current user let allowedAgents: any = []; if (WzAuthentication.userHasAgentsPermissions(userPolicies)) { allowedAgents = await getAuthorizedAgents(); allowedAgents = allowedAgents.length ? allowedAgents : ['-1']; // users without read:agent police should not view info about any agent - } + } store.dispatch(updateAllowedAgents(allowedAgents)); - // Dispatch actions to set permissions and roles store.dispatch(updateUserPermissions(userPolicies)); - store.dispatch(updateUserRoles(WzAuthentication.mapUserRolesIDToAdministratorRole(jwtPayload.rbac_roles || []))); + store.dispatch( + updateUserRoles( + WzAuthentication.mapUserRolesIDToAdministratorRole(jwtPayload.rbac_roles || []) + ) + ); store.dispatch(updateWithUserLogged(true)); - }catch(error){ - getToasts().add({ - color: 'danger', - title: 'Error getting the authorization token', - text: error.message || error, - toastLifeTimeMs: 300000 - }); + } catch (error) { + const options: UIErrorLog = { + context: `${WzAuthentication.name}.refresh`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error getting the authorization token`, + }, + }; + getErrorOrchestrator().handleError(options); store.dispatch(updateWithUserLogged(true)); return Promise.reject(error); } } - private static async getUserPolicies(){ - try{ + private static async getUserPolicies() { + try { var idHost = JSON.parse(AppState.getCurrentAPI()).id; - while(!idHost){ - await new Promise(r => setTimeout(r, 500)); + while (!idHost) { + await new Promise((r) => setTimeout(r, 500)); idHost = JSON.parse(AppState.getCurrentAPI()).id; } const response = await WzRequest.apiReq('GET', '/security/users/me/policies', { idHost }); const policies = ((response || {}).data || {}).data || {}; return policies; - }catch(error){ - return Promise.reject(error); + } catch (error) { + throw error; } } - private static mapUserRolesIDToAdministratorRole(roles){ - return roles.map((role: number) => role === WAZUH_ROLE_ADMINISTRATOR_ID ? WAZUH_ROLE_ADMINISTRATOR_NAME : role); + private static mapUserRolesIDToAdministratorRole(roles) { + return roles.map((role: number) => + role === WAZUH_ROLE_ADMINISTRATOR_ID ? WAZUH_ROLE_ADMINISTRATOR_NAME : role + ); } static async deleteExistentToken() { try { - const response = await WzRequest.apiReq('DELETE','/security/user/authenticate', {}); + const response = await WzRequest.apiReq('DELETE', '/security/user/authenticate', {}); return ((response || {}).data || {}).data || {}; } catch (error) { @@ -109,15 +126,17 @@ export class WzAuthentication{ * This function returns true only if the user has some police that need be filtered. * Returns false if the user has permission for all agents. * Returns true if the user has no one police for agent:read. - * @param policies + * @param policies * @returns boolean */ static userHasAgentsPermissions(policies) { const agentReadPolicies = policies['agent:read']; if (agentReadPolicies) { - const allIds = agentReadPolicies['agent:id:*'] == 'allow'; + const allIds = agentReadPolicies['agent:id:*'] == 'allow'; const allGroups = agentReadPolicies['agent:group:*'] == 'allow'; - const denyAgents = Object.keys(agentReadPolicies).some(k => !k.includes('*') && agentReadPolicies[k] == 'deny'); + const denyAgents = Object.keys(agentReadPolicies).some( + (k) => !k.includes('*') && agentReadPolicies[k] == 'deny' + ); return !((allIds || allGroups) && !denyAgents); } // users without read:agent police should not view info about any agent diff --git a/public/react-services/wz-security-opendistro.test.js b/public/react-services/wz-security-opendistro.test.js new file mode 100644 index 0000000000..267c13381a --- /dev/null +++ b/public/react-services/wz-security-opendistro.test.js @@ -0,0 +1,46 @@ +/* + * Wazuh app + * + * Copyright (C) 2015-2021 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 { WzSecurityOpendistro } from './wz-security-opendistro'; + +jest.mock('./generic-request', () => ({ + GenericRequest: { + request: (method, path) => { + return { + data: { + data: { + wazuh: { + hash: '', + reserved: true, + hidden: false, + backend_roles: ['admin'], + attributes: {email: 'wazuh@email.com', full_name: 'wazuh surname'}, + description: 'admin user', + opendistro_security_roles: [], + static: false, + }, + }, + }, + }; + }, + }, +})); +describe('Wazuh Internal Users', () => { + it('Should return the ODFE internal users', async () => { + const users = await WzSecurityOpendistro.getUsers(); + const expected_result = [ + { username: 'wazuh', email: 'wazuh@email.com', full_name: 'wazuh surname', roles: [] }, + ]; + expect(users).toEqual(expected_result); + }); +}); diff --git a/public/react-services/wz-security-xpack.test.js b/public/react-services/wz-security-xpack.test.js new file mode 100644 index 0000000000..018422f923 --- /dev/null +++ b/public/react-services/wz-security-xpack.test.js @@ -0,0 +1,88 @@ +/* + * Wazuh app + * + * Copyright (C) 2015-2021 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 { WzSecurityXpack } from './wz-security-xpack'; + +jest.mock('./generic-request', () => ({ + GenericRequest: { + request: (method, path) => { + return { + data: { + username: 'wazuh_system', + roles: ['kibana_system', 'wazuh'], + full_name: 'wazuh', + email: '', + metadata: {}, + enabled: true, + }, + }; + }, + }, +})); +jest.mock('./wz-request', () => ({ + WzRequest: { + apiReq: (method, path, params) => { + return { + data: { + id: 3, + name: 'agents_all_groups', + policy: { + actions: [ + 'group:read', + 'group:delete', + 'group:update_config', + 'group:modify_assignments', + ], + resources: ['group:id:*'], + effect: 'allow', + }, + roles: [1, 5], + }, + }; + }, + }, +})); +describe('Wazuh Internal Users with X-Pack', () => { + it('Should create a X-Pack policy', async () => { + const users = await WzSecurityXpack.createPolicy(); + const expected_result = { + id: 3, + name: 'agents_all_groups', + policy: { + actions: ['group:read', 'group:delete', 'group:update_config', 'group:modify_assignments'], + resources: ['group:id:*'], + effect: 'allow', + }, + roles: [1, 5], + }; + expect(users).toEqual(expected_result); + }); + it('Should create,edit and delete a X-Pack user and also gets all the X-pack internal users', async () => { + const createUser = await WzSecurityXpack.createUser(); + const users = await WzSecurityXpack.getUsers(); + const editUser = await WzSecurityXpack.editUser(); + const deleteUser = await WzSecurityXpack.deleteUser(); + const expected_result = { + username: 'wazuh_system', + roles: ['kibana_system', 'wazuh'], + full_name: 'wazuh', + email: '', + metadata: {}, + enabled: true, + }; + expect(createUser).toEqual(expected_result); + expect(editUser).toEqual(expected_result); + expect(deleteUser).toEqual(expected_result); + expect(users).toEqual(expected_result); + }); +}); From 3d8b9e9b741becdda9fd49cf13938b40cfe188ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 21 Jul 2021 10:45:59 +0200 Subject: [PATCH 104/493] Removed a console.log from use-query --- public/components/common/hooks/use-query.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/components/common/hooks/use-query.ts b/public/components/common/hooks/use-query.ts index 4c760af02c..d262cb0ed0 100644 --- a/public/components/common/hooks/use-query.ts +++ b/public/components/common/hooks/use-query.ts @@ -28,7 +28,6 @@ export function useQuery(): [ setQuery(scope.state.query); subscription = scope.$watchCollection('fetchStatus', () => { if (!_.isEqual(query, scope.state.query)) { - console.log("QUERY CHANGED") setQuery(scope.state.query); } }); From 2facf0c5d628787814346800d1258608fc04be92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 21 Jul 2021 10:47:54 +0200 Subject: [PATCH 105/493] Fixed issue with useEsSearch due preAppliedFilters not being iterable by default --- public/components/common/hooks/useEsSearch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/hooks/useEsSearch.js b/public/components/common/hooks/useEsSearch.js index 5a7dde392a..241c007a01 100644 --- a/public/components/common/hooks/useEsSearch.js +++ b/public/components/common/hooks/useEsSearch.js @@ -7,7 +7,7 @@ import _ from 'lodash'; You can find more info on how to use the preAppliedAggs object at https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html You can find more info on how to construct a filter object at https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html */ -const useEsSearch = ({ preAppliedFilters = {}, preAppliedAggs = {}, size = 10 }) => { +const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); const filterManager = useFilterManager(); From 61634bd3a4f2aa050b42e8c3fce1200ff90ce36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 21 Jul 2021 10:48:40 +0200 Subject: [PATCH 106/493] Created AggTable Component --- public/components/common/panels/agg_table.js | 54 ++++++++++++++++++++ public/components/common/panels/index.ts | 3 +- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 public/components/common/panels/agg_table.js diff --git a/public/components/common/panels/agg_table.js b/public/components/common/panels/agg_table.js new file mode 100644 index 0000000000..9fa2b75a90 --- /dev/null +++ b/public/components/common/panels/agg_table.js @@ -0,0 +1,54 @@ +import { EuiBasicTable, EuiPanel, EuiTitle } from '@elastic/eui'; +import { useEsSearch } from '../hooks'; +import React from 'react'; + +export const AggTable = ({ + onRowClick = (field, value) => {}, + aggTerm, + aggLabel, + maxRows, + tableTitle, + panelProps, + titleProps +}) => { + const preAppliedAggs = { + buckets: { + terms: { + field: aggTerm, + size: maxRows, + order: { _count: 'desc' }, + }, + }, + }; + const { esResults } = useEsSearch({ preAppliedAggs }); + const buckets = ((esResults.aggregations || {}).buckets || {}).buckets || []; + const columns = [ + { + field: 'key', + name: aggLabel, + }, + { + field: 'doc_count', + name: 'Count', + isExpander: false, + align: 'right', + }, + ]; + const getRowProps = (item) => { + const { key } = item; + return { + 'data-test-subj': `row-${key}`, + onClick: () => { + onRowClick(aggTerm, key); + }, + }; + }; + return ( + <EuiPanel data-test-subj={`${aggTerm}-aggTable`} {...panelProps}> + <EuiTitle {...titleProps}> + <h2>{tableTitle}</h2> + </EuiTitle> + <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} /> + </EuiPanel> + ); +}; diff --git a/public/components/common/panels/index.ts b/public/components/common/panels/index.ts index fe73636ad6..da00f2a759 100644 --- a/public/components/common/panels/index.ts +++ b/public/components/common/panels/index.ts @@ -1 +1,2 @@ -export * from './panel_split'; \ No newline at end of file +export * from './panel_split'; +export { AggTable } from './agg_table'; \ No newline at end of file From 6cefc4ff8bc98bc1caa8d180e173666485a9932a Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 21 Jul 2021 14:33:29 +0200 Subject: [PATCH 107/493] [Fix] Remove URL query param `redirectRule` on CDB Lists/Decoders (#3438) * fix(frontend): Remove URL param redirectRule on Lists and Decoders when clicking on a list/decoder --- CHANGELOG.md | 1 + .../management/components/management/ruleset/ruleset-table.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddebaedd72..ab2cc4cc6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Don't open the main menu when changing the seleted API or index pattern [#3440](https://github.com/wazuh/wazuh-kibana-app/pull/3440) - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed the code overflows over the line numbers in the API Console editor [#3439](https://github.com/wazuh/wazuh-kibana-app/pull/3439) +- Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/public/controllers/management/components/management/ruleset/ruleset-table.js b/public/controllers/management/components/management/ruleset/ruleset-table.js index 7c9f3afd0f..62e3484a5d 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-table.js +++ b/public/controllers/management/components/management/ruleset/ruleset-table.js @@ -286,7 +286,7 @@ class WzRulesetTable extends Component { if (this.isLoading) return; this.setState({ isLoading: true }); const { section } = this.props.state; - window.location.href = `${window.location.href}&redirectRule=${id}`; + section === RulesetResources.RULES && (window.location.href = `${window.location.href}&redirectRule=${id}`); try { if (section === RulesetResources.LISTS) { const result = await this.rulesetHandler.getFileContent(item.filename); From c17d7fd7860254c16e73f5e2ef5fa6dc8b00eba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Mart=C3=ADnez?= <pablo.martinez@wazuh.com> Date: Wed, 21 Jul 2021 14:37:27 +0200 Subject: [PATCH 108/493] Implement/try catch in services (#3478) * Impelement try catch in public/services * Add changelog * doc(changelog): update Co-authored-by: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Co-authored-by: Gabriel Wassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 3 +- public/services/check-daemon-status.js | 1 + public/services/common-data.js | 81 +++++++--------- public/services/config-handler.js | 2 +- public/services/reporting.js | 61 ++++-------- public/services/resolves/api-count.js | 32 +++---- public/services/resolves/check-timestamp.js | 1 + public/services/resolves/get-ip.js | 20 +++- public/services/resolves/get-saved-search.js | 16 +++- public/services/resolves/settings-wizard.js | 99 ++++++-------------- 10 files changed, 133 insertions(+), 183 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2cc4cc6c..ad22998845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,10 +32,9 @@ All notable changes to the Wazuh app project will be documented in this file. [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) - + [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) -- Fixed issue where configuration still asked you to save changes before exiting even after saving [#3460](https://github.com/wazuh/wazuh-kibana-app/pull/3460) ### Changed diff --git a/public/services/check-daemon-status.js b/public/services/check-daemon-status.js index 0d0976655f..558df37dd0 100644 --- a/public/services/check-daemon-status.js +++ b/public/services/check-daemon-status.js @@ -46,6 +46,7 @@ export class CheckDaemonsStatus { this.$rootScope.wazuhNotReadyYet = 'Wazuh could not be recovered.'; this.$rootScope.$applyAsync(); + throw error; } this.busy = false; diff --git a/public/services/common-data.js b/public/services/common-data.js index f7e3f31523..37824c92c0 100644 --- a/public/services/common-data.js +++ b/public/services/common-data.js @@ -11,7 +11,6 @@ */ import { AppState } from '../react-services/app-state'; import { GenericRequest } from '../react-services/generic-request'; -import { ErrorHandler } from '../react-services/error-handler'; import { ShareAgent } from '../factories/share-agent'; import { ModulesHelper } from '../components/common/modules/modules-helper'; import rison from 'rison-node'; @@ -32,7 +31,7 @@ export class CommonData { this.errorHandler = errorHandler; this.$location = $location; this.shareAgent = new ShareAgent(); -// this.globalState = globalState; + // this.globalState = globalState; this.savedTimefilter = null; this.$window = $window; this.$route = $route; @@ -41,14 +40,14 @@ export class CommonData { hostMonitoringTabs: ['general', 'fim', 'aws', 'gcp'], systemAuditTabs: ['pm', 'audit', 'oscap', 'ciscat'], securityTabs: ['vuls', 'virustotal', 'osquery', 'docker', 'mitre'], - complianceTabs: ['pci', 'gdpr', 'hipaa', 'nist', 'tsc'] + complianceTabs: ['pci', 'gdpr', 'hipaa', 'nist', 'tsc'], }; this.agentTabs = { hostMonitoringTabs: ['general', 'fim', 'syscollector'], systemAuditTabs: ['pm', 'audit', 'oscap', 'ciscat', 'sca'], securityTabs: ['vuls', 'virustotal', 'osquery', 'docker', 'mitre'], - complianceTabs: ['pci', 'gdpr', 'hipaa', 'nist', 'tsc'] + complianceTabs: ['pci', 'gdpr', 'hipaa', 'nist', 'tsc'], }; } @@ -78,7 +77,7 @@ export class CommonData { */ removeDuplicateRuleGroups(group) { if (!this.globalState || !this.globalState.filters) return; - const globalRuleGroupFilters = this.globalState.filters.map(item => { + const globalRuleGroupFilters = this.globalState.filters.map((item) => { if ( item.query && item.query.match && @@ -102,7 +101,7 @@ export class CommonData { */ removeDuplicateExists(condition) { if (!this.globalState || !this.globalState.filters) return; - const globalRuleExistsFilters = this.globalState.filters.map(item => { + const globalRuleExistsFilters = this.globalState.filters.map((item) => { if (item.exists && item.exists.field) { return item.exists.field; } @@ -111,10 +110,7 @@ export class CommonData { }); if (globalRuleExistsFilters.includes(condition)) { - this.globalState.filters.splice( - globalRuleExistsFilters.indexOf(condition), - 1 - ); + this.globalState.filters.splice(globalRuleExistsFilters.indexOf(condition), 1); } } @@ -146,16 +142,14 @@ export class CommonData { virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, - docker: { group: 'docker' } + docker: { group: 'docker' }, }; const filters = []; const isCluster = AppState.getClusterInfo().status == 'enabled'; filters.push( filterHandler.managerQuery( - isCluster - ? AppState.getClusterInfo().cluster - : AppState.getClusterInfo().manager, + isCluster ? AppState.getClusterInfo().cluster : AppState.getClusterInfo().manager, isCluster ) ); @@ -200,31 +194,27 @@ export class CommonData { const discoverScope = await ModulesHelper.getDiscoverScope(); discoverScope.loadFilters(filters, tab); } catch (error) { - ErrorHandler.handle( - 'An error occurred while creating custom filters for visualizations', - agent ? 'Agents' : 'Overview', - { warning: true } - ); + throw new Error('An error occurred while creating custom filters for visualizations'); } } removeParam(key, sourceURL) { - var rtn = sourceURL.split("?")[0], - param, - params_arr = [], - queryString = (sourceURL.indexOf("?") !== -1) ? sourceURL.split("?")[1] : ""; - if (queryString !== "") { - params_arr = queryString.split("&"); - for (var i = params_arr.length - 1; i >= 0; i -= 1) { - param = params_arr[i].split("=")[0]; - if (param === key) { - params_arr.splice(i, 1); - } + var rtn = sourceURL.split('?')[0], + param, + params_arr = [], + queryString = sourceURL.indexOf('?') !== -1 ? sourceURL.split('?')[1] : ''; + if (queryString !== '') { + params_arr = queryString.split('&'); + for (var i = params_arr.length - 1; i >= 0; i -= 1) { + param = params_arr[i].split('=')[0]; + if (param === key) { + params_arr.splice(i, 1); } - rtn = rtn + "?" + params_arr.join("&"); + } + rtn = rtn + '?' + params_arr.join('&'); } return rtn; -} + } /** Find the `_w` parameter in the url and return a list of filters if it exists */ @@ -232,7 +222,7 @@ export class CommonData { const { _w } = this.$route.current.params; if (!_w) return []; const { filters } = rison.decode(_w); - window.location.href = this.removeParam('_w', window.location.href) + window.location.href = this.removeParam('_w', window.location.href); return filters || []; } @@ -340,12 +330,11 @@ export class CommonData { duration: '-', inProgress: false, end: data.end || false, - start: data.start || false + start: data.start || false, }; if (result.end && result.start) { - result.duration = - (new Date(result.end) - new Date(result.start)) / 1000 / 60; + result.duration = (new Date(result.end) - new Date(result.start)) / 1000 / 60; result.duration = Math.round(result.duration * 100) / 100; if (result.duration <= 0) { result.inProgress = true; @@ -428,24 +417,24 @@ export class CommonData { return target.hostMonitoringTabs.includes(tab) ? target.hostMonitoringTabs : target.systemAuditTabs.includes(tab) - ? target.systemAuditTabs - : target.securityTabs.includes(tab) - ? target.securityTabs - : target.complianceTabs.includes(tab) - ? target.complianceTabs - : false; + ? target.systemAuditTabs + : target.securityTabs.includes(tab) + ? target.securityTabs + : target.complianceTabs.includes(tab) + ? target.complianceTabs + : false; } getTabsFromCurrentPanel(currentPanel, extensions, tabNames) { - const keyExists = key => Object.keys(extensions).includes(key); - const keyIsTrue = key => (extensions || [])[key]; + const keyExists = (key) => Object.keys(extensions).includes(key); + const keyIsTrue = (key) => (extensions || [])[key]; let tabs = []; - currentPanel.forEach(x => { + currentPanel.forEach((x) => { if (!keyExists(x) || keyIsTrue(x)) { tabs.push({ id: x, - name: tabNames[x] + name: tabNames[x], }); } }); diff --git a/public/services/config-handler.js b/public/services/config-handler.js index fc031d22f8..4df54cfe36 100644 --- a/public/services/config-handler.js +++ b/public/services/config-handler.js @@ -59,7 +59,7 @@ export class ConfigHandler { this.$rootScope.$broadcast('removeRestarting', {}); } catch (error) { this.$rootScope.$broadcast('removeRestarting', {}); - ErrorHandler.handle(error, 'Error restarting cluster'); + throw new Error('Error restarting cluster'); } } diff --git a/public/services/reporting.js b/public/services/reporting.js index 9bdaed1687..87eb110171 100644 --- a/public/services/reporting.js +++ b/public/services/reporting.js @@ -17,13 +17,7 @@ import { GenericRequest } from '../react-services/generic-request'; import { ErrorHandler } from '../react-services/error-handler'; export class ReportingService { - constructor( - $rootScope, - vis2png, - rawVisualizations, - visHandlers, - errorHandler - ) { + constructor($rootScope, vis2png, rawVisualizations, visHandlers, errorHandler) { this.$rootScope = $rootScope; this.vis2png = vis2png; this.rawVisualizations = rawVisualizations; @@ -38,11 +32,9 @@ export class ReportingService { } removeAgentStatusVis(idArray) { - const monitoringEnabled = this.wazuhConfig.getConfig()[ - 'wazuh.monitoring.enabled' - ]; + const monitoringEnabled = this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']; if (!monitoringEnabled) { - const visArray = idArray.filter(vis => { + const visArray = idArray.filter((vis) => { return vis !== 'Wazuh-App-Overview-General-Agents-status'; }); return visArray; @@ -62,17 +54,13 @@ export class ReportingService { this.vis2png.clear(); - const rawVisualizations = this.rawVisualizations - .getList() - .filter(this.removeTableVis); + const rawVisualizations = this.rawVisualizations.getList().filter(this.removeTableVis); let idArray = []; if (tab === 'general') { - idArray = this.removeAgentStatusVis( - rawVisualizations.map(item => item.id) - ); + idArray = this.removeAgentStatusVis(rawVisualizations.map((item) => item.id)); } else { - idArray = rawVisualizations.map(item => item.id); + idArray = rawVisualizations.map((item) => item.id); } for (const item of idArray) { @@ -80,14 +68,12 @@ export class ReportingService { this.vis2png.assignHTMLItem(item, tmpHTMLElement); } - const appliedFilters = await this.visHandlers.getAppliedFilters( - syscollectorFilters - ); + const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); const array = await this.vis2png.checkArray(idArray); - const name = `wazuh-${ - isAgents ? 'agents' : 'overview' - }-${tab}-${(Date.now() / 1000) | 0}.pdf`; + const name = `wazuh-${isAgents ? 'agents' : 'overview'}-${tab}-${ + (Date.now() / 1000) | 0 + }.pdf`; const browserTimezone = moment.tz.guess(true); @@ -102,7 +88,7 @@ export class ReportingService { tab, section: isAgents ? 'agents' : 'overview', isAgents, - browserTimezone + browserTimezone, }; await this.genericReq.request('POST', '/reports', data); @@ -110,16 +96,13 @@ export class ReportingService { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; this.$rootScope.$applyAsync(); - ErrorHandler.info( - 'Success. Go to Wazuh > Management > Reporting', - 'Reporting' - ); + ErrorHandler.info('Success. Go to Wazuh > Management > Reporting', 'Reporting'); return; } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; - ErrorHandler.handle(error.message || error); + throw error; } } @@ -129,10 +112,7 @@ export class ReportingService { this.$rootScope.reportStatus = 'Generating PDF document...'; this.$rootScope.$applyAsync(); - const docType = - type === 'agentConfig' - ? `wazuh-agent-${obj.id}` - : `wazuh-group-${obj.name}`; + const docType = type === 'agentConfig' ? `wazuh-agent-${obj.id}` : `wazuh-group-${obj.name}`; const name = `${docType}-configuration-${(Date.now() / 1000) | 0}.pdf`; const browserTimezone = moment.tz.guess(true); @@ -140,15 +120,13 @@ export class ReportingService { const data = { array: [], name, - filters: [ - type === 'agentConfig' ? { agent: obj.id } : { group: obj.name } - ], + filters: [type === 'agentConfig' ? { agent: obj.id } : { group: obj.name }], time: '', searchBar: '', tables: [], tab: type, browserTimezone, - components + components, }; await this.genericReq.request('POST', '/reports', data); @@ -156,17 +134,14 @@ export class ReportingService { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; this.$rootScope.$applyAsync(); - ErrorHandler.info( - 'Success. Go to Wazuh > Management > Reporting', - 'Reporting' - ); + ErrorHandler.info('Success. Go to Wazuh > Management > Reporting', 'Reporting'); return; } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; - ErrorHandler.handle(error.message || error); this.$rootScope.$applyAsync(); + throw error; } } } diff --git a/public/services/resolves/api-count.js b/public/services/resolves/api-count.js index 38855bac55..3b2dedd90c 100644 --- a/public/services/resolves/api-count.js +++ b/public/services/resolves/api-count.js @@ -21,23 +21,21 @@ import { AppState } from '../../react-services/app-state'; import { GenericRequest } from '../../react-services/generic-request'; -export function apiCount($q, $location) { +export async function apiCount($q, $location) { const deferred = $q.defer(); - GenericRequest.request('GET', '/hosts/apis') - .then(async data => { - if (!data || !data.data || !data.data.length) - throw new Error('No API entries found'); - if (!AppState.getCurrentAPI()) { - await tryToSetDefault(data.data, AppState); - } - deferred.resolve(); - }) - .catch(() => { - $location.search('_a', null); - $location.search('tab', 'api'); - $location.path('/settings'); - deferred.resolve(); - }); + try { + const apis = await GenericRequest.request('GET', '/hosts/apis'); + if (!apis || !apis.data || !apis.data.length) throw new Error('No API entries found'); + if (!AppState.getCurrentAPI()) { + await tryToSetDefault(data.data, AppState); + } + deferred.resolve(); + } catch (error) { + $location.search('_a', null); + $location.search('tab', 'api'); + $location.path('/settings'); + deferred.resolve(); + } return deferred.promise; } @@ -50,7 +48,7 @@ function tryToSetDefault(apis, AppState) { AppState.setCurrentAPI( JSON.stringify({ name: api.cluster_info.manager, - id: api.id + id: api.id, }) ); break; diff --git a/public/services/resolves/check-timestamp.js b/public/services/resolves/check-timestamp.js index c47416abb1..2c895b3dd0 100644 --- a/public/services/resolves/check-timestamp.js +++ b/public/services/resolves/check-timestamp.js @@ -28,5 +28,6 @@ export async function checkTimestamp(genericReq, $location, wzMisc) { wzMisc.setBlankScr(error.message || error); $location.search('tab', null); $location.path('/blank-screen'); + throw error; } } diff --git a/public/services/resolves/get-ip.js b/public/services/resolves/get-ip.js index 3f3ce4c5f5..bb825e6bd6 100644 --- a/public/services/resolves/get-ip.js +++ b/public/services/resolves/get-ip.js @@ -12,11 +12,13 @@ import { healthCheck } from './health-check'; import { AppState } from '../../react-services/app-state'; -import { ErrorHandler } from '../../react-services/error-handler'; import { getDataPlugin, getSavedObjects } from '../../kibana-services'; import { WazuhConfig } from '../../react-services/wazuh-config'; import { GenericRequest } from '../../react-services/generic-request'; import { getWzConfig } from './get-config'; +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 function getIp( $q, @@ -88,10 +90,18 @@ export function getIp( }); } catch (error) { deferred.reject(error); - wzMisc.setBlankScr( - ErrorHandler.handle(error, 'Elasticsearch', { silent: true }) - ); - $location.path('/blank-screen'); + const options = { + context: `${getIp.name}.checkWazuhPatterns`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } }; diff --git a/public/services/resolves/get-saved-search.js b/public/services/resolves/get-saved-search.js index 25ed1ad3f1..c8fc1f8640 100644 --- a/public/services/resolves/get-saved-search.js +++ b/public/services/resolves/get-saved-search.js @@ -19,6 +19,9 @@ import { getSavedObjects, getPlugins, } from '../../kibana-services'; +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 function getSavedSearch($location, $window, $route) { try { @@ -63,6 +66,17 @@ export function getSavedSearch($location, $window, $route) { }); } } catch (error) { - console.error(error); + const options = { + context: `${getSavedSearch.name}`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); } } diff --git a/public/services/resolves/settings-wizard.js b/public/services/resolves/settings-wizard.js index 7167393464..5ff292c27e 100644 --- a/public/services/resolves/settings-wizard.js +++ b/public/services/resolves/settings-wizard.js @@ -30,15 +30,11 @@ export function settingsWizard( try { const wazuhConfig = new WazuhConfig(); const deferred = $q.defer(); - const checkResponse = data => { + const checkResponse = (data) => { let fromWazuhHosts = false; if (parseInt(data.data.error) === 2) { !disableErrors && - ErrorHandler.handle( - 'Please set up Wazuh API credentials.', - '', - { warning: true } - ); + ErrorHandler.handle('Please set up Wazuh API credentials.', '', { warning: true }); } else if ( JSON.stringify(data).includes('socket hang up') || ((data || {}).data || {}).apiIsDown || @@ -81,26 +77,6 @@ export function settingsWizard( deferred.resolve(); }; - const changeCurrentApi = data => { - let currentApi = false; - try { - currentApi = JSON.parse(AppState.getCurrentAPI()).id; - } catch (error) { - // eslint-disable-next-line - console.log(`Error parsing JSON (settingsWizards.changeCurrentApi)`); - } - const clusterInfo = data.data.data.cluster_info; - - // Should change the currentAPI configuration depending on cluster - const str = - clusterInfo.status === 'disabled' - ? JSON.stringify({ name: clusterInfo.manager, id: currentApi }) - : JSON.stringify({ name: clusterInfo.cluster, id: currentApi }); - - AppState.setCurrentAPI(str); - AppState.setClusterInfo(clusterInfo); - }; - const callCheckStored = async () => { const config = wazuhConfig.getConfig(); let currentApi = false; @@ -108,10 +84,7 @@ export function settingsWizard( try { currentApi = JSON.parse(AppState.getCurrentAPI()).id; } catch (error) { - console.log( - 'Error parsing JSON (settingsWizards.callCheckStored 1)', - error - ); + throw Error('Error parsing JSON (settingsWizards.callCheckStored 1)') } const extensions = await AppState.getExtensions(currentApi); if (currentApi && !extensions) { @@ -128,7 +101,7 @@ export function settingsWizard( gcp: config['extensions.gcp'], virustotal: config['extensions.virustotal'], osquery: config['extensions.osquery'], - docker: config['extensions.docker'] + docker: config['extensions.docker'], }; AppState.setExtensions(currentApi, extensions); } @@ -150,7 +123,7 @@ export function settingsWizard( }; // Iterates them in order to set one as default - const tryToSetDefault = async apis => { + const tryToSetDefault = async (apis) => { try { let errors = 0; for (let idx in apis) { @@ -162,7 +135,7 @@ export function settingsWizard( if (api && api.cluster_info && api.cluster_info.manager) { const defaultApi = JSON.stringify({ name: api.cluster_info.manager, - id: id + id: id, }); AppState.setCurrentAPI(defaultApi); callCheckStored(); @@ -176,7 +149,7 @@ export function settingsWizard( AppState.setNavigation({ reloaded: false, discoverPrevious: false, - discoverSections: ['/overview/', '/agents', '/wazuh-dev'] + discoverSections: ['/overview/', '/agents', '/wazuh-dev'], }); throw new Error('Could not select any API entry'); } @@ -188,16 +161,9 @@ export function settingsWizard( }; const currentParams = $location.search(); - const targetedAgent = - currentParams && (currentParams.agent || currentParams.agent === '000'); - const targetedRule = - currentParams && currentParams.tab === 'ruleset' && currentParams.ruleid; - if ( - !targetedAgent && - !targetedRule && - !disableErrors && - healthCheck($window) - ) { + const targetedAgent = currentParams && (currentParams.agent || currentParams.agent === '000'); + const targetedRule = currentParams && currentParams.tab === 'ruleset' && currentParams.ruleid; + if (!targetedAgent && !targetedRule && !disableErrors && healthCheck($window)) { $location.path('/health-check'); deferred.resolve(); } else { @@ -206,23 +172,18 @@ export function settingsWizard( if (!currentApi) { genericReq .request('GET', '/hosts/apis') - .then(async data => { + .then(async (data) => { if (data.data.length > 0) { // Try to set some API entry as default const defaultApi = await tryToSetDefault(data.data); - setUpCredentials( - 'Wazuh App: Default API has been updated.', - defaultApi - ); + setUpCredentials('Wazuh App: Default API has been updated.', defaultApi); $location.path('health-check'); } else { - setUpCredentials( - 'Wazuh App: Please set up Wazuh API credentials.' - ); + setUpCredentials('Wazuh App: Please set up Wazuh API credentials.'); } deferred.resolve(); }) - .catch(error => { + .catch((error) => { !disableErrors && ErrorHandler.handle(error); wzMisc.setWizard(true); if (!$location.path().includes('/settings')) { @@ -236,31 +197,22 @@ export function settingsWizard( const apiId = (JSON.parse(currentApi) || {}).id; genericReq .request('GET', '/hosts/apis') - .then(async data => { - if ( - data.data.length > 0 && - data.data.find(api => api.id == apiId) - ) { + .then(async (data) => { + if (data.data.length > 0 && data.data.find((api) => api.id == apiId)) { callCheckStored(); } else { AppState.removeCurrentAPI(); if (data.data.length > 0) { // Try to set some as default const defaultApi = await tryToSetDefault(data.data); - setUpCredentials( - 'Wazuh App: Default API has been updated.', - defaultApi - ); + setUpCredentials('Wazuh App: Default API has been updated.', defaultApi); $location.path('health-check'); } else { - setUpCredentials( - 'Wazuh App: Please set up Wazuh API credentials.', - false - ); + setUpCredentials('Wazuh App: Please set up Wazuh API credentials.', false); } } }) - .catch(error => { + .catch((error) => { setUpCredentials('Wazuh App: Please set up Wazuh API credentials.'); }); } @@ -268,6 +220,17 @@ export function settingsWizard( AppState.setWzMenu(); return deferred.promise; } catch (error) { - !disableErrors && ErrorHandler.handle(error); + const options = { + context: `${settingsWizard.name}`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.CRITICAL, + store: true, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + !disableErrors && getErrorOrchestrator().handleError(options); } } From 4e78ef4b7d605693a3561ece903daa0bd8c717ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Wed, 21 Jul 2021 14:38:18 +0200 Subject: [PATCH 109/493] Notify when you are registering an agent without permissions (#3430) * fix(frontend_register_agent): Fix when there was an error to get the registration service info and display the callout - Applied the callout to some steps whene there is a error to get the registratrion service info - Changed the callout from warning to danger - Fix not reported errors, when the user has not permissions, some selectors didn't display --- CHANGELOG.md | 7 +-- .../agent/components/register-agent.js | 52 +++++++++++++++---- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad22998845..294406a095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,12 +45,13 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) -- Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) +- Notify when you are registering an agent without permissions [#3430](https://github.com/wazuh/wazuh-kibana-app/pull/3430) +- Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) +- Fixed the code overflows over the line numbers in the API Console editor [#3439](https://github.com/wazuh/wazuh-kibana-app/pull/3439) - Don't open the main menu when changing the seleted API or index pattern [#3440](https://github.com/wazuh/wazuh-kibana-app/pull/3440) - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) -- Fixed the code overflows over the line numbers in the API Console editor [#3439](https://github.com/wazuh/wazuh-kibana-app/pull/3439) -- Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) +- Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index d7dee6d0c6..20ac03f1de 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -135,6 +135,7 @@ export const RegisterAgent = withErrorBoundary( groups: [], selectedGroup: [], udpProtocol: false, + gotErrorRegistrationServiceInfo: false }; this.restartAgentCommand = { rpm: this.systemSelector(), @@ -169,9 +170,6 @@ export const RegisterAgent = withErrorBoundary( serverAddress, needsPassword, hidePasswordInput, - versionButtonsCentos, - architectureButtons, - architectureCentos5, wazuhPassword, udpProtocol, wazuhVersion, @@ -204,6 +202,7 @@ export const RegisterAgent = withErrorBoundary( const result = await WzRequest.apiReq('GET', '/agents/000/config/auth/auth', {}); return (result.data || {}).data || {}; } catch (error) { + this.setState({ gotErrorRegistrationServiceInfo: true }); throw new Error(error); } } @@ -404,10 +403,21 @@ export const RegisterAgent = withErrorBoundary( ); const groupInput = ( + <> + {!this.state.groups.length &&( + <> + <EuiCallOut + color="warning" + title='This section could not be configured because you do not have permission to read groups.' + iconType="iInCircle" + /> + <EuiSpacer /> + </> + )} <EuiText> <p>Select one or more existing groups</p> <EuiComboBox - placeholder="Select group" + placeholder={!this.state.groups.length ? "Default" : "Select group"} options={this.state.groups} selectedOptions={this.state.selectedGroup} onChange={(group) => { @@ -418,6 +428,7 @@ export const RegisterAgent = withErrorBoundary( data-test-subj="demoComboBox" /> </EuiText> + </> ); const passwordInput = ( @@ -469,9 +480,24 @@ export const RegisterAgent = withErrorBoundary( this.selectSYS(selectedTab.id); }; + const calloutErrorRegistrationServiceInfo = this.state.gotErrorRegistrationServiceInfo ? ( + <EuiCallOut + color="danger" + title='This section could not be displayed because you do not have permission to get access to the registration service.' + iconType="iInCircle" + /> + ) : null; + const guide = ( <div> - {this.state.selectedOS && ( + {(this.state.gotErrorRegistrationServiceInfo) ? ( + <EuiCallOut + color="danger" + title='This section could not be displayed because you do not have permission to get access to the registration service.' + iconType="iInCircle" + /> + ) : + this.state.selectedOS && ( <EuiText> <p> You can use this command to install and enroll the Wazuh agent in one or more hosts. @@ -591,7 +617,7 @@ export const RegisterAgent = withErrorBoundary( <EuiButtonGroup color="primary" legend="Choose the architecture" - options={this.state.architectureCentos5} + options={architectureCentos5} idSelected={this.state.selectedArchitecture} onChange={(architecture) => this.setArchitecture(architecture)} /> @@ -608,7 +634,7 @@ export const RegisterAgent = withErrorBoundary( <EuiButtonGroup color="primary" legend="Choose the architecture" - options={this.state.architectureButtons} + options={architectureButtons} idSelected={this.state.selectedArchitecture} onChange={(architecture) => this.setArchitecture(architecture)} /> @@ -634,7 +660,9 @@ export const RegisterAgent = withErrorBoundary( }, { title: 'Install and enroll the agent', - children: missingOSSelection.length ? ( + children: this.state.gotErrorRegistrationServiceInfo ? + calloutErrorRegistrationServiceInfo + : missingOSSelection.length ? ( <EuiCallOut color="warning" title={`Please select the ${missingOSSelection.join(', ')}.`} @@ -648,7 +676,9 @@ export const RegisterAgent = withErrorBoundary( ? [ { title: 'Start the agent', - children: missingOSSelection.length ? ( + children: this.state.gotErrorRegistrationServiceInfo ? + calloutErrorRegistrationServiceInfo + : missingOSSelection.length ? ( <EuiCallOut color="warning" title={`Please select the ${missingOSSelection.join(', ')}.`} @@ -672,7 +702,9 @@ export const RegisterAgent = withErrorBoundary( ? [ { title: 'Start the agent', - children: ( + children: this.state.gotErrorRegistrationServiceInfo ? + calloutErrorRegistrationServiceInfo + : ( <EuiFlexGroup direction="column"> <EuiText> <EuiCodeBlock style={codeBlock} language={language}> From 1749d499a057819bc38e1f1922fe3c35e8d67188 Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Wed, 21 Jul 2021 14:39:57 +0200 Subject: [PATCH 110/493] fix error message in conf managment and add test (#3443) --- CHANGELOG.md | 1 + .../alerts-configurations.test.tsx.snap | 62 +++++++++++++++++++ .../alerts/alerts-configurations.test.tsx | 57 +++++++++++++++++ .../util-components/no-config.js | 4 +- 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 public/controllers/management/components/management/configuration/alerts/__snapshots__/alerts-configurations.test.tsx.snap create mode 100644 public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 294406a095..b7220bd2bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) - Fixed the code overflows over the line numbers in the API Console editor [#3439](https://github.com/wazuh/wazuh-kibana-app/pull/3439) - Don't open the main menu when changing the seleted API or index pattern [#3440](https://github.com/wazuh/wazuh-kibana-app/pull/3440) +- Fix error message in conf managment [#3443](https://github.com/wazuh/wazuh-kibana-app/pull/3443) - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) diff --git a/public/controllers/management/components/management/configuration/alerts/__snapshots__/alerts-configurations.test.tsx.snap b/public/controllers/management/components/management/configuration/alerts/__snapshots__/alerts-configurations.test.tsx.snap new file mode 100644 index 0000000000..eb5dab7a8c --- /dev/null +++ b/public/controllers/management/components/management/configuration/alerts/__snapshots__/alerts-configurations.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WzConfigurationAlerts component mount OK renders correctly to match the snapshot 1`] = ` +<ContextProvider + value={ + Object { + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "subscription": Subscription { + "handleChangeWrapper": [Function], + "listeners": Object { + "notify": [Function], + }, + "onStateChange": [Function], + "parentSub": undefined, + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "unsubscribe": null, + }, + } + } +> + <Connect(WithLoading(Connect(WzConfigurationAlerts))) + agent={ + Object { + "id": "000", + } + } + clusterNodeSelected="master-node" + currentConfig={ + Object { + "analysis-alerts": Object { + "alerts": Object { + "email_alert_level": 12, + "log_alert_level": 3, + }, + }, + "analysis-labels": Object { + "labels": Array [], + }, + "csyslog-csyslog": "Fetch configuration. 3013 - Error connecting with socket", + "mail-alerts": "Fetch configuration. 3013 - Error connecting with socket", + "monitor-reports": Object {}, + } + } + refreshTime={false} + wazuhNotReadyYet="" + /> +</ContextProvider> +`; diff --git a/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx b/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx new file mode 100644 index 0000000000..311992fdb8 --- /dev/null +++ b/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import WzConfigurationAlerts from './alerts'; +import { shallow } from 'enzyme'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; + +jest.mock('../../../../../../kibana-services', () => ({ + getUiSettings:() => ({ + get:() => { + return false + } + }), +})); + +const mockProps = { + "clusterNodeSelected":"master-node", + "agent":{ + "id":"000" + }, + "refreshTime":false, + "currentConfig":{ + "analysis-alerts":{ + "alerts":{ + "email_alert_level":12, + "log_alert_level":3 + } + }, + "analysis-labels":{ + "labels":[ + + ] + }, + "mail-alerts":"Fetch configuration. 3013 - Error connecting with socket", + "monitor-reports":{ + + }, + "csyslog-csyslog":"Fetch configuration. 3013 - Error connecting with socket" + }, + "wazuhNotReadyYet":"" + } + + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('WzConfigurationAlerts component mount OK', () => { + + it('renders correctly to match the snapshot', () => { + const wrapper = shallow( + <Provider store={store}> + <WzConfigurationAlerts {...mockProps} /> + </Provider> + ); + expect(wrapper).toMatchSnapshot(); + }); + +}); \ No newline at end of file diff --git a/public/controllers/management/components/management/configuration/util-components/no-config.js b/public/controllers/management/components/management/configuration/util-components/no-config.js index 07d7d2ab14..4e3d111616 100644 --- a/public/controllers/management/components/management/configuration/util-components/no-config.js +++ b/public/controllers/management/components/management/configuration/util-components/no-config.js @@ -36,9 +36,7 @@ class WzNoConfig extends Component { <EuiFlexItem> <div style={{ textAlign: 'center' }}> <EuiIcon type="help" style={{ marginRight: '4px' }} /> - {(error === 'not-present' && ( <span>Configuration not available</span> - )) || <span>Error fetching configuration</span>} {help && <WzHelpButtonPopover links={help} />} <EuiHorizontalRule margin="s" /> {(error === 'not-present' && ( @@ -46,7 +44,7 @@ class WzNoConfig extends Component { )) || ( <span> There was a problem while fetching the configuration for this - section. + section. It may be a server problem or the configuration doesn't exist. </span> )} <EuiSpacer size="s" /> From c929faec290403f2b0ef4a1fe48b0ec75f52f213 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 21 Jul 2021 14:41:12 +0200 Subject: [PATCH 111/493] [FIX] Index pattern selector doesn't display the ignored index patterns (#3458) * fix(frontend): Fix index pattern selector on menu doesn't ignore the index patterns defined in `ip.ignore` app setting * fix(frontend): Fix problem that set as unsaved changes when selecting a input with array type on Settings > Configuration --- CHANGELOG.md | 1 + .../category/components/field-form.tsx | 19 ++++++++++++------- public/components/wz-menu/wz-menu.js | 13 +++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7220bd2bc..5591927a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix error message in conf managment [#3443](https://github.com/wazuh/wazuh-kibana-app/pull/3443) - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) +- Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx index f93fdfa852..3700e0e257 100644 --- a/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx +++ b/public/components/settings/configuration/components/categories/components/category/components/field-form.tsx @@ -32,6 +32,7 @@ import { } from '../../../../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../../../../../react-services/common-services'; +import _ from 'lodash'; interface IFieldForm { item: ISetting @@ -111,8 +112,8 @@ const ArrayForm: React.FunctionComponent<IFieldForm> = (props) => { const [list, setList] = useState(JSON.stringify(getValue(props))); useEffect(() => { - setList(JSON.stringify(getValue(props))) - }, [props.updatedConfig]) + checkErrors(); + }, [list]); const checkErrors = () => { try { @@ -140,7 +141,7 @@ const ArrayForm: React.FunctionComponent<IFieldForm> = (props) => { width='100%' value={list} onChange={setList} - onBlur={checkErrors} /> + /> ); } @@ -154,10 +155,14 @@ const getValue = ({ item, updatedConfig }: IFieldForm) => typeof updatedConfig[i const onChange = (value: string | number | boolean | [], props: IFieldForm) => { const { updatedConfig, setUpdatedConfig, item } = props; - setUpdatedConfig({ - ...updatedConfig, - [item.setting]: value, - }) + if(!_.isEqual(item.value,value)){ + setUpdatedConfig({ + ...updatedConfig, + [item.setting]: value, + }) + }else{ + deleteChange(props); + } } const deleteChange = (props: IFieldForm) => { diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index dbd020dcb9..4658eae101 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -152,8 +152,9 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { loadIndexPatternsList = async () => { try { - const list = await PatternHandler.getPatternList('api'); + let list = await PatternHandler.getPatternList('api'); if (!list) return; + this.props?.appConfig?.data?.['ip.ignore']?.length && (list = list.filter(indexPattern => !this.props?.appConfig?.data?.['ip.ignore'].includes(indexPattern.title))); // Abort if we have disabled the pattern selector if (!AppState.getPatternSelector()) return; @@ -222,7 +223,9 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { this.setState({ currentAPI: this.props.state.currentAPI }); } } - + if(!_.isEqual(prevProps?.appConfig?.data?.['ip.ignore'], this.props?.appConfig?.data?.['ip.ignore'])){ + this.loadIndexPatternsList(); + } } async load() { @@ -238,8 +241,9 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { if (currentTab !== this.state.currentMenuTab) { this.setState({ currentMenuTab: currentTab, hover: currentTab }); } - const list = await PatternHandler.getPatternList('api'); + let list = await PatternHandler.getPatternList('api'); if (!list || (list && !list.length)) return; + this.props?.appConfig?.data?.['ip.ignore']?.length && (list = list.filter(indexPattern => !this.props?.appConfig?.data?.['ip.ignore'].includes(indexPattern.title))); // Abort if we have disabled the pattern selector if (!AppState.getPatternSelector()) return; @@ -1061,7 +1065,8 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { const mapStateToProps = state => { return { - state: state.appStateReducers + state: state.appStateReducers, + appConfig: state.appConfig }; }; From 7b104eb575ba921fa4c71a675f416c889f5f9bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 21 Jul 2021 15:50:37 +0200 Subject: [PATCH 112/493] Renamed files to kebab case --- public/components/common/hooks/index.ts | 2 +- .../common/hooks/{useEsSearch.js => use-es-search.ts} | 2 +- public/components/common/panels/{agg_table.js => agg-table.tsx} | 0 public/components/common/panels/index.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename public/components/common/hooks/{useEsSearch.js => use-es-search.ts} (96%) rename public/components/common/panels/{agg_table.js => agg-table.tsx} (100%) diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 4f03e95fe8..8ee1cbd2ff 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,4 +24,4 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; -export { useEsSearch } from './useEsSearch'; +export { useEsSearch } from './use-es-search'; diff --git a/public/components/common/hooks/useEsSearch.js b/public/components/common/hooks/use-es-search.ts similarity index 96% rename from public/components/common/hooks/useEsSearch.js rename to public/components/common/hooks/use-es-search.ts index 241c007a01..1459bf86e6 100644 --- a/public/components/common/hooks/useEsSearch.js +++ b/public/components/common/hooks/use-es-search.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useQuery, useIndexPattern, useFilterManager } from '../hooks'; +import { useQuery, useIndexPattern, useFilterManager } from '.'; import _ from 'lodash'; /* diff --git a/public/components/common/panels/agg_table.js b/public/components/common/panels/agg-table.tsx similarity index 100% rename from public/components/common/panels/agg_table.js rename to public/components/common/panels/agg-table.tsx diff --git a/public/components/common/panels/index.ts b/public/components/common/panels/index.ts index da00f2a759..394f1a4025 100644 --- a/public/components/common/panels/index.ts +++ b/public/components/common/panels/index.ts @@ -1,2 +1,2 @@ export * from './panel_split'; -export { AggTable } from './agg_table'; \ No newline at end of file +export { AggTable } from './agg-table'; \ No newline at end of file From 561af0204c9bd518ab8073594686c39b4f95512f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 21 Jul 2021 19:31:49 +0200 Subject: [PATCH 113/493] Refactored base module and config files --- .../components/common/modules/panel/index.tsx | 2 - .../common/modules/panel/main-panel.tsx | 133 +-- .../common/modules/panel/module-drilldown.tsx | 14 - .../modules/panel/module-side-panel.scss | 13 + .../modules/panel/module-side-panel.tsx | 45 +- .../office-panel/config/drilldown-config.ts | 37 - .../office-panel/config/drilldown-config.tsx | 34 + .../overview/office-panel/config/index.ts | 1 + .../office-panel/config/main-view-config.ts | 55 -- .../office-panel/config/main-view-config.tsx | 21 + .../office-panel/config/module-config.tsx | 15 + .../overview/office-panel/mockup-tables.tsx | 72 -- .../overview/office-panel/office-panel.tsx | 10 +- .../overview/office-panel/views/index.ts | 3 + .../office-panel/views/office-body.tsx} | 22 +- .../office-panel/views/office-drilldown.tsx | 30 + .../office-stats.tsx} | 4 +- .../overview/overview-office.ts | 876 +++++++++++------- 18 files changed, 784 insertions(+), 603 deletions(-) delete mode 100644 public/components/common/modules/panel/module-drilldown.tsx create mode 100644 public/components/common/modules/panel/module-side-panel.scss delete mode 100644 public/components/overview/office-panel/config/drilldown-config.ts create mode 100644 public/components/overview/office-panel/config/drilldown-config.tsx delete mode 100644 public/components/overview/office-panel/config/main-view-config.ts create mode 100644 public/components/overview/office-panel/config/main-view-config.tsx create mode 100644 public/components/overview/office-panel/config/module-config.tsx delete mode 100644 public/components/overview/office-panel/mockup-tables.tsx create mode 100644 public/components/overview/office-panel/views/index.ts rename public/components/{common/modules/panel/module-body.tsx => overview/office-panel/views/office-body.tsx} (50%) create mode 100644 public/components/overview/office-panel/views/office-drilldown.tsx rename public/components/overview/office-panel/{module-stats.tsx => views/office-stats.tsx} (88%) diff --git a/public/components/common/modules/panel/index.tsx b/public/components/common/modules/panel/index.tsx index 385d5bc1a1..4e260ac370 100644 --- a/public/components/common/modules/panel/index.tsx +++ b/public/components/common/modules/panel/index.tsx @@ -13,5 +13,3 @@ export { MainPanel } from './main-panel'; export { ModuleSidePanel } from './module-side-panel'; -export { ModuleBody } from './module-body'; -export { ModuleDrilldown } from './module-drilldown'; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 65e2b8d967..87281a9b94 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -1,73 +1,96 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiPage, - EuiPageSideBar, - EuiPageBody, + EuiFlexGroup, + EuiFlexItem, + EuiPageBody, } from '@elastic/eui'; -import { ModuleSidePanel, ModuleBody, ModuleDrilldown } from './'; +import { ModuleSidePanel } from './'; import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; -//@ts-ignore import { getDataPlugin } from '../../../../kibana-services'; import { KbnSearchBar } from '../../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../../src/plugins/data/common'; +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'; -export const MainPanel = ({ sidePanelChildren, moduleConfig = {}, drilldownConfig = {}, ...props }) => { +export const MainPanel = ({ sidePanelChildren, tab='general', moduleConfig = {}, ...props }) => { - const [isDrilldownOpen, setIsDrilldownOpen] = useState(false); - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; - const timefilter = KibanaServices.timefilter.timefilter; + const [viewId, setViewId] = useState('main'); - const [isLoading, setLoading] = useState(false); - const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters() || [], - query: { language: 'kuery', query: '' }, - time: timefilter.getTime(), - }); + const KibanaServices = getDataPlugin().query; + const filterManager = KibanaServices.filterManager; + const timefilter = KibanaServices.timefilter.timefilter; + const [isLoading, setLoading] = useState(false); + const [filterParams, setFilterParams] = useState({ + filters: filterManager.getFilters() || [], + query: { language: 'kuery', query: '' }, + time: timefilter.getTime(), + }); - const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { - const { query, dateRange } = payload; - const filters = { query, time: dateRange, filters: filterParams.filters }; - setLoading(true); - setFilterParams(filters); - } + useEffect(() => { + (async () => { + const tabVisualizations = new TabVisualizations(); + tabVisualizations.removeAll(); + tabVisualizations.setTab(tab); + tabVisualizations.assign({ + [tab]: moduleConfig[viewId].length(), + }); + const filterHandler = new FilterHandler(AppState.getCurrentPattern()); + await VisFactoryHandler.buildOverviewVisualizations(filterHandler, tab, null); + })() + }, [viewId]) - const onFiltersUpdated = (filters: Filter[]) => { - const { query, time } = filterParams; - const updatedFilterParams = { query, time, filters }; - setLoading(true); - setFilterParams(updatedFilterParams); - } + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { + const { query, dateRange } = payload; + const filters = { query, time: dateRange, filters: filterParams.filters }; + setLoading(true); + setFilterParams(filters); - const toggleDrilldown = () => { - setIsDrilldownOpen(!isDrilldownOpen); - } + } - const bodyProps = { ...moduleConfig, toggleDrilldown }; - const drilldownProps = { ...drilldownConfig, toggleDrilldown }; - - return ( - <EuiFlexGroup style={{ margin: '0 8px' }}> - <EuiFlexItem> - {sidePanelChildren && <ModuleSidePanel> - {sidePanelChildren} - </ModuleSidePanel > - } - <EuiPageBody> - <KbnSearchBar - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> - {!isDrilldownOpen && <ModuleBody {...bodyProps} />} - {drilldownConfig && isDrilldownOpen && <ModuleDrilldown {...drilldownProps} />} - </EuiPageBody> - </EuiFlexItem> - </EuiFlexGroup> - ); + const onFiltersUpdated = (filters: Filter[]) => { + const { query, time } = filterParams; + const updatedFilterParams = { query, time, filters }; + setLoading(true); + setFilterParams(updatedFilterParams); + } + + const toggleView = (id = 'main') => { + setViewId(id); + } + + /** + * Builds active view + * @param props + * @returns React.Component + */ + const ModuleContent = () => { + + const View = moduleConfig[viewId].component; + return <WzReduxProvider><View changeView={toggleView} /></WzReduxProvider> + } + + return ( + <EuiFlexGroup style={{ margin: '0 8px' }}> + <EuiFlexItem> + {sidePanelChildren && <ModuleSidePanel> + {sidePanelChildren} + </ModuleSidePanel > + } + <EuiPageBody> + <KbnSearchBar + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} /> + <ModuleContent /> + </EuiPageBody> + </EuiFlexItem> + </EuiFlexGroup> + ); }; diff --git a/public/components/common/modules/panel/module-drilldown.tsx b/public/components/common/modules/panel/module-drilldown.tsx deleted file mode 100644 index 8b1ff3b0ba..0000000000 --- a/public/components/common/modules/panel/module-drilldown.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import WzReduxProvider from '../../../../redux/wz-redux-provider'; - - -export const ModuleDrilldown = ({ toggleDrilldown, rows = [], children, ...props }) => { - - return <> - <WzReduxProvider> - <EuiFlexGroup><EuiFlexItem grow={false}><div><EuiButtonEmpty onClick={toggleDrilldown} iconType={"sortLeft"}></EuiButtonEmpty></div></EuiFlexItem></EuiFlexGroup> - {children} - </WzReduxProvider> - </> -} \ No newline at end of file diff --git a/public/components/common/modules/panel/module-side-panel.scss b/public/components/common/modules/panel/module-side-panel.scss new file mode 100644 index 0000000000..e3963d3f2d --- /dev/null +++ b/public/components/common/modules/panel/module-side-panel.scss @@ -0,0 +1,13 @@ +.sidepanel-infoBtnStyle{ + border-radius: 0 5px 5px 0; + background: #ffffffab; + border: 1px solid #006bb459; + box-shadow: 1px 1px 3px -1px #000; + position: fixed; + left: 0; + z-index: 2001; + top: 50%; + width: 26px; + padding-left: 6px; + height: 55px; +} \ No newline at end of file diff --git a/public/components/common/modules/panel/module-side-panel.tsx b/public/components/common/modules/panel/module-side-panel.tsx index 01e9039830..de87d1291a 100644 --- a/public/components/common/modules/panel/module-side-panel.tsx +++ b/public/components/common/modules/panel/module-side-panel.tsx @@ -1,28 +1,29 @@ import { EuiCollapsibleNav, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useState } from 'react'; +import './module-side-panel.scss'; export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => { - const [navIsOpen, setNavIsOpen] = useState(false); + const [navIsOpen, setNavIsOpen] = useState(false); - const infoBtnStyle = { borderRadius: '0 5px 5px 0', background: '#0000000a', zIndex: 2001 }; - return ( - <EuiCollapsibleNav - isOpen={navIsOpen} - isDocked={navIsDocked} - showCloseButton={true} - maskProps={{ headerZindexLocation: 'below', className: 'wz-no-display' }} - button={ - <EuiButtonEmpty style={{ position: "fixed", left: 0, ...infoBtnStyle, }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'iInCircle'}> - </EuiButtonEmpty> - } - onClose={() => setNavIsOpen(false)}> - <div> - <EuiButtonEmpty style={{ float: 'right' }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'}> - </EuiButtonEmpty> - <div style={{ padding: 16 }}> - {children} - </div> - </div> - </EuiCollapsibleNav> - ); + + return ( + <EuiCollapsibleNav + isOpen={navIsOpen} + isDocked={navIsDocked} + showCloseButton={true} + maskProps={{ headerZindexLocation: 'below', className: 'wz-no-display' }} + button={ + <EuiButtonEmpty className={'sidepanel-infoBtnStyle'} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'iInCircle'}> + </EuiButtonEmpty> + } + onClose={() => setNavIsOpen(false)}> + <div> + <EuiButtonEmpty style={{ float: 'right' }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'}> + </EuiButtonEmpty> + <div style={{ padding: 16 }}> + {children} + </div> + </div> + </EuiCollapsibleNav> + ); }; \ No newline at end of file diff --git a/public/components/overview/office-panel/config/drilldown-config.ts b/public/components/overview/office-panel/config/drilldown-config.ts deleted file mode 100644 index 21b4a7232c..0000000000 --- a/public/components/overview/office-panel/config/drilldown-config.ts +++ /dev/null @@ -1,37 +0,0 @@ -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; - - -export const DrilldownConfig = { - rows: [ - { - height: 300, - columns: [ - { - width: 50, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } - }, - { - width: 50, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } - }, - ] - }, - { - height: 300, - columns: [ - { - width: 70, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', tab: 'office' } - }, - { - width: 30, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', tab: 'office' } - }, - ] - }, - ] -}; diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx new file mode 100644 index 0000000000..26dcbb092f --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; + + +export const DrilldownConfig = { + rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> + }, + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-Evolution' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 70, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Attacks-By-Agent' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Attacks-By-Technique' tab='office' {...props} /> + }, + ] + }, + ] +}; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 4a739e937e..83c428b963 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -1,2 +1,3 @@ export { DrilldownConfig } from './drilldown-config'; export { MainViewConfig } from './main-view-config'; +export { ModuleConfig } from './module-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.ts b/public/components/overview/office-panel/config/main-view-config.ts deleted file mode 100644 index 622ac3299e..0000000000 --- a/public/components/overview/office-panel/config/main-view-config.ts +++ /dev/null @@ -1,55 +0,0 @@ -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; -import { MockupTables } from '../mockup-tables'; - - -export const MainViewConfig = { - rows: [ - { - height: 300, - columns: [ - { - width: 50, - component: MockupTables, - enableToggleDrilldown: true, - props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } - }, - { - width: 50, - component: MockupTables, - enableToggleDrilldown: true, - props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } - }, - ] - }, - { - height: 300, - columns: [ - { - width: 50, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE', tab: 'office' } - }, - { - width: 50, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', tab: 'office' } - }, - ] - }, - { - height: 300, - columns: [ - { - width: 70, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', tab: 'office' } - }, - { - width: 30, - component: KibanaVis, - props: { visID: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', tab: 'office' } - }, - ] - }, - ] -}; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx new file mode 100644 index 0000000000..94b99549cf --- /dev/null +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; + + +export const MainViewConfig = { + rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <div ><button onClick={() => props.onRowClick('drilldown')}>change view</button></div> + }, + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> + }, + ] + } + ] +}; \ No newline at end of file diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx new file mode 100644 index 0000000000..07b5926697 --- /dev/null +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { OfficeBody, OfficeDrilldown } from '../views'; +import { MainViewConfig, DrilldownConfig } from './'; + + +export const ModuleConfig = { + main: { + length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} /> + }, + drilldown: { + length: () => DrilldownConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeDrilldown {...{ ...DrilldownConfig, ...props }} /> + } +}; diff --git a/public/components/overview/office-panel/mockup-tables.tsx b/public/components/overview/office-panel/mockup-tables.tsx deleted file mode 100644 index 1c2d3b5758..0000000000 --- a/public/components/overview/office-panel/mockup-tables.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiBasicTable, EuiPanel, EuiTitle, Random } from '@elastic/eui'; - -export const MockupTables = ({ toggleDrilldown, ...props }) => { - const random = new Random(); - const tables = [ - { - title: 'Events', - minWidth: '750px', - width: '750px', - columns: [ - { - field: 'rule', - name: 'Description', - render: (rule) => (<span onClick={toggleDrilldown}>{rule.description}</span>), - }, - { - field: 'rule', - name: 'Level', - render: (rule) => (<span onClick={toggleDrilldown}>{rule.level}</span>), - }, - ], - }, - ]; - const generateItems = () => { - - const rules = [ - { - level: 1, - description: 'A simple rule', - }, - { - level: 5, - description: 'A worrysome rule', - }, - { - level: 12, - description: 'Oh snap', - }, - ]; - const subscriptions = ['netflix', 'spotify', 'prime']; - const tenant = ['David', 'Doctor', 'Crowlie']; - const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].map((id) => { - return { - id, - rule: random.oneOf(rules), - tenant: random.oneOf(tenant), - subscription: random.oneOf(subscriptions), - count: random.integer(0, 10), - }; - }); - return items; - }; - const TitlePanel = ({ title, children, panelProps, titleProps = { size: 's' } }) => { - return <EuiPanel {...panelProps}> - <EuiTitle {...titleProps}><h2>{title}</h2></EuiTitle> - {children} - </EuiPanel> - } - const renderTables = (items, tables) => { - const table = tables[0]; - return <EuiFlexItem style={{ minWidth: table.minWidth, width: table.width || '' }}> - <TitlePanel title={table.title}> - <EuiBasicTable items={items} columns={table.columns} /> - </TitlePanel> - </EuiFlexItem> - - }; - - - return <EuiFlexGroup wrap>{renderTables(generateItems(), tables)}</EuiFlexGroup> -} \ No newline at end of file diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 3fb7a5d3a6..a600d5222f 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -12,11 +12,11 @@ */ import React, { useEffect, useState } from 'react'; -import { MainPanel } from '../../../components/common/modules/panel'; +import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; -import { ModuleStats } from './module-stats'; +import { OfficeStats } from './views'; import { queryConfig } from '../../../react-services/query-config'; -import { DrilldownConfig, MainViewConfig } from './config/'; +import { ModuleConfig } from './config'; export const OfficePanel = withErrorBoundary(({ ...props }) => { @@ -41,7 +41,7 @@ export const OfficePanel = withErrorBoundary(({ ...props }) => { )(); }, []) return ( - <MainPanel moduleConfig={MainViewConfig} drillDownConfig={DrilldownConfig} - sidePanelChildren={<ModuleStats listItems={moduleStatsList} />} /> + <MainPanel moduleConfig={ModuleConfig} tab={'office'} + sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> ) }); diff --git a/public/components/overview/office-panel/views/index.ts b/public/components/overview/office-panel/views/index.ts new file mode 100644 index 0000000000..39563eccfe --- /dev/null +++ b/public/components/overview/office-panel/views/index.ts @@ -0,0 +1,3 @@ +export { OfficeStats } from './office-stats' +export { OfficeBody } from './office-body' +export { OfficeDrilldown } from './office-drilldown' \ No newline at end of file diff --git a/public/components/common/modules/panel/module-body.tsx b/public/components/overview/office-panel/views/office-body.tsx similarity index 50% rename from public/components/common/modules/panel/module-body.tsx rename to public/components/overview/office-panel/views/office-body.tsx index 64203264cb..07f864fedb 100644 --- a/public/components/common/modules/panel/module-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,29 +1,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import WzReduxProvider from '../../../../redux/wz-redux-provider'; +export const OfficeBody = ({ changeView, rows = [], ...props }) => { -export const ModuleBody = ({ toggleDrilldown, rows = [], ...props }) => { - - return <> - <WzReduxProvider> + return <> { rows.map((row, key) => { return <EuiFlexGroup key={key} style={{ height: row.height || (150 + 'px') - }}>{ + }}> + { row.columns.map((column, key) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - const drilldownHandler = column.enableToggleDrilldown ? toggleDrilldown : undefined; - const visProps = { ...(column.props || {}), toggleDrilldown: drilldownHandler }; - + return <EuiFlexItem key={key} grow={growthFactor}> - <column.component {...visProps} /> + <column.component onRowClick={()=>changeView('drilldown')}/> </EuiFlexItem> }) - }</EuiFlexGroup> + } + </EuiFlexGroup> }) - } - </WzReduxProvider> + } </> } \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx new file mode 100644 index 0000000000..b27a3b30ed --- /dev/null +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; + +export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { + + return <> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <div><EuiButtonEmpty onClick={()=>changeView()} iconType={"sortLeft"}>Go Back</EuiButtonEmpty></div> + </EuiFlexItem> + </EuiFlexGroup> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} style={{ + height: row.height || (150 + 'px') + }}> + { + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + + return <EuiFlexItem key={key} grow={growthFactor}> + <column.component onRowClick={()=>changeView('main')}/> + </EuiFlexItem> + }) + } + </EuiFlexGroup> + }) + } + </> +} \ No newline at end of file diff --git a/public/components/overview/office-panel/module-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx similarity index 88% rename from public/components/overview/office-panel/module-stats.tsx rename to public/components/overview/office-panel/views/office-stats.tsx index f293829867..a7b2f07b16 100644 --- a/public/components/overview/office-panel/module-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -1,9 +1,9 @@ import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; -import moduleLogo from '../../../assets/office365.svg'; +import moduleLogo from '../../../../assets/office365.svg'; import React from 'react'; -export const ModuleStats = ({ listItems = [] }) => { +export const OfficeStats = ({ listItems = [] }) => { const logoStyle = { width: 30 }; return ( <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 25109ddcc1..952de3dfef 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - Module for Overview Office visualizations + * Wazuh app - Module for Overview/General visualizations * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -11,70 +11,15 @@ */ export default [ { - _id: 'Wazuh-App-Overview-OFFICE', + _id: 'Wazuh-App-Overview-Office-Agents-status', _source: { - title: 'Mitre attack count', + title: 'Agents status', visState: JSON.stringify({ - aggs: [ - { enabled: true, id: '1', params: {}, schema: 'metric', type: 'count' }, - { - enabled: true, - id: '2', - params: { - field: 'rule.mitre.id', - customLabel: 'Attack ID', - missingBucket: false, - missingBucketLabel: 'Missing', - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - size: 244, - }, - schema: 'bucket', - type: 'terms', - }, - ], - params: { - dimensions: { - buckets: [], - metrics: [{ accessor: 0, aggType: 'count', format: { id: 'number' }, params: {} }], - }, - perPage: 10, - percentageCol: '', - showMetricsAtAllLevels: false, - showPartialRows: false, - showTotal: false, - showToolbar: true, - sort: { columnIndex: null, direction: null }, - totalFunc: 'sum', - }, - title: 'mitre', - type: 'table', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { language: 'lucene', query: '' }, - }), - }, - }, - _type: 'visualization', - }, - { - _id: 'Wazuh-App-Overview-OFFICE-Alerts-Evolution', - _source: { - title: 'Mitre alerts evolution', - visState: JSON.stringify({ - title: 'Alert Evolution', - type: 'line', + title: 'Agents Status', + type: 'histogram', params: { - type: 'line', - grid: { categoryLines: false }, + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, categoryAxes: [ { id: 'CategoryAxis-1', @@ -102,14 +47,15 @@ export default [ ], seriesParams: [ { - show: 'true', - type: 'line', + show: true, mode: 'normal', - data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', + type: 'line', drawLinesBetweenPoints: true, showCircles: true, - lineWidth: 2, + interpolate: 'cardinal', + lineWidth: 3.5, + data: { id: '4', label: 'Unique count of id' }, + valueAxis: 'ValueAxis-1', }, ], addTooltip: true, @@ -117,51 +63,18 @@ export default [ legendPosition: 'right', times: [], addTimeMarker: false, - labels: {}, - thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, - dimensions: { - x: { - accessor: 0, - format: { id: 'date', params: { pattern: 'YYYY-MM-DD HH:mm' } }, - params: { - date: true, - interval: 'PT3H', - format: 'YYYY-MM-DD HH:mm', - bounds: { min: '2019-11-07T15:45:45.770Z', max: '2019-11-14T15:45:45.770Z' }, - }, - aggType: 'date_histogram', - }, - y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], - series: [ - { - accessor: 1, - format: { - id: 'terms', - params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - }, - }, - params: {}, - aggType: 'terms', - }, - ], - }, }, aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { id: '2', enabled: true, type: 'date_histogram', + interval: '1ms', schema: 'segment', params: { field: 'timestamp', - timeRange: { from: 'now-7d', to: 'now' }, - useNormalizedEsInterval: true, - interval: 'auto', - drop_partials: false, + interval: '1ms', + customInterval: '2h', min_doc_count: 1, extended_bounds: {}, }, @@ -171,110 +84,345 @@ export default [ enabled: true, type: 'terms', schema: 'group', - params: { - field: 'rule.mitre.technique', - customLabel: 'Attack ID', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, + params: { field: 'status', size: 5, order: 'desc', orderBy: '_term' }, + }, + { + id: '4', + enabled: true, + type: 'cardinality', + schema: 'metric', + params: { field: 'id' }, }, ], }), - uiStateJSON: '{}', + uiStateJSON: JSON.stringify({ + vis: { colors: { never_connected: '#447EBC', active: '#E5AC0E' } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', + index: 'wazuh-monitoring', filter: [], - query: { language: 'lucene', query: '' }, + query: { query: '', language: 'lucene' }, }), }, }, _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-OFFICE-Attacks-By-Agent', + _id: 'Wazuh-App-Overview-Office-Metric-alerts', _source: { - title: 'Mitre techniques by agent', + title: 'Metric alerts', visState: JSON.stringify({ - title: 'attack by agent', - type: 'pie', + title: 'Metric Alerts', + type: 'metric', params: { - type: 'pie', addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - dimensions: { - metric: { accessor: 0, format: { id: 'number' }, params: {}, aggType: 'count' }, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, }, }, aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { - id: '2', + id: '1', enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'agent.name', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'count', + schema: 'metric', + params: { customLabel: 'Alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Level-12-alerts', + _source: { + title: 'Level 12 alerts', + visState: JSON.stringify({ + title: 'Count Level 12 Alerts', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Level 12 or above alerts' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + disabled: false, + index: 'wazuh-alerts', + key: 'rule.level', + negate: false, + params: { + gte: 12, + lt: null, + }, + type: 'range', + value: '12 to +∞', + }, + range: { + 'rule.level': { + gte: 12, + lt: null, + }, + }, }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Authentication-failure', + _source: { + title: 'Authentication failure', + visState: JSON.stringify({ + title: 'Count Authentication Failure', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, }, + }, + aggs: [ { - id: '3', + id: '1', enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'rule.mitre.technique', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication failure' }, + }, + ], + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [ + { + meta: { + index: 'wazuh-alerts', + type: 'phrases', + key: 'rule.groups', + value: 'win_authentication_failed, authentication_failed, authentication_failures', + params: [ + 'win_authentication_failed', + 'authentication_failed', + 'authentication_failures', + ], + negate: false, + disabled: false, + alias: null, + }, + query: { + bool: { + should: [ + { + match_phrase: { + 'rule.groups': 'win_authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failed', + }, + }, + { + match_phrase: { + 'rule.groups': 'authentication_failures', + }, + }, + ], + minimum_should_match: 1, + }, + }, + $state: { + store: 'appState', + }, }, + ], + query: { query: '', language: 'lucene' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Authentication-success', + _source: { + title: 'Authentication success', + visState: JSON.stringify({ + title: 'Count Authentication Success', + type: 'metric', + params: { + addTooltip: true, + addLegend: false, + type: 'gauge', + gauge: { + verticalSplit: false, + autoExtend: false, + percentageMode: false, + gaugeType: 'Metric', + gaugeStyle: 'Full', + backStyle: 'Full', + orientation: 'vertical', + colorSchema: 'Green to Red', + gaugeColorMode: 'None', + useRange: false, + colorsRange: [{ from: 0, to: 100 }], + invertColors: false, + labels: { show: true, color: 'black' }, + scale: { show: false, labels: false, color: '#333', width: 2 }, + type: 'simple', + style: { fontSize: 20, bgColor: false, labelColor: false, subText: '' }, + }, + }, + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + schema: 'metric', + params: { customLabel: 'Authentication success' }, }, ], }), - uiStateJSON: '{}', + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: JSON.stringify({ index: 'wazuh-alerts', - filter: [], - query: { language: 'lucene', query: '' }, + filter: [ + { + meta: { + index: 'wazuh-alerts', + negate: false, + disabled: false, + alias: null, + type: 'phrase', + key: 'rule.groups', + value: 'authentication_success', + params: { + query: 'authentication_success', + type: 'phrase', + }, + }, + query: { + match: { + 'rule.groups': { + query: 'authentication_success', + type: 'phrase', + }, + }, + }, + $state: { + store: 'appState', + }, + }, + ], + query: { query: '', language: 'lucene' }, }), }, }, _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-OFFICE-Attacks-By-Technique', + _id: 'Wazuh-App-Overview-Office-Alert-level-evolution', _source: { - title: 'Attacks by technique', + title: 'Alert level evolution', visState: JSON.stringify({ - title: 'Attacks by tactic', - type: 'histogram', + title: 'Alert level evolution', + type: 'area', params: { - type: 'histogram', - grid: { categoryLines: false }, + type: 'area', + grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, categoryAxes: [ { id: 'CategoryAxis-1', @@ -303,12 +451,13 @@ export default [ seriesParams: [ { show: 'true', - type: 'histogram', + type: 'area', mode: 'stacked', data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', drawLinesBetweenPoints: true, showCircles: true, + interpolate: 'cardinal', + valueAxis: 'ValueAxis-1', }, ], addTooltip: true, @@ -316,56 +465,36 @@ export default [ legendPosition: 'right', times: [], addTimeMarker: false, - labels: { show: false }, - thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#34130C' }, - dimensions: { - x: null, - y: [{ accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }], - series: [ - { - accessor: 0, - format: { - id: 'terms', - params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - }, - }, - params: {}, - aggType: 'terms', - }, - ], - }, }, aggs: [ { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { id: '2', enabled: true, - type: 'terms', - schema: 'group', + type: 'date_histogram', + schema: 'segment', params: { - field: 'rule.mitre.technique', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', + field: 'timestamp', + timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, + useNormalizedEsInterval: true, + interval: 'auto', + time_zone: 'Europe/Berlin', + drop_partials: false, + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, }, }, { id: '3', enabled: true, type: 'terms', - schema: 'segment', + schema: 'group', params: { - field: 'rule.mitre.tactic', - orderBy: '1', + field: 'rule.level', + size: '15', order: 'desc', - size: 5, + orderBy: '1', otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, @@ -381,122 +510,87 @@ export default [ searchSourceJSON: JSON.stringify({ index: 'wazuh-alerts', filter: [], - query: { language: 'lucene', query: '' }, + query: { query: '', language: 'lucene' }, }), }, }, _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Top-Tactics-By-Agent', + _id: 'Wazuh-App-Overview-Office-Alerts-Top-Mitre', _source: { - title: 'Top tactics by agent', + title: 'Alerts', visState: JSON.stringify({ - title: 'Top tactics by agent - vertical', - type: 'area', - params: { - addLegend: true, - addTimeMarker: false, - addTooltip: true, - categoryAxes: [ - { - id: 'CategoryAxis-1', - labels: { filter: true, show: true, truncate: 10 }, - position: 'bottom', - scale: { type: 'linear' }, - show: true, - style: {}, - title: {}, - type: 'category', - }, - ], - dimensions: { - x: { - accessor: 1, - format: { - id: 'terms', - params: { id: 'string', otherBucketLabel: 'Other', missingBucketLabel: 'Missing' }, - }, - params: {}, - aggType: 'terms', - }, - y: [{ accessor: 2, format: { id: 'number' }, params: {}, aggType: 'count' }], - series: [ - { - accessor: 0, - format: { - id: 'terms', - params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - }, - }, - params: {}, - aggType: 'terms', - }, - ], - }, - grid: { categoryLines: false, valueAxis: 'ValueAxis-1' }, - labels: {}, - legendPosition: 'right', - seriesParams: [ - { - data: { id: '1', label: 'Count' }, - drawLinesBetweenPoints: true, - interpolate: 'linear', - mode: 'normal', - show: 'true', - showCircles: true, - type: 'histogram', - valueAxis: 'ValueAxis-1', - }, - ], - thresholdLine: { color: '#34130C', show: false, style: 'full', value: 10, width: 1 }, - times: [], - type: 'area', - valueAxes: [ - { - id: 'ValueAxis-1', - labels: { filter: false, rotate: 0, show: true, truncate: 100 }, - name: 'LeftAxis-1', - position: 'left', - scale: { mode: 'normal', type: 'linear' }, - show: true, - style: {}, - title: { text: 'Count' }, - type: 'value', - }, - ], - }, + type: 'pie', aggs: [ { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { - id: '3', + id: '2', enabled: true, type: 'terms', - schema: 'group', + schema: 'segment', params: { - field: 'rule.mitre.tactic', + field: 'rule.mitre.technique', orderBy: '1', order: 'desc', - size: 5, + size: 20, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', }, }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + title: 'mitre top', + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Top-5-agents', + _source: { + title: 'Top 5 agents', + visState: JSON.stringify({ + title: 'Top 5 agents', + type: 'pie', + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { show: false, values: true, last_level: true, truncate: 100 }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { - id: '4', + id: '2', enabled: true, type: 'terms', schema: 'segment', params: { field: 'agent.name', - orderBy: '1', - order: 'desc', size: 5, + order: 'desc', + orderBy: '1', otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, @@ -505,64 +599,53 @@ export default [ }, ], }), - uiStateJSON: '{}', + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: JSON.stringify({ index: 'wazuh-alerts', filter: [], - query: { language: 'lucene', query: '' }, + query: { query: '', language: 'lucene' }, }), }, }, _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Top-Tactics', + _id: 'Wazuh-App-Overview-Office-Top-5-agents-Evolution', _source: { - title: 'Top tactics', + title: 'Top 5 rule groups', visState: JSON.stringify({ - title: 'Top tactics PIE2', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: false, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - dimensions: { - metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, - buckets: [ - { - accessor: 0, - format: { - id: 'terms', - params: { - id: 'string', - otherBucketLabel: 'Other', - missingBucketLabel: 'Missing', - }, - }, - params: {}, - aggType: 'terms', - }, - ], - }, - }, + type: 'histogram', aggs: [ { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { id: '2', enabled: true, - type: 'terms', + type: 'date_histogram', schema: 'segment', params: { - field: 'rule.mitre.tactic', + field: 'timestamp', + timeRange: { from: '2020-07-19T16:18:13.637Z', to: '2020-07-28T13:58:33.357Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { + field: 'agent.name', orderBy: '1', order: 'desc', - size: 10, + size: 5, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, @@ -570,22 +653,72 @@ export default [ }, }, ], + params: { + type: 'area', + grid: { categoryLines: false }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, + labels: {}, + }, + title: 'top 5 agents evolution', }), - uiStateJSON: '{}', + uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), description: '', version: 1, kibanaSavedObjectMeta: { searchSourceJSON: JSON.stringify({ index: 'wazuh-alerts', filter: [], - query: { language: 'lucene', query: '' }, + query: { query: '', language: 'lucene' }, }), }, }, _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-MITRE-Alerts-summary', + _id: 'Wazuh-App-Overview-Office-Alerts-summary', _type: 'visualization', _source: { title: 'Alerts summary', @@ -614,7 +747,7 @@ export default [ otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - size: 50, + size: 1000, order: 'desc', orderBy: '1', customLabel: 'Rule ID', @@ -656,7 +789,98 @@ export default [ }, ], }), - uiStateJSON: '{"vis":{"params":{"sort":{"columnIndex":3,"direction":"desc"}}}}', + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Alerts-evolution-Top-5-agents', + _type: 'visualization', + _source: { + title: 'Alerts evolution Top 5 agents', + visState: JSON.stringify({ + title: 'Alerts evolution Top 5 agents', + type: 'histogram', + params: { + type: 'histogram', + grid: { categoryLines: false, style: { color: '#eee' } }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { type: 'linear' }, + labels: { show: true, filter: true, truncate: 100 }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { type: 'linear', mode: 'normal' }, + labels: { show: true, rotate: 0, filter: false, truncate: 100 }, + title: { text: 'Count' }, + }, + ], + seriesParams: [ + { + show: 'true', + type: 'histogram', + mode: 'stacked', + data: { label: 'Count', id: '1' }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '3', + enabled: true, + type: 'terms', + schema: 'group', + params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + schema: 'segment', + params: { + field: 'timestamp', + interval: 'auto', + customInterval: '2h', + min_doc_count: 1, + extended_bounds: {}, + }, + }, + ], + }), + uiStateJSON: '{}', description: '', version: 1, kibanaSavedObjectMeta: { From 9d92d2ddeff03a25e403f0bd1a4d05e573057c78 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Thu, 22 Jul 2021 11:35:07 +0200 Subject: [PATCH 114/493] Drilldown changes --- .../office-panel/views/office-drilldown.tsx | 14 ++++++++++++-- .../visualize/components/security-alerts.tsx | 6 ++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index b27a3b30ed..5bbfe810bd 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -1,12 +1,19 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { return <> <EuiFlexGroup> <EuiFlexItem grow={false}> - <div><EuiButtonEmpty onClick={()=>changeView()} iconType={"sortLeft"}>Go Back</EuiButtonEmpty></div> + <div><EuiButtonEmpty onClick={()=>changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="s"> + <h3>User Activity</h3> + </EuiTitle> + <p>wazuh@wazuh.com</p> </EuiFlexItem> </EuiFlexGroup> { @@ -26,5 +33,8 @@ export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { </EuiFlexGroup> }) } + <EuiFlexGroup> + <SecurityAlerts /> + </EuiFlexGroup> </> } \ No newline at end of file diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index 0f7a0ff0d4..275100946f 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -14,7 +14,7 @@ import { useFilterManager, useQuery, useRefreshAngularDiscover } from '../../com import { Discover } from '../../common/modules/discover'; import { useAllowedAgents } from '../../common/hooks/useAllowedAgents' -export const SecurityAlerts = () => { +export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']}) => { const [query] = useQuery(); const filterManager = useFilterManager(); const copyOfFilterManager = filterManager @@ -23,12 +23,14 @@ export const SecurityAlerts = () => { const customFilterWithAllowedAgents = []; const {allowedAgents, filterAllowedAgents} = useAllowedAgents(); filterAllowedAgents && customFilterWithAllowedAgents.push(filterAllowedAgents); + //Sacar esta linea antes del commit final, solo para mostrar security events sin alertas de office + filterManager.setFilters(filterManager.getFilters[1]) return ( <Discover shareFilterManager={filterManager} shareFilterManagerWithUserAuthorized={customFilterWithAllowedAgents} query={query} - initialColumns={["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']} + initialColumns={initialColumns} implicitFilters={[]} initialFilters={[]} updateTotalHits={(total) => { }} From 24e7ad78f5dd4a080e24d35dca9e77c85ff06f1d Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 22 Jul 2021 15:53:30 +0200 Subject: [PATCH 115/493] Added useCallback to prevent View re-renders --- .../common/modules/panel/main-panel.tsx | 11 +++--- .../office-panel/config/drilldown-config.tsx | 8 ++-- .../office-panel/config/main-view-config.tsx | 30 +++++++++++++-- .../overview/office-panel/office-panel.tsx | 2 +- .../office-panel/views/office-body.tsx | 38 +++++++++---------- .../office-panel/views/office-drilldown.tsx | 2 +- public/services/common-data.js | 2 +- 7 files changed, 58 insertions(+), 35 deletions(-) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 87281a9b94..49d8bc6913 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -16,7 +16,7 @@ import { FilterHandler } from '../../../../utils/filter-handler'; import { TabVisualizations } from '../../../../factories/tab-visualizations'; -export const MainPanel = ({ sidePanelChildren, tab='general', moduleConfig = {}, ...props }) => { +export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { const [viewId, setViewId] = useState('main'); @@ -61,7 +61,8 @@ export const MainPanel = ({ sidePanelChildren, tab='general', moduleConfig = {}, } const toggleView = (id = 'main') => { - setViewId(id); + if (id != viewId) + setViewId(id); } /** @@ -69,11 +70,11 @@ export const MainPanel = ({ sidePanelChildren, tab='general', moduleConfig = {}, * @param props * @returns React.Component */ - const ModuleContent = () => { + const ModuleContent = useCallback(() => { const View = moduleConfig[viewId].component; return <WzReduxProvider><View changeView={toggleView} /></WzReduxProvider> - } + }, [viewId]) return ( <EuiFlexGroup style={{ margin: '0 8px' }}> diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx index 26dcbb092f..09905a6a62 100644 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -5,7 +5,7 @@ import KibanaVis from '../../../../kibana-integrations/kibana-vis'; export const DrilldownConfig = { rows: [ { - height: 300, + height: 110, columns: [ { width: 50, @@ -13,7 +13,7 @@ export const DrilldownConfig = { }, { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-Evolution' tab='office' {...props} /> + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Authentication-success' tab='office' {...props} /> }, ] }, @@ -22,11 +22,11 @@ export const DrilldownConfig = { columns: [ { width: 70, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Attacks-By-Agent' tab='office' {...props} /> + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Agents-status' tab='office' {...props} /> }, { width: 30, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Attacks-By-Technique' tab='office' {...props} /> + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> }, ] }, diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 94b99549cf..3b56384e4c 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -2,10 +2,10 @@ import React from 'react'; import KibanaVis from '../../../../kibana-integrations/kibana-vis'; -export const MainViewConfig = { +export const MainViewConfig = { rows: [ { - height: 300, + height: 110, columns: [ { width: 50, @@ -13,9 +13,31 @@ export const MainViewConfig = { }, { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-Top-Mitre' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> + }, + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Top-5-agents-Evolution' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> }, ] - } + }, ] }; \ No newline at end of file diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index a600d5222f..5a82e0dbad 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -18,7 +18,7 @@ import { OfficeStats } from './views'; import { queryConfig } from '../../../react-services/query-config'; import { ModuleConfig } from './config'; -export const OfficePanel = withErrorBoundary(({ ...props }) => { +export const OfficePanel = withErrorBoundary(() => { const [moduleStatsList, setModuleStatsList] = useState([]); useEffect(() => { diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index 07f864fedb..e03ca63abf 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,25 +1,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -export const OfficeBody = ({ changeView, rows = [], ...props }) => { +export const OfficeBody = ({ changeView, rows = [] }) => { - return <> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} style={{ - height: row.height || (150 + 'px') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - - return <EuiFlexItem key={key} grow={growthFactor}> - <column.component onRowClick={()=>changeView('drilldown')}/> - </EuiFlexItem> - }) - } - </EuiFlexGroup> - }) - } + return <> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} style={{ + height: row.height || (150 + 'px') + }}> + { + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + + return <EuiFlexItem key={key} grow={growthFactor}> + <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /></div> + </EuiFlexItem> + }) + } + </EuiFlexGroup> + }) + } </> } \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index 5bbfe810bd..0d16ff2bc6 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -26,7 +26,7 @@ export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); return <EuiFlexItem key={key} grow={growthFactor}> - <column.component onRowClick={()=>changeView('main')}/> + <div style={{ height: '100%' }}><column.component onRowClick={()=>changeView('main')}/></div> </EuiFlexItem> }) } diff --git a/public/services/common-data.js b/public/services/common-data.js index 53b272e940..f4d8d4ce3a 100644 --- a/public/services/common-data.js +++ b/public/services/common-data.js @@ -143,7 +143,7 @@ export class CommonData { tsc: { group: 'tsc' }, aws: { group: 'amazon' }, gcp: { group: 'gcp' }, - office: { group: 'office365' }, + office: { group: '' }, virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, From 75d6835ac861fe2d7b1cee031e2803aa68ada08c Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Thu, 22 Jul 2021 16:07:23 +0200 Subject: [PATCH 116/493] Reverting change in security alerts --- public/components/visualize/components/security-alerts.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index 275100946f..bd2c67bb37 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -23,8 +23,7 @@ export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mit const customFilterWithAllowedAgents = []; const {allowedAgents, filterAllowedAgents} = useAllowedAgents(); filterAllowedAgents && customFilterWithAllowedAgents.push(filterAllowedAgents); - //Sacar esta linea antes del commit final, solo para mostrar security events sin alertas de office - filterManager.setFilters(filterManager.getFilters[1]) + return ( <Discover shareFilterManager={filterManager} From a853dcaa051fcb7b83f3fcb27e6f51a6ba371a12 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 22 Jul 2021 17:19:57 +0200 Subject: [PATCH 117/493] fixed styles and html structure --- .../office-panel/config/drilldown-config.tsx | 2 +- .../office-panel/config/main-view-config.tsx | 3 ++- .../office-panel/views/office-body.tsx | 6 ++--- .../office-panel/views/office-drilldown.tsx | 24 ++++++++++--------- public/styles/common.scss | 4 ++++ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx index 09905a6a62..7613e686ac 100644 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -5,7 +5,7 @@ import KibanaVis from '../../../../kibana-integrations/kibana-vis'; export const DrilldownConfig = { rows: [ { - height: 110, + height: 130, columns: [ { width: 50, diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 3b56384e4c..400d1becad 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,3 +1,4 @@ +import { EuiPanel } from '@elastic/eui'; import React from 'react'; import KibanaVis from '../../../../kibana-integrations/kibana-vis'; @@ -5,7 +6,7 @@ import KibanaVis from '../../../../kibana-integrations/kibana-vis'; export const MainViewConfig = { rows: [ { - height: 110, + height: 150, columns: [ { width: 50, diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index e03ca63abf..834cdb202f 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; export const OfficeBody = ({ changeView, rows = [] }) => { return <> { rows.map((row, key) => { - return <EuiFlexGroup key={key} style={{ + return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ height: row.height || (150 + 'px') }}> { @@ -14,7 +14,7 @@ export const OfficeBody = ({ changeView, rows = [] }) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); return <EuiFlexItem key={key} grow={growthFactor}> - <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /></div> + <EuiPanel paddingSize={'s'} ><div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /></div></EuiPanel> </EuiFlexItem> }) } diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index 0d16ff2bc6..f19a26deab 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { return <> - <EuiFlexGroup> + <EuiFlexGroup className={'wz-margin-0'}> <EuiFlexItem grow={false}> - <div><EuiButtonEmpty onClick={()=>changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> + <div><EuiButtonEmpty onClick={() => changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiTitle size="s"> @@ -18,23 +18,25 @@ export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { </EuiFlexGroup> { rows.map((row, key) => { - return <EuiFlexGroup key={key} style={{ + return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ height: row.height || (150 + 'px') }}> { row.columns.map((column, key) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - - return <EuiFlexItem key={key} grow={growthFactor}> - <div style={{ height: '100%' }}><column.component onRowClick={()=>changeView('main')}/></div> - </EuiFlexItem> + + return <EuiFlexItem key={key} grow={growthFactor}> + <EuiPanel paddingSize={'s'} ><div style={{ height: '100%' }}><column.component onRowClick={() => changeView('main')} /></div></EuiPanel> + </EuiFlexItem> }) } </EuiFlexGroup> }) } - <EuiFlexGroup> - <SecurityAlerts /> - </EuiFlexGroup> + <EuiFlexGroup className={'wz-margin-0'}> + <EuiFlexItem> + <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> </> } \ No newline at end of file diff --git a/public/styles/common.scss b/public/styles/common.scss index 10e7d3bf6e..d6ce5e9edc 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1168,6 +1168,10 @@ wz-xml-file-editor { margin-left: -10px; } +.wz-margin-0 { + margin: 0px; +} + .header-global-wrapper + .app-wrapper:not(.hidden-chrome) { top: 48px!important; left: 48px!important; From 583f0855e924148684157176c97ce4c29c97e454 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 22 Jul 2021 18:13:06 +0200 Subject: [PATCH 118/493] Fixed typo --- public/components/common/modules/panel/main-panel.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 49d8bc6913..96a3b81958 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -24,7 +24,7 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { const filterManager = KibanaServices.filterManager; const timefilter = KibanaServices.timefilter.timefilter; - const [isLoading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [filterParams, setFilterParams] = useState({ filters: filterManager.getFilters() || [], query: { language: 'kuery', query: '' }, @@ -48,15 +48,15 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; const filters = { query, time: dateRange, filters: filterParams.filters }; - setLoading(true); + setIsLoading(true); setFilterParams(filters); - + setIsLoading(false); } const onFiltersUpdated = (filters: Filter[]) => { const { query, time } = filterParams; const updatedFilterParams = { query, time, filters }; - setLoading(true); + setIsLoading(true); setFilterParams(updatedFilterParams); } @@ -71,9 +71,8 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { * @returns React.Component */ const ModuleContent = useCallback(() => { - const View = moduleConfig[viewId].component; - return <WzReduxProvider><View changeView={toggleView} /></WzReduxProvider> + return <WzReduxProvider><View changeView={toggleView}/></WzReduxProvider> }, [viewId]) return ( From b4fef38a636ac0a935aabceeaa001a667c759e8e Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 22 Jul 2021 18:18:02 +0200 Subject: [PATCH 119/493] add searchbar component --- .../custom-search-bar/custom-search-bar.tsx | 164 ++++++++++++++++++ .../common/custom-search-bar/index.ts | 13 ++ .../office-panel/search-bar-config.ts | 43 +++++ 3 files changed, 220 insertions(+) create mode 100644 public/components/common/custom-search-bar/custom-search-bar.tsx create mode 100644 public/components/common/custom-search-bar/index.ts create mode 100644 public/components/overview/office-panel/search-bar-config.ts diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx new file mode 100644 index 0000000000..3bd1af2910 --- /dev/null +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect } from 'react'; + +import { getIndexPattern } from '../../overview/mitre/lib' +import { Filter } from '../../../../../../src/plugins/data/public/'; +import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../src/plugins/data/common'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiComboBox, + EuiSwitch, + EuiSpacer, + } from '@elastic/eui'; + +//@ts-ignore +import { getDataPlugin } from '../../../kibana-services'; +import { KbnSearchBar } from '../../kbn-search-bar'; +import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; + + + +export const CustomSearchBar = ({ ...props }) => { + + const KibanaServices = getDataPlugin().query; + const filterManager = KibanaServices.filterManager; + const timefilter = KibanaServices.timefilter.timefilter; + const [filterParams, setFilterParams] = useState({ + filters: filterManager.getFilters() || [], + query: { language: 'kuery', query: '' }, + time: timefilter.getTime(), + }); + const [isLoading, setLoading] = useState(false); + const [customFilters, setCustomFilters] = useState(props.filtersValues) + const [avancedFiltersState, setAvancedFiltersState] = useState(false); + const [selectedOptions, setSelectedOptions] = useState([]); + const [defaultFilters, setDefaultFilters] = useState(filterManager.getFilters()); + + + + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { + const { query, dateRange } = payload; + const filters = { query, time: dateRange, filters: filterParams.filters }; + setLoading(true); + setFilterParams(filters); + + } + + const onFiltersUpdated = (filters: Filter[]) => { + const { query, time } = filterParams; + const updatedFilterParams = { query, time, filters }; + refreshCustomFilters(filters) + setLoading(true); + setFilterParams(updatedFilterParams); + } + + const refreshCustomFilters = (filters) => { + const deleteDefaultFilters = [] + filters.forEach(filter => { + if(!defaultFilters.some(item => item.meta.key === filter.meta.key)){ + deleteDefaultFilters.push(filter) + } + }) + } + + const changeSwitch = () => { + avancedFiltersState ? setAvancedFiltersState(false) : setAvancedFiltersState(true); + } + + const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?:any, key?:any): Filter => { + const meta: FilterMeta = { + disabled: false, + negate: false, + key:key, + params: {query:querySearch}, + alias: null, + type: "phrase", + index, + }; + const $state: FilterState = { + store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + }; + const query = { + match_phrase: { + [key] : { + query: querySearch + } + } + } + + return { meta, $state, query }; + }; + + const setFilters = async (values) => { + const indexPattern = await getIndexPattern() + const newFilters = [] + if(!values.length){ + filterManager.removeAll() + filterManager.addFilters(defaultFilters) + }else{ + filterManager.removeAll() + newFilters.push(defaultFilters); + values.forEach(element => { + const customFilter = buildCustomFilter(false,indexPattern.title,element.label,element.key) + newFilters.push(customFilter); + }); + filterManager.addFilters(newFilters) + } + } + + const onChange = (values) => { + setFilters(values) + setSelectedOptions(values); + }; + + const getComponent = (item) => { + var types = { + 'default': <></>, + 'combobox': <EuiComboBox + placeholder={"Select "+item.values[0].key+" or create options"} + options={item.values} + selectedOptions={selectedOptions || []} + onChange={onChange} + isClearable={true} + /> + }; + return types[item.type] || types['default']; + } + + return ( + <> + <EuiFlexGroup alignItems='center'> + { + avancedFiltersState === false ? + customFilters.map((item) => ( + <EuiFlexItem grow={2}> + {getComponent(item)} + </EuiFlexItem> + )) + : + '' + } + <EuiFlexItem> + <KbnSearchBar + showFilterBar={avancedFiltersState} + showQueryInput={avancedFiltersState} + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={false} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup justifyContent='flexEnd'> + <EuiFlexItem grow={false} style={{ paddingTop: '10px' }}> + <EuiSwitch + label="Advanced filters" + checked={avancedFiltersState} + onChange={() => changeSwitch()} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + </> + ) +}; \ No newline at end of file diff --git a/public/components/common/custom-search-bar/index.ts b/public/components/common/custom-search-bar/index.ts new file mode 100644 index 0000000000..04745e107e --- /dev/null +++ b/public/components/common/custom-search-bar/index.ts @@ -0,0 +1,13 @@ +/* + * Wazuh app - React component to integrate Custom search bar + * Copyright (C) 2015-2021 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 { CustomSearchBar } from './custom-search-bar'; \ No newline at end of file diff --git a/public/components/overview/office-panel/search-bar-config.ts b/public/components/overview/office-panel/search-bar-config.ts new file mode 100644 index 0000000000..64d7e1818b --- /dev/null +++ b/public/components/overview/office-panel/search-bar-config.ts @@ -0,0 +1,43 @@ +export const filtersValues = [ + { + type: 'combobox', + key: 'agent.id', + values:[ + { + key:'agent.id', + label: '001', + + }, + { + key:'agent.id', + label: '002', + }, + { + key:'agent.id', + label: '003', + }, + { + key:'agent.id', + label: '004', + }, + { + key:'agent.id', + label: '006', + } + ] + }, + { + type: 'combobox', + key: 'cluster.name', + values:[ + { + key:'cluster.name', + label: 'wazuh', + }, + { + key:'cluster.name', + label: 'test', + }, + ] + } +] \ No newline at end of file From afaa297590507f33c67225825f7a49bb6fd76f0e Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 22 Jul 2021 18:56:45 +0200 Subject: [PATCH 120/493] add searchbar component --- public/components/common/modules/panel/main-panel.tsx | 4 ---- public/components/overview/office-panel/office-panel.tsx | 5 +++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 96a3b81958..e6c4cdd719 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -83,10 +83,6 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { </ModuleSidePanel > } <EuiPageBody> - <KbnSearchBar - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> <ModuleContent /> </EuiPageBody> </EuiFlexItem> diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 5a82e0dbad..01b9c9e898 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -14,6 +14,8 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; +import { CustomSearchBar } from '../../common/custom-search-bar'; +import { filtersValues } from './search-bar-config'; import { OfficeStats } from './views'; import { queryConfig } from '../../../react-services/query-config'; import { ModuleConfig } from './config'; @@ -41,7 +43,10 @@ export const OfficePanel = withErrorBoundary(() => { )(); }, []) return ( + <> + <CustomSearchBar filtersValues={filtersValues}/> <MainPanel moduleConfig={ModuleConfig} tab={'office'} sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> + </> ) }); From fc3ac389efb3156f9e54623ea28a8ec1b1872ff6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 22 Jul 2021 19:42:00 +0200 Subject: [PATCH 121/493] fix await query config and constant error --- public/react-services/query-config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/react-services/query-config.js b/public/react-services/query-config.js index ae9a5944aa..7bb5292c89 100644 --- a/public/react-services/query-config.js +++ b/public/react-services/query-config.js @@ -14,6 +14,7 @@ import { WzRequest } from './wz-request'; import { getErrorOrchestrator } from './common-services'; import { AppState } from './app-state'; import { UI_LOGGER_LEVELS } from '../../common/constants'; +import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; export const queryConfig = async (agentId, sections, node = false) => { try { @@ -29,7 +30,7 @@ export const queryConfig = async (agentId, sections, node = false) => { } const result = {}; - for (const section in sections) { + await Promise.all(sections.map(async(section)=> { const { component, configuration } = section; if ( !component || @@ -64,7 +65,7 @@ export const queryConfig = async (agentId, sections, node = false) => { }; getErrorOrchestrator().handleError(options); } - } + })); return result; } catch (error) { const options = { From c36143fad2b1f112e86022259d09d403641d7c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 10:13:29 +0200 Subject: [PATCH 122/493] Changed office365alerts so they use the full logs Using all the logs provided by core and randomized sample data on new fields. --- .../generate-alerts/generate-alerts-script.js | 69 +- .../lib/generate-alerts/sample-data/office.js | 1642 ++++++++++++++++- 2 files changed, 1585 insertions(+), 126 deletions(-) diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index 3d8f4f79d1..a4e4108e88 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -315,46 +315,33 @@ function generateAlert(params) { if (params.office) { const beforeDate = new Date(new Date(alert.timestamp) - 3 * 24 * 60 * 60 * 1000); const IntraID = randomArrayItem(Office.arrayUuidOffice); - const InterID = randomArrayItem(Office.arrayUuidOffice); const OrgID = randomArrayItem(Office.arrayUuidOffice); const objID = randomArrayItem(Office.arrayUuidOffice); - const userID = randomArrayItem(Office.arrayUuidOffice); - const appID = randomArrayItem(Office.arrayUuidOffice); + const userKey = randomArrayItem(Office.arrayUuidOffice); + const userID = randomArrayItem(Office.arrayUserId); + const userType = randomArrayItem([0, 2, 4]); + const resultStatus = randomArrayItem(['Succeeded', 'PartiallySucceeded', 'Failed']); + const log = randomArrayItem(Office.arrayLogs); + const ruleData = Office.officeRules[log.RecordType]; - alert.rule = randomArrayItem(Office.arrayRulesOffice); + alert.rule = ruleData.rule; alert.decoder = randomArrayItem(Office.arrayDecoderOffice); alert.GeoLocation = randomArrayItem(GeoLocation); - alert.data.integration = "Office365"; + alert.data.integration = 'Office365'; alert.location = Office.arrayLocationOffice; alert.data.office365 = { - CreationTime: formatDate(beforeDate, 'Y-M-DTh:m:s.lZ'), + ...log, + ...ruleData.data.office365, Id: IntraID, - Operation: "UserLoggedIn", + CreationTime: formatDate(beforeDate, 'Y-M-DTh:m:s.lZ'), OrganizationId: OrgID, - RecordType: "15", - ResultStatus: "Success", - UserKey: userID, - UserType: "0", - Version: "1", - Workload: "AzureActiveDirectory", - ClientIP: "77.231.182.17", + UserType: userType, + UserKey: userKey, + ResultStatus: resultStatus, ObjectId: objID, - UserId: "testing.account@wazuh.com", - AzureActiveDirectoryEventType: "1", - ExtendedProperties: Office.arrayExtendedPropertiesOffice, - ModifiedProperties: [], - Actor: Office.arrayActorOffice, - ActorContextId: OrgID, - ActorIpAddress: "77.231.182.17", - InterSystemsId: InterID, - IntraSystemId: IntraID, - Target: Office.arrayTargetOffice, - TargetContextId: OrgID, - ApplicationId: appID, - DeviceProperties: Office.arrayDevicePropertiesOffice, - ErrorNumber: "0", - Subscription: "Audit.AzureActiveDirectory" - } + UserId: userID, + ClientIP: randomArrayItem(Office.arrayIp), + }; } if (params.gcp) { @@ -1042,17 +1029,17 @@ const dayNames = { function formatDate(date, format) { // It could use "moment" library to format strings too const tokens = { - D: d => formatterNumber(d.getDate(), 2), // 01-31 - A: d => dayNames.long[d.getDay()], // 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' - E: d => dayNames.short[d.getDay()], // 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' - M: d => formatterNumber(d.getMonth() + 1, 2), // 01-12 - J: d => monthNames.long[d.getMonth()], // 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' - N: d => monthNames.short[d.getMonth()], // 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - Y: d => d.getFullYear(), // 2020 - h: d => formatterNumber(d.getHours(), 2), // 00-23 - m: d => formatterNumber(d.getMinutes(), 2), // 00-59 - s: d => formatterNumber(d.getSeconds(), 2), // 00-59 - l: d => formatterNumber(d.getMilliseconds(), 3), // 000-999 + D: (d) => formatterNumber(d.getDate(), 2), // 01-31 + A: (d) => dayNames.long[d.getDay()], // 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + E: (d) => dayNames.short[d.getDay()], // 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' + M: (d) => formatterNumber(d.getMonth() + 1, 2), // 01-12 + J: (d) => monthNames.long[d.getMonth()], // 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' + N: (d) => monthNames.short[d.getMonth()], // 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + Y: (d) => d.getFullYear(), // 2020 + h: (d) => formatterNumber(d.getHours(), 2), // 00-23 + m: (d) => formatterNumber(d.getMinutes(), 2), // 00-59 + s: (d) => formatterNumber(d.getSeconds(), 2), // 00-59 + l: (d) => formatterNumber(d.getMilliseconds(), 3), // 000-999 }; return format.split('').reduce((accum, token) => { diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js index e5129664b6..cf7e3d420d 100644 --- a/server/lib/generate-alerts/sample-data/office.js +++ b/server/lib/generate-alerts/sample-data/office.js @@ -10,113 +10,1585 @@ * Find more information about this on the LICENSE file. */ -export const arrayOfficeGroups = ["office365", "AzureActiveDirectoryStsLogon"]; +export const arrayOfficeGroups = ['office365', 'AzureActiveDirectoryStsLogon']; -export const arrayLocationOffice = "office365"; +export const arrayLocationOffice = 'office365'; export const arrayDecoderOffice = [ { - name: "json" - } -] + name: 'json', + }, +]; export const arrayUuidOffice = [ - "a8080009-aa85-4d65-a0f0-74fe0331edce", - "4e93c8e3-52c1-4a4e-ab69-9e61ccf6cd00", - "d14aa5cb-b070-42f8-8709-0f8afd942fc0", - "92a7e893-0f4a-4635-af0d-83891d4ff9c0", - "ce013f05-a783-4186-9d85-5a14998b6111", - "4f686e03-7cf6-44a8-9212-b8a91b128082", - "cc58e817-c6d3-4457-b011-54e881e230ec", - "825f9d6e-12c0-4b59-807d-1b41c6e48a3a", - "d36253fb-24a1-481c-a199-f778534ccb5f", - "9083369e-679b-4e8b-9249-323a51d5bf9c", - "6d872bf8-e462-4de8-9e16-c36761050fb7", - "b9a73c0f-55f2-4e95-9626-1c264d02eac3", - "bbab91ad-bc8a-4c86-9010-3c84b39fde0d", - "b5359092-dad2-4060-b93d-3791e4da0dec", - "e8493b26-c1f9-42eb-9756-dfd363149852", - "ca2044fc-32ca-478b-8b0d-ff6fdd3b1e5a", - "a0995136-91d8-4acf-8449-28c275ffb7e3", - "c3482b5d-b1a9-4f44-8df0-a601e18cf5c3", - "49fd4642-cfe5-4170-9488-25d847e3579f", - "29f96271-5c1b-47ec-9652-a41d5cb17cb4" -] - -export const arrayDevicePropertiesOffice = [{ - "Name": "BrowserType", - "Value": "Chrome" -}, { - "Name": "IsCompliantAndManaged", - "Value": "False" -}, { - "Name": "SessionId", - "Value": "2a1fb8c4-ceb6-4fa0-826c-3d43f87de897" -}] + 'a8080009-aa85-4d65-a0f0-74fe0331edce', + '4e93c8e3-52c1-4a4e-ab69-9e61ccf6cd00', + 'd14aa5cb-b070-42f8-8709-0f8afd942fc0', + '92a7e893-0f4a-4635-af0d-83891d4ff9c0', + 'ce013f05-a783-4186-9d85-5a14998b6111', + '4f686e03-7cf6-44a8-9212-b8a91b128082', + 'cc58e817-c6d3-4457-b011-54e881e230ec', + '825f9d6e-12c0-4b59-807d-1b41c6e48a3a', + 'd36253fb-24a1-481c-a199-f778534ccb5f', + '9083369e-679b-4e8b-9249-323a51d5bf9c', + '6d872bf8-e462-4de8-9e16-c36761050fb7', + 'b9a73c0f-55f2-4e95-9626-1c264d02eac3', + 'bbab91ad-bc8a-4c86-9010-3c84b39fde0d', + 'b5359092-dad2-4060-b93d-3791e4da0dec', + 'e8493b26-c1f9-42eb-9756-dfd363149852', + 'ca2044fc-32ca-478b-8b0d-ff6fdd3b1e5a', + 'a0995136-91d8-4acf-8449-28c275ffb7e3', + 'c3482b5d-b1a9-4f44-8df0-a601e18cf5c3', + '49fd4642-cfe5-4170-9488-25d847e3579f', + '29f96271-5c1b-47ec-9652-a41d5cb17cb4', +]; -export const arrayTargetOffice = [{ - "ID": "797f4846-ba00-4fd7-ba43-dac1f8f63013", - "Type": 0 -}] +export const arrayDevicePropertiesOffice = [ + { + Name: 'BrowserType', + Value: 'Chrome', + }, + { + Name: 'IsCompliantAndManaged', + Value: 'False', + }, + { + Name: 'SessionId', + Value: '2a1fb8c4-ceb6-4fa0-826c-3d43f87de897', + }, +]; -export const arrayActorOffice = [{ - "ID": "a39dd957-d295-4548-b537-2055469bafbb", - "Type": 0 - }, { - "ID": "alberto.rodriguez@wazuh.com", - "Type": 5 -}] +export const arrayIp = [ + '77.231.182.17', + '172.217.204.94', + '108.177.13.101', + '13.226.52.66', + '13.226.52.2', + '13.226.52.104', + '13.226.52.89', + '140.82.113.3', +]; +export const arrayUserId = [ + 'ale@communities.net', + 'samu@vacaciones.santillana', + 'franco@stress.very', + 'manu@draw.pile', + 'antuan@god.net', +]; +export const arrayTargetOffice = [ + { + ID: '797f4846-ba00-4fd7-ba43-dac1f8f63013', + Type: 0, + }, +]; -export const arrayExtendedPropertiesOffice = [{ - "Name": "ResultStatusDetail", - "Value": "Success" - }, { - "Name": "UserAgent", - "Value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36" - }, { - "Name": "RequestType", - "Value": "OAuth2:Authorize" - }] +export const arrayActorOffice = [ + { + ID: 'a39dd957-d295-4548-b537-2055469bafbb', + Type: 0, + }, + { + ID: 'albe@wazuh.com', + Type: 5, + }, +]; -export const arrayRulesOffice = [ +export const arrayExtendedPropertiesOffice = [ { + Name: 'ResultStatusDetail', + Value: 'Success', + }, + { + Name: 'UserAgent', + Value: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36', + }, + { + Name: 'RequestType', + Value: 'OAuth2:Authorize', + }, +]; + +export const officeRules = { + 1: { + data: { + office365: { + RecordType: 1, + Subscription: 'Audit.Exchange', + }, + }, + rule: { level: 3, - description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", - id: "91545", + description: 'Office 365: Events from the Exchange admin audit log.', + id: '91533', + mail: false, firedtimes: 3, + groups: ['ExchangeAdmin', 'hipaa_164.312.b', 'pci_dss_10.2.2', 'pci_dss_10.6.1'], + }, + }, + 2: { + data: { + office365: { + RecordType: 2, + Subscription: 'Audit.Exchange', + }, + }, + rule: { + level: 3, + description: + 'Office 365: Events from an Exchange mailbox audit log for actions that are performed on a single item, such as creating or receiving an email message.', + id: '91534', mail: false, - groups: ["office365", "AzureActiveDirectoryStsLogon"] + firedtimes: 3, + groups: ['ExchangeItem', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, }, - { + 4: { + data: { + office365: { + RecordType: 4, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint events.', + id: '91536', + mail: false, + firedtimes: 3, + groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 6: { + data: { + office365: { + RecordType: 6, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint file operation events.', + id: '91537', + mail: false, + firedtimes: 3, + groups: [ + 'SharePointFileOperation', + 'hipaa_164.312.b', + 'hipaa_164.312.c.1', + 'pci_dss_10.6.2', + 'pci_dss_11.5', + ], + }, + }, + 8: { + data: { + office365: { + RecordType: 8, + Subscription: 'Audit.AzureActiveDirectory', + }, + }, + rule: { + level: 3, + description: 'Office 365: Azure Active Directory events.', + id: '91539', + mail: false, + firedtimes: 3, + groups: ['AzureActiveDirectory', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 14: { + data: { + office365: { + RecordType: 14, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint sharing events.', + id: '91544', + mail: false, + firedtimes: 3, + groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 15: { + data: { + office365: { + RecordType: 15, + Subscription: 'Audit.AzureActiveDirectory', + }, + }, + rule: { + level: 3, + description: 'Office 365: Secure Token Service (STS) logon events in Azure Active Directory.', + id: '91545', + mail: false, + firedtimes: 3, + groups: [ + 'AzureActiveDirectoryStsLogon', + 'hipaa_164.312.a.2.I,hipaa_164.312.b', + 'hipaa_164.312.d', + 'hipaa_164.312.e.2.II', + 'pci_dss_8.3,pci_dss_10.6.1', + ], + }, + }, + 18: { + data: { + office365: { + RecordType: 18, + Subscription: 'Audit.General', + }, + }, + rule: { level: 5, - description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", - id: "91546", - firedtimes: 6, + description: 'Office 365: Admin actions from the Security and Compliance Center.', + id: '91548', mail: false, - groups: ["office365", "AzureActiveDirectoryStsLogon"] + firedtimes: 3, + groups: [ + 'SecurityComplianceCenterEOPCmdlet', + 'hipaa_164.312.b', + 'pci_dss_10.2.2', + 'pci_dss_10.6.1', + ], + }, }, - { - level: 7, - description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", - id: "91547", + 36: { + data: { + office365: { + RecordType: 36, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint List events.', + id: '91564', + mail: false, firedtimes: 3, + groups: ['SharePointListOperation', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 52: { + data: { + office365: { + RecordType: 52, + Subscription: 'Audit.General', + }, + }, + rule: { + level: 3, + description: 'Office 365: Data Insights REST API events.', + id: '91580', mail: false, - groups: ["office365", "AzureActiveDirectoryStsLogon"] + firedtimes: 4, + groups: ['DataInsightsRestApiAudit', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, }, +}; +export const arrayLogs = [ { - level: 9, - description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", - id: "91548", - firedtimes: 5, - mail: false, - groups: ["office365", "AzureActiveDirectoryStsLogon"] + Id: '35ab8b89-cfea-4214-5249-08d91a06e537', + Operation: 'SearchDataInsightsSubscription', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: 'fake@email.not', + UserType: 5, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: 'fake@email.not', + AadAppId: '80ccca67-54bd-44ab-8625-4b79c4dc7775', + DataType: 'DataInsightsSubscription', + DatabaseType: 'Directory', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/DataInsightsSubscription?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b', + ResultCount: '1', + }, + { + Id: '27ee2e95-6f55-4723-f91d-08d91a26b9a4', + Operation: 'SearchAlert', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + AadAppId: 'fc780465-2017-40d4-a0c5-307022471b92', + DataType: 'Alert', + DatabaseType: 'DataInsights', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/Alert?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b&PageSize=100&Filter=StartDate+eq+2021-04-18T17%3a59%3a40.8820655Z+and+EndDate+eq+2021-05-18T17%3a59%3a40.8820655Z+and+AlertCategory+any+1%2c3%2c7%2c5%2c4+and+AlertSource+eq+%27Office+365+Security+%26+Compliance%27', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T17:59:52', + Id: '7d3a9d35-6c04-4f02-e8fe-08d91a26bc79', + Operation: 'SearchAlertAggregate', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + AadAppId: 'fc780465-2017-40d4-a0c5-307022471b92', + DataType: 'AlertAggregate', + DatabaseType: 'DataInsights', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/AlertAggregate?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b&PageSize=540&Filter=StartDate+eq+2021-04-18T17%3a59%3a48.3504050Z+and+EndDate+eq+2021-05-18T17%3a59%3a48.3504050Z+and+AlertCategory+any+1%2c3%2c7%2c5%2c4+and+AlertSource+eq+%27Office+365+Security+%26+Compliance%27', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T17:59:46', + Id: 'eb9775cb-59f7-42ea-3ee0-08d91a26b92b', + Operation: 'ValidaterbacAccessCheck', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: 'fake@email.not', + UserType: 5, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: 'fake@email.not', + AadAppId: 'd6fdaa33-e821-4211-83d0-cf74736489e1', + DataType: 'rbacAccessCheck', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/validate/rbacAccessCheck?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T14:12:53', + Id: 'c0eada1b-52b2-450d-84df-6d461420d621', + Operation: 'Get-RetentionCompliancePolicy', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T14:12:53', + UserServicePlan: '', }, { - level: 12, - description: "Office 365: Secure Token Service (STS) logon events in Azure Active Directory.", - id: "91549", - firedtimes: 1, - mail: true, - groups: ["office365", "AzureActiveDirectoryStsLogon"] - } + CreationTime: '2021-05-18T15:52:26', + Id: '45a0d7c4-de73-466a-8e6c-c25f9c035714', + Operation: 'Get-SupervisoryReviewPolicyV2', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:26', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:31', + Id: 'f9912868-b431-435c-8337-0fc3b4370815', + Operation: 'Get-SupervisoryReviewReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: + '-StartDate "<SNIP-PII>" -EndDate "<SNIP-PII>" -PageSize "<SNIP-PII>" -Page "<SNIP-PII>"', + Parameters: + '-StartDate "5/12/2021 12:00:00 AM" -EndDate "5/18/2021 11:59:59 PM" -PageSize "300" -Page "1"', + StartTime: '2021-05-18T15:52:31', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:30', + Id: 'dcecd87a-3061-4dea-9bff-4fbfc23ca328', + Operation: 'Get-SupervisoryReviewOverallProgressReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:30', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:30', + Id: '5641d062-f279-4ca4-9577-50d7ecbfeedb', + Operation: 'Get-SupervisoryReviewTopCasesReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:30', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:15', + Id: '8c7c9f81-68e9-452b-a22d-1333eb9cd647', + Operation: 'Get-ComplianceSearchAction', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-Export "<SNIP-PII>"', + Parameters: '-Export "True"', + StartTime: '2021-05-18T17:50:15', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:12', + Id: '4692201f-8101-455e-b89d-6727ef75c223', + Operation: 'Get-ComplianceTag', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-IncludingLabelState "<SNIP-PII>"', + Parameters: '-IncludingLabelState "True"', + StartTime: '2021-05-18T17:50:12', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:12', + Id: '7d41f1f2-587c-492f-b6ff-2f9d1a519c60', + Operation: 'Get-ComplianceSearch', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-ResultSize "Unlimited"', + Parameters: '-ResultSize "Unlimited"', + StartTime: '2021-05-18T17:50:12', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:59:45', + Id: 'ebcfc2bf-8799-413c-add4-6c2b53cb68e7', + Operation: 'Get-DlpSensitiveInformationType', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: '', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-Organization "0fea4e03-8146-453b-b889-54b4bd11565b"', + Parameters: '-Organization "0fea4e03-8146-453b-b889-54b4bd11565b"', + StartTime: '2021-05-18T17:59:45', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T14:11:41', + Id: '7aeca226-b3e7-4033-9a7f-d067622e8d00', + Operation: 'UserLoggedIn', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 15, + ResultStatus: 'Success', + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ClientIP: '190.16.9.176', + ObjectId: '5f09333a-842c-47da-a157-57da27fcbca5', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'ResultStatusDetail', + Value: 'Redirect', + }, + { + Name: 'UserAgent', + Value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + }, + { + Name: 'RequestType', + Value: 'OAuth2:Authorize', + }, + ], + ModifiedProperties: [], + Actor: [ + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 0, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + ActorIpAddress: '190.16.9.176', + InterSystemsId: 'a3798792-fef1-4b53-bd44-bbbd94cf0e5c', + IntraSystemId: '7aeca226-b3e7-4033-9a7f-d067622e8d00', + SupportTicketId: '', + Target: [ + { + ID: '5f09333a-842c-47da-a157-57da27fcbca5', + Type: 0, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + ApplicationId: '89bee1f7-5e6e-4d8a-9f3d-ecd601259da7', + DeviceProperties: [ + { + Name: 'OS', + Value: 'Windows 10', + }, + { + Name: 'BrowserType', + Value: 'Chrome', + }, + { + Name: 'IsCompliantAndManaged', + Value: 'False', + }, + { + Name: 'SessionId', + Value: '714c4935-a22d-400d-8563-fbbd8bfc2301', + }, + ], + ErrorNumber: '0', + }, + { + CreationTime: '2021-05-18T17:49:11', + Id: '4e621563-394f-42a9-8a8a-8549e1ffa771', + Operation: 'Add service principal.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: 'Not Available', + UserType: 4, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'f738ef14-47dc-4564-b53b-45069484ccc7', + UserId: 'ServicePrincipal_4bf80788-0ec4-481a-ae7b-b71647bf3b57', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'ServicePrincipal', + }, + ], + ModifiedProperties: [ + { + Name: 'AccountEnabled', + NewValue: '[\r\n true\r\n]', + OldValue: '[]', + }, + { + Name: 'AppPrincipalId', + NewValue: '[\r\n "f738ef14-47dc-4564-b53b-45069484ccc7"\r\n]', + OldValue: '[]', + }, + { + Name: 'DisplayName', + NewValue: '[\r\n "Marketplace Api"\r\n]', + OldValue: '[]', + }, + { + Name: 'ServicePrincipalName', + NewValue: '[\r\n "f738ef14-47dc-4564-b53b-45069484ccc7"\r\n]', + OldValue: '[]', + }, + { + Name: 'Credential', + NewValue: + '[\r\n {\r\n "CredentialType": 2,\r\n "KeyStoreId": "291154f0-a9f5-45bb-87be-9c8ee5b6d62c",\r\n "KeyGroupId": "1c5aa04b-dea5-4284-9908-47edd1e12d13"\r\n }\r\n]', + OldValue: '[]', + }, + { + Name: 'Included Updated Properties', + NewValue: 'AccountEnabled, AppPrincipalId, DisplayName, ServicePrincipalName, Credential', + OldValue: '', + }, + { + Name: 'TargetId.ServicePrincipalNames', + NewValue: 'f738ef14-47dc-4564-b53b-45069484ccc7', + OldValue: '', + }, + ], + Actor: [ + { + ID: 'Windows Azure Service Management API', + Type: 1, + }, + { + ID: '797f4846-ba00-4fd7-ba43-dac1f8f63013', + Type: 2, + }, + { + ID: 'ServicePrincipal_4bf80788-0ec4-481a-ae7b-b71647bf3b57', + Type: 2, + }, + { + ID: '4bf80788-0ec4-481a-ae7b-b71647bf3b57', + Type: 2, + }, + { + ID: 'ServicePrincipal', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '9cfba3bb-b478-44aa-a140-465ee7f29274', + IntraSystemId: '21051805-2413-594a-ab5d-006014005348', + SupportTicketId: '', + Target: [ + { + ID: 'ServicePrincipal_f6d2eabc-d020-4643-80a8-2b92b163d1de', + Type: 2, + }, + { + ID: 'f6d2eabc-d020-4643-80a8-2b92b163d1de', + Type: 2, + }, + { + ID: 'ServicePrincipal', + Type: 2, + }, + { + ID: 'Marketplace Api', + Type: 1, + }, + { + ID: 'f738ef14-47dc-4564-b53b-45069484ccc7', + Type: 2, + }, + { + ID: 'f738ef14-47dc-4564-b53b-45069484ccc7', + Type: 4, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-18T21:42:25', + Id: 'af4e552f-0bca-4b02-92c9-4bd430f24f75', + Operation: 'Change user license.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: '100320014080D3AD@wazuh.com', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'fake@email.not', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'User', + }, + ], + ModifiedProperties: [], + Actor: [ + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '1fd09d6b-54d3-4a58-acfe-71cc2c429d97', + IntraSystemId: '0a8ae201-e404-4f6f-99db-a3c92a5bd022', + SupportTicketId: '', + Target: [ + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-18T21:42:25', + Id: 'b27eab84-1ef7-4372-bc68-7213af8ab3fb', + Operation: 'Update user.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: '100320014080D3AD@wazuh.com', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'fake@email.not', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{"UserType":"Member"}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'User', + }, + ], + ModifiedProperties: [ + { + Name: 'AssignedLicense', + NewValue: + '[\r\n "[SkuName=POWER_BI_STANDARD, AccountId=0fea4e03-8146-453b-b889-54b4bd11565b, SkuId=a403ebcc-fae0-4ca2-8c8c-7a907fd6c235, DisabledPlans=[]]"\r\n]', + OldValue: '[]', + }, + { + Name: 'AssignedPlan', + NewValue: + '[\r\n {\r\n "SubscribedPlanId": "c976d07f-fd0f-49eb-bdc2-26c17481e1c5",\r\n "ServiceInstance": "AzureAnalysis/SDF",\r\n "CapabilityStatus": 0,\r\n "AssignedTimestamp": "2021-05-18T21:42:25.3894164Z",\r\n "InitialState": null,\r\n "Capability": null,\r\n "ServicePlanId": "2049e525-b859-401b-b2a0-e0a31c4b1fe4"\r\n }\r\n]', + OldValue: '[]', + }, + { + Name: 'Included Updated Properties', + NewValue: 'AssignedLicense, AssignedPlan', + OldValue: '', + }, + { + Name: 'TargetId.UserType', + NewValue: 'Member', + OldValue: '', + }, + ], + Actor: [ + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '1fd09d6b-54d3-4a58-acfe-71cc2c429d97', + IntraSystemId: '0a8ae201-e404-4f6f-99db-a3c92a5bd022', + SupportTicketId: '', + Target: [ + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '8c3d0215-66f0-41b0-3205-08d91bb6b63c', + Operation: 'SharingPolicyChanged', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Site', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + ModifiedProperties: [ + { + Name: 'ShareUsingAnonymousLinks', + NewValue: 'Enabled', + OldValue: 'Disabled', + }, + ], + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '35a1b515-2a0e-4bd6-d0a3-08d91bb6b639', + Operation: 'SiteCollectionCreated', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Site', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + EventData: + '<SiteCreationSource>API</SiteCreationSource><TenantSettings.ShowCreateSiteCommand>True</TenantSettings.ShowCreateSiteCommand><TenantSettings.UseCustomSiteCreationForm>False</TenantSettings.UseCustomSiteCreationForm>', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '344f9139-f437-4290-9566-08d91bb6b61f', + Operation: 'SiteCollectionAdminRemoved', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: 'a9d15b23-6ac9-43c5-af3c-b4a0916631c1', + ModifiedProperties: [ + { + Name: 'SiteAdmin', + NewValue: '', + OldValue: '', + }, + ], + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + TargetUserOrGroupName: 'SHAREPOINT\\system', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: 'd36e4b4d-1e8b-4634-6dd8-08d91bb6b618', + Operation: 'SiteCollectionAdminAdded', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: 'a9d15b23-6ac9-43c5-af3c-b4a0916631c1', + ModifiedProperties: [ + { + Name: 'SiteAdmin', + NewValue: 'fake@email.not', + OldValue: '', + }, + ], + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + TargetUserOrGroupName: 'fake@email.not', + }, + { + CreationTime: '2021-05-20T17:43:22', + Id: '0d6a62d3-e4bd-44ee-ce8d-08d91bb6c392', + Operation: 'PageViewed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/_layouts/15/CreateGroup.aspx', + UserId: 'fake@email.not', + CorrelationId: 'ccd0c99f-309b-2000-df13-3fcca9a8c8e1', + CustomUniqueId: true, + EventSource: 'SharePoint', + ItemType: 'Page', + ListItemUniqueId: '59a8433d-9bb8-cfef-65b7-ef35de00c8f6', + Site: 'f7fbb805-5f6b-4950-b681-2365eb46081f', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '3b56db49-60e3-410e-acbd-d8765467388a', + }, + { + CreationTime: '2021-05-20T17:45:57', + Id: '18bb351b-49e1-47df-8f4d-08d91bb71ffd', + Operation: 'AddedToGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'fake@email.not', + CorrelationId: 'f1d0c99f-3094-2000-da82-454f034ca629', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + EventData: '<Group>Site Owners</Group>', + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + TargetUserOrGroupName: 'SHAREPOINT\\system', + }, + { + CreationTime: '2021-05-20T17:46:26', + Id: '29bde84a-d3ec-4388-4600-08d91bb730bc', + Operation: 'FileAccessed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/Shared Documents/Forms/AllItems.aspx', + UserId: 'fake@email.not', + CorrelationId: 'f9d0c99f-b04f-2000-da82-4bb2abf6168f', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + ListItemUniqueId: '3c9d8943-846e-41f3-a647-72a5e4e3decf', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'AllItems.aspx', + SourceRelativeUrl: 'Shared Documents/Forms', + }, + { + CreationTime: '2021-05-20T17:46:25', + Id: '087e5b68-fc3f-4e01-1efc-08d91bb730b5', + Operation: 'ListViewed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 36, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/fd2ebaf0-900b-4dff-8fc2-d348be51e677', + UserId: 'fake@email.not', + CorrelationId: 'f9d0c99f-b04f-2000-da82-4bb2abf6168f', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'List', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + CustomizedDoclib: false, + FromApp: true, + IsDocLib: true, + ItemCount: 0, + ListBaseTemplateType: '101', + ListBaseType: 'DocumentLibrary', + ListColor: '', + ListIcon: '', + Source: 'Unknown', + TemplateTypeId: '', + ListTitle: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + }, + { + CreationTime: '2021-05-20T17:52:29', + Id: '41225487-31c1-4e24-b8b0-08d91bb8094c', + Operation: 'PagePrefetched', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'fake@email.not', + CorrelationId: '52d1c99f-3000-2000-df13-3ab1e8fb9f92', + CustomUniqueId: false, + EventSource: 'SharePoint', + ItemType: 'Page', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:51:49', + Id: 'd930cc5c-2658-45df-6361-08d91bb7f179', + Operation: 'FileCheckedOut', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '48d1c99f-f03c-2000-df13-38983a6608f8', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + HighPriorityMediaProcessing: false, + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'Home.aspx', + SourceRelativeUrl: 'SitePages', + }, + { + CreationTime: '2021-05-20T17:51:51', + Id: '89d76362-e493-4c20-3b69-08d91bb7f288', + Operation: 'ListUpdated', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 36, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + UserId: 'fake@email.not', + CorrelationId: '48d1c99f-f0a8-2000-da82-41be3f973267', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'List', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + CustomizedDoclib: false, + FromApp: false, + IsDocLib: true, + ItemCount: 1, + ListBaseTemplateType: '119', + ListBaseType: 'DocumentLibrary', + ListColor: '', + ListIcon: '', + Source: 'Unknown', + TemplateTypeId: '', + ListTitle: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + }, + { + CreationTime: '2021-05-20T17:52:36', + Id: '7a91dd8c-560b-4fbe-2585-08d91bb80d46', + Operation: 'ClientViewSignaled', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '53d1c99f-b0aa-2000-df13-3efea9e41071', + CustomUniqueId: false, + EventSource: 'SharePoint', + ItemType: 'Page', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:53:37', + Id: '9695afcd-19ff-491f-a6ee-08d91bb831d1', + Operation: 'FileModified', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '62d1c99f-d09c-2000-df13-37ddf480e717', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'Home.aspx', + SourceRelativeUrl: 'SitePages', + }, + { + CreationTime: '2021-05-20T17:57:03', + Id: '551fd7d5-bac1-4bb4-11d2-08d91bb8ac9e', + Operation: 'FileAccessedExtended', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/Shared Documents/Forms/AllItems.aspx', + UserId: 'fake@email.not', + CorrelationId: '94d1c99f-20eb-2000-df13-35746d02911e', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + ListItemUniqueId: '3c9d8943-846e-41f3-a647-72a5e4e3decf', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'AllItems.aspx', + SourceRelativeUrl: 'Shared Documents/Forms', + }, + { + CreationTime: '2021-05-20T17:59:55', + Id: 'eb1f0911-9bed-4f15-10e5-08d91bb91372', + Operation: 'SiteDeleted', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'S-1-0-0', + UserType: 4, + Version: 1, + Workload: 'SharePoint', + ClientIP: '', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'AAD to SharePoint Sync', + CorrelationId: 'bed1c99f-20ee-2000-df13-306cb6803c92', + EventSource: 'SharePoint', + ItemType: 'Web', + ListItemUniqueId: '00000000-0000-0000-0000-000000000000', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: '', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + DestinationFileExtension: '', + SourceFileExtension: '', + DestinationFileName: 'TestSharePoint', + DestinationRelativeUrl: '../../https://wazuh.sharepoint.com/sites', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'TestSharePoint', + SourceRelativeUrl: '..', + }, + { + CreationTime: '2021-05-20T17:59:11', + Id: '0d20a3e1-e9cb-436c-799f-08d91bb8f92f', + Operation: 'PageViewedExtended', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/_layouts/15/online/handlers/SpoSuiteLinks.ashx', + UserId: 'fake@email.not', + CorrelationId: 'b4d1c99f-0043-2000-da82-41b63e1d91f4', + EventSource: 'SharePoint', + ItemType: 'Page', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:44:27', + Id: '30ef2f70-a12d-4b31-1e70-08d91bb6ea2e', + Operation: 'Set-Mailbox', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ClientIP: '52.233.237.141:40638', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/tomas.turina', + UserId: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + AppId: '61109738-7d2b-4a0b-9fe3-660b1ff83505', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AM9PR04MB8986 (15.20.4150.023)', + Parameters: [ + { + Name: 'Identity', + Value: + 'MGZlYTRlMDMtODE0Ni00NTNiLWI4ODktNTRiNGJkMTE1NjViXGJkYmI4MjM2LTBmNDgtNGZjNi05Zjc3LTkxNGNkY2MwMmIzYw2', + }, + { + Name: 'ResourceEmailAddresses', + Value: 'True', + }, + { + Name: 'BypassLiveId', + Value: 'True', + }, + { + Name: 'Force', + Value: 'True', + }, + { + Name: 'DomainController', + Value: 'HE1PR04A010DC03.EURPR04A010.prod.outlook.com', + }, + { + Name: 'EmailAddresses', + Value: + 'SIP:fake@email.not;SMTP:fake@email.not;SPO:SPO_f49feae4-033d-4028-97d1-3acd55341f69@SPO_0fea4e03-8146-453b-b889-54b4bd11565b', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T17:45:59', + Id: '48c00930-b25d-4ccc-ccb3-08d91bb720f6', + Operation: 'ModifyFolderPermissions', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 2, + ResultStatus: 'Succeeded', + UserKey: 'S-1-5-18', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '::1', + UserId: 'S-1-5-18', + ClientIPAddress: '::1', + ClientInfoString: 'Client=WebServices;Action=ConfigureGroupMailbox', + ExternalAccess: true, + InternalLogonType: 1, + LogonType: 1, + LogonUserSid: 'S-1-5-18', + MailboxGuid: 'fc108b45-9d51-4b87-a473-9d5a0e404966', + MailboxOwnerMasterAccountSid: 'S-1-5-10', + MailboxOwnerSid: 'S-1-5-21-2986565805-1835265550-1383574073-20743067', + MailboxOwnerUPN: 'TestSharePoint@wazuh.com', + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AS8PR04MB8465 (15.20.4150.023)\r\n', + Item: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAENAAAC', + ParentFolder: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAENAAAC', + MemberRights: + 'ReadAny, Create, EditOwned, DeleteOwned, EditAny, DeleteAny, Visible, FreeBusySimple, FreeBusyDetailed', + MemberSid: 'S-1-8-4228942661-1267178833-1520268196-1716076558-1', + MemberUpn: 'Member@local', + Name: 'Calendar', + Path: '\\Calendar', + }, + }, + }, + { + CreationTime: '2021-05-20T17:45:58', + Id: 'bb03b48e-609d-477b-cb80-08d91bb72077', + Operation: 'Create', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 2, + ResultStatus: 'Succeeded', + UserKey: 'S-1-5-18', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '::1', + UserId: 'S-1-5-18', + ClientIPAddress: '::1', + ClientInfoString: 'Client=WebServices;Action=ConfigureGroupMailbox', + ExternalAccess: true, + InternalLogonType: 1, + LogonType: 1, + LogonUserSid: 'S-1-5-18', + MailboxGuid: 'fc108b45-9d51-4b87-a473-9d5a0e404966', + MailboxOwnerMasterAccountSid: 'S-1-5-10', + MailboxOwnerSid: 'S-1-5-21-2986565805-1835265550-1383574073-20743067', + MailboxOwnerUPN: 'TestSharePoint@wazuh.com', + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AS8PR04MB8465 (15.20.4150.023)\r\n', + Item: { + Attachments: + 'warming_email_03_2017_calendar.png (646b); warming_email_03_2017_conversation.png (661b); warming_email_03_2017_links.png (1450b); google_play_store_badge.png (4871b); apple_store_badge.png (4493b); windows_store_badge.png (3728b); warming_email_03_2017_files.png (856b); warming_email_03_2017_sharePoint.png (1479b)', + Id: + 'RgAAAAA6tVhba3JWSaGmky7/7OvfBwDRwKc47c1sT4Waab6O4zbPAAAAAAEMAADRwKc47c1sT4Waab6O4zbPAAAAAAk9AAAJ', + InternetMessageId: + '<AS8PR04MB846542106D3939F2D1952D05D32A9@AS8PR04MB8465.eurprd04.prod.outlook.com>', + IsRecord: false, + ParentFolder: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAEMAAAB', + Path: '\\Inbox', + }, + Subject: 'The new TestSharePoint group is ready', + }, + }, + { + CreationTime: '2021-05-20T17:59:59', + Id: 'e855fb12-2d48-45f3-ac8d-08d91bb91569', + Operation: 'Remove-UnifiedGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'NT AUTHORITY\\SYSTEM (w3wp)', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '[2a01:111:f402:ac00::f134]:51514', + ObjectId: 'TestSharePoint_b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + UserId: 'NT AUTHORITY\\SYSTEM (w3wp)', + AppId: '00000003-0000-0ff1-ce00-000000000000', + ClientAppId: '00000003-0000-0ff1-ce00-000000000000', + ExternalAccess: false, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'VI1PR04MB6125 (15.20.4129.033)', + Parameters: [ + { + Name: 'Identity', + Value: 'b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T18:04:37', + Id: 'f111c82c-7961-473d-112a-08d91bb9bb91', + Operation: 'Set-UnifiedGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ClientIP: '51.144.33.14:58849', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/Soft Deleted Objects/TestSharePoint_b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + UserId: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + AppId: '61109738-7d2b-4a0b-9fe3-660b1ff83505', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'VI1PR0402MB3326 (15.20.4129.033)', + Parameters: [ + { + Name: 'Identity', + Value: + 'MGZlYTRlMDMtODE0Ni00NTNiLWI4ODktNTRiNGJkMTE1NjViXDFlYjFjNjZhLTRhYWQtNGY2Mi04NjAzLTdjMDRkZTIxYWE3Mg2', + }, + { + Name: 'EmailAddresses', + Value: 'smtp:TestSharePoint@wazuh.testytest.com;SMTP:TestSharePoint@wazuh.com', + }, + { + Name: 'IncludeSoftDeletedObjects', + Value: 'True', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T18:59:49', + Id: '32229114-e357-4b56-9d08-08d91bc1717c', + Operation: 'Set-User', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.Management.ForwardSync)', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/tomas.turina', + UserId: 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.Management.ForwardSync)', + AppId: '', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'DB8PR04MB7065 (15.20.4150.023)', + Parameters: [ + { + Name: 'Identity', + Value: '0fea4e03-8146-453b-b889-54b4bd11565b\\bdbb8236-0f48-4fc6-9f77-914cdcc02b3c', + }, + { + Name: 'SyncMailboxLocationGuids', + Value: 'True', + }, + { + Name: 'ErrorAction', + Value: 'Stop', + }, + { + Name: 'WarningAction', + Value: 'SilentlyContinue', + }, + ], + }, ]; From e8bc7ef11eb563d958d8a12de2646d97efeafe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 10:24:32 +0200 Subject: [PATCH 123/493] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc557c2785..8f0b3e1a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) ### Changed From fc063d29130c1b96dc678fe6fe88ed49e1af3748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 10:28:28 +0200 Subject: [PATCH 124/493] Fixed typo in the copiright card --- server/lib/generate-alerts/sample-data/office.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js index cf7e3d420d..75d3cd3903 100644 --- a/server/lib/generate-alerts/sample-data/office.js +++ b/server/lib/generate-alerts/sample-data/office.js @@ -1,5 +1,5 @@ /* - * Wazuh app - AWS sample data + * Wazuh app - Office365 sample data * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify From 586d12ff7347cfa4a10b7e751f7d26e2916852aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 10:31:14 +0200 Subject: [PATCH 125/493] Fixed another typo --- public/components/add-modules-data/guides/office.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/add-modules-data/guides/office.js b/public/components/add-modules-data/guides/office.js index 9eee5e4b86..c99a56e25c 100644 --- a/public/components/add-modules-data/guides/office.js +++ b/public/components/add-modules-data/guides/office.js @@ -1,5 +1,5 @@ /* -* Wazuh app - Amazon Web Services interactive extension guide +* Wazuh app - Office365 interactive extension guide * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify From 563e4b1436bb58adb3f44c20f7148435194424d8 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Fri, 23 Jul 2021 11:01:39 +0200 Subject: [PATCH 126/493] Added Office365 sample data (#3424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Office365 sample data * Office365 sample data Edit some texts adding office keywords * Changed office365alerts so they use the full logs Using all the logs provided by core and randomized sample data on new fields. * Updated changelog * Fixed typo in the copiright card * Fixed another typo Co-authored-by: Manuel Gómez Castro <manuel.gomez@wazuh.com> --- CHANGELOG.md | 1 + common/constants.ts | 1 + .../add-modules-data/guides/office.js | 181 ++ .../add-modules-data/sample-data.tsx | 2 +- .../generate-alerts/generate-alerts-script.js | 56 +- .../lib/generate-alerts/sample-data/office.js | 1594 +++++++++++++++++ 6 files changed, 1823 insertions(+), 12 deletions(-) create mode 100644 public/components/add-modules-data/guides/office.js create mode 100644 server/lib/generate-alerts/sample-data/office.js diff --git a/CHANGELOG.md b/CHANGELOG.md index cc557c2785..8f0b3e1a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) +- Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) ### Changed diff --git a/common/constants.ts b/common/constants.ts index 23bb111a8c..d3e0439891 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -64,6 +64,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { [WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY]: [ { syscheck: true }, { aws: true }, + { office: true }, { gcp: true }, { authentication: true }, { ssh: true }, diff --git a/public/components/add-modules-data/guides/office.js b/public/components/add-modules-data/guides/office.js new file mode 100644 index 0000000000..c99a56e25c --- /dev/null +++ b/public/components/add-modules-data/guides/office.js @@ -0,0 +1,181 @@ +/* +* Wazuh app - Office365 interactive extension guide +* Copyright (C) 2015-2021 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 default { + id: 'office', + name: 'Office 365', + wodle_name: 'office', + description: 'Configuration options of the Office 365 wodle.', + category: 'Security information management', + documentation_link: 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/wodle-s3.html', + icon: 'logoOfficeMono', + avaliable_for_manager: true, + avaliable_for_agent: true, + steps: [ + // { + // title: 'Required settings', + // description: '', + // elements: [ + // { + // name: 'disabled', + // description: `Disables the AWS-S3 wodle.`, + // type: 'switch', + // required: true + // }, + // { + // name: 'interval', + // description: 'Frequency for reading from the S3 bucket.', + // type: 'input', + // required: true, + // placeholder: 'Positive number with suffix character indicating a time unit', + // default_value: '10m', + // validate_error_message: 'A positive number that should contain a suffix character indicating a time unit, such as, s (seconds), m (minutes), h (hours), d (days). e.g. 10m', + // validate_regex: /^[1-9]\d*[s|m|h|d]$/ + // }, + // { + // name: 'run_on_start', + // description: 'Run evaluation immediately when service is started.', + // type: 'switch', + // required: true, + // default_value: true + // }, + + // ] + // }, + // { + // title: 'Optional settings', + // description: '', + // elements: [ + // { + // name: 'remove_from_bucket', + // description: 'Define if you want to remove logs from your S3 bucket after they are read by the wodle.', + // type: 'switch', + // default_value: true + // }, + // { + // name: 'skip_on_error', + // description: 'When unable to process and parse a CloudTrail log, skip the log and continue processing', + // type: 'switch', + // default_value: true + // } + // ] + // }, + // { + // title: 'Buckets', + // description: 'Defines one or more buckets to process.', + // elements: [ + // { + // name: 'bucket', + // description: 'Defines a bucket to process.', + // removable: true, + // required: true, + // repeatable: true, + // repeatable_insert_first: true, + // repeatable_insert_first_properties: { + // removable: false + // }, + // validate_error_message: 'Any directory or file name.', + // show_attributes: true, + // attributes: [ + // { + // name: 'type', + // description: 'Specifies type of bucket.', + // info: 'Different configurations as macie has custom type.', + // type: 'select', + // required: true, + // values: [ + // {value: 'cloudtrail', text: 'cloudtrail'}, + // {value: 'guardduty', text: 'guardduty'}, + // {value: 'vpcflow', text: 'vpcflow'}, + // {value: 'config', text: 'config'}, + // {value: 'custom', text: 'custom'} + // ], + // default_value: 'cloudtrail' + // } + // ], + // show_options: true, + // options: [ + // { + // name: 'name', + // description: 'Name of the S3 bucket from where logs are read.', + // type: 'input', + // required: true, + // placeholder: 'Name of the S3 bucket' + // }, + // { + // name: 'aws_account_id', + // description: 'The AWS Account ID for the bucket logs. Only works with CloudTrail buckets.', + // type: 'input', + // placeholder: 'Comma list of 12 digit AWS Account ID' + // }, + // { + // name: 'aws_account_alias', + // description: 'A user-friendly name for the AWS account.', + // type: 'input', + // placeholder: 'AWS account user-friendly name' + // }, + // { + // name: 'access_key', + // description: 'The access key ID for the IAM user with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Any alphanumerical key.' + // }, + // { + // name: 'secret_key', + // description: 'The secret key created for the IAM user with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Any alphanumerical key.' + // }, + // { + // name: 'aws_profile', + // description: 'A valid profile name from a Shared Credential File or AWS Config File with the permission to read logs from the bucket.', + // type: 'input', + // placeholder: 'Valid profile name' + // }, + // { + // name: 'iam_role_arn', + // description: 'A valid role arn with permission to read logs from the bucket.Valid role arn', + // type: 'input', + // placeholder: 'Valid role arn' + // }, + // { + // name: 'path', + // description: 'If defined, the path or prefix for the bucket.', + // type: 'input', + // placeholder: 'Path or prefix for the bucket.' + // }, + // { + // name: 'only_logs_after', + // description: 'A valid date, in YYYY-MMM-DD format, that only logs from after that date will be parsed. All logs from before that date will be skipped.', + // type: 'input', + // placeholder: 'Date, e.g.: 2020-APR-02', + // validate_regex: /^[1-9]\d{3}-((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OCT)|(NOV)|(DEC))-\d{2}$/, + // validate_error_message: 'A valid date, in YYYY-MMM-DD format' + // }, + // { + // name: 'regions', + // description: 'A comma-delimited list of regions to limit parsing of logs. Only works with CloudTrail buckets.', + // type: 'input', + // default_value: 'All regions', + // placeholder: 'Comma-delimited list of valid regions' + // }, + // { + // name: 'aws_organization_id', + // description: 'Name of AWS organization. Only works with CloudTrail buckets.', + // type: 'input', + // placeholder: 'Valid AWS organization name' + // } + // ] + // } + // ] + // } + ] +} \ No newline at end of file diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index f4457d7be0..5d1a2f2278 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -47,7 +47,7 @@ export default class WzSampleData extends Component { this.categories = [ { title: 'Sample security information', - description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Google Cloud Platform, authorization, ssh, web).', + description: 'Sample data, visualizations and dashboards for security information (integrity monitoring, Amazon AWS services, Office 365, Google Cloud Platform, authorization, ssh, web).', image: '', categorySampleAlertsIndex: 'security' }, diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index 61ed911ec1..a4e4108e88 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -40,6 +40,7 @@ import * as Vulnerability from './sample-data/vulnerabilities'; import * as SSH from './sample-data/ssh'; import * as Apache from './sample-data/apache'; import * as Web from './sample-data/web'; +import * as Office from './sample-data/office'; //Alert const alertIDMax = 6000; @@ -59,6 +60,7 @@ const ruleMaxLevel = 14; * @param {any} params - params to configure the alert * @param {boolean} params.aws - if true, set aws fields * @param {boolean} params.audit - if true, set System Auditing fields + * @param {boolean} params.office - if true, set office fields * @param {boolean} params.ciscat - if true, set CIS-CAT fields * @param {boolean} params.gcp - if true, set GCP fields * @param {boolean} params.docker - if true, set Docker fields @@ -310,6 +312,38 @@ function generateAlert(params) { alert.GeoLocation = randomArrayItem(GeoLocation); } + if (params.office) { + const beforeDate = new Date(new Date(alert.timestamp) - 3 * 24 * 60 * 60 * 1000); + const IntraID = randomArrayItem(Office.arrayUuidOffice); + const OrgID = randomArrayItem(Office.arrayUuidOffice); + const objID = randomArrayItem(Office.arrayUuidOffice); + const userKey = randomArrayItem(Office.arrayUuidOffice); + const userID = randomArrayItem(Office.arrayUserId); + const userType = randomArrayItem([0, 2, 4]); + const resultStatus = randomArrayItem(['Succeeded', 'PartiallySucceeded', 'Failed']); + const log = randomArrayItem(Office.arrayLogs); + const ruleData = Office.officeRules[log.RecordType]; + + alert.rule = ruleData.rule; + alert.decoder = randomArrayItem(Office.arrayDecoderOffice); + alert.GeoLocation = randomArrayItem(GeoLocation); + alert.data.integration = 'Office365'; + alert.location = Office.arrayLocationOffice; + alert.data.office365 = { + ...log, + ...ruleData.data.office365, + Id: IntraID, + CreationTime: formatDate(beforeDate, 'Y-M-DTh:m:s.lZ'), + OrganizationId: OrgID, + UserType: userType, + UserKey: userKey, + ResultStatus: resultStatus, + ObjectId: objID, + UserId: userID, + ClientIP: randomArrayItem(Office.arrayIp), + }; + } + if (params.gcp) { alert.rule = randomArrayItem(GCP.arrayRules); alert.data.integration = 'gcp'; @@ -995,17 +1029,17 @@ const dayNames = { function formatDate(date, format) { // It could use "moment" library to format strings too const tokens = { - D: d => formatterNumber(d.getDate(), 2), // 01-31 - A: d => dayNames.long[d.getDay()], // 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' - E: d => dayNames.short[d.getDay()], // 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' - M: d => formatterNumber(d.getMonth() + 1, 2), // 01-12 - J: d => monthNames.long[d.getMonth()], // 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' - N: d => monthNames.short[d.getMonth()], // 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' - Y: d => d.getFullYear(), // 2020 - h: d => formatterNumber(d.getHours(), 2), // 00-23 - m: d => formatterNumber(d.getMinutes(), 2), // 00-59 - s: d => formatterNumber(d.getSeconds(), 2), // 00-59 - l: d => formatterNumber(d.getMilliseconds(), 3), // 000-999 + D: (d) => formatterNumber(d.getDate(), 2), // 01-31 + A: (d) => dayNames.long[d.getDay()], // 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + E: (d) => dayNames.short[d.getDay()], // 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' + M: (d) => formatterNumber(d.getMonth() + 1, 2), // 01-12 + J: (d) => monthNames.long[d.getMonth()], // 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' + N: (d) => monthNames.short[d.getMonth()], // 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + Y: (d) => d.getFullYear(), // 2020 + h: (d) => formatterNumber(d.getHours(), 2), // 00-23 + m: (d) => formatterNumber(d.getMinutes(), 2), // 00-59 + s: (d) => formatterNumber(d.getSeconds(), 2), // 00-59 + l: (d) => formatterNumber(d.getMilliseconds(), 3), // 000-999 }; return format.split('').reduce((accum, token) => { diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js new file mode 100644 index 0000000000..75d3cd3903 --- /dev/null +++ b/server/lib/generate-alerts/sample-data/office.js @@ -0,0 +1,1594 @@ +/* + * Wazuh app - Office365 sample data + * Copyright (C) 2015-2021 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 arrayOfficeGroups = ['office365', 'AzureActiveDirectoryStsLogon']; + +export const arrayLocationOffice = 'office365'; + +export const arrayDecoderOffice = [ + { + name: 'json', + }, +]; + +export const arrayUuidOffice = [ + 'a8080009-aa85-4d65-a0f0-74fe0331edce', + '4e93c8e3-52c1-4a4e-ab69-9e61ccf6cd00', + 'd14aa5cb-b070-42f8-8709-0f8afd942fc0', + '92a7e893-0f4a-4635-af0d-83891d4ff9c0', + 'ce013f05-a783-4186-9d85-5a14998b6111', + '4f686e03-7cf6-44a8-9212-b8a91b128082', + 'cc58e817-c6d3-4457-b011-54e881e230ec', + '825f9d6e-12c0-4b59-807d-1b41c6e48a3a', + 'd36253fb-24a1-481c-a199-f778534ccb5f', + '9083369e-679b-4e8b-9249-323a51d5bf9c', + '6d872bf8-e462-4de8-9e16-c36761050fb7', + 'b9a73c0f-55f2-4e95-9626-1c264d02eac3', + 'bbab91ad-bc8a-4c86-9010-3c84b39fde0d', + 'b5359092-dad2-4060-b93d-3791e4da0dec', + 'e8493b26-c1f9-42eb-9756-dfd363149852', + 'ca2044fc-32ca-478b-8b0d-ff6fdd3b1e5a', + 'a0995136-91d8-4acf-8449-28c275ffb7e3', + 'c3482b5d-b1a9-4f44-8df0-a601e18cf5c3', + '49fd4642-cfe5-4170-9488-25d847e3579f', + '29f96271-5c1b-47ec-9652-a41d5cb17cb4', +]; + +export const arrayDevicePropertiesOffice = [ + { + Name: 'BrowserType', + Value: 'Chrome', + }, + { + Name: 'IsCompliantAndManaged', + Value: 'False', + }, + { + Name: 'SessionId', + Value: '2a1fb8c4-ceb6-4fa0-826c-3d43f87de897', + }, +]; + +export const arrayIp = [ + '77.231.182.17', + '172.217.204.94', + '108.177.13.101', + '13.226.52.66', + '13.226.52.2', + '13.226.52.104', + '13.226.52.89', + '140.82.113.3', +]; +export const arrayUserId = [ + 'ale@communities.net', + 'samu@vacaciones.santillana', + 'franco@stress.very', + 'manu@draw.pile', + 'antuan@god.net', +]; +export const arrayTargetOffice = [ + { + ID: '797f4846-ba00-4fd7-ba43-dac1f8f63013', + Type: 0, + }, +]; + +export const arrayActorOffice = [ + { + ID: 'a39dd957-d295-4548-b537-2055469bafbb', + Type: 0, + }, + { + ID: 'albe@wazuh.com', + Type: 5, + }, +]; + +export const arrayExtendedPropertiesOffice = [ + { + Name: 'ResultStatusDetail', + Value: 'Success', + }, + { + Name: 'UserAgent', + Value: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36', + }, + { + Name: 'RequestType', + Value: 'OAuth2:Authorize', + }, +]; + +export const officeRules = { + 1: { + data: { + office365: { + RecordType: 1, + Subscription: 'Audit.Exchange', + }, + }, + rule: { + level: 3, + description: 'Office 365: Events from the Exchange admin audit log.', + id: '91533', + mail: false, + firedtimes: 3, + groups: ['ExchangeAdmin', 'hipaa_164.312.b', 'pci_dss_10.2.2', 'pci_dss_10.6.1'], + }, + }, + 2: { + data: { + office365: { + RecordType: 2, + Subscription: 'Audit.Exchange', + }, + }, + rule: { + level: 3, + description: + 'Office 365: Events from an Exchange mailbox audit log for actions that are performed on a single item, such as creating or receiving an email message.', + id: '91534', + mail: false, + firedtimes: 3, + groups: ['ExchangeItem', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 4: { + data: { + office365: { + RecordType: 4, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint events.', + id: '91536', + mail: false, + firedtimes: 3, + groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 6: { + data: { + office365: { + RecordType: 6, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint file operation events.', + id: '91537', + mail: false, + firedtimes: 3, + groups: [ + 'SharePointFileOperation', + 'hipaa_164.312.b', + 'hipaa_164.312.c.1', + 'pci_dss_10.6.2', + 'pci_dss_11.5', + ], + }, + }, + 8: { + data: { + office365: { + RecordType: 8, + Subscription: 'Audit.AzureActiveDirectory', + }, + }, + rule: { + level: 3, + description: 'Office 365: Azure Active Directory events.', + id: '91539', + mail: false, + firedtimes: 3, + groups: ['AzureActiveDirectory', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 14: { + data: { + office365: { + RecordType: 14, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint sharing events.', + id: '91544', + mail: false, + firedtimes: 3, + groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 15: { + data: { + office365: { + RecordType: 15, + Subscription: 'Audit.AzureActiveDirectory', + }, + }, + rule: { + level: 3, + description: 'Office 365: Secure Token Service (STS) logon events in Azure Active Directory.', + id: '91545', + mail: false, + firedtimes: 3, + groups: [ + 'AzureActiveDirectoryStsLogon', + 'hipaa_164.312.a.2.I,hipaa_164.312.b', + 'hipaa_164.312.d', + 'hipaa_164.312.e.2.II', + 'pci_dss_8.3,pci_dss_10.6.1', + ], + }, + }, + 18: { + data: { + office365: { + RecordType: 18, + Subscription: 'Audit.General', + }, + }, + rule: { + level: 5, + description: 'Office 365: Admin actions from the Security and Compliance Center.', + id: '91548', + mail: false, + firedtimes: 3, + groups: [ + 'SecurityComplianceCenterEOPCmdlet', + 'hipaa_164.312.b', + 'pci_dss_10.2.2', + 'pci_dss_10.6.1', + ], + }, + }, + 36: { + data: { + office365: { + RecordType: 36, + Subscription: 'Audit.SharePoint', + }, + }, + rule: { + level: 3, + description: 'Office 365: SharePoint List events.', + id: '91564', + mail: false, + firedtimes: 3, + groups: ['SharePointListOperation', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, + 52: { + data: { + office365: { + RecordType: 52, + Subscription: 'Audit.General', + }, + }, + rule: { + level: 3, + description: 'Office 365: Data Insights REST API events.', + id: '91580', + mail: false, + firedtimes: 4, + groups: ['DataInsightsRestApiAudit', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + }, + }, +}; +export const arrayLogs = [ + { + Id: '35ab8b89-cfea-4214-5249-08d91a06e537', + Operation: 'SearchDataInsightsSubscription', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: 'fake@email.not', + UserType: 5, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: 'fake@email.not', + AadAppId: '80ccca67-54bd-44ab-8625-4b79c4dc7775', + DataType: 'DataInsightsSubscription', + DatabaseType: 'Directory', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/DataInsightsSubscription?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b', + ResultCount: '1', + }, + { + Id: '27ee2e95-6f55-4723-f91d-08d91a26b9a4', + Operation: 'SearchAlert', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + AadAppId: 'fc780465-2017-40d4-a0c5-307022471b92', + DataType: 'Alert', + DatabaseType: 'DataInsights', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/Alert?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b&PageSize=100&Filter=StartDate+eq+2021-04-18T17%3a59%3a40.8820655Z+and+EndDate+eq+2021-05-18T17%3a59%3a40.8820655Z+and+AlertCategory+any+1%2c3%2c7%2c5%2c4+and+AlertSource+eq+%27Office+365+Security+%26+Compliance%27', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T17:59:52', + Id: '7d3a9d35-6c04-4f02-e8fe-08d91a26bc79', + Operation: 'SearchAlertAggregate', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + AadAppId: 'fc780465-2017-40d4-a0c5-307022471b92', + DataType: 'AlertAggregate', + DatabaseType: 'DataInsights', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/Find/AlertAggregate?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b&PageSize=540&Filter=StartDate+eq+2021-04-18T17%3a59%3a48.3504050Z+and+EndDate+eq+2021-05-18T17%3a59%3a48.3504050Z+and+AlertCategory+any+1%2c3%2c7%2c5%2c4+and+AlertSource+eq+%27Office+365+Security+%26+Compliance%27', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T17:59:46', + Id: 'eb9775cb-59f7-42ea-3ee0-08d91a26b92b', + Operation: 'ValidaterbacAccessCheck', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 52, + UserKey: 'fake@email.not', + UserType: 5, + Version: 1, + Workload: 'SecurityComplianceCenter', + UserId: 'fake@email.not', + AadAppId: 'd6fdaa33-e821-4211-83d0-cf74736489e1', + DataType: 'rbacAccessCheck', + RelativeUrl: + '/DataInsights/DataInsightsService.svc/validate/rbacAccessCheck?tenantid=0fea4e03-8146-453b-b889-54b4bd11565b', + ResultCount: '0', + }, + { + CreationTime: '2021-05-18T14:12:53', + Id: 'c0eada1b-52b2-450d-84df-6d461420d621', + Operation: 'Get-RetentionCompliancePolicy', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T14:12:53', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:26', + Id: '45a0d7c4-de73-466a-8e6c-c25f9c035714', + Operation: 'Get-SupervisoryReviewPolicyV2', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:26', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:31', + Id: 'f9912868-b431-435c-8337-0fc3b4370815', + Operation: 'Get-SupervisoryReviewReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: + '-StartDate "<SNIP-PII>" -EndDate "<SNIP-PII>" -PageSize "<SNIP-PII>" -Page "<SNIP-PII>"', + Parameters: + '-StartDate "5/12/2021 12:00:00 AM" -EndDate "5/18/2021 11:59:59 PM" -PageSize "300" -Page "1"', + StartTime: '2021-05-18T15:52:31', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:30', + Id: 'dcecd87a-3061-4dea-9bff-4fbfc23ca328', + Operation: 'Get-SupervisoryReviewOverallProgressReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:30', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T15:52:30', + Id: '5641d062-f279-4ca4-9577-50d7ecbfeedb', + Operation: 'Get-SupervisoryReviewTopCasesReport', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '', + Parameters: '', + StartTime: '2021-05-18T15:52:30', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:15', + Id: '8c7c9f81-68e9-452b-a22d-1333eb9cd647', + Operation: 'Get-ComplianceSearchAction', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-Export "<SNIP-PII>"', + Parameters: '-Export "True"', + StartTime: '2021-05-18T17:50:15', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:12', + Id: '4692201f-8101-455e-b89d-6727ef75c223', + Operation: 'Get-ComplianceTag', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-IncludingLabelState "<SNIP-PII>"', + Parameters: '-IncludingLabelState "True"', + StartTime: '2021-05-18T17:50:12', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:50:12', + Id: '7d41f1f2-587c-492f-b6ff-2f9d1a519c60', + Operation: 'Get-ComplianceSearch', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 2, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: 'EMC', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-ResultSize "Unlimited"', + Parameters: '-ResultSize "Unlimited"', + StartTime: '2021-05-18T17:50:12', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T17:59:45', + Id: 'ebcfc2bf-8799-413c-add4-6c2b53cb68e7', + Operation: 'Get-DlpSensitiveInformationType', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 18, + ResultStatus: 'Success', + UserKey: 'fake@email.not', + UserType: 0, + Version: 1, + Workload: 'SecurityComplianceCenter', + ObjectId: '', + UserId: 'fake@email.not', + SecurityComplianceCenterEventType: 0, + ClientApplication: '', + CmdletVersion: '...', + EffectiveOrganization: 'wazuh.testytest.com', + NonPIIParameters: '-Organization "0fea4e03-8146-453b-b889-54b4bd11565b"', + Parameters: '-Organization "0fea4e03-8146-453b-b889-54b4bd11565b"', + StartTime: '2021-05-18T17:59:45', + UserServicePlan: '', + }, + { + CreationTime: '2021-05-18T14:11:41', + Id: '7aeca226-b3e7-4033-9a7f-d067622e8d00', + Operation: 'UserLoggedIn', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 15, + ResultStatus: 'Success', + UserKey: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ClientIP: '190.16.9.176', + ObjectId: '5f09333a-842c-47da-a157-57da27fcbca5', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'ResultStatusDetail', + Value: 'Redirect', + }, + { + Name: 'UserAgent', + Value: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + }, + { + Name: 'RequestType', + Value: 'OAuth2:Authorize', + }, + ], + ModifiedProperties: [], + Actor: [ + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 0, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + ActorIpAddress: '190.16.9.176', + InterSystemsId: 'a3798792-fef1-4b53-bd44-bbbd94cf0e5c', + IntraSystemId: '7aeca226-b3e7-4033-9a7f-d067622e8d00', + SupportTicketId: '', + Target: [ + { + ID: '5f09333a-842c-47da-a157-57da27fcbca5', + Type: 0, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + ApplicationId: '89bee1f7-5e6e-4d8a-9f3d-ecd601259da7', + DeviceProperties: [ + { + Name: 'OS', + Value: 'Windows 10', + }, + { + Name: 'BrowserType', + Value: 'Chrome', + }, + { + Name: 'IsCompliantAndManaged', + Value: 'False', + }, + { + Name: 'SessionId', + Value: '714c4935-a22d-400d-8563-fbbd8bfc2301', + }, + ], + ErrorNumber: '0', + }, + { + CreationTime: '2021-05-18T17:49:11', + Id: '4e621563-394f-42a9-8a8a-8549e1ffa771', + Operation: 'Add service principal.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: 'Not Available', + UserType: 4, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'f738ef14-47dc-4564-b53b-45069484ccc7', + UserId: 'ServicePrincipal_4bf80788-0ec4-481a-ae7b-b71647bf3b57', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'ServicePrincipal', + }, + ], + ModifiedProperties: [ + { + Name: 'AccountEnabled', + NewValue: '[\r\n true\r\n]', + OldValue: '[]', + }, + { + Name: 'AppPrincipalId', + NewValue: '[\r\n "f738ef14-47dc-4564-b53b-45069484ccc7"\r\n]', + OldValue: '[]', + }, + { + Name: 'DisplayName', + NewValue: '[\r\n "Marketplace Api"\r\n]', + OldValue: '[]', + }, + { + Name: 'ServicePrincipalName', + NewValue: '[\r\n "f738ef14-47dc-4564-b53b-45069484ccc7"\r\n]', + OldValue: '[]', + }, + { + Name: 'Credential', + NewValue: + '[\r\n {\r\n "CredentialType": 2,\r\n "KeyStoreId": "291154f0-a9f5-45bb-87be-9c8ee5b6d62c",\r\n "KeyGroupId": "1c5aa04b-dea5-4284-9908-47edd1e12d13"\r\n }\r\n]', + OldValue: '[]', + }, + { + Name: 'Included Updated Properties', + NewValue: 'AccountEnabled, AppPrincipalId, DisplayName, ServicePrincipalName, Credential', + OldValue: '', + }, + { + Name: 'TargetId.ServicePrincipalNames', + NewValue: 'f738ef14-47dc-4564-b53b-45069484ccc7', + OldValue: '', + }, + ], + Actor: [ + { + ID: 'Windows Azure Service Management API', + Type: 1, + }, + { + ID: '797f4846-ba00-4fd7-ba43-dac1f8f63013', + Type: 2, + }, + { + ID: 'ServicePrincipal_4bf80788-0ec4-481a-ae7b-b71647bf3b57', + Type: 2, + }, + { + ID: '4bf80788-0ec4-481a-ae7b-b71647bf3b57', + Type: 2, + }, + { + ID: 'ServicePrincipal', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '9cfba3bb-b478-44aa-a140-465ee7f29274', + IntraSystemId: '21051805-2413-594a-ab5d-006014005348', + SupportTicketId: '', + Target: [ + { + ID: 'ServicePrincipal_f6d2eabc-d020-4643-80a8-2b92b163d1de', + Type: 2, + }, + { + ID: 'f6d2eabc-d020-4643-80a8-2b92b163d1de', + Type: 2, + }, + { + ID: 'ServicePrincipal', + Type: 2, + }, + { + ID: 'Marketplace Api', + Type: 1, + }, + { + ID: 'f738ef14-47dc-4564-b53b-45069484ccc7', + Type: 2, + }, + { + ID: 'f738ef14-47dc-4564-b53b-45069484ccc7', + Type: 4, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-18T21:42:25', + Id: 'af4e552f-0bca-4b02-92c9-4bd430f24f75', + Operation: 'Change user license.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: '100320014080D3AD@wazuh.com', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'fake@email.not', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'User', + }, + ], + ModifiedProperties: [], + Actor: [ + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '1fd09d6b-54d3-4a58-acfe-71cc2c429d97', + IntraSystemId: '0a8ae201-e404-4f6f-99db-a3c92a5bd022', + SupportTicketId: '', + Target: [ + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-18T21:42:25', + Id: 'b27eab84-1ef7-4372-bc68-7213af8ab3fb', + Operation: 'Update user.', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 8, + ResultStatus: 'Success', + UserKey: '100320014080D3AD@wazuh.com', + UserType: 0, + Version: 1, + Workload: 'AzureActiveDirectory', + ObjectId: 'fake@email.not', + UserId: 'fake@email.not', + AzureActiveDirectoryEventType: 1, + ExtendedProperties: [ + { + Name: 'additionalDetails', + Value: '{"UserType":"Member"}', + }, + { + Name: 'extendedAuditEventCategory', + Value: 'User', + }, + ], + ModifiedProperties: [ + { + Name: 'AssignedLicense', + NewValue: + '[\r\n "[SkuName=POWER_BI_STANDARD, AccountId=0fea4e03-8146-453b-b889-54b4bd11565b, SkuId=a403ebcc-fae0-4ca2-8c8c-7a907fd6c235, DisabledPlans=[]]"\r\n]', + OldValue: '[]', + }, + { + Name: 'AssignedPlan', + NewValue: + '[\r\n {\r\n "SubscribedPlanId": "c976d07f-fd0f-49eb-bdc2-26c17481e1c5",\r\n "ServiceInstance": "AzureAnalysis/SDF",\r\n "CapabilityStatus": 0,\r\n "AssignedTimestamp": "2021-05-18T21:42:25.3894164Z",\r\n "InitialState": null,\r\n "Capability": null,\r\n "ServicePlanId": "2049e525-b859-401b-b2a0-e0a31c4b1fe4"\r\n }\r\n]', + OldValue: '[]', + }, + { + Name: 'Included Updated Properties', + NewValue: 'AssignedLicense, AssignedPlan', + OldValue: '', + }, + { + Name: 'TargetId.UserType', + NewValue: 'Member', + OldValue: '', + }, + ], + Actor: [ + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + ], + ActorContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + InterSystemsId: '1fd09d6b-54d3-4a58-acfe-71cc2c429d97', + IntraSystemId: '0a8ae201-e404-4f6f-99db-a3c92a5bd022', + SupportTicketId: '', + Target: [ + { + ID: 'User_910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: '910ed5ca-4ecf-414c-a1be-d53511bfe1a5', + Type: 2, + }, + { + ID: 'User', + Type: 2, + }, + { + ID: 'fake@email.not', + Type: 5, + }, + { + ID: '100320014080D3AD', + Type: 3, + }, + ], + TargetContextId: '0fea4e03-8146-453b-b889-54b4bd11565b', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '8c3d0215-66f0-41b0-3205-08d91bb6b63c', + Operation: 'SharingPolicyChanged', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Site', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + ModifiedProperties: [ + { + Name: 'ShareUsingAnonymousLinks', + NewValue: 'Enabled', + OldValue: 'Disabled', + }, + ], + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '35a1b515-2a0e-4bd6-d0a3-08d91bb6b639', + Operation: 'SiteCollectionCreated', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Site', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + EventData: + '<SiteCreationSource>API</SiteCreationSource><TenantSettings.ShowCreateSiteCommand>True</TenantSettings.ShowCreateSiteCommand><TenantSettings.UseCustomSiteCreationForm>False</TenantSettings.UseCustomSiteCreationForm>', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: '344f9139-f437-4290-9566-08d91bb6b61f', + Operation: 'SiteCollectionAdminRemoved', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: 'a9d15b23-6ac9-43c5-af3c-b4a0916631c1', + ModifiedProperties: [ + { + Name: 'SiteAdmin', + NewValue: '', + OldValue: '', + }, + ], + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + TargetUserOrGroupName: 'SHAREPOINT\\system', + }, + { + CreationTime: '2021-05-20T17:43:00', + Id: 'd36e4b4d-1e8b-4634-6dd8-08d91bb6b618', + Operation: 'SiteCollectionAdminAdded', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'OneDrive', + ClientIP: '20.190.157.27', + ObjectId: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + UserId: 'fake@email.not', + CorrelationId: 'fd9ac79d-1100-48aa-92c5-40a73a1d443f', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'f49feae4-033d-4028-97d1-3acd55341f69', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: 'a9d15b23-6ac9-43c5-af3c-b4a0916631c1', + ModifiedProperties: [ + { + Name: 'SiteAdmin', + NewValue: 'fake@email.not', + OldValue: '', + }, + ], + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh-my.sharepoint.com/personal/tomas_turina_wazuh_com', + TargetUserOrGroupName: 'fake@email.not', + }, + { + CreationTime: '2021-05-20T17:43:22', + Id: '0d6a62d3-e4bd-44ee-ce8d-08d91bb6c392', + Operation: 'PageViewed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/_layouts/15/CreateGroup.aspx', + UserId: 'fake@email.not', + CorrelationId: 'ccd0c99f-309b-2000-df13-3fcca9a8c8e1', + CustomUniqueId: true, + EventSource: 'SharePoint', + ItemType: 'Page', + ListItemUniqueId: '59a8433d-9bb8-cfef-65b7-ef35de00c8f6', + Site: 'f7fbb805-5f6b-4950-b681-2365eb46081f', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '3b56db49-60e3-410e-acbd-d8765467388a', + }, + { + CreationTime: '2021-05-20T17:45:57', + Id: '18bb351b-49e1-47df-8f4d-08d91bb71ffd', + Operation: 'AddedToGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 14, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'fake@email.not', + CorrelationId: 'f1d0c99f-3094-2000-da82-454f034ca629', + EventSource: 'SharePoint', + ItemType: 'Web', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + EventData: '<Group>Site Owners</Group>', + TargetUserOrGroupType: 'Member', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + TargetUserOrGroupName: 'SHAREPOINT\\system', + }, + { + CreationTime: '2021-05-20T17:46:26', + Id: '29bde84a-d3ec-4388-4600-08d91bb730bc', + Operation: 'FileAccessed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/Shared Documents/Forms/AllItems.aspx', + UserId: 'fake@email.not', + CorrelationId: 'f9d0c99f-b04f-2000-da82-4bb2abf6168f', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + ListItemUniqueId: '3c9d8943-846e-41f3-a647-72a5e4e3decf', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'AllItems.aspx', + SourceRelativeUrl: 'Shared Documents/Forms', + }, + { + CreationTime: '2021-05-20T17:46:25', + Id: '087e5b68-fc3f-4e01-1efc-08d91bb730b5', + Operation: 'ListViewed', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 36, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/fd2ebaf0-900b-4dff-8fc2-d348be51e677', + UserId: 'fake@email.not', + CorrelationId: 'f9d0c99f-b04f-2000-da82-4bb2abf6168f', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'List', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + CustomizedDoclib: false, + FromApp: true, + IsDocLib: true, + ItemCount: 0, + ListBaseTemplateType: '101', + ListBaseType: 'DocumentLibrary', + ListColor: '', + ListIcon: '', + Source: 'Unknown', + TemplateTypeId: '', + ListTitle: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + }, + { + CreationTime: '2021-05-20T17:52:29', + Id: '41225487-31c1-4e24-b8b0-08d91bb8094c', + Operation: 'PagePrefetched', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'fake@email.not', + CorrelationId: '52d1c99f-3000-2000-df13-3ab1e8fb9f92', + CustomUniqueId: false, + EventSource: 'SharePoint', + ItemType: 'Page', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:51:49', + Id: 'd930cc5c-2658-45df-6361-08d91bb7f179', + Operation: 'FileCheckedOut', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '48d1c99f-f03c-2000-df13-38983a6608f8', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + HighPriorityMediaProcessing: false, + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'Home.aspx', + SourceRelativeUrl: 'SitePages', + }, + { + CreationTime: '2021-05-20T17:51:51', + Id: '89d76362-e493-4c20-3b69-08d91bb7f288', + Operation: 'ListUpdated', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 36, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + UserId: 'fake@email.not', + CorrelationId: '48d1c99f-f0a8-2000-da82-41be3f973267', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'List', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + CustomizedDoclib: false, + FromApp: false, + IsDocLib: true, + ItemCount: 1, + ListBaseTemplateType: '119', + ListBaseType: 'DocumentLibrary', + ListColor: '', + ListIcon: '', + Source: 'Unknown', + TemplateTypeId: '', + ListTitle: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + }, + { + CreationTime: '2021-05-20T17:52:36', + Id: '7a91dd8c-560b-4fbe-2585-08d91bb80d46', + Operation: 'ClientViewSignaled', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '53d1c99f-b0aa-2000-df13-3efea9e41071', + CustomUniqueId: false, + EventSource: 'SharePoint', + ItemType: 'Page', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:53:37', + Id: '9695afcd-19ff-491f-a6ee-08d91bb831d1', + Operation: 'FileModified', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint/SitePages/Home.aspx', + UserId: 'fake@email.not', + CorrelationId: '62d1c99f-d09c-2000-df13-37ddf480e717', + DoNotDistributeEvent: true, + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'e4c9ce2e-d8c2-468e-baf5-f362f8c2f2f3', + ListItemUniqueId: '36db3168-c1b2-44e9-9ffd-e9a8e04bb2f5', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'Home.aspx', + SourceRelativeUrl: 'SitePages', + }, + { + CreationTime: '2021-05-20T17:57:03', + Id: '551fd7d5-bac1-4bb4-11d2-08d91bb8ac9e', + Operation: 'FileAccessedExtended', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/Shared Documents/Forms/AllItems.aspx', + UserId: 'fake@email.not', + CorrelationId: '94d1c99f-20eb-2000-df13-35746d02911e', + EventSource: 'SharePoint', + ItemType: 'File', + ListId: 'fd2ebaf0-900b-4dff-8fc2-d348be51e677', + ListItemUniqueId: '3c9d8943-846e-41f3-a647-72a5e4e3decf', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + SourceFileExtension: 'aspx', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'AllItems.aspx', + SourceRelativeUrl: 'Shared Documents/Forms', + }, + { + CreationTime: '2021-05-20T17:59:55', + Id: 'eb1f0911-9bed-4f15-10e5-08d91bb91372', + Operation: 'SiteDeleted', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 6, + UserKey: 'S-1-0-0', + UserType: 4, + Version: 1, + Workload: 'SharePoint', + ClientIP: '', + ObjectId: 'https://wazuh.sharepoint.com/sites/TestSharePoint', + UserId: 'AAD to SharePoint Sync', + CorrelationId: 'bed1c99f-20ee-2000-df13-306cb6803c92', + EventSource: 'SharePoint', + ItemType: 'Web', + ListItemUniqueId: '00000000-0000-0000-0000-000000000000', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: '', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + DestinationFileExtension: '', + SourceFileExtension: '', + DestinationFileName: 'TestSharePoint', + DestinationRelativeUrl: '../../https://wazuh.sharepoint.com/sites', + SiteUrl: 'https://wazuh.sharepoint.com/sites/TestSharePoint/', + SourceFileName: 'TestSharePoint', + SourceRelativeUrl: '..', + }, + { + CreationTime: '2021-05-20T17:59:11', + Id: '0d20a3e1-e9cb-436c-799f-08d91bb8f92f', + Operation: 'PageViewedExtended', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 4, + UserKey: 'i:0h.f|membership|100320014080d3ad@live.com', + UserType: 0, + Version: 1, + Workload: 'SharePoint', + ClientIP: '190.16.9.176', + ObjectId: + 'https://wazuh.sharepoint.com/sites/TestSharePoint/_layouts/15/online/handlers/SpoSuiteLinks.ashx', + UserId: 'fake@email.not', + CorrelationId: 'b4d1c99f-0043-2000-da82-41b63e1d91f4', + EventSource: 'SharePoint', + ItemType: 'Page', + Site: 'dd58ef08-faea-4cb5-847a-35bb5c01e757', + UserAgent: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', + WebId: '00c32555-e0d8-425f-9fbd-ef5539bfecf7', + }, + { + CreationTime: '2021-05-20T17:44:27', + Id: '30ef2f70-a12d-4b31-1e70-08d91bb6ea2e', + Operation: 'Set-Mailbox', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ClientIP: '52.233.237.141:40638', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/tomas.turina', + UserId: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + AppId: '61109738-7d2b-4a0b-9fe3-660b1ff83505', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AM9PR04MB8986 (15.20.4150.023)', + Parameters: [ + { + Name: 'Identity', + Value: + 'MGZlYTRlMDMtODE0Ni00NTNiLWI4ODktNTRiNGJkMTE1NjViXGJkYmI4MjM2LTBmNDgtNGZjNi05Zjc3LTkxNGNkY2MwMmIzYw2', + }, + { + Name: 'ResourceEmailAddresses', + Value: 'True', + }, + { + Name: 'BypassLiveId', + Value: 'True', + }, + { + Name: 'Force', + Value: 'True', + }, + { + Name: 'DomainController', + Value: 'HE1PR04A010DC03.EURPR04A010.prod.outlook.com', + }, + { + Name: 'EmailAddresses', + Value: + 'SIP:fake@email.not;SMTP:fake@email.not;SPO:SPO_f49feae4-033d-4028-97d1-3acd55341f69@SPO_0fea4e03-8146-453b-b889-54b4bd11565b', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T17:45:59', + Id: '48c00930-b25d-4ccc-ccb3-08d91bb720f6', + Operation: 'ModifyFolderPermissions', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 2, + ResultStatus: 'Succeeded', + UserKey: 'S-1-5-18', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '::1', + UserId: 'S-1-5-18', + ClientIPAddress: '::1', + ClientInfoString: 'Client=WebServices;Action=ConfigureGroupMailbox', + ExternalAccess: true, + InternalLogonType: 1, + LogonType: 1, + LogonUserSid: 'S-1-5-18', + MailboxGuid: 'fc108b45-9d51-4b87-a473-9d5a0e404966', + MailboxOwnerMasterAccountSid: 'S-1-5-10', + MailboxOwnerSid: 'S-1-5-21-2986565805-1835265550-1383574073-20743067', + MailboxOwnerUPN: 'TestSharePoint@wazuh.com', + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AS8PR04MB8465 (15.20.4150.023)\r\n', + Item: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAENAAAC', + ParentFolder: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAENAAAC', + MemberRights: + 'ReadAny, Create, EditOwned, DeleteOwned, EditAny, DeleteAny, Visible, FreeBusySimple, FreeBusyDetailed', + MemberSid: 'S-1-8-4228942661-1267178833-1520268196-1716076558-1', + MemberUpn: 'Member@local', + Name: 'Calendar', + Path: '\\Calendar', + }, + }, + }, + { + CreationTime: '2021-05-20T17:45:58', + Id: 'bb03b48e-609d-477b-cb80-08d91bb72077', + Operation: 'Create', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 2, + ResultStatus: 'Succeeded', + UserKey: 'S-1-5-18', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '::1', + UserId: 'S-1-5-18', + ClientIPAddress: '::1', + ClientInfoString: 'Client=WebServices;Action=ConfigureGroupMailbox', + ExternalAccess: true, + InternalLogonType: 1, + LogonType: 1, + LogonUserSid: 'S-1-5-18', + MailboxGuid: 'fc108b45-9d51-4b87-a473-9d5a0e404966', + MailboxOwnerMasterAccountSid: 'S-1-5-10', + MailboxOwnerSid: 'S-1-5-21-2986565805-1835265550-1383574073-20743067', + MailboxOwnerUPN: 'TestSharePoint@wazuh.com', + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'AS8PR04MB8465 (15.20.4150.023)\r\n', + Item: { + Attachments: + 'warming_email_03_2017_calendar.png (646b); warming_email_03_2017_conversation.png (661b); warming_email_03_2017_links.png (1450b); google_play_store_badge.png (4871b); apple_store_badge.png (4493b); windows_store_badge.png (3728b); warming_email_03_2017_files.png (856b); warming_email_03_2017_sharePoint.png (1479b)', + Id: + 'RgAAAAA6tVhba3JWSaGmky7/7OvfBwDRwKc47c1sT4Waab6O4zbPAAAAAAEMAADRwKc47c1sT4Waab6O4zbPAAAAAAk9AAAJ', + InternetMessageId: + '<AS8PR04MB846542106D3939F2D1952D05D32A9@AS8PR04MB8465.eurprd04.prod.outlook.com>', + IsRecord: false, + ParentFolder: { + Id: 'LgAAAAA6tVhba3JWSaGmky7/7OvfAQDRwKc47c1sT4Waab6O4zbPAAAAAAEMAAAB', + Path: '\\Inbox', + }, + Subject: 'The new TestSharePoint group is ready', + }, + }, + { + CreationTime: '2021-05-20T17:59:59', + Id: 'e855fb12-2d48-45f3-ac8d-08d91bb91569', + Operation: 'Remove-UnifiedGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'NT AUTHORITY\\SYSTEM (w3wp)', + UserType: 2, + Version: 1, + Workload: 'Exchange', + ClientIP: '[2a01:111:f402:ac00::f134]:51514', + ObjectId: 'TestSharePoint_b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + UserId: 'NT AUTHORITY\\SYSTEM (w3wp)', + AppId: '00000003-0000-0ff1-ce00-000000000000', + ClientAppId: '00000003-0000-0ff1-ce00-000000000000', + ExternalAccess: false, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'VI1PR04MB6125 (15.20.4129.033)', + Parameters: [ + { + Name: 'Identity', + Value: 'b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T18:04:37', + Id: 'f111c82c-7961-473d-112a-08d91bb9bb91', + Operation: 'Set-UnifiedGroup', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ClientIP: '51.144.33.14:58849', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/Soft Deleted Objects/TestSharePoint_b47e06bf-895d-48c4-8ae4-a0fdc60ec249', + UserId: 'SpoolsProvisioning-ApplicationAccount@eurprd04.prod.outlook.com', + AppId: '61109738-7d2b-4a0b-9fe3-660b1ff83505', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'VI1PR0402MB3326 (15.20.4129.033)', + Parameters: [ + { + Name: 'Identity', + Value: + 'MGZlYTRlMDMtODE0Ni00NTNiLWI4ODktNTRiNGJkMTE1NjViXDFlYjFjNjZhLTRhYWQtNGY2Mi04NjAzLTdjMDRkZTIxYWE3Mg2', + }, + { + Name: 'EmailAddresses', + Value: 'smtp:TestSharePoint@wazuh.testytest.com;SMTP:TestSharePoint@wazuh.com', + }, + { + Name: 'IncludeSoftDeletedObjects', + Value: 'True', + }, + ], + SessionId: '', + }, + { + CreationTime: '2021-05-20T18:59:49', + Id: '32229114-e357-4b56-9d08-08d91bc1717c', + Operation: 'Set-User', + OrganizationId: '0fea4e03-8146-453b-b889-54b4bd11565b', + RecordType: 1, + ResultStatus: 'True', + UserKey: 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.Management.ForwardSync)', + UserType: 3, + Version: 1, + Workload: 'Exchange', + ObjectId: + 'EURPR04A010.prod.outlook.com/Microsoft Exchange Hosted Organizations/wazuh.testytest.com/tomas.turina', + UserId: 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.Management.ForwardSync)', + AppId: '', + ClientAppId: '', + ExternalAccess: true, + OrganizationName: 'wazuh.testytest.com', + OriginatingServer: 'DB8PR04MB7065 (15.20.4150.023)', + Parameters: [ + { + Name: 'Identity', + Value: '0fea4e03-8146-453b-b889-54b4bd11565b\\bdbb8236-0f48-4fc6-9f77-914cdcc02b3c', + }, + { + Name: 'SyncMailboxLocationGuids', + Value: 'True', + }, + { + Name: 'ErrorAction', + Value: 'Stop', + }, + { + Name: 'WarningAction', + Value: 'SilentlyContinue', + }, + ], + }, +]; From 348103cb4ce22dddd9c4b5fbe9794e694b446d79 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 23 Jul 2021 11:05:59 +0200 Subject: [PATCH 127/493] partial vis-card commit --- .../custom-search-bar/custom-search-bar.tsx | 4 +- .../common/modules/panel/main-panel.tsx | 26 ---- .../modules/panel/module-side-panel.tsx | 4 +- .../office-panel/components/vis-card.tsx | 28 ++++ .../overview/office-panel/config/index.ts | 1 + .../{ => config}/search-bar-config.ts | 27 ++++ .../overview/office-panel/office-panel.tsx | 60 ++++---- .../office-panel/views/office-body.tsx | 24 +++- public/components/visualize/visualizations.js | 67 ++++++++- public/components/visualize/wz-visualize.js | 135 +++++++++++++++--- public/factories/tab-visualizations.js | 6 +- public/styles/layout.scss | 3 + 12 files changed, 297 insertions(+), 88 deletions(-) create mode 100644 public/components/overview/office-panel/components/vis-card.tsx rename public/components/overview/office-panel/{ => config}/search-bar-config.ts (60%) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 3bd1af2910..242d77c0d1 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -131,8 +131,8 @@ export const CustomSearchBar = ({ ...props }) => { <EuiFlexGroup alignItems='center'> { avancedFiltersState === false ? - customFilters.map((item) => ( - <EuiFlexItem grow={2}> + customFilters.map((item, key) => ( + <EuiFlexItem grow={2} key={key}> {getComponent(item)} </EuiFlexItem> )) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index e6c4cdd719..c1f21a3340 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -20,18 +20,6 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { const [viewId, setViewId] = useState('main'); - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; - const timefilter = KibanaServices.timefilter.timefilter; - - const [isLoading, setIsLoading] = useState(false); - const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters() || [], - query: { language: 'kuery', query: '' }, - time: timefilter.getTime(), - }); - - useEffect(() => { (async () => { const tabVisualizations = new TabVisualizations(); @@ -45,20 +33,6 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { })() }, [viewId]) - const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { - const { query, dateRange } = payload; - const filters = { query, time: dateRange, filters: filterParams.filters }; - setIsLoading(true); - setFilterParams(filters); - setIsLoading(false); - } - - const onFiltersUpdated = (filters: Filter[]) => { - const { query, time } = filterParams; - const updatedFilterParams = { query, time, filters }; - setIsLoading(true); - setFilterParams(updatedFilterParams); - } const toggleView = (id = 'main') => { if (id != viewId) diff --git a/public/components/common/modules/panel/module-side-panel.tsx b/public/components/common/modules/panel/module-side-panel.tsx index de87d1291a..e8b3885c2b 100644 --- a/public/components/common/modules/panel/module-side-panel.tsx +++ b/public/components/common/modules/panel/module-side-panel.tsx @@ -1,4 +1,4 @@ -import { EuiCollapsibleNav, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCollapsibleNav, EuiButtonEmpty } from '@elastic/eui'; import React, { useState } from 'react'; import './module-side-panel.scss'; @@ -20,7 +20,7 @@ export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => <div> <EuiButtonEmpty style={{ float: 'right' }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'}> </EuiButtonEmpty> - <div style={{ padding: 16 }}> + <div className={'wz-padding-16'}> {children} </div> </div> diff --git a/public/components/overview/office-panel/components/vis-card.tsx b/public/components/overview/office-panel/components/vis-card.tsx new file mode 100644 index 0000000000..29078a2877 --- /dev/null +++ b/public/components/overview/office-panel/components/vis-card.tsx @@ -0,0 +1,28 @@ +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; + +export const OfficeBody = ({ changeView, rows = [] }) => { + + const [expandedVis, setExpandedVis] = useState(false); + + const toggleExpand = id => { + setExpandedVis(expandedVis === id ? false : id); + }; + + return <> <EuiFlexItem key={key} grow={growthFactor}> + <EuiPanel paddingSize={'s'} className={expandedVis === vis.id ? 'fullscreen h-100' : 'h-100'}> + <EuiTitle> + {vis.title} + </EuiTitle> + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => toggleExpand(vis.id)} + iconType="expand" + aria-label="Expand" + /> + <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /> + </div></EuiPanel> + </EuiFlexItem> + </> +} \ No newline at end of file diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 83c428b963..9628da5002 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -1,3 +1,4 @@ export { DrilldownConfig } from './drilldown-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; +export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts similarity index 60% rename from public/components/overview/office-panel/search-bar-config.ts rename to public/components/overview/office-panel/config/search-bar-config.ts index 64d7e1818b..1e93904f9f 100644 --- a/public/components/overview/office-panel/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -26,6 +26,33 @@ export const filtersValues = [ } ] }, + { + type: 'combobox', + key: 'agent.name', + values:[ + { + key:'agent.name', + label: 'Amazon', + + }, + { + key:'agent.name', + label: 'Centos', + }, + { + key:'agent.name', + label: '003', + }, + { + key:'agent.name', + label: '004', + }, + { + key:'agent.name', + label: '006', + } + ] + }, { type: 'combobox', key: 'cluster.name', diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 01b9c9e898..8087941d0b 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -15,38 +15,40 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { CustomSearchBar } from '../../common/custom-search-bar'; -import { filtersValues } from './search-bar-config'; import { OfficeStats } from './views'; import { queryConfig } from '../../../react-services/query-config'; -import { ModuleConfig } from './config'; +import { ModuleConfig, filtersValues } from './config'; export const OfficePanel = withErrorBoundary(() => { - const [moduleStatsList, setModuleStatsList] = useState([]); - useEffect(() => { - (async () => { - try { - const modulesConfig = await queryConfig( - '000', - [{ component: 'wmodules', configuration: 'wmodules' }] - ); - const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules - .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { - const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; - return { title: configProp[0], description } - }) - setModuleStatsList(config); - } catch (error) { - setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); - } - } - )(); - }, []) - return ( - <> - <CustomSearchBar filtersValues={filtersValues}/> - <MainPanel moduleConfig={ModuleConfig} tab={'office'} - sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> - </> - ) + const [moduleStatsList, setModuleStatsList] = useState([]); + + /** Get Office 365 Side Panel Module info **/ + useEffect(() => { + (async () => { + try { + const modulesConfig = await queryConfig( + '000', + [{ component: 'wmodules', configuration: 'wmodules' }] + ); + const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules + .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { //<-- change module name + const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; + return { title: configProp[0], description } + }) + setModuleStatsList(config); + } catch (error) { + setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); + } + } + )(); + }, []) + + return ( + <> + <CustomSearchBar filtersValues={filtersValues} /> + <MainPanel moduleConfig={ModuleConfig} tab={'office'} + sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> + </> + ) }); diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index 834cdb202f..1a8ab77ac7 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,8 +1,14 @@ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; export const OfficeBody = ({ changeView, rows = [] }) => { + const [expandedVis, setExpandedVis] = useState(false); + + const toggleExpand = id => { + setExpandedVis(expandedVis === id ? false : id); + }; + return <> { rows.map((row, key) => { @@ -14,7 +20,19 @@ export const OfficeBody = ({ changeView, rows = [] }) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); return <EuiFlexItem key={key} grow={growthFactor}> - <EuiPanel paddingSize={'s'} ><div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /></div></EuiPanel> + <EuiPanel paddingSize={'s'} className={expandedVis === vis.id ? 'fullscreen h-100' : 'h-100'}> + <EuiTitle> + {vis.title} + </EuiTitle> + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => toggleExpand(vis.id)} + iconType="expand" + aria-label="Expand" + /> + <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /> + </div></EuiPanel> </EuiFlexItem> }) } diff --git a/public/components/visualize/visualizations.js b/public/components/visualize/visualizations.js index 7675656924..fb2561bac3 100644 --- a/public/components/visualize/visualizations.js +++ b/public/components/visualize/visualizations.js @@ -954,5 +954,70 @@ export const visualizations = { ] } ] - } + }, + office: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alert level evolution', + id: 'Wazuh-App-Overview-Office-Level-12-alerts', + width: 60 + } + ] + } + ] + }, + office_exchange: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alert level evolution', + id: 'Wazuh-App-Overview-Office-Top-5-agents', + width: 60 + } + ] + } + ] + }, + office_azuread: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alert level evolution', + id: 'Wazuh-App-Overview-Office-Alerts-evolution-Top-5-agents', + width: 60 + } + ] + } + ] + }, + office_sharepoint: { + rows: [ + { + height: 360, + vis: [ + { + title: 'Alert level evolution', + id: 'Wazuh-App-Overview-Office-Alerts-Top-Mitre', + width: 60 + } + ] + }, + { + hide: true, + vis: [ + { + title: 'Alerts summary', + id: 'Wazuh-App-Overview-Office-Alerts-summary', + } + ] + } + ] + }, }; diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index a3f1cfc4a0..e0213069e4 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -23,7 +23,8 @@ import { EuiButtonIcon, EuiDescriptionList, EuiCallOut, - EuiLink + EuiLink, + EuiButtonGroup } from '@elastic/eui'; import WzReduxProvider from '../../redux/wz-redux-provider'; import { WazuhConfig } from '../../react-services/wazuh-config'; @@ -33,7 +34,7 @@ import { VisHandlers } from '../../factories/vis-handlers'; import { RawVisualizations } from '../../factories/raw-visualizations'; import { Metrics } from '../overview/metrics/metrics'; import { PatternHandler } from '../../react-services/pattern-handler'; -import { getToasts } from '../../kibana-services'; +import { getToasts, getAngularModule, getDataPlugin } from '../../kibana-services'; import { SecurityAlerts } from './components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { withReduxProvider,withErrorBoundary } from '../common/hocs'; @@ -41,6 +42,9 @@ 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'; +import { AppState } from '../../react-services/'; +import { buildPhraseFilter } from '../../../../../src/plugins/data/common'; +import { getIndexPattern } from '../overview/mitre/lib'; const visHandler = new VisHandlers(); @@ -55,7 +59,8 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class thereAreSampleAlerts: false, hasRefreshedKnownFields: false, refreshingKnownFields: [], - refreshingIndex: true + refreshingIndex: true, + dashboard: 'main' }; this.metricValues = false; this.rawVisualizations = new RawVisualizations(); @@ -73,6 +78,8 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this._isMount = true; visHandler.removeAll(); this.agentsStatus = false; + const app = getAngularModule(); + this.$rootScope = app.$injector.get('$rootScope'); if (!this.monitoringEnabled) { const data = await this.wzReq.apiReq('GET', '/agents/summary/status', {}); const result = ((data || {}).data || {}).data || false; @@ -122,12 +129,29 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class } } - async componentDidUpdate(prevProps) { + async componentDidUpdate(prevProps, prevState) { if (prevProps.isAgent !== this.props.isAgent) { this._isMount && this.setState({ visualizations: !!this.props.isAgent ? agentVisualizations : visualizations }); typeof prevProps.isAgent !== 'undefined' && visHandler.removeAll(); } + + // if(prevState.dashboard !== this.state.dashboard){ + // console.log(prevState.dashboard, this.state.dashboard); + // this.$rootScope.showModuleDashboard = this.props.selectedTab; + // await ModulesHelper.getDiscoverScope(); + // if (this._isMount) { + // this.$rootScope.moduleDiscoverReady = true; + // this.$rootScope.$applyAsync(); + // } + // VisFactoryHandler.clearAll(); + // await VisFactoryHandler.buildOverviewVisualizations( + // new FilterHandler(AppState.getCurrentPattern()), + // this.props.selectedTab, + // 'dashboard', + // false + // ); + // } } componentWillUnmount() { @@ -274,6 +298,66 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class return ( <Fragment> + {this.props.selectedTab === 'office' && ( + <EuiFlexGroup justifyContent='flexEnd' gutterSize='s'style={{ margin: '0 8px 16px 8px' }}> + <EuiFlexItem grow={false}> + <EuiButtonGroup + legend="This is a basic group" + options={['Main','AzureAD', 'Exchange', 'Sharepoint'].map(office365Dashboard => ({id: office365Dashboard.toLowerCase(), label: office365Dashboard}))} + idSelected={this.state.dashboard} + onChange={(id) => { + this.setState({dashboard: id}); + // getDataPlugin().query.filterManager. + + getIndexPattern().then(indexPattern => { + // const filter = { + // ...buildPhraseFilter({ name: 'syscheck.path', type: 'text' }, id, indexPattern/*AppState.getCurrentPattern()*/), + // $state: { store: 'appState', isImplicit: true }, + // }; + // delete filter.meta.removable + // filter.query = { + // "match": { + // 'syscheck.path': { + // query: id, + // type: 'phrase' + // } + // } + // } + + const filter = id !== 'main' ? [{ + "meta": { + "alias": null, + "disabled": false, + "key": "syscheck.path", + "negate": false, + "params": { "query": id }, + "type": "phrase", + "index": AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN, + "controlledBy": "wazuh" + }, + "query": { + "match": { + 'syscheck.path': { + query: id, + type: 'phrase' + } + } + }, + "$state": { "store": "appState", "isImplicit": true}, + }] : []; + + const filters = getDataPlugin().query.filterManager.getFilters() + .filter(filter => filter.meta.controlledBy !== 'wazuh') + // getDataPlugin().query.filterManager.addFilters([filter]); + getDataPlugin().query.filterManager.setFilters([...filters, ...filter]); + }) + }} + color="primary" + /> + </EuiFlexItem> + </EuiFlexGroup> + )} + {/* Sample alerts Callout */} {this.state.thereAreSampleAlerts && this.props.resultState === 'ready' && ( <EuiCallOut title='This dashboard contains sample data' color='warning' iconType='alert' style={{ margin: '0 8px 16px 8px' }}> @@ -295,25 +379,30 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class {selectedTab && selectedTab !== 'welcome' && visualizations[selectedTab] && - visualizations[selectedTab].rows.map((row, i) => { - return ( - <EuiFlexGroup - key={i} - style={{ - display: row.hide && 'none', - height: row.height || 0 + 'px', - margin: 0, - maxWidth: '100%' - }} - > - {row.vis.map((vis, n) => { - return !vis.hasRows - ? renderVisualizations(vis) - : renderVisualizationRow(vis.rows, vis.width, n); - })} - </EuiFlexGroup> - ); - })} + Object.keys(visualizations).filter(visualization => visualization.startsWith(selectedTab)).map(visualization => { + console.log(visualization, this.state.dashboard, (this.state.dashboard !== 'main' && `${selectedTab}_${this.state.dashboard}`.toLowerCase() === visualization) || this.state.dashboard === 'main' && selectedTab === visualization) + return visualizations[visualization].rows.map((row, i) => { + return ( + <EuiFlexGroup + key={i} + style={{ + display: (row.hide || (this.state.dashboard !== 'main' && `${selectedTab}_${this.state.dashboard}`.toLowerCase() !== visualization) || (this.state.dashboard === 'main' && selectedTab !== visualization) ) && 'none', + height: row.height || 0 + 'px', + margin: 0, + maxWidth: '100%' + }} + data-id={visualization} + > + {row.vis.map((vis, n) => { + return !vis.hasRows + ? renderVisualizations(vis) + : renderVisualizationRow(vis.rows, vis.width, n); + })} + </EuiFlexGroup> + ); + }); + } + )} </EuiFlexItem> <EuiFlexGroup style={{ margin: 0 }}> <EuiFlexItem> diff --git a/public/factories/tab-visualizations.js b/public/factories/tab-visualizations.js index 6a6a30ffdd..f2483d3896 100644 --- a/public/factories/tab-visualizations.js +++ b/public/factories/tab-visualizations.js @@ -39,7 +39,8 @@ export class TabVisualizations { configuration: 0, osquery: 5, docker: 5, - mitre: 6 + mitre: 6, + office: 6 }; this.overview = { @@ -62,7 +63,8 @@ export class TabVisualizations { sca: 0, docker: 5, mitre: 6, - tsc: 6 + tsc: 6, + office: 6 }; this.tabVisualizations = {}; diff --git a/public/styles/layout.scss b/public/styles/layout.scss index 0e85a96547..a71fb14b79 100644 --- a/public/styles/layout.scss +++ b/public/styles/layout.scss @@ -203,6 +203,9 @@ html{ padding: 21px !important; } +.wz-padding-16 { + padding: 16px !important; +} .wz-lateral-padding-16 { padding-left: 16px; From f55aeab82d0c034282e2c2155c21553eae2c2b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 11:38:58 +0200 Subject: [PATCH 128/493] Added office365 group to the rules --- .../lib/generate-alerts/sample-data/office.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js index 75d3cd3903..3ab0228a17 100644 --- a/server/lib/generate-alerts/sample-data/office.js +++ b/server/lib/generate-alerts/sample-data/office.js @@ -123,7 +123,7 @@ export const officeRules = { id: '91533', mail: false, firedtimes: 3, - groups: ['ExchangeAdmin', 'hipaa_164.312.b', 'pci_dss_10.2.2', 'pci_dss_10.6.1'], + groups: ['office365', 'ExchangeAdmin', 'hipaa_164.312.b', 'pci_dss_10.2.2', 'pci_dss_10.6.1'], }, }, 2: { @@ -140,7 +140,7 @@ export const officeRules = { id: '91534', mail: false, firedtimes: 3, - groups: ['ExchangeItem', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'ExchangeItem', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, 4: { @@ -156,7 +156,7 @@ export const officeRules = { id: '91536', mail: false, firedtimes: 3, - groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, 6: { @@ -173,6 +173,7 @@ export const officeRules = { mail: false, firedtimes: 3, groups: [ + 'office365', 'SharePointFileOperation', 'hipaa_164.312.b', 'hipaa_164.312.c.1', @@ -194,7 +195,7 @@ export const officeRules = { id: '91539', mail: false, firedtimes: 3, - groups: ['AzureActiveDirectory', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'AzureActiveDirectory', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, 14: { @@ -210,7 +211,7 @@ export const officeRules = { id: '91544', mail: false, firedtimes: 3, - groups: ['SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'SharePoint', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, 15: { @@ -227,6 +228,7 @@ export const officeRules = { mail: false, firedtimes: 3, groups: [ + 'office365', 'AzureActiveDirectoryStsLogon', 'hipaa_164.312.a.2.I,hipaa_164.312.b', 'hipaa_164.312.d', @@ -249,6 +251,7 @@ export const officeRules = { mail: false, firedtimes: 3, groups: [ + 'office365', 'SecurityComplianceCenterEOPCmdlet', 'hipaa_164.312.b', 'pci_dss_10.2.2', @@ -269,7 +272,7 @@ export const officeRules = { id: '91564', mail: false, firedtimes: 3, - groups: ['SharePointListOperation', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'SharePointListOperation', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, 52: { @@ -285,7 +288,7 @@ export const officeRules = { id: '91580', mail: false, firedtimes: 4, - groups: ['DataInsightsRestApiAudit', 'hipaa_164.312.b', 'pci_dss_10.6.2'], + groups: ['office365', 'DataInsightsRestApiAudit', 'hipaa_164.312.b', 'pci_dss_10.6.2'], }, }, }; From be1a3a8b2b72304cb008c82dc419f9e5d3486808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 13:23:11 +0200 Subject: [PATCH 129/493] Changed the naming convention of the files Added loading state to tables Fixed typing in useEsSearch --- .../components/common/hooks/use-es-search.ts | 30 ++++++++++--------- public/components/common/panels/agg-table.tsx | 8 ++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 1459bf86e6..0a0ed036cc 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useQuery, useIndexPattern, useFilterManager } from '.'; import _ from 'lodash'; +import { Filter, IndexPattern } from 'src/plugins/data/public'; /* You can find more info on how to use the preAppliedAggs object at https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html @@ -12,13 +13,19 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) const indexPattern = useIndexPattern(); const filterManager = useFilterManager(); const [esResults, setEsResults] = useState({}); - const [managedFilters, setManagedFilters] = useState([]); + const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); + const [isLoading, setIsLoading] = useState(true); const [page, setPage] = useState(0); const [query] = useQuery(); useEffect(() => { - search().then((result) => { - setEsResults(result); - }); + setIsLoading(true); + search() + .then((result) => { + setEsResults(result); + }) + .finally(() => { + setIsLoading(false); + }); }, [indexPattern, query, managedFilters, page]); useEffect(() => { let filterSubscriber = filterManager.getUpdates$().subscribe(() => { @@ -30,21 +37,21 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) filterSubscriber.unsubscribe(); }; }); - }); + }, []); const search = async () => { if (indexPattern) { - const esQuery = await data.query.getEsQuery(indexPattern); + const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; const results = await searchSource .setParent(undefined) .setField('filter', combined) - .setField('query', esQuery) + .setField('query', query) .setField('aggs', preAppliedAggs) .setField('size', size) .setField('from', page * size) - .setField('index', indexPattern) + .setField('index', indexPattern as IndexPattern) .fetch(); return results; } else { @@ -59,12 +66,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setPage(page - 1 < 0 ? 0 : page - 1); }; - return { - esResults, - setPage, - nextPage, - prevPage, - }; + return {esResults, isLoading, setPage, nextPage, prevPage}; }; export { useEsSearch }; diff --git a/public/components/common/panels/agg-table.tsx b/public/components/common/panels/agg-table.tsx index 9fa2b75a90..98dcc43d5b 100644 --- a/public/components/common/panels/agg-table.tsx +++ b/public/components/common/panels/agg-table.tsx @@ -1,4 +1,4 @@ -import { EuiBasicTable, EuiPanel, EuiTitle } from '@elastic/eui'; +import { EuiBasicTable, EuiPanel, EuiTitle, EuiBasicTableColumn } from '@elastic/eui'; import { useEsSearch } from '../hooks'; import React from 'react'; @@ -20,9 +20,9 @@ export const AggTable = ({ }, }, }; - const { esResults } = useEsSearch({ preAppliedAggs }); + const {esResults, isLoading} = useEsSearch({ preAppliedAggs }); const buckets = ((esResults.aggregations || {}).buckets || {}).buckets || []; - const columns = [ + const columns:EuiBasicTableColumn<any>[] = [ { field: 'key', name: aggLabel, @@ -48,7 +48,7 @@ export const AggTable = ({ <EuiTitle {...titleProps}> <h2>{tableTitle}</h2> </EuiTitle> - <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} /> + <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} loading={isLoading} /> </EuiPanel> ); }; From 6b5048640ae4ab5233a2eb482550d06aa11c35f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 23 Jul 2021 13:27:15 +0200 Subject: [PATCH 130/493] Added error catch to useEsSearch --- public/components/common/hooks/use-es-search.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 0a0ed036cc..06d69f3274 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -14,6 +14,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) const filterManager = useFilterManager(); const [esResults, setEsResults] = useState({}); const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); + const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const [page, setPage] = useState(0); const [query] = useQuery(); @@ -22,6 +23,10 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) search() .then((result) => { setEsResults(result); + setError(null); + }) + .catch((error)=>{ + setError(error); }) .finally(() => { setIsLoading(false); @@ -66,7 +71,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setPage(page - 1 < 0 ? 0 : page - 1); }; - return {esResults, isLoading, setPage, nextPage, prevPage}; + return {esResults, isLoading, error, setPage, nextPage, prevPage}; }; export { useEsSearch }; From c675abb147b675414d698e38ac0f24725685b064 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 23 Jul 2021 13:32:03 +0200 Subject: [PATCH 131/493] added vis card and reverse applied patch --- .../office-panel/components/vis-card.tsx | 41 +++--- .../office-panel/config/drilldown-config.tsx | 12 +- .../office-panel/config/main-view-config.tsx | 13 +- .../office-panel/views/office-body.tsx | 27 +--- .../office-panel/views/office-drilldown.tsx | 5 +- public/components/visualize/visualizations.js | 67 +-------- public/components/visualize/wz-visualize.js | 135 +++--------------- public/factories/tab-visualizations.js | 6 +- 8 files changed, 67 insertions(+), 239 deletions(-) diff --git a/public/components/overview/office-panel/components/vis-card.tsx b/public/components/overview/office-panel/components/vis-card.tsx index 29078a2877..ba8d688006 100644 --- a/public/components/overview/office-panel/components/vis-card.tsx +++ b/public/components/overview/office-panel/components/vis-card.tsx @@ -1,28 +1,37 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; -export const OfficeBody = ({ changeView, rows = [] }) => { +export const VisCard = ({ changeView = () => { }, title = 'Vis Title', id, width, tab, ...props }) => { const [expandedVis, setExpandedVis] = useState(false); const toggleExpand = id => { setExpandedVis(expandedVis === id ? false : id); }; - - return <> <EuiFlexItem key={key} grow={growthFactor}> - <EuiPanel paddingSize={'s'} className={expandedVis === vis.id ? 'fullscreen h-100' : 'h-100'}> - <EuiTitle> - {vis.title} - </EuiTitle> - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => toggleExpand(vis.id)} - iconType="expand" - aria-label="Expand" - /> - <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /> - </div></EuiPanel> + + return <> <EuiFlexItem grow={width}> + <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> + <EuiFlexGroup direction={'column'} className={'h-100'}> + <EuiFlexItem grow={false}> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem>{title && <EuiTitle size={'xxs'}><h4>{title}</h4></EuiTitle>}</EuiFlexItem> + <EuiFlexItem grow={false} > + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => toggleExpand(id)} + iconType="expand" + aria-label="Expand" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <div className={'h-100'}><KibanaVis visID={id} tab={tab} onRowClick={() => changeView('drilldown')} {...props} /></div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> </EuiFlexItem> </> } \ No newline at end of file diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx index 7613e686ac..d0af06e764 100644 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -1,19 +1,19 @@ import React from 'react'; -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; +import { VisCard } from '../components/vis-card'; export const DrilldownConfig = { rows: [ { - height: 130, + height: 230, columns: [ { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> }, { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Authentication-success' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Authentication-success' tab='office' {...props} /> }, ] }, @@ -22,11 +22,11 @@ export const DrilldownConfig = { columns: [ { width: 70, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Agents-status' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Agents-status' tab='office' {...props} /> }, { width: 30, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> }, ] }, diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 400d1becad..3cd72b95d9 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,12 +1,11 @@ -import { EuiPanel } from '@elastic/eui'; import React from 'react'; -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; +import { VisCard } from '../components/vis-card'; export const MainViewConfig = { rows: [ { - height: 150, + height: 200, columns: [ { width: 50, @@ -14,7 +13,7 @@ export const MainViewConfig = { }, { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-Top-Mitre' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Top-Mitre' tab='office' title='' {...props} /> }, ] }, @@ -23,11 +22,11 @@ export const MainViewConfig = { columns: [ { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> }, { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Top-5-agents-Evolution' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-5-agents-Evolution' tab='office' {...props} /> }, ] }, @@ -36,7 +35,7 @@ export const MainViewConfig = { columns: [ { width: 50, - component: (props) => <KibanaVis visID='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> }, ] }, diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index 1a8ab77ac7..fe3d41889c 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,14 +1,8 @@ -import React, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import React from 'react'; +import { EuiFlexGroup} from '@elastic/eui'; export const OfficeBody = ({ changeView, rows = [] }) => { - const [expandedVis, setExpandedVis] = useState(false); - - const toggleExpand = id => { - setExpandedVis(expandedVis === id ? false : id); - }; - return <> { rows.map((row, key) => { @@ -18,22 +12,7 @@ export const OfficeBody = ({ changeView, rows = [] }) => { { row.columns.map((column, key) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - - return <EuiFlexItem key={key} grow={growthFactor}> - <EuiPanel paddingSize={'s'} className={expandedVis === vis.id ? 'fullscreen h-100' : 'h-100'}> - <EuiTitle> - {vis.title} - </EuiTitle> - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => toggleExpand(vis.id)} - iconType="expand" - aria-label="Expand" - /> - <div style={{ height: '100%' }}><column.component onRowClick={() => changeView('drilldown')} /> - </div></EuiPanel> - </EuiFlexItem> + return <column.component width={growthFactor} key={key} onRowClick={() => changeView('drilldown')} /> }) } </EuiFlexGroup> diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index f19a26deab..9ab3eff7b7 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -24,10 +24,7 @@ export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { { row.columns.map((column, key) => { const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - - return <EuiFlexItem key={key} grow={growthFactor}> - <EuiPanel paddingSize={'s'} ><div style={{ height: '100%' }}><column.component onRowClick={() => changeView('main')} /></div></EuiPanel> - </EuiFlexItem> + return <column.component key={key} width={growthFactor} onRowClick={() => changeView('main')} /> }) } </EuiFlexGroup> diff --git a/public/components/visualize/visualizations.js b/public/components/visualize/visualizations.js index fb2561bac3..7675656924 100644 --- a/public/components/visualize/visualizations.js +++ b/public/components/visualize/visualizations.js @@ -954,70 +954,5 @@ export const visualizations = { ] } ] - }, - office: { - rows: [ - { - height: 360, - vis: [ - { - title: 'Alert level evolution', - id: 'Wazuh-App-Overview-Office-Level-12-alerts', - width: 60 - } - ] - } - ] - }, - office_exchange: { - rows: [ - { - height: 360, - vis: [ - { - title: 'Alert level evolution', - id: 'Wazuh-App-Overview-Office-Top-5-agents', - width: 60 - } - ] - } - ] - }, - office_azuread: { - rows: [ - { - height: 360, - vis: [ - { - title: 'Alert level evolution', - id: 'Wazuh-App-Overview-Office-Alerts-evolution-Top-5-agents', - width: 60 - } - ] - } - ] - }, - office_sharepoint: { - rows: [ - { - height: 360, - vis: [ - { - title: 'Alert level evolution', - id: 'Wazuh-App-Overview-Office-Alerts-Top-Mitre', - width: 60 - } - ] - }, - { - hide: true, - vis: [ - { - title: 'Alerts summary', - id: 'Wazuh-App-Overview-Office-Alerts-summary', - } - ] - } - ] - }, + } }; diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index e0213069e4..a3f1cfc4a0 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -23,8 +23,7 @@ import { EuiButtonIcon, EuiDescriptionList, EuiCallOut, - EuiLink, - EuiButtonGroup + EuiLink } from '@elastic/eui'; import WzReduxProvider from '../../redux/wz-redux-provider'; import { WazuhConfig } from '../../react-services/wazuh-config'; @@ -34,7 +33,7 @@ import { VisHandlers } from '../../factories/vis-handlers'; import { RawVisualizations } from '../../factories/raw-visualizations'; import { Metrics } from '../overview/metrics/metrics'; import { PatternHandler } from '../../react-services/pattern-handler'; -import { getToasts, getAngularModule, getDataPlugin } from '../../kibana-services'; +import { getToasts } from '../../kibana-services'; import { SecurityAlerts } from './components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { withReduxProvider,withErrorBoundary } from '../common/hocs'; @@ -42,9 +41,6 @@ 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'; -import { AppState } from '../../react-services/'; -import { buildPhraseFilter } from '../../../../../src/plugins/data/common'; -import { getIndexPattern } from '../overview/mitre/lib'; const visHandler = new VisHandlers(); @@ -59,8 +55,7 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class thereAreSampleAlerts: false, hasRefreshedKnownFields: false, refreshingKnownFields: [], - refreshingIndex: true, - dashboard: 'main' + refreshingIndex: true }; this.metricValues = false; this.rawVisualizations = new RawVisualizations(); @@ -78,8 +73,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class this._isMount = true; visHandler.removeAll(); this.agentsStatus = false; - const app = getAngularModule(); - this.$rootScope = app.$injector.get('$rootScope'); if (!this.monitoringEnabled) { const data = await this.wzReq.apiReq('GET', '/agents/summary/status', {}); const result = ((data || {}).data || {}).data || false; @@ -129,29 +122,12 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class } } - async componentDidUpdate(prevProps, prevState) { + async componentDidUpdate(prevProps) { if (prevProps.isAgent !== this.props.isAgent) { this._isMount && this.setState({ visualizations: !!this.props.isAgent ? agentVisualizations : visualizations }); typeof prevProps.isAgent !== 'undefined' && visHandler.removeAll(); } - - // if(prevState.dashboard !== this.state.dashboard){ - // console.log(prevState.dashboard, this.state.dashboard); - // this.$rootScope.showModuleDashboard = this.props.selectedTab; - // await ModulesHelper.getDiscoverScope(); - // if (this._isMount) { - // this.$rootScope.moduleDiscoverReady = true; - // this.$rootScope.$applyAsync(); - // } - // VisFactoryHandler.clearAll(); - // await VisFactoryHandler.buildOverviewVisualizations( - // new FilterHandler(AppState.getCurrentPattern()), - // this.props.selectedTab, - // 'dashboard', - // false - // ); - // } } componentWillUnmount() { @@ -298,66 +274,6 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class return ( <Fragment> - {this.props.selectedTab === 'office' && ( - <EuiFlexGroup justifyContent='flexEnd' gutterSize='s'style={{ margin: '0 8px 16px 8px' }}> - <EuiFlexItem grow={false}> - <EuiButtonGroup - legend="This is a basic group" - options={['Main','AzureAD', 'Exchange', 'Sharepoint'].map(office365Dashboard => ({id: office365Dashboard.toLowerCase(), label: office365Dashboard}))} - idSelected={this.state.dashboard} - onChange={(id) => { - this.setState({dashboard: id}); - // getDataPlugin().query.filterManager. - - getIndexPattern().then(indexPattern => { - // const filter = { - // ...buildPhraseFilter({ name: 'syscheck.path', type: 'text' }, id, indexPattern/*AppState.getCurrentPattern()*/), - // $state: { store: 'appState', isImplicit: true }, - // }; - // delete filter.meta.removable - // filter.query = { - // "match": { - // 'syscheck.path': { - // query: id, - // type: 'phrase' - // } - // } - // } - - const filter = id !== 'main' ? [{ - "meta": { - "alias": null, - "disabled": false, - "key": "syscheck.path", - "negate": false, - "params": { "query": id }, - "type": "phrase", - "index": AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN, - "controlledBy": "wazuh" - }, - "query": { - "match": { - 'syscheck.path': { - query: id, - type: 'phrase' - } - } - }, - "$state": { "store": "appState", "isImplicit": true}, - }] : []; - - const filters = getDataPlugin().query.filterManager.getFilters() - .filter(filter => filter.meta.controlledBy !== 'wazuh') - // getDataPlugin().query.filterManager.addFilters([filter]); - getDataPlugin().query.filterManager.setFilters([...filters, ...filter]); - }) - }} - color="primary" - /> - </EuiFlexItem> - </EuiFlexGroup> - )} - {/* Sample alerts Callout */} {this.state.thereAreSampleAlerts && this.props.resultState === 'ready' && ( <EuiCallOut title='This dashboard contains sample data' color='warning' iconType='alert' style={{ margin: '0 8px 16px 8px' }}> @@ -379,30 +295,25 @@ export const WzVisualize = compose (withErrorBoundary,withReduxProvider) (class {selectedTab && selectedTab !== 'welcome' && visualizations[selectedTab] && - Object.keys(visualizations).filter(visualization => visualization.startsWith(selectedTab)).map(visualization => { - console.log(visualization, this.state.dashboard, (this.state.dashboard !== 'main' && `${selectedTab}_${this.state.dashboard}`.toLowerCase() === visualization) || this.state.dashboard === 'main' && selectedTab === visualization) - return visualizations[visualization].rows.map((row, i) => { - return ( - <EuiFlexGroup - key={i} - style={{ - display: (row.hide || (this.state.dashboard !== 'main' && `${selectedTab}_${this.state.dashboard}`.toLowerCase() !== visualization) || (this.state.dashboard === 'main' && selectedTab !== visualization) ) && 'none', - height: row.height || 0 + 'px', - margin: 0, - maxWidth: '100%' - }} - data-id={visualization} - > - {row.vis.map((vis, n) => { - return !vis.hasRows - ? renderVisualizations(vis) - : renderVisualizationRow(vis.rows, vis.width, n); - })} - </EuiFlexGroup> - ); - }); - } - )} + visualizations[selectedTab].rows.map((row, i) => { + return ( + <EuiFlexGroup + key={i} + style={{ + display: row.hide && 'none', + height: row.height || 0 + 'px', + margin: 0, + maxWidth: '100%' + }} + > + {row.vis.map((vis, n) => { + return !vis.hasRows + ? renderVisualizations(vis) + : renderVisualizationRow(vis.rows, vis.width, n); + })} + </EuiFlexGroup> + ); + })} </EuiFlexItem> <EuiFlexGroup style={{ margin: 0 }}> <EuiFlexItem> diff --git a/public/factories/tab-visualizations.js b/public/factories/tab-visualizations.js index f2483d3896..6a6a30ffdd 100644 --- a/public/factories/tab-visualizations.js +++ b/public/factories/tab-visualizations.js @@ -39,8 +39,7 @@ export class TabVisualizations { configuration: 0, osquery: 5, docker: 5, - mitre: 6, - office: 6 + mitre: 6 }; this.overview = { @@ -63,8 +62,7 @@ export class TabVisualizations { sca: 0, docker: 5, mitre: 6, - tsc: 6, - office: 6 + tsc: 6 }; this.tabVisualizations = {}; From a6e16a03bbcb88ed52d77fb4d821a75f5a64cbd5 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 23 Jul 2021 14:47:07 +0200 Subject: [PATCH 132/493] fixed viscard title --- .../overview/office-panel/components/vis-card.tsx | 11 +++++++++-- public/services/common-data.js | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/public/components/overview/office-panel/components/vis-card.tsx b/public/components/overview/office-panel/components/vis-card.tsx index ba8d688006..2f037b953d 100644 --- a/public/components/overview/office-panel/components/vis-card.tsx +++ b/public/components/overview/office-panel/components/vis-card.tsx @@ -1,15 +1,22 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import { RawVisualizations } from '../../../../factories/raw-visualizations'; import KibanaVis from '../../../../kibana-integrations/kibana-vis'; -export const VisCard = ({ changeView = () => { }, title = 'Vis Title', id, width, tab, ...props }) => { +export const VisCard = ({ changeView = () => { }, id, width, tab, ...props }) => { const [expandedVis, setExpandedVis] = useState(false); + + const title = (() => { + const visList = new RawVisualizations().getList(); + const rawVis = visList ? visList.filter((item) => item && item.id === id) : []; + return rawVis.length && rawVis[0]?.attributes?.title; + })() const toggleExpand = id => { setExpandedVis(expandedVis === id ? false : id); }; - + return <> <EuiFlexItem grow={width}> <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> <EuiFlexGroup direction={'column'} className={'h-100'}> diff --git a/public/services/common-data.js b/public/services/common-data.js index f4d8d4ce3a..53b272e940 100644 --- a/public/services/common-data.js +++ b/public/services/common-data.js @@ -143,7 +143,7 @@ export class CommonData { tsc: { group: 'tsc' }, aws: { group: 'amazon' }, gcp: { group: 'gcp' }, - office: { group: '' }, + office: { group: 'office365' }, virustotal: { group: 'virustotal' }, osquery: { group: 'osquery' }, sca: { group: 'sca' }, From f4f558b8123f13349c8fe08b647080ed45205366 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Fri, 23 Jul 2021 17:53:03 +0200 Subject: [PATCH 133/493] Adding visualizations --- .../overview/overview-office.ts | 676 ++++++++++++++++++ 1 file changed, 676 insertions(+) diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 952de3dfef..7a65fd2b1b 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -892,4 +892,680 @@ export default [ }, }, }, + { + _id: 'Wazuh-App-Overview-Office-Drilldown 1-1', + _type: 'visualization', + _source: { + title: 'Stats', + visState: JSON.stringify({ + title: 'Drilldown 1-1', + type: 'metric', + aggs: [ + { + id: '2', + enabled: true, + type: 'count', + params: { + customLabel: 'Total Alerts', + }, + schema: 'metric', + }, + { + id: '1', + enabled: true, + type: 'top_hits', + params: { + field: 'rule.level', + aggregate: 'concat', + size: 1, + sortField: 'rule.level', + sortOrder: 'desc', + customLabel: 'Max rule level detected', + }, + schema: 'metric', + }, + ], + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [ + { + from: 0, + to: 10000, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Drilldown 1-3', + _type: 'visualization', + _source: { + title: 'Registered IPs for User', + visState: JSON.stringify({ + title: 'Drilldown Office 1-3', + type: 'table', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Actor.ID', + orderBy: '_key', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Top Users', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'agent.id', + orderBy: '_key', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent ID', + }, + schema: 'bucket', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Agent name', + }, + schema: 'bucket', + }, + ], + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Drilldown 1-2', + _type: 'visualization', + _source: { + title: 'Top Events', + visState: JSON.stringify({ + title: 'Cake', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: false, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + row: true, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Drilldown 2', + _type: 'visualization', + _source: { + title: 'Alerts evolution over time', + visState: JSON.stringify({ + title: 'Drilldown Office 2', + type: 'line', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1y', + to: 'now', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'h', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Actor.ID', + orderBy: '1', + order: 'asc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + params: { + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'User-by-operation-result', + _type: 'visualization', + _source: { + title: 'User by Operation result', + visState: JSON.stringify({ + title: 'User By Operation result', + type: 'table', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Operation', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Operation', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'User', + }, + schema: 'bucket', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.ResultStatus', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'bucket', + }, + ], + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Severity-by-user', + _type: 'visualization', + _source: { + title: 'Severity by user', + visState: JSON.stringify({ + title: 'Severity by User', + type: 'histogram', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '_key', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severity', + }, + schema: 'segment', + }, + ], + params: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { + show: false, + }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Rule-Description', + _type: 'visualization', + _source: { + title: 'Rule-Description', + visState: JSON.stringify({ + title: 'Rule Description', + type: 'table', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule description', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severity', + }, + schema: 'bucket', + }, + ], + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; From 20b253f5d48ccf041bb079bbd0a1986e71643e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 23 Jul 2021 17:57:12 +0200 Subject: [PATCH 134/493] fix(hook): Fixed useQueryManager hook to use in the KbnSearchBar - Subscribe to the changes --- public/components/common/hooks/use-query.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-query.ts b/public/components/common/hooks/use-query.ts index 2a6e4d9984..1c4d54e67c 100644 --- a/public/components/common/hooks/use-query.ts +++ b/public/components/common/hooks/use-query.ts @@ -38,5 +38,12 @@ export function useQuery(): [{ } export const useQueryManager = () => { - return useState(getDataPlugin().query.queryString.getQuery()); + const [query, setQuery] = useState(getDataPlugin().query.queryString.getQuery()); + useEffect(() => { + const { unsubscribe } = getDataPlugin().query.queryString.getUpdates$().subscribe((q) => { + setQuery(q); + }); + return () => unsubscribe(); + },[]); + return [query, getDataPlugin().query.queryString.setQuery]; } From edadc051f0bd3cd1512a4c4d8ea103c39f66f038 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 23 Jul 2021 18:01:41 +0200 Subject: [PATCH 135/493] fix search bar --- .../custom-search-bar/custom-search-bar.tsx | 50 +++++++++++++------ .../office-panel/search-bar-config.ts | 10 ++-- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 3bd1af2910..a9361438ef 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -33,9 +33,8 @@ export const CustomSearchBar = ({ ...props }) => { const [customFilters, setCustomFilters] = useState(props.filtersValues) const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState([]); - const [defaultFilters, setDefaultFilters] = useState(filterManager.getFilters()); - + const [defaultFilters, setDefaultFilters] = useState(filterManager.getFilters()); const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; @@ -48,18 +47,20 @@ export const CustomSearchBar = ({ ...props }) => { const onFiltersUpdated = (filters: Filter[]) => { const { query, time } = filterParams; const updatedFilterParams = { query, time, filters }; - refreshCustomFilters(filters) setLoading(true); + refreshCustomSelectedFilter() setFilterParams(updatedFilterParams); } - const refreshCustomFilters = (filters) => { + const getCustomFilters = (filters) => { const deleteDefaultFilters = [] filters.forEach(filter => { if(!defaultFilters.some(item => item.meta.key === filter.meta.key)){ deleteDefaultFilters.push(filter) } }) + + return deleteDefaultFilters } const changeSwitch = () => { @@ -90,7 +91,7 @@ export const CustomSearchBar = ({ ...props }) => { return { meta, $state, query }; }; - const setFilters = async (values) => { + const setKibanaFilters = async (values) => { const indexPattern = await getIndexPattern() const newFilters = [] if(!values.length){ @@ -107,18 +108,36 @@ export const CustomSearchBar = ({ ...props }) => { } } - const onChange = (values) => { - setFilters(values) - setSelectedOptions(values); + const refreshCustomSelectedFilter = () => { + const filters = filterManager.getFilters() + const customFilters = getCustomFilters(filters) + const filtersUpdated = [] + customFilters.forEach(item => { + const filterObj = { + key: item.meta.key, + label: item.meta.params.query, + } + filtersUpdated.push(filterObj) + + }) + setSelectedOptions(filtersUpdated) + console.log(customFilters) + + }; + + const onChange = async(values) => { + await setKibanaFilters(values) + refreshCustomSelectedFilter(); }; + const getComponent = (item) => { var types = { 'default': <></>, 'combobox': <EuiComboBox - placeholder={"Select "+item.values[0].key+" or create options"} + placeholder={"Select "+item.key} options={item.values} - selectedOptions={selectedOptions || []} + selectedOptions={selectedOptions} onChange={onChange} isClearable={true} /> @@ -128,11 +147,11 @@ export const CustomSearchBar = ({ ...props }) => { return ( <> - <EuiFlexGroup alignItems='center'> + <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> { avancedFiltersState === false ? - customFilters.map((item) => ( - <EuiFlexItem grow={2}> + customFilters.map((item,key) => ( + <EuiFlexItem grow={2} key={key}> {getComponent(item)} </EuiFlexItem> )) @@ -149,8 +168,8 @@ export const CustomSearchBar = ({ ...props }) => { /> </EuiFlexItem> </EuiFlexGroup> - <EuiFlexGroup justifyContent='flexEnd'> - <EuiFlexItem grow={false} style={{ paddingTop: '10px' }}> + <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> + <EuiFlexItem grow={false}> <EuiSwitch label="Advanced filters" checked={avancedFiltersState} @@ -158,7 +177,6 @@ export const CustomSearchBar = ({ ...props }) => { /> </EuiFlexItem> </EuiFlexGroup> - <EuiSpacer /> </> ) }; \ No newline at end of file diff --git a/public/components/overview/office-panel/search-bar-config.ts b/public/components/overview/office-panel/search-bar-config.ts index 64d7e1818b..bd0ceb82b8 100644 --- a/public/components/overview/office-panel/search-bar-config.ts +++ b/public/components/overview/office-panel/search-bar-config.ts @@ -28,15 +28,15 @@ export const filtersValues = [ }, { type: 'combobox', - key: 'cluster.name', + key: 'agent.ip', values:[ { - key:'cluster.name', - label: 'wazuh', + key:'agent.ip', + label: '24.273.97.14', }, { - key:'cluster.name', - label: 'test', + key:'agent.ip', + label: '197.17.1.4', }, ] } From b37611551addeef2e29ec29be1ea7adf103658b9 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 26 Jul 2021 10:47:22 +0200 Subject: [PATCH 136/493] Two more visualizations --- .../overview/overview-office.ts | 367 +++++++++++++++++- 1 file changed, 348 insertions(+), 19 deletions(-) diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 7a65fd2b1b..3d3ff247e3 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -1493,13 +1493,13 @@ export default [ }, }, { - _id: 'Rule-Description', + _id: 'Rule-Level-Histogram', _type: 'visualization', _source: { - title: 'Rule-Description', + title: 'Rule level histrogram', visState: JSON.stringify({ - title: 'Rule Description', - type: 'table', + title: 'Rule level histogram', + type: 'area', aggs: [ { id: '1', @@ -1511,9 +1511,28 @@ export default [ { id: '2', enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now/w', + to: 'now/w', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '3h', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, type: 'terms', params: { - field: 'rule.description', + field: 'rule.level', orderBy: '1', order: 'desc', size: 5, @@ -1521,16 +1540,246 @@ export default [ otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - customLabel: 'Rule description', }, - schema: 'bucket', + schema: 'group', + }, + ], + params: { + type: 'area', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + rotate: 0, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'IPs-by-user', + _type: 'visualization', + _source: { + title: 'IPs by user', + visState: JSON.stringify({ + title: 'IPs by user', + type: 'histogram', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', }, { - id: '3', + id: '2', enabled: true, type: 'terms', params: { field: 'rule.level', + orderBy: '_key', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Severity', + }, + schema: 'segment', + }, + ], + params: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { + show: false, + }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Severity-by-user', + _type: 'visualization', + _source: { + title: 'Severity by user', + visState: JSON.stringify({ + title: 'IPs by User', + type: 'horizontal_bar', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'agent.ip', orderBy: '1', order: 'desc', size: 5, @@ -1538,22 +1787,102 @@ export default [ otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - customLabel: 'Severity', + customLabel: 'IP', }, - schema: 'bucket', + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', }, ], params: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 200, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 75, + filter: true, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', }, }), uiStateJSON: '{}', From 53d3a24bb75b477ff4bb5dda28a47b7a18c2dce6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 26 Jul 2021 10:54:38 +0200 Subject: [PATCH 137/493] added map visualization --- .../overview/overview-office.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 3d3ff247e3..c0986efb18 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -1897,4 +1897,72 @@ export default [ }, }, }, + { + _id: 'Wazuh-App-Overview-Office-geo', + _type: 'visualization', + _source: { + title: 'Geolocation map', + visState: JSON.stringify({ + title: 'Geolocation map', + type: 'tile_map', + params: { + colorSchema: 'Green to Red', + mapType: 'Scaled Circle Markers', + isDesaturated: false, + addTooltip: true, + heatClusterSize: 1.5, + legendPosition: 'bottomright', + mapZoom: 1, + mapCenter: [0, 0], + wms: { enabled: false, options: { format: 'image/png', transparent: true } }, + dimensions: { + metric: { accessor: 1, format: { id: 'number' }, params: {}, aggType: 'count' }, + geohash: { + accessor: 0, + format: { id: 'string' }, + params: { precision: 2, useGeocentroid: true }, + aggType: 'geohash_grid', + }, + geocentroid: { + accessor: 2, + format: { id: 'string' }, + params: {}, + aggType: 'geo_centroid', + }, + }, + }, + aggs: [ + { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + { + id: '2', + enabled: true, + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'GeoLocation.location', + autoPrecision: true, + precision: 2, + useGeocentroid: true, + isFilteredByCollar: true, + mapZoom: 1, + mapCenter: [0, 0], + }, + }, + ], + }), + uiStateJSON: JSON.stringify({ + mapZoom: 2, + mapCenter: [38.685509760012025, -31.816406250000004], + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + query: { query: '', language: 'lucene' }, + filter: [], + }), + }, + }, + }, ]; From 683e6d0323a79808f481946fae6ec249bd9695e8 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 26 Jul 2021 17:47:26 +0200 Subject: [PATCH 138/493] added new dashboard visualizations --- .../common/modules/panel/main-panel.tsx | 4 - .../office-panel/config/drilldown-config.tsx | 20 +- .../office-panel/config/main-view-config.tsx | 8 +- public/components/visualize/visualizations.js | 87 ++ .../overview/overview-office.ts | 1158 ++++++++++------- 5 files changed, 792 insertions(+), 485 deletions(-) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index c1f21a3340..94aa64e5a5 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -5,10 +5,6 @@ import { EuiPageBody, } from '@elastic/eui'; import { ModuleSidePanel } from './'; -import { FilterManager, Filter } from '../../../../../../../src/plugins/data/public/'; -import { getDataPlugin } from '../../../../kibana-services'; -import { KbnSearchBar } from '../../../kbn-search-bar'; -import { TimeRange, Query } from '../../../../../../../src/plugins/data/common'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; import { VisFactoryHandler } from '../../../../react-services/vis-factory-handler'; import { AppState } from '../../../../react-services/app-state'; diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx index d0af06e764..9a02539d12 100644 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -8,12 +8,16 @@ export const DrilldownConfig = { height: 230, columns: [ { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-alerts' tab='office' {...props} /> + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> }, { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Authentication-success' tab='office' {...props} /> + width: 40, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> }, ] }, @@ -21,12 +25,8 @@ export const DrilldownConfig = { height: 300, columns: [ { - width: 70, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Agents-status' tab='office' {...props} /> - }, - { - width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> + width: 100, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> }, ] }, diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 3cd72b95d9..22398876ba 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -5,7 +5,7 @@ import { VisCard } from '../components/vis-card'; export const MainViewConfig = { rows: [ { - height: 200, + height: 300, columns: [ { width: 50, @@ -13,7 +13,7 @@ export const MainViewConfig = { }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Top-Mitre' tab='office' title='' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-User-By-Operation-Result' tab='office' title='' {...props} /> }, ] }, @@ -22,11 +22,11 @@ export const MainViewConfig = { columns: [ { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alert-level-evolution' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-5-agents-Evolution' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Rule-Level-Histogram' tab='office' {...props} /> }, ] }, diff --git a/public/components/visualize/visualizations.js b/public/components/visualize/visualizations.js index 7675656924..be0d463be2 100644 --- a/public/components/visualize/visualizations.js +++ b/public/components/visualize/visualizations.js @@ -111,6 +111,93 @@ export const visualizations = { } ] }, + office: { + rows: [ + { + height: 160, + vis: [ + { + // title: 'Max Rule Level', + id: 'Wazuh-App-Overview-Office-Metric-Max-Rule-Level', + width: 25 + }, + { + // title: 'Malware Alerts', + id: 'Wazuh-App-Overview-Office-Metric-Malware-Alerts', + width: 25 + }, + { + // title: 'Suspicious Downloads', + id: 'Wazuh-App-Overview-Office-Metric-Suspicious-Downloads', + width: 25 + }, + { + // title: 'Full Access Permissions', + id: 'Wazuh-App-Overview-Office-Metric-FullAccess-Permissions', + width: 25 + } + ] + }, + { + height: 320, + vis: [ + { + title: 'Events by severity over time', + id: 'Wazuh-App-Overview-Office-Rule-Level-Histogram', + width: 40 + }, + { + title: 'IP by Users', + id: 'Wazuh-App-Overview-Office-IPs-By-User-Barchart', + width: 30 + }, + { + title: 'Top Users By Subscription', + id: 'Wazuh-App-Overview-Office-Top-Users-By-Subscription-Barchart', + width: 30 + }, + ] + }, + { + height: 350, + vis: [ + { + title: 'Users by Operation Result', + id: 'Wazuh-App-Overview-Office-User-By-Operation-Result', + width: 35 + }, + { + title: 'Severity by User', + id: 'Wazuh-App-Overview-Office-Severity-By-User-Barchart', + width: 30 + }, + { + title: 'Rule Description by Level', + id: 'Wazuh-App-Overview-Office-Rule-Description-Level-Table', + width: 35 + }, + ] + }, + { + height: 570, + vis: [ + { + title: 'Geolocation map', + id: 'Wazuh-App-Overview-Office-Location' + } + ] + }, + { + hide: true, + vis: [ + { + title: 'Alerts summary', + id: 'Wazuh-App-Overview-Office-Alerts-summary' + } + ] + } + ] + }, aws: { rows: [ { diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index c0986efb18..9474bfd5d7 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -111,7 +111,7 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-Office-Metric-alerts', + _id: 'Wazuh-App-Overview-Office-Metric-Alerts', _source: { title: 'Metric alerts', visState: JSON.stringify({ @@ -161,7 +161,289 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-Office-Level-12-alerts', + _id: 'Wazuh-App-Overview-Office-Metric-Max-Rule-Level', + _source: { + title: 'Max Rule Level', + visState: JSON.stringify({ + "title": "Max Rule Level", + "type": "metric", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "max", + "params": { + "field": "rule.level", + "customLabel": "Max Rule Level" + }, + "schema": "metric" + } + ], + "params": { + "addTooltip": true, + "addLegend": false, + "type": "metric", + "metric": { + "percentageMode": false, + "useRanges": false, + "colorSchema": "Green to Red", + "metricColorMode": "Labels", + "colorsRange": [ + { + "from": 0, + "to": 7 + }, + { + "from": 7, + "to": 10 + }, + { + "from": 10, + "to": 20 + } + ], + "labels": { + "show": true + }, + "invertColors": false, + "style": { + "bgFill": "#000", + "bgColor": false, + "labelColor": false, + "subText": "", + "fontSize": 26 + } + } + } + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Metric-Suspicious-Downloads', + _source: { + title: 'Suspicious Downloads', + visState: JSON.stringify({ + "title": "Suspicious Downloads Count", + "type": "metric", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "filters", + "params": { + "filters": [ + { + "input": { + "query": "rule.id: \"91724\"", + "language": "kuery" + }, + "label": "Suspicious Downloads" + } + ] + }, + "schema": "group" + } + ], + "params": { + "addTooltip": true, + "addLegend": false, + "type": "metric", + "metric": { + "percentageMode": false, + "useRanges": false, + "colorSchema": "Green to Red", + "metricColorMode": "Labels", + "colorsRange": [ + { + "from": 0, + "to": 1 + } + ], + "labels": { + "show": true + }, + "invertColors": false, + "style": { + "bgFill": "#000", + "bgColor": false, + "labelColor": false, + "subText": "", + "fontSize": 26 + } + } + } + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Metric-Malware-Alerts', + _source: { + title: 'Malware Alerts', + visState: JSON.stringify({ + "title": "Malware Alerts Count", + "type": "metric", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "filters", + "params": { + "filters": [ + { + "input": { + "query": "rule.id: \"91556\" or rule.id: \"91575\" or rule.id: \"91700\" ", + "language": "kuery" + }, + "label": "Malware Alerts" + } + ] + }, + "schema": "group" + } + ], + "params": { + "addTooltip": true, + "addLegend": false, + "type": "metric", + "metric": { + "percentageMode": false, + "useRanges": false, + "colorSchema": "Green to Red", + "metricColorMode": "None", + "colorsRange": [ + { + "from": 0, + "to": 10000 + } + ], + "labels": { + "show": true + }, + "invertColors": false, + "style": { + "bgFill": "#000", + "bgColor": false, + "labelColor": false, + "subText": "", + "fontSize": 26 + } + } + } + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Metric-FullAccess-Permissions', + _source: { + title: 'Full Access Permissions', + visState: JSON.stringify({ + "title": "Full Access Permission Count", + "type": "metric", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "filters", + "params": { + "filters": [ + { + "input": { + "query": "rule.id: \"91725\"", + "language": "kuery" + }, + "label": "Full Access Permissions" + } + ] + }, + "schema": "group" + } + ], + "params": { + "addTooltip": true, + "addLegend": false, + "type": "metric", + "metric": { + "percentageMode": false, + "useRanges": false, + "colorSchema": "Green to Red", + "metricColorMode": "None", + "colorsRange": [ + { + "from": 0, + "to": 10000 + } + ], + "labels": { + "show": true + }, + "invertColors": false, + "style": { + "bgFill": "#000", + "bgColor": false, + "labelColor": false, + "subText": "", + "fontSize": 26 + } + } + } + }), + uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-Office-Level-12-Alerts', _source: { title: 'Level 12 alerts', visState: JSON.stringify({ @@ -414,7 +696,7 @@ export default [ _type: 'visualization', }, { - _id: 'Wazuh-App-Overview-Office-Alert-level-evolution', + _id: 'Wazuh-App-Overview-Office-Alert-Level-Evolution', _source: { title: 'Alert level evolution', visState: JSON.stringify({ @@ -516,207 +798,6 @@ export default [ }, _type: 'visualization', }, - { - _id: 'Wazuh-App-Overview-Office-Alerts-Top-Mitre', - _source: { - title: 'Alerts', - visState: JSON.stringify({ - type: 'pie', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'rule.mitre.technique', - orderBy: '1', - order: 'desc', - size: 20, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - title: 'mitre top', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - _type: 'visualization', - }, - { - _id: 'Wazuh-App-Overview-Office-Top-5-agents', - _source: { - title: 'Top 5 agents', - visState: JSON.stringify({ - title: 'Top 5 agents', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'agent.name', - size: 5, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - }), - uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - _type: 'visualization', - }, - { - _id: 'Wazuh-App-Overview-Office-Top-5-agents-Evolution', - _source: { - title: 'Top 5 rule groups', - visState: JSON.stringify({ - type: 'histogram', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: 'timestamp', - timeRange: { from: '2020-07-19T16:18:13.637Z', to: '2020-07-28T13:58:33.357Z' }, - useNormalizedEsInterval: true, - scaleMetricValues: false, - interval: 'auto', - drop_partials: false, - min_doc_count: 1, - extended_bounds: {}, - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'agent.name', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - params: { - type: 'area', - grid: { categoryLines: false }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: true, - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: 'linear', - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, - labels: {}, - }, - title: 'top 5 agents evolution', - }), - uiStateJSON: JSON.stringify({ vis: { legendOpen: true } }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - _type: 'visualization', - }, { _id: 'Wazuh-App-Overview-Office-Alerts-summary', _type: 'visualization', @@ -737,150 +818,61 @@ export default [ aggs: [ { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'rule.id', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - size: 1000, - order: 'desc', - orderBy: '1', - customLabel: 'Rule ID', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'rule.description', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - size: 20, - order: 'desc', - orderBy: '1', - customLabel: 'Description', - }, - }, - { - id: '4', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'rule.level', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - size: 12, - order: 'desc', - orderBy: '1', - customLabel: 'Level', - }, - }, - ], - }), - uiStateJSON: JSON.stringify({ - vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, - }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-Office-Alerts-evolution-Top-5-agents', - _type: 'visualization', - _source: { - title: 'Alerts evolution Top 5 agents', - visState: JSON.stringify({ - title: 'Alerts evolution Top 5 agents', - type: 'histogram', - params: { - type: 'histogram', - grid: { categoryLines: false, style: { color: '#eee' } }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { + id: '2', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'rule.id', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 1000, + order: 'desc', + orderBy: '1', + customLabel: 'Rule ID', + }, + }, + { id: '3', enabled: true, type: 'terms', - schema: 'group', - params: { field: 'agent.name', size: 5, order: 'desc', orderBy: '1' }, + schema: 'bucket', + params: { + field: 'rule.description', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 20, + order: 'desc', + orderBy: '1', + customLabel: 'Description', + }, }, { - id: '2', + id: '4', enabled: true, - type: 'date_histogram', - schema: 'segment', + type: 'terms', + schema: 'bucket', params: { - field: 'timestamp', - interval: 'auto', - customInterval: '2h', - min_doc_count: 1, - extended_bounds: {}, + field: 'rule.level', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + size: 12, + order: 'desc', + orderBy: '1', + customLabel: 'Level', }, }, ], }), - uiStateJSON: '{}', + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), description: '', version: 1, kibanaSavedObjectMeta: { @@ -893,12 +885,12 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-Drilldown 1-1', + _id: 'Wazuh-App-Overview-Office-Metric-Stats', _type: 'visualization', _source: { title: 'Stats', visState: JSON.stringify({ - title: 'Drilldown 1-1', + title: 'Metric Stats', type: 'metric', aggs: [ { @@ -967,12 +959,12 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-Drilldown 1-3', + _id: 'Wazuh-App-Overview-Office-IPs-By-User-Table', _type: 'visualization', _source: { title: 'Registered IPs for User', visState: JSON.stringify({ - title: 'Drilldown Office 1-3', + title: 'Registered IPs for User', type: 'table', aggs: [ { @@ -1060,7 +1052,7 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-Drilldown 1-2', + _id: 'Wazuh-App-Overview-Office-Top-Events-Pie', _type: 'visualization', _source: { title: 'Top Events', @@ -1136,12 +1128,12 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-Drilldown 2', + _id: 'Wazuh-App-Overview-Office-Alerts-Evolution-By-User', _type: 'visualization', _source: { title: 'Alerts evolution over time', visState: JSON.stringify({ - title: 'Drilldown Office 2', + title: 'Alerts evolution over time', type: 'line', aggs: [ { @@ -1277,7 +1269,7 @@ export default [ }, }, { - _id: 'User-by-operation-result', + _id: 'Wazuh-App-Overview-Office-User-By-Operation-Result', _type: 'visualization', _source: { title: 'User by Operation result', @@ -1339,12 +1331,13 @@ export default [ otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', + customLabel: 'Result Status', }, schema: 'bucket', }, ], params: { - perPage: 10, + perPage: 5, showPartialRows: false, showMetricsAtAllLevels: false, sort: { @@ -1369,7 +1362,83 @@ export default [ }, }, { - _id: 'Severity-by-user', + _id: 'Wazuh-App-Overview-Office-Rule-Description-Level-Table', + _type: 'visualization', + _source: { + title: 'Rule Description by Level', + visState: JSON.stringify({ + "title": "Rule Description Level Table", + "type": "table", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.description", + "orderBy": "1", + "order": "desc", + "size": 500, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Rule Description" + }, + "schema": "bucket" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.level", + "orderBy": "1", + "order": "desc", + "size": 20, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Rule Level" + }, + "schema": "bucket" + } + ], + "params": { + "perPage": 5, + "showPartialRows": false, + "showMetricsAtAllLevels": false, + "sort": { + "columnIndex": null, + "direction": null + }, + "showTotal": false, + "totalFunc": "sum", + "percentageCol": "" + } + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Severity-By-User-Histogram', _type: 'visualization', _source: { title: 'Severity by user', @@ -1493,7 +1562,7 @@ export default [ }, }, { - _id: 'Rule-Level-Histogram', + _id: 'Wazuh-App-Overview-Office-Rule-Level-Histogram', _type: 'visualization', _source: { title: 'Rule level histrogram', @@ -1635,13 +1704,13 @@ export default [ }, }, { - _id: 'IPs-by-user', + _id: 'Wazuh-App-Overview-Office-IPs-By-User-Barchart', _type: 'visualization', _source: { title: 'IPs by user', visState: JSON.stringify({ - title: 'IPs by user', - type: 'histogram', + title: 'IPs by User', + type: 'horizontal_bar', aggs: [ { id: '1', @@ -1655,18 +1724,34 @@ export default [ enabled: true, type: 'terms', params: { - field: 'rule.level', - orderBy: '_key', + field: 'data.office365.ClientIP', + orderBy: '1', order: 'desc', - size: 10, + size: 5, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - customLabel: 'Severity', + customLabel: 'IP', }, schema: 'segment', }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, ], params: { type: 'histogram', @@ -1677,7 +1762,7 @@ export default [ { id: 'CategoryAxis-1', type: 'category', - position: 'bottom', + position: 'left', show: true, style: {}, scale: { @@ -1685,8 +1770,9 @@ export default [ }, labels: { show: true, - filter: true, - truncate: 100, + rotate: 0, + filter: false, + truncate: 200, }, title: {}, }, @@ -1696,7 +1782,7 @@ export default [ id: 'ValueAxis-1', name: 'LeftAxis-1', type: 'value', - position: 'left', + position: 'bottom', show: true, style: {}, scale: { @@ -1705,8 +1791,8 @@ export default [ }, labels: { show: true, - rotate: 0, - filter: false, + rotate: 75, + filter: true, truncate: 100, }, title: { @@ -1734,9 +1820,7 @@ export default [ legendPosition: 'right', times: [], addTimeMarker: false, - labels: { - show: false, - }, + labels: {}, thresholdLine: { show: false, value: 10, @@ -1759,131 +1843,271 @@ export default [ }, }, { - _id: 'Severity-by-user', + _id: 'Wazuh-App-Overview-Office-Severity-By-User-Barchart', _type: 'visualization', _source: { title: 'Severity by user', visState: JSON.stringify({ - title: 'IPs by User', - type: 'horizontal_bar', - aggs: [ + "title": "Severity By User Barchart", + "type": "histogram", + "aggs": [ { - id: '1', - enabled: true, - type: 'count', - params: {}, - schema: 'metric', + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" }, { - id: '2', - enabled: true, - type: 'terms', - params: { - field: 'agent.ip', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'IP', - }, - schema: 'segment', + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.level", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": true, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" }, { - id: '3', - enabled: true, - type: 'terms', - params: { - field: 'data.office365.UserId', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - schema: 'group', - }, + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.UserId", + "orderBy": "1", + "order": "desc", + "size": 20, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } ], - params: { - type: 'histogram', - grid: { - categoryLines: false, + "params": { + "type": "histogram", + "grid": { + "categoryLines": false }, - categoryAxes: [ + "categoryAxes": [ { - id: 'CategoryAxis-1', - type: 'category', - position: 'left', - show: true, - style: {}, - scale: { - type: 'linear', + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" }, - labels: { - show: true, - rotate: 0, - filter: false, - truncate: 200, + "labels": { + "show": true, + "filter": true, + "truncate": 100 }, - title: {}, - }, + "title": {} + } ], - valueAxes: [ + "valueAxes": [ { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'bottom', - show: true, - style: {}, - scale: { - type: 'linear', - mode: 'normal', - }, - labels: { - show: true, - rotate: 75, - filter: true, - truncate: 100, + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" }, - title: { - text: 'Count', + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 }, - }, + "title": { + "text": "Count" + } + } ], - seriesParams: [ + "seriesParams": [ { - show: true, - type: 'histogram', - mode: 'stacked', - data: { - label: 'Count', - id: '1', + "show": true, + "type": "histogram", + "mode": "stacked", + "data": { + "label": "Count", + "id": "1" }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - }, + "valueAxis": "ValueAxis-1", + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "showCircles": true + } ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - labels: {}, - thresholdLine: { - show: false, - value: 10, - width: 1, - style: 'full', - color: '#E7664C', + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "labels": { + "show": false + }, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + } + } + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Top-Users-By-Subscription-Barchart', + _type: 'visualization', + _source: { + title: 'Top User By Subscription', + visState: JSON.stringify({ + "title": "Top User By Subscription", + "type": "histogram", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" }, - }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.UserId", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "segment" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.Subscription", + "orderBy": "1", + "order": "desc", + "size": 5, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing" + }, + "schema": "group" + } + ], + "params": { + "type": "histogram", + "grid": { + "categoryLines": false + }, + "categoryAxes": [ + { + "id": "CategoryAxis-1", + "type": "category", + "position": "bottom", + "show": true, + "style": {}, + "scale": { + "type": "linear" + }, + "labels": { + "show": true, + "filter": true, + "truncate": 100, + "rotate": 0 + }, + "title": {} + } + ], + "valueAxes": [ + { + "id": "ValueAxis-1", + "name": "LeftAxis-1", + "type": "value", + "position": "left", + "show": true, + "style": {}, + "scale": { + "type": "linear", + "mode": "normal" + }, + "labels": { + "show": true, + "rotate": 0, + "filter": false, + "truncate": 100 + }, + "title": { + "text": "Count" + } + } + ], + "seriesParams": [ + { + "show": true, + "type": "histogram", + "mode": "stacked", + "data": { + "label": "Count", + "id": "1" + }, + "valueAxis": "ValueAxis-1", + "drawLinesBetweenPoints": true, + "lineWidth": 2, + "showCircles": true + } + ], + "addTooltip": true, + "addLegend": true, + "legendPosition": "right", + "times": [], + "addTimeMarker": false, + "labels": { + "show": false + }, + "thresholdLine": { + "show": false, + "value": 10, + "width": 1, + "style": "full", + "color": "#E7664C" + } + } }), uiStateJSON: '{}', description: '', @@ -1898,7 +2122,7 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-geo', + _id: 'Wazuh-App-Overview-Office-Location', _type: 'visualization', _source: { title: 'Geolocation map', From 5c795e469c520c521d606e92bdec7616734e4dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 26 Jul 2021 17:51:33 +0200 Subject: [PATCH 139/493] fix(frontend_panel): Fix problem with filters in CustomSearchBar component - Avoid reset filters when rendering a Panel view - Hide button to remove the filters on implicit filters used by the Wazuh module --- .../custom-search-bar/custom-search-bar.tsx | 16 ++++++++++++---- .../common/modules/panel/main-panel.tsx | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 4921bee99a..b723b0376f 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useLayoutEffect } from 'react'; import { getIndexPattern } from '../../overview/mitre/lib' import { Filter } from '../../../../../../src/plugins/data/public/'; @@ -16,6 +16,7 @@ import { import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; +import { ModulesHelper } from '../modules/modules-helper'; @@ -25,13 +26,13 @@ export const CustomSearchBar = ({ ...props }) => { const filterManager = KibanaServices.filterManager; const timefilter = KibanaServices.timefilter.timefilter; const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters() || [], + filters: filterManager.getFilters().map(({meta: {removable, ...restMeta}, ...rest}) => ({...rest,meta: restMeta})) || [], query: { language: 'kuery', query: '' }, time: timefilter.getTime(), }); const [isLoading, setLoading] = useState(false); const [customFilters, setCustomFilters] = useState(props.filtersValues) - const [avancedFiltersState, setAvancedFiltersState] = useState(false); + const [avancedFiltersState, setAvancedFiltersState] = useState(true); const [selectedOptions, setSelectedOptions] = useState([]); const [defaultFilters, setDefaultFilters] = useState(filterManager.getFilters()); @@ -64,9 +65,15 @@ export const CustomSearchBar = ({ ...props }) => { } const changeSwitch = () => { - avancedFiltersState ? setAvancedFiltersState(false) : setAvancedFiltersState(true); + setAvancedFiltersState(state => !state); } + useEffect(() => { + if(avancedFiltersState){ + setTimeout(() => ModulesHelper.hideCloseButtons(), 10); + }; + }, [avancedFiltersState]); + const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?:any, key?:any): Filter => { const meta: FilterMeta = { disabled: false, @@ -145,6 +152,7 @@ export const CustomSearchBar = ({ ...props }) => { return types[item.type] || types['default']; } + return ( <> <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 94aa64e5a5..6efb1790a8 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -25,7 +25,7 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { [tab]: moduleConfig[viewId].length(), }); const filterHandler = new FilterHandler(AppState.getCurrentPattern()); - await VisFactoryHandler.buildOverviewVisualizations(filterHandler, tab, null); + await VisFactoryHandler.buildOverviewVisualizations(filterHandler, tab, null, true); })() }, [viewId]) From 1ac7cebc7c1e4d21c40c24225416c2134fec9397 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 26 Jul 2021 19:24:21 +0200 Subject: [PATCH 140/493] Added office365 dashboard metrics --- .../components/overview/metrics/metrics.tsx | 8 +++++- public/components/visualize/visualizations.js | 25 ------------------- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index 939bc5da9b..8ada6b0819 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -101,7 +101,13 @@ export const Metrics = withAllowedAgents(class Metrics extends Component { { name: "Last scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}}, { name: "Highest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "secondary"}, { name: "Lowest scan score", type: "custom", filter: { phrase: "oscap-report", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "data.oscap.scan.score", "order": { "_term": "asc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "data.oscap.scan.score" } } } }}, color: "danger"}, - ] + ], + office: [ + { name: "Max Rule Level", type: "custom", filter: { phrase: "office365", field:"rule.groups"} , agg: { "customAggResult": { "terms": { "field": "timestamp", "order": { "_term": "desc" }, "size": 1 }, "aggs": { "aggResult": { "terms": { "field": "rule.level" } } } }}}, + { name: "Suspicious Downloads", type: "phrase", value: "91724", field: "rule.id", color: "danger"}, + { name: "Full Access Permissions", type: "phrase", value: "91725", field: "rule.id"}, + { name: "Phishing and Malware", type: "phrases", values: ["91556", "91575", "91700"], field: "rule.id",color: "danger"}, + ], } } diff --git a/public/components/visualize/visualizations.js b/public/components/visualize/visualizations.js index be0d463be2..cf126df665 100644 --- a/public/components/visualize/visualizations.js +++ b/public/components/visualize/visualizations.js @@ -113,31 +113,6 @@ export const visualizations = { }, office: { rows: [ - { - height: 160, - vis: [ - { - // title: 'Max Rule Level', - id: 'Wazuh-App-Overview-Office-Metric-Max-Rule-Level', - width: 25 - }, - { - // title: 'Malware Alerts', - id: 'Wazuh-App-Overview-Office-Metric-Malware-Alerts', - width: 25 - }, - { - // title: 'Suspicious Downloads', - id: 'Wazuh-App-Overview-Office-Metric-Suspicious-Downloads', - width: 25 - }, - { - // title: 'Full Access Permissions', - id: 'Wazuh-App-Overview-Office-Metric-FullAccess-Permissions', - width: 25 - } - ] - }, { height: 320, vis: [ From d464a8cbe14093e4f92837c9ba57c1d33f93ae20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 27 Jul 2021 10:49:13 +0200 Subject: [PATCH 141/493] Used error message in tables --- public/components/common/panels/agg-table.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/components/common/panels/agg-table.tsx b/public/components/common/panels/agg-table.tsx index 98dcc43d5b..da9a46a731 100644 --- a/public/components/common/panels/agg-table.tsx +++ b/public/components/common/panels/agg-table.tsx @@ -20,7 +20,7 @@ export const AggTable = ({ }, }, }; - const {esResults, isLoading} = useEsSearch({ preAppliedAggs }); + const {esResults, isLoading, error} = useEsSearch({ preAppliedAggs }); const buckets = ((esResults.aggregations || {}).buckets || {}).buckets || []; const columns:EuiBasicTableColumn<any>[] = [ { @@ -43,12 +43,13 @@ export const AggTable = ({ }, }; }; + return ( <EuiPanel data-test-subj={`${aggTerm}-aggTable`} {...panelProps}> <EuiTitle {...titleProps}> <h2>{tableTitle}</h2> </EuiTitle> - <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} loading={isLoading} /> + <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} loading={isLoading} error={error ? error.message : undefined} /> </EuiPanel> ); }; From 0d377b691b2fbe12520ce5b928ed5ac1c58ef091 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 27 Jul 2021 11:43:00 +0200 Subject: [PATCH 142/493] Integrated Agg Table --- .../panel/components}/agg-table.tsx | 2 +- .../common/modules/panel/components/index.ts | 2 ++ .../{ => components}/module-side-panel.scss | 0 .../{ => components}/module-side-panel.tsx | 0 .../modules/panel/{index.tsx => index.ts} | 2 +- .../common/modules/panel/main-panel.tsx | 2 +- public/components/common/panels/index.ts | 3 +- .../office-panel/config/main-view-config.tsx | 28 +++++++++++++++++-- 8 files changed, 32 insertions(+), 7 deletions(-) rename public/components/common/{panels => modules/panel/components}/agg-table.tsx (96%) create mode 100644 public/components/common/modules/panel/components/index.ts rename public/components/common/modules/panel/{ => components}/module-side-panel.scss (100%) rename public/components/common/modules/panel/{ => components}/module-side-panel.tsx (100%) rename public/components/common/modules/panel/{index.tsx => index.ts} (88%) diff --git a/public/components/common/panels/agg-table.tsx b/public/components/common/modules/panel/components/agg-table.tsx similarity index 96% rename from public/components/common/panels/agg-table.tsx rename to public/components/common/modules/panel/components/agg-table.tsx index da9a46a731..a859b1179b 100644 --- a/public/components/common/panels/agg-table.tsx +++ b/public/components/common/modules/panel/components/agg-table.tsx @@ -1,5 +1,5 @@ import { EuiBasicTable, EuiPanel, EuiTitle, EuiBasicTableColumn } from '@elastic/eui'; -import { useEsSearch } from '../hooks'; +import { useEsSearch } from '../../../hooks'; import React from 'react'; export const AggTable = ({ diff --git a/public/components/common/modules/panel/components/index.ts b/public/components/common/modules/panel/components/index.ts new file mode 100644 index 0000000000..b41f7f0cd2 --- /dev/null +++ b/public/components/common/modules/panel/components/index.ts @@ -0,0 +1,2 @@ +export { ModuleSidePanel } from './module-side-panel'; +export { AggTable } from './agg-table'; \ No newline at end of file diff --git a/public/components/common/modules/panel/module-side-panel.scss b/public/components/common/modules/panel/components/module-side-panel.scss similarity index 100% rename from public/components/common/modules/panel/module-side-panel.scss rename to public/components/common/modules/panel/components/module-side-panel.scss diff --git a/public/components/common/modules/panel/module-side-panel.tsx b/public/components/common/modules/panel/components/module-side-panel.tsx similarity index 100% rename from public/components/common/modules/panel/module-side-panel.tsx rename to public/components/common/modules/panel/components/module-side-panel.tsx diff --git a/public/components/common/modules/panel/index.tsx b/public/components/common/modules/panel/index.ts similarity index 88% rename from public/components/common/modules/panel/index.tsx rename to public/components/common/modules/panel/index.ts index 4e260ac370..40147da54d 100644 --- a/public/components/common/modules/panel/index.tsx +++ b/public/components/common/modules/panel/index.ts @@ -12,4 +12,4 @@ export { MainPanel } from './main-panel'; -export { ModuleSidePanel } from './module-side-panel'; +export * from './components/'; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 6efb1790a8..b161d54b21 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -4,7 +4,7 @@ import { EuiFlexItem, EuiPageBody, } from '@elastic/eui'; -import { ModuleSidePanel } from './'; +import { ModuleSidePanel } from './components/'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; import { VisFactoryHandler } from '../../../../react-services/vis-factory-handler'; import { AppState } from '../../../../react-services/app-state'; diff --git a/public/components/common/panels/index.ts b/public/components/common/panels/index.ts index 394f1a4025..fe73636ad6 100644 --- a/public/components/common/panels/index.ts +++ b/public/components/common/panels/index.ts @@ -1,2 +1 @@ -export * from './panel_split'; -export { AggTable } from './agg-table'; \ No newline at end of file +export * from './panel_split'; \ No newline at end of file diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 22398876ba..2b6048fb04 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,7 +1,25 @@ import React from 'react'; import { VisCard } from '../components/vis-card'; +import { AggTable } from '../../../common/modules/panel'; - +// AggTable = ({ +// onRowClick = (field, value) => {}, +// aggTerm, +// aggLabel, +// maxRows, +// tableTitle, +// panelProps, +// titleProps +// } +// { +// + flexProps: { style: { minWidth: 500 }, grow:6 }, +// + tableProps: { +// + aggTerm: 'agent.name', +// + aggLabel: 'Agent Name', +// + maxRows: '10', +// + tableTitle: 'Agents by name', +// + }, +// + }, export const MainViewConfig = { rows: [ { @@ -9,7 +27,13 @@ export const MainViewConfig = { columns: [ { width: 50, - component: (props) => <div ><button onClick={() => props.onRowClick('drilldown')}>change view</button></div> + component: (props) => ( + <AggTable + tableTitle={'Users'} + aggTerm={'data.office365.UserId'} + aggLabel={'User'} + maxRows={'7'} + onRowClick={(field, value) => props.onRowClick('drilldown')} />) }, { width: 50, From 7add297be904f627bd87646196cedbb8f14f4858 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 27 Jul 2021 11:58:28 +0200 Subject: [PATCH 143/493] Changes adding GitHub Panel --- .../common/modules/main-overview.tsx | 3 + .../common/modules/modules-defaults.js | 2 +- .../github-panel/components/vis-card.tsx | 44 ++++++++++++ .../github-panel/config/drilldown-config.tsx | 34 +++++++++ .../overview/github-panel/config/index.ts | 4 ++ .../github-panel/config/main-view-config.tsx | 43 ++++++++++++ .../github-panel/config/module-config.tsx | 15 ++++ .../github-panel/config/search-bar-config.ts | 70 +++++++++++++++++++ .../overview/github-panel/github-panel.tsx | 54 ++++++++++++++ .../overview/github-panel/index.tsx | 14 ++++ .../overview/github-panel/views/index.ts | 3 + .../github-panel/views/office-body.tsx | 22 ++++++ .../github-panel/views/office-drilldown.tsx | 39 +++++++++++ .../github-panel/views/office-stats.tsx | 28 ++++++++ 14 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 public/components/overview/github-panel/components/vis-card.tsx create mode 100644 public/components/overview/github-panel/config/drilldown-config.tsx create mode 100644 public/components/overview/github-panel/config/index.ts create mode 100644 public/components/overview/github-panel/config/main-view-config.tsx create mode 100644 public/components/overview/github-panel/config/module-config.tsx create mode 100644 public/components/overview/github-panel/config/search-bar-config.ts create mode 100644 public/components/overview/github-panel/github-panel.tsx create mode 100644 public/components/overview/github-panel/index.tsx create mode 100644 public/components/overview/github-panel/views/index.ts create mode 100644 public/components/overview/github-panel/views/office-body.tsx create mode 100644 public/components/overview/github-panel/views/office-drilldown.tsx create mode 100644 public/components/overview/github-panel/views/office-stats.tsx diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index f8651ab108..b0cc5cf83b 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -46,6 +46,7 @@ import { compose } from 'redux'; import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; import { OfficePanel } from '../../overview/office-panel'; +import { GitHubPanel } from '../../overview/github-panel'; export class MainModuleOverview extends Component { constructor(props) { @@ -191,6 +192,8 @@ const ModuleTabViewer = compose( {section === 'sca' && selectView === 'inventory' && <MainSca {...props} />} {section === 'vuls' && selectView === 'inventory' && <MainVuls {...props} />} {section === 'office' && selectView === 'inventory' && <OfficePanel {...props} />} + {section === 'github' && selectView === 'inventory' && <GitHubPanel {...props} />} + {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index d5d97ee794..73ca046a8e 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -77,7 +77,7 @@ export const ModulesDefaults = { }, github: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Audit logs' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], + tabs: [{ id: 'inventory', name: 'Panel' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], buttons: ['reporting'] }, syscollector: { diff --git a/public/components/overview/github-panel/components/vis-card.tsx b/public/components/overview/github-panel/components/vis-card.tsx new file mode 100644 index 0000000000..2f037b953d --- /dev/null +++ b/public/components/overview/github-panel/components/vis-card.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import { RawVisualizations } from '../../../../factories/raw-visualizations'; +import KibanaVis from '../../../../kibana-integrations/kibana-vis'; + +export const VisCard = ({ changeView = () => { }, id, width, tab, ...props }) => { + + const [expandedVis, setExpandedVis] = useState(false); + + const title = (() => { + const visList = new RawVisualizations().getList(); + const rawVis = visList ? visList.filter((item) => item && item.id === id) : []; + return rawVis.length && rawVis[0]?.attributes?.title; + })() + + const toggleExpand = id => { + setExpandedVis(expandedVis === id ? false : id); + }; + + return <> <EuiFlexItem grow={width}> + <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> + <EuiFlexGroup direction={'column'} className={'h-100'}> + <EuiFlexItem grow={false}> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem>{title && <EuiTitle size={'xxs'}><h4>{title}</h4></EuiTitle>}</EuiFlexItem> + <EuiFlexItem grow={false} > + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => toggleExpand(id)} + iconType="expand" + aria-label="Expand" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <div className={'h-100'}><KibanaVis visID={id} tab={tab} onRowClick={() => changeView('drilldown')} {...props} /></div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + </> +} \ No newline at end of file diff --git a/public/components/overview/github-panel/config/drilldown-config.tsx b/public/components/overview/github-panel/config/drilldown-config.tsx new file mode 100644 index 0000000000..9a02539d12 --- /dev/null +++ b/public/components/overview/github-panel/config/drilldown-config.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { VisCard } from '../components/vis-card'; + + +export const DrilldownConfig = { + rows: [ + { + height: 230, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + }, + { + width: 40, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> + }, + ] + }, + ] +}; diff --git a/public/components/overview/github-panel/config/index.ts b/public/components/overview/github-panel/config/index.ts new file mode 100644 index 0000000000..9628da5002 --- /dev/null +++ b/public/components/overview/github-panel/config/index.ts @@ -0,0 +1,4 @@ +export { DrilldownConfig } from './drilldown-config'; +export { MainViewConfig } from './main-view-config'; +export { ModuleConfig } from './module-config'; +export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/github-panel/config/main-view-config.tsx b/public/components/overview/github-panel/config/main-view-config.tsx new file mode 100644 index 0000000000..22398876ba --- /dev/null +++ b/public/components/overview/github-panel/config/main-view-config.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { VisCard } from '../components/vis-card'; + + +export const MainViewConfig = { + rows: [ + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <div ><button onClick={() => props.onRowClick('drilldown')}>change view</button></div> + }, + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-User-By-Operation-Result' tab='office' title='' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + }, + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Rule-Level-Histogram' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> + }, + ] + }, + ] +}; \ No newline at end of file diff --git a/public/components/overview/github-panel/config/module-config.tsx b/public/components/overview/github-panel/config/module-config.tsx new file mode 100644 index 0000000000..07b5926697 --- /dev/null +++ b/public/components/overview/github-panel/config/module-config.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { OfficeBody, OfficeDrilldown } from '../views'; +import { MainViewConfig, DrilldownConfig } from './'; + + +export const ModuleConfig = { + main: { + length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} /> + }, + drilldown: { + length: () => DrilldownConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeDrilldown {...{ ...DrilldownConfig, ...props }} /> + } +}; diff --git a/public/components/overview/github-panel/config/search-bar-config.ts b/public/components/overview/github-panel/config/search-bar-config.ts new file mode 100644 index 0000000000..3c1de9e8c2 --- /dev/null +++ b/public/components/overview/github-panel/config/search-bar-config.ts @@ -0,0 +1,70 @@ +export const filtersValues = [ + { + type: 'combobox', + key: 'agent.id', + values:[ + { + key:'agent.id', + label: '001', + + }, + { + key:'agent.id', + label: '002', + }, + { + key:'agent.id', + label: '003', + }, + { + key:'agent.id', + label: '004', + }, + { + key:'agent.id', + label: '006', + } + ] + }, + { + type: 'combobox', + key: 'agent.name', + values:[ + { + key:'agent.name', + label: 'Amazon', + + }, + { + key:'agent.name', + label: 'Centos', + }, + { + key:'agent.name', + label: '003', + }, + { + key:'agent.name', + label: '004', + }, + { + key:'agent.name', + label: '006', + } + ] + }, + { + type: 'combobox', + key: 'agent.ip', + values:[ + { + key:'agent.ip', + label: '24.273.97.14', + }, + { + key:'agent.ip', + label: '197.17.1.4', + }, + ] + } +] \ No newline at end of file diff --git a/public/components/overview/github-panel/github-panel.tsx b/public/components/overview/github-panel/github-panel.tsx new file mode 100644 index 0000000000..f86e73a067 --- /dev/null +++ b/public/components/overview/github-panel/github-panel.tsx @@ -0,0 +1,54 @@ +/* + * Wazuh app - Office 365 Panel react component. + * + * Copyright (C) 2015-2021 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, { useEffect, useState } from 'react'; +import { MainPanel } from '../../common/modules/panel'; +import { withErrorBoundary } from '../../common/hocs'; +import { CustomSearchBar } from '../../common/custom-search-bar'; +import { OfficeStats } from './views'; +import { queryConfig } from '../../../react-services/query-config'; +import { ModuleConfig, filtersValues } from './config'; + +export const GitHubPanel = withErrorBoundary(() => { + + const [moduleStatsList, setModuleStatsList] = useState([]); + + /** Get Office 365 Side Panel Module info **/ + useEffect(() => { + (async () => { + try { + const modulesConfig = await queryConfig( + '000', + [{ component: 'wmodules', configuration: 'wmodules' }] + ); + const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules + .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { //<-- change module name + const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; + return { title: configProp[0], description } + }) + setModuleStatsList(config); + } catch (error) { + setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); + } + } + )(); + }, []) + + return ( + <> + <CustomSearchBar filtersValues={filtersValues} /> + <MainPanel moduleConfig={ModuleConfig} tab={'office'} + sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> + </> + ) +}); diff --git a/public/components/overview/github-panel/index.tsx b/public/components/overview/github-panel/index.tsx new file mode 100644 index 0000000000..3d8eea8e58 --- /dev/null +++ b/public/components/overview/github-panel/index.tsx @@ -0,0 +1,14 @@ +/* + * Wazuh app - Office 365 Panel index. + * + * Copyright (C) 2015-2021 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 * from './github-panel'; \ No newline at end of file diff --git a/public/components/overview/github-panel/views/index.ts b/public/components/overview/github-panel/views/index.ts new file mode 100644 index 0000000000..39563eccfe --- /dev/null +++ b/public/components/overview/github-panel/views/index.ts @@ -0,0 +1,3 @@ +export { OfficeStats } from './office-stats' +export { OfficeBody } from './office-body' +export { OfficeDrilldown } from './office-drilldown' \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-body.tsx b/public/components/overview/github-panel/views/office-body.tsx new file mode 100644 index 0000000000..fe3d41889c --- /dev/null +++ b/public/components/overview/github-panel/views/office-body.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { EuiFlexGroup} from '@elastic/eui'; + +export const OfficeBody = ({ changeView, rows = [] }) => { + + return <> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ + height: row.height || (150 + 'px') + }}> + { + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + return <column.component width={growthFactor} key={key} onRowClick={() => changeView('drilldown')} /> + }) + } + </EuiFlexGroup> + }) + } + </> +} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-drilldown.tsx b/public/components/overview/github-panel/views/office-drilldown.tsx new file mode 100644 index 0000000000..9ab3eff7b7 --- /dev/null +++ b/public/components/overview/github-panel/views/office-drilldown.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { + + return <> + <EuiFlexGroup className={'wz-margin-0'}> + <EuiFlexItem grow={false}> + <div><EuiButtonEmpty onClick={() => changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="s"> + <h3>User Activity</h3> + </EuiTitle> + <p>wazuh@wazuh.com</p> + </EuiFlexItem> + </EuiFlexGroup> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ + height: row.height || (150 + 'px') + }}> + { + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + return <column.component key={key} width={growthFactor} onRowClick={() => changeView('main')} /> + }) + } + </EuiFlexGroup> + }) + } + <EuiFlexGroup className={'wz-margin-0'}> + <EuiFlexItem> + <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> +} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-stats.tsx b/public/components/overview/github-panel/views/office-stats.tsx new file mode 100644 index 0000000000..a7b2f07b16 --- /dev/null +++ b/public/components/overview/github-panel/views/office-stats.tsx @@ -0,0 +1,28 @@ +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import moduleLogo from '../../../../assets/office365.svg'; +import React from 'react'; + + +export const OfficeStats = ({ listItems = [] }) => { + const logoStyle = { width: 30 }; + return ( + <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem> + <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size={"xs"}><h4>Office 365</h4></EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiDescriptionList + listItems={listItems} + compressed + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; From fa711f8e2544eaa479bfdc50cd8ff08ab2061f1c Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 27 Jul 2021 13:50:51 +0200 Subject: [PATCH 144/493] fix kbn custom search bar --- .../custom-search-bar/custom-search-bar.tsx | 98 ++++++------- .../office-panel/config/search-bar-config.ts | 133 +++++++++--------- 2 files changed, 116 insertions(+), 115 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 4921bee99a..64afa64256 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { getIndexPattern } from '../../overview/mitre/lib' import { Filter } from '../../../../../../src/plugins/data/public/'; @@ -9,7 +9,6 @@ import { EuiFlexItem, EuiComboBox, EuiSwitch, - EuiSpacer, } from '@elastic/eui'; //@ts-ignore @@ -17,29 +16,34 @@ import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; - - export const CustomSearchBar = ({ ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; const timefilter = KibanaServices.timefilter.timefilter; + const indexPattern = getIndexPattern(); const [filterParams, setFilterParams] = useState({ filters: filterManager.getFilters() || [], query: { language: 'kuery', query: '' }, time: timefilter.getTime(), }); + const defaultSelectedOptions = () => { + const array = [] + props.filtersValues.forEach(item =>{ + array[item.key] = [] + }) + + return array + } const [isLoading, setLoading] = useState(false); const [customFilters, setCustomFilters] = useState(props.filtersValues) const [avancedFiltersState, setAvancedFiltersState] = useState(false); - const [selectedOptions, setSelectedOptions] = useState([]); - - const [defaultFilters, setDefaultFilters] = useState(filterManager.getFilters()); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + const [currentSelectName, setCurrentSelectName] = useState(''); const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; const filters = { query, time: dateRange, filters: filterParams.filters }; - setLoading(true); setFilterParams(filters); } @@ -47,20 +51,8 @@ export const CustomSearchBar = ({ ...props }) => { const onFiltersUpdated = (filters: Filter[]) => { const { query, time } = filterParams; const updatedFilterParams = { query, time, filters }; - setLoading(true); - refreshCustomSelectedFilter() setFilterParams(updatedFilterParams); - } - - const getCustomFilters = (filters) => { - const deleteDefaultFilters = [] - filters.forEach(filter => { - if(!defaultFilters.some(item => item.meta.key === filter.meta.key)){ - deleteDefaultFilters.push(filter) - } - }) - - return deleteDefaultFilters + refreshCustomSelectedFilter() } const changeSwitch = () => { @@ -91,55 +83,63 @@ export const CustomSearchBar = ({ ...props }) => { return { meta, $state, query }; }; - const setKibanaFilters = async (values) => { - const indexPattern = await getIndexPattern() + const setKibanaFilters = (values) => { + setLoading(true) const newFilters = [] - if(!values.length){ - filterManager.removeAll() - filterManager.addFilters(defaultFilters) + if(!values.length){ + const currentFilters = filterManager.getFilters().filter(item => item.meta.key != currentSelectName) + filterManager.removeAll() + filterManager.addFilters(currentFilters) + }else{ + const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].key) filterManager.removeAll() - newFilters.push(defaultFilters); + filterManager.addFilters(currentFilters) values.forEach(element => { const customFilter = buildCustomFilter(false,indexPattern.title,element.label,element.key) newFilters.push(customFilter); }); filterManager.addFilters(newFilters) - } + } } const refreshCustomSelectedFilter = () => { + setSelectedOptions(defaultSelectedOptions) const filters = filterManager.getFilters() - const customFilters = getCustomFilters(filters) - const filtersUpdated = [] - customFilters.forEach(item => { - const filterObj = { - key: item.meta.key, - label: item.meta.params.query, - } - filtersUpdated.push(filterObj) - - }) - setSelectedOptions(filtersUpdated) - console.log(customFilters) - + const filterCustom = filters.map(item => { + return { + key: item.meta.key, + label: item.meta.params.query, + } + }).filter(element => Object.keys(selectedOptions).includes(element.key)) + + if(filterCustom.length != 0){ + filterCustom.forEach(item => { + setSelectedOptions(prevState => ({ + ...prevState, + [item.key]: [...prevState[item.key],item], + })); + + }) + } + setLoading(false) }; - const onChange = async(values) => { - await setKibanaFilters(values) - refreshCustomSelectedFilter(); + const onChange = (values) => { + setKibanaFilters(values) + refreshCustomSelectedFilter(); }; - const getComponent = (item) => { var types = { 'default': <></>, 'combobox': <EuiComboBox placeholder={"Select "+item.key} options={item.values} - selectedOptions={selectedOptions} + selectedOptions={selectedOptions[item.key] || []} onChange={onChange} - isClearable={true} + onClick={() => setCurrentSelectName(item.key)} + isClearable={false} /> }; return types[item.type] || types['default']; @@ -152,7 +152,7 @@ export const CustomSearchBar = ({ ...props }) => { avancedFiltersState === false ? customFilters.map((item, key) => ( <EuiFlexItem grow={2} key={key}> - {getComponent(item)} + {getComponent(item)} </EuiFlexItem> )) : @@ -164,7 +164,7 @@ export const CustomSearchBar = ({ ...props }) => { showQueryInput={avancedFiltersState} onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} - isLoading={false} + isLoading={isLoading} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 3c1de9e8c2..351d606598 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -1,70 +1,71 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + export const filtersValues = [ - { - type: 'combobox', + { + type: 'combobox', + key: 'agent.id', + values: [ + { key: 'agent.id', - values:[ - { - key:'agent.id', - label: '001', - - }, - { - key:'agent.id', - label: '002', - }, - { - key:'agent.id', - label: '003', - }, - { - key:'agent.id', - label: '004', - }, - { - key:'agent.id', - label: '006', - } - ] - }, - { - type: 'combobox', + label: '001', + }, + { + key: 'agent.id', + label: '002', + }, + { + key: 'agent.id', + label: '003', + }, + { + key: 'agent.id', + label: '004', + }, + { + key: 'agent.id', + label: '006', + }, + ], + }, + { + type: 'combobox', + key: 'agent.name', + values: [ + { + key: 'agent.name', + label: 'Amazon', + }, + { key: 'agent.name', - values:[ - { - key:'agent.name', - label: 'Amazon', - - }, - { - key:'agent.name', - label: 'Centos', - }, - { - key:'agent.name', - label: '003', - }, - { - key:'agent.name', - label: '004', - }, - { - key:'agent.name', - label: '006', - } - ] - }, - { - type: 'combobox', + label: 'Centos', + } + ], + }, + { + type: 'combobox', + key: 'agent.ip', + values: [ + { + key: 'agent.ip', + label: '24.273.97.14', + }, + { key: 'agent.ip', - values:[ - { - key:'agent.ip', - label: '24.273.97.14', - }, - { - key:'agent.ip', - label: '197.17.1.4', - }, - ] - } -] \ No newline at end of file + label: '197.17.1.4', + }, + ], + }, +]; From e7fb8e9d2f176f01aac07ab5d742e84d00ce9b91 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 27 Jul 2021 11:04:03 -0300 Subject: [PATCH 145/493] feat(useEsSearch): Applied TS on hook, added errorOrchestrator to handler errors and applied prettier. --- .../components/common/hooks/use-es-search.ts | 81 +++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 06d69f3274..ec68258090 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -1,37 +1,73 @@ -import { useState, useEffect } from 'react'; +import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useQuery, useIndexPattern, useFilterManager } from '.'; +import { useFilterManager, useIndexPattern, useQuery } from '.'; import _ from 'lodash'; import { Filter, IndexPattern } from 'src/plugins/data/public'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { Dispatch } from 'x-pack/node_modules/@types/react'; /* -You can find more info on how to use the preAppliedAggs object at https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html +You can find more info on how to use the preAppliedAggs object at + https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html You can find more info on how to construct a filter object at https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html */ -const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) => { + +interface IEsResults { + aggregations: { buckets: [buckets: any] }; +} +interface IUseEsSearch { + esResults: IEsResults; + isLoading: boolean; + error: Error | undefined; + setPage: Dispatch<SetStateAction<number>>; + nextPage: () => void; + prevPage: () => void; +} + +const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }): IUseEsSearch => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); const filterManager = useFilterManager(); - const [esResults, setEsResults] = useState({}); - const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [page, setPage] = useState(0); const [query] = useQuery(); + const [esResults, setEsResults] = useState<IEsResults>({ + aggregations: { buckets: [{}] }, + }); + const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); + const [error, setError] = useState<Error>(); + const [isLoading, setIsLoading] = useState<boolean>(true); + const [page, setPage] = useState<number>(0); + useEffect(() => { setIsLoading(true); - search() - .then((result) => { - setEsResults(result); - setError(null); - }) - .catch((error)=>{ + (async function () { + try { + setEsResults(await search()); + } catch (error) { setError(error); - }) - .finally(() => { + const options: UIErrorLog = { + context: `${useEsSearch.name}.search`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } finally { setIsLoading(false); - }); + } + })(); }, [indexPattern, query, managedFilters, page]); + useEffect(() => { let filterSubscriber = filterManager.getUpdates$().subscribe(() => { const newFilters = filterManager.getFilters(); @@ -44,12 +80,12 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) }); }, []); - const search = async () => { + const search = async (): Promise<IEsResults> => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; - const results = await searchSource + return await searchSource .setParent(undefined) .setField('filter', combined) .setField('query', query) @@ -58,9 +94,8 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) .setField('from', page * size) .setField('index', indexPattern as IndexPattern) .fetch(); - return results; } else { - return {}; + return { aggregations: { buckets: [null] } }; } }; @@ -71,7 +106,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setPage(page - 1 < 0 ? 0 : page - 1); }; - return {esResults, isLoading, error, setPage, nextPage, prevPage}; + return { esResults, isLoading, error, setPage, nextPage, prevPage }; }; export { useEsSearch }; From 2de66ea2b05e1b0966d2381b635fd286fd74d783 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 27 Jul 2021 11:04:37 -0300 Subject: [PATCH 146/493] feat(useEsSearch): Added copyright --- public/components/common/hooks/use-es-search.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index ec68258090..df675bb4ba 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -1,3 +1,15 @@ +/* + * Wazuh app - React hook for search ElasticSearch DB. + * Copyright (C) 2015-2021 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 { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useFilterManager, useIndexPattern, useQuery } from '.'; From 6f6b7ce8ba46f6c8724daaff4e088dbcc4dafddc Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 27 Jul 2021 18:16:06 +0200 Subject: [PATCH 147/493] fix search bar filters and add test --- .../custom-search-bar.test.tsx | 41 +++++++++++++++++++ .../custom-search-bar/custom-search-bar.tsx | 28 +++++++++++-- public/styles/common.scss | 10 +++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 public/components/common/custom-search-bar/custom-search-bar.test.tsx diff --git a/public/components/common/custom-search-bar/custom-search-bar.test.tsx b/public/components/common/custom-search-bar/custom-search-bar.test.tsx new file mode 100644 index 0000000000..cdb4574d33 --- /dev/null +++ b/public/components/common/custom-search-bar/custom-search-bar.test.tsx @@ -0,0 +1,41 @@ + +/* + * Wazuh app - Custom Search Bar Component - Test + * + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { CustomSearchBar } from '../custom-search-bar'; + +describe('Search Bar container', () => { + const mock = [{ + type: 'combobox', + key: 'agent.name', + values: [ + { + key: 'agent.name', + label: 'Amazon', + }, + { + key: 'agent.name', + label: 'Centos', + } + ], + },] + + test('should render the component', () => { + const component = shallow(<CustomSearchBar filtersValues={mock}/>); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index fca6581b49..3ec6223cd9 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -39,9 +39,19 @@ export const CustomSearchBar = ({ ...props }) => { const [isLoading, setLoading] = useState(false); const [customFilters, setCustomFilters] = useState(props.filtersValues) const [currentSelectName, setCurrentSelectName] = useState(''); - const [avancedFiltersState, setAvancedFiltersState] = useState(true); - const [selectedOptions, setSelectedOptions] = useState([]); + const [avancedFiltersState, setAvancedFiltersState] = useState(false); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const newFilters = filterManager.getFilters(); + onFiltersUpdated(newFilters) + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }, []); + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; const filters = { query, time: dateRange, filters: filterParams.filters }; @@ -141,7 +151,8 @@ export const CustomSearchBar = ({ ...props }) => { var types = { 'default': <></>, 'combobox': <EuiComboBox - placeholder={"Select "+item.key} + className={'filters-custom-combobox'} + placeholder={'Select '+item.key} options={item.values} selectedOptions={selectedOptions[item.key] || []} onChange={onChange} @@ -168,7 +179,7 @@ export const CustomSearchBar = ({ ...props }) => { } <EuiFlexItem> <KbnSearchBar - showFilterBar={avancedFiltersState} + showFilterBar={false} showQueryInput={avancedFiltersState} onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} @@ -177,6 +188,15 @@ export const CustomSearchBar = ({ ...props }) => { </EuiFlexItem> </EuiFlexGroup> <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> + <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> + <KbnSearchBar + showDatePicker={false} + showQueryInput={false} + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} + /> + </EuiFlexItem> <EuiFlexItem grow={false}> <EuiSwitch label="Advanced filters" diff --git a/public/styles/common.scss b/public/styles/common.scss index d6ce5e9edc..80155fb8b6 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1740,4 +1740,14 @@ iframe.width-changed { .wz-width-100{ width: 100%; +} + +/* Custom Searchbar styles */ + +.filters-custom-combobox .euiBadge__iconButton{ + display: none; +} + +.application .filters-search-bar .globalQueryBar, .app-container .filters-search-bar .globalQueryBar{ + padding: 0 !important; } \ No newline at end of file From 30aa68fd68db5883c44d47274bd3b1156968fe05 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 27 Jul 2021 14:16:54 -0300 Subject: [PATCH 148/493] feat(useEsSearch): Changed interface of search return --- public/components/common/hooks/use-es-search.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index df675bb4ba..ef7caf85a0 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -24,6 +24,7 @@ import { import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; import { Dispatch } from 'x-pack/node_modules/@types/react'; +import { SearchResponse } from 'src/core/server'; /* You can find more info on how to use the preAppliedAggs object at @@ -31,11 +32,8 @@ You can find more info on how to use the preAppliedAggs object at You can find more info on how to construct a filter object at https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html */ -interface IEsResults { - aggregations: { buckets: [buckets: any] }; -} interface IUseEsSearch { - esResults: IEsResults; + esResults: SearchResponse; isLoading: boolean; error: Error | undefined; setPage: Dispatch<SetStateAction<number>>; @@ -48,11 +46,9 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) const indexPattern = useIndexPattern(); const filterManager = useFilterManager(); const [query] = useQuery(); - const [esResults, setEsResults] = useState<IEsResults>({ - aggregations: { buckets: [{}] }, - }); + const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); - const [error, setError] = useState<Error>(); + const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); const [page, setPage] = useState<number>(0); @@ -92,11 +88,12 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) }); }, []); - const search = async (): Promise<IEsResults> => { + const search = async (): Promise<SearchResponse> => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; + return await searchSource .setParent(undefined) .setField('filter', combined) @@ -107,7 +104,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) .setField('index', indexPattern as IndexPattern) .fetch(); } else { - return { aggregations: { buckets: [null] } }; + return {} as SearchResponse; } }; From a47a40b0614eb28d71ad1640ea1bd204859fe550 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 27 Jul 2021 19:54:58 +0200 Subject: [PATCH 149/493] Added Panel row selection + VisConfigLayout --- .../common/modules/panel/components/index.ts | 4 +- .../modules/panel}/components/vis-card.tsx | 4 +- .../panel/components/vis-config-layout.tsx | 22 ++++++++ .../common/modules/panel/main-panel.tsx | 56 ++++++++++++++++++- .../office-panel/config/drilldown-config.tsx | 14 ++++- .../office-panel/config/main-view-config.tsx | 23 +------- .../overview/office-panel/office-panel.tsx | 2 +- .../office-panel/views/office-body.tsx | 26 +++------ .../office-panel/views/office-drilldown.tsx | 37 +++++------- 9 files changed, 118 insertions(+), 70 deletions(-) rename public/components/{overview/office-panel => common/modules/panel}/components/vis-card.tsx (91%) create mode 100644 public/components/common/modules/panel/components/vis-config-layout.tsx diff --git a/public/components/common/modules/panel/components/index.ts b/public/components/common/modules/panel/components/index.ts index b41f7f0cd2..1ae8ddbdc6 100644 --- a/public/components/common/modules/panel/components/index.ts +++ b/public/components/common/modules/panel/components/index.ts @@ -1,2 +1,4 @@ export { ModuleSidePanel } from './module-side-panel'; -export { AggTable } from './agg-table'; \ No newline at end of file +export { AggTable } from './agg-table'; +export { VisCard } from './vis-card'; +export { VisConfigLayout } from './vis-config-layout'; \ No newline at end of file diff --git a/public/components/overview/office-panel/components/vis-card.tsx b/public/components/common/modules/panel/components/vis-card.tsx similarity index 91% rename from public/components/overview/office-panel/components/vis-card.tsx rename to public/components/common/modules/panel/components/vis-card.tsx index 2f037b953d..05b9a949d4 100644 --- a/public/components/overview/office-panel/components/vis-card.tsx +++ b/public/components/common/modules/panel/components/vis-card.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; -import { RawVisualizations } from '../../../../factories/raw-visualizations'; -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; +import { RawVisualizations } from '../../../../../factories/raw-visualizations'; +import KibanaVis from '../../../../../kibana-integrations/kibana-vis'; export const VisCard = ({ changeView = () => { }, id, width, tab, ...props }) => { diff --git a/public/components/common/modules/panel/components/vis-config-layout.tsx b/public/components/common/modules/panel/components/vis-config-layout.tsx new file mode 100644 index 0000000000..3220a96f48 --- /dev/null +++ b/public/components/common/modules/panel/components/vis-config-layout.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; + +export const VisConfigLayout = ({ rowClickHandler, rows = [] }) => { + + return <> + { + rows.map((row, key) => { + return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ + height: row.height || (150 + 'px') + }}> + { + row.columns.map((column, key) => { + const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); + return <column.component width={growthFactor} key={key} onRowClick={rowClickHandler} /> + }) + } + </EuiFlexGroup> + }) + } + </> +} diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index b161d54b21..ea4919815c 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -8,13 +8,18 @@ import { ModuleSidePanel } from './components/'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; import { VisFactoryHandler } from '../../../../react-services/vis-factory-handler'; import { AppState } from '../../../../react-services/app-state'; +import { useFilterManager } from '../../hooks'; import { FilterHandler } from '../../../../utils/filter-handler'; import { TabVisualizations } from '../../../../factories/tab-visualizations'; +import { Filter } from '../../../../../../../src/plugins/data/public/'; +import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../../src/plugins/data/common'; export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { const [viewId, setViewId] = useState('main'); + const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); + const filterManager = useFilterManager(); useEffect(() => { (async () => { @@ -29,12 +34,61 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { })() }, [viewId]) + /** + * When a filter is toggled applies de selection + */ + useEffect(() => { + const appliedFilters = filterManager.getAppFilters(); + + const filters = appliedFilters.filter((filter) => { + return filter.meta.key != selectedFilter.field; + }); + if (selectedFilter.value) { + const customFilter = buildCustomFilter(selectedFilter); + filters.push(customFilter); + } + filterManager.setFilters(filters); + }, [selectedFilter]) + + + /** + * Builds selected filter structure + * @param value + * @param field + */ + const buildCustomFilter = ({ field, value }): Filter => { + const meta: FilterMeta = { + disabled: false, + negate: false, + key: field, + params: { query: value }, + alias: null, + type: "phrase", + index: AppState.getCurrentPattern(), + }; + const $state: FilterState = { + store: FilterStateStore.APP_STATE, + }; + const query = { + match_phrase: { + [field]: { + query: value + } + } + } + + return { meta, $state, query }; + } const toggleView = (id = 'main') => { if (id != viewId) setViewId(id); } + const toggleFilter = (field = '', value = '') => { + setSelectedFilter({ field, value }); + } + /** * Builds active view * @param props @@ -42,7 +96,7 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { */ const ModuleContent = useCallback(() => { const View = moduleConfig[viewId].component; - return <WzReduxProvider><View changeView={toggleView}/></WzReduxProvider> + return <WzReduxProvider><View selectedFilter={selectedFilter} toggleFilter={toggleFilter} changeView={toggleView} /></WzReduxProvider> }, [viewId]) return ( diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx index 9a02539d12..3cb6a7ad8a 100644 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-config.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { VisCard } from '../components/vis-card'; - +import { VisCard } from '../../../common/modules/panel/'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; export const DrilldownConfig = { rows: [ @@ -30,5 +31,14 @@ export const DrilldownConfig = { }, ] }, + { + height: 300, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, ] }; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 2b6048fb04..75fc45f17f 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,25 +1,6 @@ import React from 'react'; -import { VisCard } from '../components/vis-card'; -import { AggTable } from '../../../common/modules/panel'; +import { VisCard, AggTable } from '../../../common/modules/panel/'; -// AggTable = ({ -// onRowClick = (field, value) => {}, -// aggTerm, -// aggLabel, -// maxRows, -// tableTitle, -// panelProps, -// titleProps -// } -// { -// + flexProps: { style: { minWidth: 500 }, grow:6 }, -// + tableProps: { -// + aggTerm: 'agent.name', -// + aggLabel: 'Agent Name', -// + maxRows: '10', -// + tableTitle: 'Agents by name', -// + }, -// + }, export const MainViewConfig = { rows: [ { @@ -33,7 +14,7 @@ export const MainViewConfig = { aggTerm={'data.office365.UserId'} aggLabel={'User'} maxRows={'7'} - onRowClick={(field, value) => props.onRowClick('drilldown')} />) + onRowClick={(field, value) => props.onRowClick(field, value)} />) }, { width: 50, diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 8087941d0b..ba5783166d 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -32,7 +32,7 @@ export const OfficePanel = withErrorBoundary(() => { [{ component: 'wmodules', configuration: 'wmodules' }] ); const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules - .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { //<-- change module name + .filter((module) => { return Object.keys(module)[0] == 'office365' })[0]['office365']).map((configProp) => { const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; return { title: configProp[0], description } }) diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index fe3d41889c..7610a180ee 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,22 +1,12 @@ import React from 'react'; -import { EuiFlexGroup} from '@elastic/eui'; +import { VisConfigLayout } from '../../../common/modules/panel'; -export const OfficeBody = ({ changeView, rows = [] }) => { +export const OfficeBody = ({ changeView, toggleFilter, rows = [] }) => { - return <> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || (150 + 'px') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - return <column.component width={growthFactor} key={key} onRowClick={() => changeView('drilldown')} /> - }) - } - </EuiFlexGroup> - }) - } - </> + const rowClickHandler = (field, value) => { + toggleFilter(field, value); + changeView('drilldown'); + } + + return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler}/> } \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index 9ab3eff7b7..881423de09 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -1,39 +1,28 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle, EuiPanel } from '@elastic/eui'; -import { SecurityAlerts } from '../../../visualize/components'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import { VisConfigLayout } from '../../../common/modules/panel/components'; -export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { + +export const OfficeDrilldown = ({ changeView, toggleFilter, rows = [], selectedFilter = { field: '', value: '' } }) => { + + const rowClickHandler = () => { + toggleFilter(selectedFilter.field); + changeView('main'); + } return <> <EuiFlexGroup className={'wz-margin-0'}> <EuiFlexItem grow={false}> - <div><EuiButtonEmpty onClick={() => changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> + <div><EuiButtonEmpty onClick={() => rowClickHandler()} iconType={"sortLeft"}></EuiButtonEmpty></div> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiTitle size="s"> <h3>User Activity</h3> </EuiTitle> - <p>wazuh@wazuh.com</p> - </EuiFlexItem> - </EuiFlexGroup> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || (150 + 'px') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - return <column.component key={key} width={growthFactor} onRowClick={() => changeView('main')} /> - }) - } - </EuiFlexGroup> - }) - } - <EuiFlexGroup className={'wz-margin-0'}> - <EuiFlexItem> - <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> + <p>{selectedFilter.value}</p> </EuiFlexItem> </EuiFlexGroup> + <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler} /> + </> } \ No newline at end of file From 26d8a0e1091ccb9ce39617ff9faf11e217d62af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 28 Jul 2021 10:15:09 +0200 Subject: [PATCH 150/493] fix(frontend): Add tables to GitHub Panel - main view - Adapted Panel to GitHub module --- .../github-panel/components/vis-card.tsx | 44 ----------- .../github-panel/config/drilldown-config.tsx | 34 -------- .../github-panel/config/drilldown.tsx | 52 +++++++++++++ .../overview/github-panel/config/index.ts | 4 +- .../github-panel/config/main-view-config.tsx | 43 ---------- .../overview/github-panel/config/main.tsx | 78 +++++++++++++++++++ .../github-panel/config/module-config.tsx | 18 ++++- .../overview/github-panel/github-panel.tsx | 6 +- .../overview/github-panel/views/drilldown.tsx | 38 +++++++++ .../overview/github-panel/views/index.ts | 6 +- .../overview/github-panel/views/main.tsx | 25 ++++++ .../github-panel/views/office-body.tsx | 22 ------ .../github-panel/views/office-drilldown.tsx | 39 ---------- .../views/{office-stats.tsx => stats.tsx} | 22 ++++-- 14 files changed, 231 insertions(+), 200 deletions(-) delete mode 100644 public/components/overview/github-panel/components/vis-card.tsx delete mode 100644 public/components/overview/github-panel/config/drilldown-config.tsx create mode 100644 public/components/overview/github-panel/config/drilldown.tsx delete mode 100644 public/components/overview/github-panel/config/main-view-config.tsx create mode 100644 public/components/overview/github-panel/config/main.tsx create mode 100644 public/components/overview/github-panel/views/drilldown.tsx create mode 100644 public/components/overview/github-panel/views/main.tsx delete mode 100644 public/components/overview/github-panel/views/office-body.tsx delete mode 100644 public/components/overview/github-panel/views/office-drilldown.tsx rename public/components/overview/github-panel/views/{office-stats.tsx => stats.tsx} (52%) diff --git a/public/components/overview/github-panel/components/vis-card.tsx b/public/components/overview/github-panel/components/vis-card.tsx deleted file mode 100644 index 2f037b953d..0000000000 --- a/public/components/overview/github-panel/components/vis-card.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; -import { RawVisualizations } from '../../../../factories/raw-visualizations'; -import KibanaVis from '../../../../kibana-integrations/kibana-vis'; - -export const VisCard = ({ changeView = () => { }, id, width, tab, ...props }) => { - - const [expandedVis, setExpandedVis] = useState(false); - - const title = (() => { - const visList = new RawVisualizations().getList(); - const rawVis = visList ? visList.filter((item) => item && item.id === id) : []; - return rawVis.length && rawVis[0]?.attributes?.title; - })() - - const toggleExpand = id => { - setExpandedVis(expandedVis === id ? false : id); - }; - - return <> <EuiFlexItem grow={width}> - <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> - <EuiFlexGroup direction={'column'} className={'h-100'}> - <EuiFlexItem grow={false}> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem>{title && <EuiTitle size={'xxs'}><h4>{title}</h4></EuiTitle>}</EuiFlexItem> - <EuiFlexItem grow={false} > - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => toggleExpand(id)} - iconType="expand" - aria-label="Expand" - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <div className={'h-100'}><KibanaVis visID={id} tab={tab} onRowClick={() => changeView('drilldown')} {...props} /></div> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </EuiFlexItem> - </> -} \ No newline at end of file diff --git a/public/components/overview/github-panel/config/drilldown-config.tsx b/public/components/overview/github-panel/config/drilldown-config.tsx deleted file mode 100644 index 9a02539d12..0000000000 --- a/public/components/overview/github-panel/config/drilldown-config.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { VisCard } from '../components/vis-card'; - - -export const DrilldownConfig = { - rows: [ - { - height: 230, - columns: [ - { - width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> - }, - { - width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> - }, - { - width: 40, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 100, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> - }, - ] - }, - ] -}; diff --git a/public/components/overview/github-panel/config/drilldown.tsx b/public/components/overview/github-panel/config/drilldown.tsx new file mode 100644 index 0000000000..76f124199f --- /dev/null +++ b/public/components/overview/github-panel/config/drilldown.tsx @@ -0,0 +1,52 @@ +/* + * Wazuh app - GitHub Panel tab - Drilldown layout configuration + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel/'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const DrilldownConfig = { + rows: [ + { + height: 230, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + }, + { + width: 40, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + }, + ] + }, + { + columns: [ + { + width: 100, + component: (props) => ( + <EuiFlexGroup className={'wz-margin-0'}> + <EuiFlexItem> + <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + ) + }, + ] + }, + ] +}; diff --git a/public/components/overview/github-panel/config/index.ts b/public/components/overview/github-panel/config/index.ts index 9628da5002..b9ba3c2cee 100644 --- a/public/components/overview/github-panel/config/index.ts +++ b/public/components/overview/github-panel/config/index.ts @@ -1,4 +1,4 @@ -export { DrilldownConfig } from './drilldown-config'; -export { MainViewConfig } from './main-view-config'; +export { DrilldownConfig } from './drilldown'; +export { MainViewConfig } from './main'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/github-panel/config/main-view-config.tsx b/public/components/overview/github-panel/config/main-view-config.tsx deleted file mode 100644 index 22398876ba..0000000000 --- a/public/components/overview/github-panel/config/main-view-config.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { VisCard } from '../components/vis-card'; - - -export const MainViewConfig = { - rows: [ - { - height: 300, - columns: [ - { - width: 50, - component: (props) => <div ><button onClick={() => props.onRowClick('drilldown')}>change view</button></div> - }, - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-User-By-Operation-Result' tab='office' title='' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> - }, - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Rule-Level-Histogram' tab='office' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> - }, - ] - }, - ] -}; \ No newline at end of file diff --git a/public/components/overview/github-panel/config/main.tsx b/public/components/overview/github-panel/config/main.tsx new file mode 100644 index 0000000000..740a6c14fe --- /dev/null +++ b/public/components/overview/github-panel/config/main.tsx @@ -0,0 +1,78 @@ +/* + * Wazuh app - GitHub Panel tab - Main layout configuration + * Copyright (C) 2015-2021 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 from 'react'; +import { AggTable } from '../../../common/modules/panel'; +import { + EuiFlexItem +} from '@elastic/eui'; + +export const MainViewConfig = { + rows: [ + { + columns: [ + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.width}> + <AggTable + tableTitle='Actors' + aggTerm='data.github.actor' + aggLabel='Actor' + maxRows={5} + onRowClick={props.onRowClick} /> + </EuiFlexItem>) + }, + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.width}> + <AggTable + tableTitle='Organizations' + aggTerm='data.github.org' + aggLabel='Organization' + maxRows={5} + onRowClick={props.onRowClick} /> + </EuiFlexItem>) + } + ] + }, + { + columns: [ + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.width}> + <AggTable + tableTitle='Repositories' + aggTerm='data.github.repo' + aggLabel='Respository' + maxRows={5} + onRowClick={props.onRowClick} /> + </EuiFlexItem>) + }, + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.width}> + <AggTable + tableTitle='Actions' + aggTerm='data.github.action' + aggLabel='Action' + maxRows={5} + onRowClick={props.onRowClick} /> + </EuiFlexItem>) + } + ] + } + ] +}; \ No newline at end of file diff --git a/public/components/overview/github-panel/config/module-config.tsx b/public/components/overview/github-panel/config/module-config.tsx index 07b5926697..67b8fe68a0 100644 --- a/public/components/overview/github-panel/config/module-config.tsx +++ b/public/components/overview/github-panel/config/module-config.tsx @@ -1,15 +1,25 @@ +/* + * Wazuh app - GitHub Panel tab - Views configurations + * Copyright (C) 2015-2021 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 from 'react'; -import { OfficeBody, OfficeDrilldown } from '../views'; +import { Main, Drilldown } from '../views'; import { MainViewConfig, DrilldownConfig } from './'; - export const ModuleConfig = { main: { length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} /> + component: (props) => <Main {...{ ...MainViewConfig, ...props }} /> }, drilldown: { length: () => DrilldownConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeDrilldown {...{ ...DrilldownConfig, ...props }} /> + component: (props) => <Drilldown {...{ ...DrilldownConfig, ...props }} /> } }; diff --git a/public/components/overview/github-panel/github-panel.tsx b/public/components/overview/github-panel/github-panel.tsx index f86e73a067..092bf0559e 100644 --- a/public/components/overview/github-panel/github-panel.tsx +++ b/public/components/overview/github-panel/github-panel.tsx @@ -15,7 +15,7 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { CustomSearchBar } from '../../common/custom-search-bar'; -import { OfficeStats } from './views'; +import { Stats } from './views'; import { queryConfig } from '../../../react-services/query-config'; import { ModuleConfig, filtersValues } from './config'; @@ -47,8 +47,8 @@ export const GitHubPanel = withErrorBoundary(() => { return ( <> <CustomSearchBar filtersValues={filtersValues} /> - <MainPanel moduleConfig={ModuleConfig} tab={'office'} - sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> + <MainPanel moduleConfig={ModuleConfig} tab={'github'} + sidePanelChildren={<Stats listItems={moduleStatsList} />} /> </> ) }); diff --git a/public/components/overview/github-panel/views/drilldown.tsx b/public/components/overview/github-panel/views/drilldown.tsx new file mode 100644 index 0000000000..840ca592fb --- /dev/null +++ b/public/components/overview/github-panel/views/drilldown.tsx @@ -0,0 +1,38 @@ +/* + * Wazuh app - GitHub Panel tab - Drilldown layout + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import { VisConfigLayout } from '../../../common/modules/panel'; + +export const Drilldown = ({ changeView, rows = [], toggleFilter, selectedFilter = { field: '', value: '' }, ...props }) => { + + const rowClickHandler = () => { + toggleFilter(selectedFilter.field); + changeView('main'); + }; + + return <> + <EuiFlexGroup className='wz-margin-0'> + <EuiFlexItem grow={false}> + <div><EuiButtonEmpty onClick={() => rowClickHandler()} iconType='sortLeft'></EuiButtonEmpty></div> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size='s'> + <h3>User Activity</h3> + </EuiTitle> + <p>{selectedFilter.value}</p> + </EuiFlexItem> + </EuiFlexGroup> + <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler} /> + </> +} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/index.ts b/public/components/overview/github-panel/views/index.ts index 39563eccfe..a5d24c10b4 100644 --- a/public/components/overview/github-panel/views/index.ts +++ b/public/components/overview/github-panel/views/index.ts @@ -1,3 +1,3 @@ -export { OfficeStats } from './office-stats' -export { OfficeBody } from './office-body' -export { OfficeDrilldown } from './office-drilldown' \ No newline at end of file +export * from './stats'; +export * from './main'; +export * from './drilldown'; \ No newline at end of file diff --git a/public/components/overview/github-panel/views/main.tsx b/public/components/overview/github-panel/views/main.tsx new file mode 100644 index 0000000000..e3aaa4704d --- /dev/null +++ b/public/components/overview/github-panel/views/main.tsx @@ -0,0 +1,25 @@ +/* + * Wazuh app - GitHub Panel tab - Main layout + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiFlexGroup} from '@elastic/eui'; +import { VisConfigLayout } from '../../../common/modules/panel'; + +export const Main = ({ changeView, toggleFilter, rows = [] }) => { + + const rowClickHandler = (field, value) => { + toggleFilter(field, value); + changeView('drilldown'); + } + + return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler}/> +} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-body.tsx b/public/components/overview/github-panel/views/office-body.tsx deleted file mode 100644 index fe3d41889c..0000000000 --- a/public/components/overview/github-panel/views/office-body.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { EuiFlexGroup} from '@elastic/eui'; - -export const OfficeBody = ({ changeView, rows = [] }) => { - - return <> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || (150 + 'px') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - return <column.component width={growthFactor} key={key} onRowClick={() => changeView('drilldown')} /> - }) - } - </EuiFlexGroup> - }) - } - </> -} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-drilldown.tsx b/public/components/overview/github-panel/views/office-drilldown.tsx deleted file mode 100644 index 9ab3eff7b7..0000000000 --- a/public/components/overview/github-panel/views/office-drilldown.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle, EuiPanel } from '@elastic/eui'; -import { SecurityAlerts } from '../../../visualize/components'; - -export const OfficeDrilldown = ({ changeView, rows = [], ...props }) => { - - return <> - <EuiFlexGroup className={'wz-margin-0'}> - <EuiFlexItem grow={false}> - <div><EuiButtonEmpty onClick={() => changeView()} iconType={"sortLeft"}></EuiButtonEmpty></div> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiTitle size="s"> - <h3>User Activity</h3> - </EuiTitle> - <p>wazuh@wazuh.com</p> - </EuiFlexItem> - </EuiFlexGroup> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || (150 + 'px') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - return <column.component key={key} width={growthFactor} onRowClick={() => changeView('main')} /> - }) - } - </EuiFlexGroup> - }) - } - <EuiFlexGroup className={'wz-margin-0'}> - <EuiFlexItem> - <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - </> -} \ No newline at end of file diff --git a/public/components/overview/github-panel/views/office-stats.tsx b/public/components/overview/github-panel/views/stats.tsx similarity index 52% rename from public/components/overview/github-panel/views/office-stats.tsx rename to public/components/overview/github-panel/views/stats.tsx index a7b2f07b16..5e36cc6f93 100644 --- a/public/components/overview/github-panel/views/office-stats.tsx +++ b/public/components/overview/github-panel/views/stats.tsx @@ -1,19 +1,29 @@ -import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; -import moduleLogo from '../../../../assets/office365.svg'; -import React from 'react'; +/* + * Wazuh app - GitHub Panel tab - Stats + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiIcon } from '@elastic/eui'; -export const OfficeStats = ({ listItems = [] }) => { +export const Stats = ({ listItems = [] }) => { const logoStyle = { width: 30 }; return ( <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> <EuiFlexItem> <EuiFlexGroup> <EuiFlexItem> - <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> + <EuiIcon type='logoGithub' style={logoStyle}/> </EuiFlexItem> <EuiFlexItem> - <EuiTitle size={"xs"}><h4>Office 365</h4></EuiTitle> + <EuiTitle size={"xs"}><h4>GitHub</h4></EuiTitle> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> From eea9a0c3e85453d03394193482b379c1acbae3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 12:16:10 +0200 Subject: [PATCH 151/493] Created use-suggestions hook --- public/components/common/hooks/index.ts | 1 + .../common/hooks/use-value-suggestions.ts | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 public/components/common/hooks/use-value-suggestions.ts diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 270031ca7f..e6f5dca3bd 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,3 +24,4 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; +export { useValueSuggestions } from './use-value-suggestions'; diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts new file mode 100644 index 0000000000..5d70a086ec --- /dev/null +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react'; +import { getDataPlugin } from '../../../kibana-services'; +import { useIndexPattern } from '.'; +import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import React from 'react' + + +export interface IValueSuggestiions { + filterOptions: string[] | boolean[]; + isLoading: boolean; + setQuery: React.Dispatch<React.SetStateAction<string>> +} + +export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') => { + const [filterOptions, setFilterOptions] = useState<string[] | boolean[]>([]); + const [query, setQuery] = useState<string>(''); + const [isLoading, setIsLoading] = useState(true); + const data = getDataPlugin(); + const indexPattern = useIndexPattern(); + + useEffect(() => { + if (indexPattern) { + setIsLoading(true); + const field = { + type: type, + name: filterField, + aggregatable: true, + } as IFieldType; + data.autocomplete + .getValueSuggestions({ + query, + indexPattern: indexPattern as IIndexPattern, + field, + }) + .then((result) => { + setFilterOptions(result); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [indexPattern, query, filterField, type]); + + return { filterOptions, isLoading, setQuery }; +}; From 0cedbe8079cc25ed1f2c961eab5b152c20a358ab Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 12:17:43 +0200 Subject: [PATCH 152/493] Added SampleData Warning --- public/components/common/modules/panel/main-panel.tsx | 2 ++ .../components/common/welcome/components/agent-sections.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index ea4919815c..8e90d04a37 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -13,6 +13,7 @@ import { FilterHandler } from '../../../../utils/filter-handler'; import { TabVisualizations } from '../../../../factories/tab-visualizations'; import { Filter } from '../../../../../../../src/plugins/data/public/'; import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../../src/plugins/data/common'; +import { SampleDataWarning } from '../../../visualize/components'; export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { @@ -107,6 +108,7 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { </ModuleSidePanel > } <EuiPageBody> + <SampleDataWarning /> <ModuleContent /> </EuiPageBody> </EuiFlexItem> diff --git a/public/components/common/welcome/components/agent-sections.ts b/public/components/common/welcome/components/agent-sections.ts index 3d90232fdb..d41a6e9e62 100644 --- a/public/components/common/welcome/components/agent-sections.ts +++ b/public/components/common/welcome/components/agent-sections.ts @@ -44,6 +44,11 @@ export const getAgentSections = (menuAgent) => { text: 'Integrity monitoring', isPin: menuAgent.fim ? menuAgent.fim : false, }, + office: { + id: WAZUH_MODULES_ID.OFFICE_365, + text: 'Office 365', + isPin: menuAgent.office ? menuAgent.office : false + }, aws: { id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS', From 1e26441f3628b63ae7e63930867ce9db3bb270c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 12:38:11 +0200 Subject: [PATCH 153/493] Added error handling --- .../common/hooks/use-value-suggestions.ts | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts index 5d70a086ec..d4cb5fa20c 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -2,13 +2,20 @@ import { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useIndexPattern } from '.'; import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; -import React from 'react' - +import React from 'react'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; export interface IValueSuggestiions { filterOptions: string[] | boolean[]; isLoading: boolean; - setQuery: React.Dispatch<React.SetStateAction<string>> + setQuery: React.Dispatch<React.SetStateAction<string>>; } export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') => { @@ -21,23 +28,36 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole useEffect(() => { if (indexPattern) { setIsLoading(true); - const field = { - type: type, - name: filterField, - aggregatable: true, - } as IFieldType; - data.autocomplete - .getValueSuggestions({ - query, - indexPattern: indexPattern as IIndexPattern, - field, - }) - .then((result) => { - setFilterOptions(result); - }) - .finally(() => { + (async () => { + const field = { + type: type, + name: filterField, + aggregatable: true, + } as IFieldType; + try { + setFilterOptions( + await data.autocomplete.getValueSuggestions({ + query, + indexPattern: indexPattern as IIndexPattern, + field, + }) + ); + } catch (error) { + const options: UIErrorLog = { + context: `${useValueSuggestions.name}.valueSuggestions`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, + error: { + error, + message: error.message || error, + title: error.name, + }, + }; + getErrorOrchestrator().handleError(options); + } finally { setIsLoading(false); - }); + } + })(); } }, [indexPattern, query, filterField, type]); From b90d411bb241a2fb88fa17a894722f4aded27747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 12:43:30 +0200 Subject: [PATCH 154/493] Added copyright --- .../components/common/hooks/use-value-suggestions.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts index d4cb5fa20c..4a739d184d 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -1,3 +1,14 @@ +/* + * Wazuh app - React hook for getting value suggestions + * Copyright (C) 2015-2021 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 { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useIndexPattern } from '.'; From 2e942960c3f06928fe539fedd8d5979945ecf717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 12:57:02 +0200 Subject: [PATCH 155/493] Renamed output --- public/components/common/hooks/index.ts | 2 +- public/components/common/hooks/use-value-suggestions.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index e6f5dca3bd..6313cc00f1 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,4 +24,4 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; -export { useValueSuggestions } from './use-value-suggestions'; +export { useValueSuggestions, IValueSuggestiions } from './use-value-suggestions'; diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts index 4a739d184d..e417237487 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -24,13 +24,13 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; export interface IValueSuggestiions { - filterOptions: string[] | boolean[]; + suggestedValues: string[] | boolean[]; isLoading: boolean; setQuery: React.Dispatch<React.SetStateAction<string>>; } export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') => { - const [filterOptions, setFilterOptions] = useState<string[] | boolean[]>([]); + const [suggestedValues, setSuggestedValues] = useState<string[] | boolean[]>([]); const [query, setQuery] = useState<string>(''); const [isLoading, setIsLoading] = useState(true); const data = getDataPlugin(); @@ -46,7 +46,7 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole aggregatable: true, } as IFieldType; try { - setFilterOptions( + setSuggestedValues( await data.autocomplete.getValueSuggestions({ query, indexPattern: indexPattern as IIndexPattern, @@ -72,5 +72,5 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole } }, [indexPattern, query, filterField, type]); - return { filterOptions, isLoading, setQuery }; + return { suggestedValues, isLoading, setQuery }; }; From cb09a45900003c8e3b75e9f7d3ef3853ae627422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 28 Jul 2021 13:00:14 +0200 Subject: [PATCH 156/493] fix(merge): Fix merge problems --- .../common/welcome/components/menu-agent.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index 352f7879c7..ac93ba2aa6 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -15,13 +15,8 @@ import { connect } from 'react-redux'; import { AppState } from '../../../../react-services/app-state'; import { hasAgentSupportModule } from '../../../../react-services/wz-agents'; import { getAngularModule, getToasts } from '../../../../kibana-services'; -<<<<<<< HEAD -import { WAZUH_MODULES_ID } from '../../../../../common/constants'; -import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; -======= import { updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; import { getAgentSections } from './agent-sections'; ->>>>>>> 0cedbe8079cc25ed1f2c961eab5b152c20a358ab class WzMenuAgent extends Component { constructor(props) { @@ -227,19 +222,8 @@ const mapStateToProps = (state) => { }; }; -<<<<<<< HEAD -const mapDispatchToProps = dispatch => ({ - updateCurrentAgentData: (agentData) => dispatch(updateCurrentAgentData(agentData)) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzMenuAgent); -======= const mapDispatchToProps = (dispatch) => ({ updateCurrentAgentData: (agentData) => dispatch(updateCurrentAgentData(agentData)), }); export default connect(mapStateToProps, mapDispatchToProps)(WzMenuAgent); ->>>>>>> 0cedbe8079cc25ed1f2c961eab5b152c20a358ab From dd3362ea932654cb08a2424a6a103b845ae12ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 13:02:58 +0200 Subject: [PATCH 157/493] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce611e8172..45de11b2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) +- Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) ### Changed From cbaa719416ecb7f989df686764c0b0320ef2819c Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 13:15:03 +0200 Subject: [PATCH 158/493] Added User and IP drilldown config --- .../office-panel/config/drilldown-config.tsx | 44 ------------------- .../overview/office-panel/config/index.ts | 3 +- .../office-panel/config/main-view-config.tsx | 29 +++++------- .../office-panel/config/module-config.tsx | 14 +++--- .../office-panel/views/office-body.tsx | 3 +- .../office-panel/views/office-drilldown.tsx | 4 +- 6 files changed, 27 insertions(+), 70 deletions(-) delete mode 100644 public/components/overview/office-panel/config/drilldown-config.tsx diff --git a/public/components/overview/office-panel/config/drilldown-config.tsx b/public/components/overview/office-panel/config/drilldown-config.tsx deleted file mode 100644 index 3cb6a7ad8a..0000000000 --- a/public/components/overview/office-panel/config/drilldown-config.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { VisCard } from '../../../common/modules/panel/'; -import { EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { SecurityAlerts } from '../../../visualize/components'; - -export const DrilldownConfig = { - rows: [ - { - height: 230, - columns: [ - { - width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> - }, - { - width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> - }, - { - width: 40, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 100, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> - }, - ] - }, - ] -}; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 9628da5002..529b681a5d 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -1,4 +1,5 @@ -export { DrilldownConfig } from './drilldown-config'; +export { DrilldownUserConfig } from './drilldown-user-config'; +export { DrilldownIPConfig } from './drilldown-ip-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 75fc45f17f..c98b42e3d4 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { VisCard, AggTable } from '../../../common/modules/panel/'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; export const MainViewConfig = { rows: [ @@ -10,7 +12,7 @@ export const MainViewConfig = { width: 50, component: (props) => ( <AggTable - tableTitle={'Users'} + tableTitle={''} aggTerm={'data.office365.UserId'} aggLabel={'User'} maxRows={'7'} @@ -18,20 +20,13 @@ export const MainViewConfig = { }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-User-By-Operation-Result' tab='office' title='' {...props} /> - }, - ] - }, - { - height: 300, - columns: [ - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> - }, - { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Rule-Level-Histogram' tab='office' {...props} /> + component: (props) => ( + <AggTable + tableTitle={''} + aggTerm={'data.office365.ClientIP'} + aggLabel={'Client IP'} + maxRows={'7'} + onRowClick={(field, value) => props.onRowClick(field, value)} />) }, ] }, @@ -39,8 +34,8 @@ export const MainViewConfig = { height: 300, columns: [ { - width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-summary' tab='office' {...props} /> + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> }, ] }, diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 07b5926697..3bd23c35ae 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; -import { MainViewConfig, DrilldownConfig } from './'; +import { MainViewConfig, DrilldownUserConfig, DrilldownIPConfig } from './'; export const ModuleConfig = { @@ -8,8 +8,12 @@ export const ModuleConfig = { length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} /> }, - drilldown: { - length: () => DrilldownConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeDrilldown {...{ ...DrilldownConfig, ...props }} /> - } + 'data.office365.UserId': { + length: () => DrilldownUserConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeDrilldown title={"User Activity"} {...{ ...DrilldownUserConfig, ...props }} /> + }, + 'data.office365.ClientIP': { + length: () => DrilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <OfficeDrilldown title={"Client IP"} {...{ ...DrilldownIPConfig, ...props }} /> + }, }; diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index 7610a180ee..3d65ba69c9 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -5,7 +5,8 @@ export const OfficeBody = ({ changeView, toggleFilter, rows = [] }) => { const rowClickHandler = (field, value) => { toggleFilter(field, value); - changeView('drilldown'); + + changeView(field); } return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler}/> diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index 881423de09..c036f5e41e 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -3,7 +3,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eu import { VisConfigLayout } from '../../../common/modules/panel/components'; -export const OfficeDrilldown = ({ changeView, toggleFilter, rows = [], selectedFilter = { field: '', value: '' } }) => { +export const OfficeDrilldown = ({ title = '', changeView, toggleFilter, rows = [], selectedFilter = { field: '', value: '' } }) => { const rowClickHandler = () => { toggleFilter(selectedFilter.field); @@ -17,7 +17,7 @@ export const OfficeDrilldown = ({ changeView, toggleFilter, rows = [], selectedF </EuiFlexItem> <EuiFlexItem grow={false}> <EuiTitle size="s"> - <h3>User Activity</h3> + <h3>{title}</h3> </EuiTitle> <p>{selectedFilter.value}</p> </EuiFlexItem> From 8a6eaff218ac2d92b1a2b3fb8ac88f8dd823cfd6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 13:15:36 +0200 Subject: [PATCH 159/493] Added User and IP drilldown config files --- .../config/drilldown-ip-config.tsx | 44 +++++++++++++++++++ .../config/drilldown-user-config.tsx | 44 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 public/components/overview/office-panel/config/drilldown-ip-config.tsx create mode 100644 public/components/overview/office-panel/config/drilldown-user-config.tsx diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx new file mode 100644 index 0000000000..a4b59bc544 --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { VisCard } from '../../../common/modules/panel'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const DrilldownIPConfig = { + rows: [ + { + height: 230, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + }, + { + width: 40, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, + ] +}; diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx new file mode 100644 index 0000000000..785052d114 --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { VisCard } from '../../../common/modules/panel'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const DrilldownUserConfig = { + rows: [ + { + height: 230, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + }, + { + width: 40, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, + ] +}; From 5af6e7ffde2e52a53445879071a840b122d7e7d4 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 13:59:28 +0200 Subject: [PATCH 160/493] Changed default height to unset --- .../common/modules/panel/components/vis-config-layout.tsx | 2 +- .../overview/office-panel/config/main-view-config.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/common/modules/panel/components/vis-config-layout.tsx b/public/components/common/modules/panel/components/vis-config-layout.tsx index 3220a96f48..5b3114d49e 100644 --- a/public/components/common/modules/panel/components/vis-config-layout.tsx +++ b/public/components/common/modules/panel/components/vis-config-layout.tsx @@ -7,7 +7,7 @@ export const VisConfigLayout = ({ rowClickHandler, rows = [] }) => { { rows.map((row, key) => { return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || (150 + 'px') + height: row.height || ('unset') }}> { row.columns.map((column, key) => { diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index c98b42e3d4..5109a55dc3 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -6,7 +6,7 @@ import { SecurityAlerts } from '../../../visualize/components'; export const MainViewConfig = { rows: [ { - height: 300, + // height: 300, columns: [ { width: 50, From 0ba6f8bc41a4525e1d855ea7259a439c6586c6f5 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 28 Jul 2021 14:43:23 +0200 Subject: [PATCH 161/493] Adding new Github Visualization and addapting github drilldown tables --- .../github-panel/config/drilldown-action.tsx | 61 + .../github-panel/config/drilldown-actor.tsx | 61 + .../config/drilldown-organization.tsx | 61 + ...drilldown.tsx => drilldown-repository.tsx} | 35 +- .../overview/github-panel/config/index.ts | 5 +- .../github-panel/config/module-config.tsx | 22 +- .../overview/github-panel/views/drilldown.tsx | 4 +- .../overview/github-panel/views/main.tsx | 2 +- .../overview/overview-github.ts | 1501 ++++++++++++----- 9 files changed, 1338 insertions(+), 414 deletions(-) create mode 100644 public/components/overview/github-panel/config/drilldown-action.tsx create mode 100644 public/components/overview/github-panel/config/drilldown-actor.tsx create mode 100644 public/components/overview/github-panel/config/drilldown-organization.tsx rename public/components/overview/github-panel/config/{drilldown.tsx => drilldown-repository.tsx} (59%) diff --git a/public/components/overview/github-panel/config/drilldown-action.tsx b/public/components/overview/github-panel/config/drilldown-action.tsx new file mode 100644 index 0000000000..a5c86e0a86 --- /dev/null +++ b/public/components/overview/github-panel/config/drilldown-action.tsx @@ -0,0 +1,61 @@ +/* + * Wazuh app - GitHub Panel tab - Drilldown layout configuration + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel/'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + + +export const DrilldownConfigAction = { + rows: [ + { + height: 300, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + }, + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + }, + ] + }, + { + height: 800, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, + ] +}; diff --git a/public/components/overview/github-panel/config/drilldown-actor.tsx b/public/components/overview/github-panel/config/drilldown-actor.tsx new file mode 100644 index 0000000000..5d96c6864e --- /dev/null +++ b/public/components/overview/github-panel/config/drilldown-actor.tsx @@ -0,0 +1,61 @@ +/* + * Wazuh app - GitHub Panel tab - Drilldown layout configuration + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel/'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + + +export const DrilldownConfigActor = { + rows: [ + { + height: 300, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + }, + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + }, + ] + }, + { + height: 800, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, + ] +}; diff --git a/public/components/overview/github-panel/config/drilldown-organization.tsx b/public/components/overview/github-panel/config/drilldown-organization.tsx new file mode 100644 index 0000000000..c29e7e02b8 --- /dev/null +++ b/public/components/overview/github-panel/config/drilldown-organization.tsx @@ -0,0 +1,61 @@ +/* + * Wazuh app - GitHub Panel tab - Drilldown layout configuration + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel/'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + + +export const DrilldownConfigOrganization = { + rows: [ + { + height: 300, + columns: [ + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + }, + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + }, + ] + }, + { + height: 800, + columns: [ + { + width: 100, + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + }, + ] + }, + ] +}; diff --git a/public/components/overview/github-panel/config/drilldown.tsx b/public/components/overview/github-panel/config/drilldown-repository.tsx similarity index 59% rename from public/components/overview/github-panel/config/drilldown.tsx rename to public/components/overview/github-panel/config/drilldown-repository.tsx index 76f124199f..78e1aa8505 100644 --- a/public/components/overview/github-panel/config/drilldown.tsx +++ b/public/components/overview/github-panel/config/drilldown-repository.tsx @@ -15,36 +15,45 @@ import { VisCard } from '../../../common/modules/panel/'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; -export const DrilldownConfig = { + +export const DrilldownConfigRepository = { rows: [ { - height: 230, + height: 300, columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + }, + { + width: 30, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + }, + ] + }, + { + height: 300, + columns: [ + { + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> }, { - width: 40, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + width: 50, + component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> }, ] }, { + height: 800, columns: [ { width: 100, - component: (props) => ( - <EuiFlexGroup className={'wz-margin-0'}> - <EuiFlexItem> - <EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - ) + component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> }, ] }, diff --git a/public/components/overview/github-panel/config/index.ts b/public/components/overview/github-panel/config/index.ts index b9ba3c2cee..eeca36a3c8 100644 --- a/public/components/overview/github-panel/config/index.ts +++ b/public/components/overview/github-panel/config/index.ts @@ -1,4 +1,7 @@ -export { DrilldownConfig } from './drilldown'; +export { DrilldownConfigAction } from './drilldown-action'; +export { DrilldownConfigOrganization } from './drilldown-organization'; +export { DrilldownConfigActor } from './drilldown-actor'; +export { DrilldownConfigRepository } from './drilldown-repository'; export { MainViewConfig } from './main'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/github-panel/config/module-config.tsx b/public/components/overview/github-panel/config/module-config.tsx index 67b8fe68a0..a20450c71e 100644 --- a/public/components/overview/github-panel/config/module-config.tsx +++ b/public/components/overview/github-panel/config/module-config.tsx @@ -11,15 +11,27 @@ */ import React from 'react'; import { Main, Drilldown } from '../views'; -import { MainViewConfig, DrilldownConfig } from './'; +import { MainViewConfig, DrilldownConfigAction, DrilldownConfigActor, DrilldownConfigOrganization, DrilldownConfigRepository } from './'; export const ModuleConfig = { main: { length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), component: (props) => <Main {...{ ...MainViewConfig, ...props }} /> }, - drilldown: { - length: () => DrilldownConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <Drilldown {...{ ...DrilldownConfig, ...props }} /> - } + 'data.github.actor': { + length: () => DrilldownConfigActor.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <Drilldown title={"Actor Activity"} {...{ ...DrilldownConfigActor, ...props }} /> + }, + 'data.github.org': { + length: () => DrilldownConfigOrganization.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <Drilldown title={"Organization Activity"} {...{ ...DrilldownConfigOrganization, ...props }} /> + }, + 'data.github.repo': { + length: () => DrilldownConfigRepository.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <Drilldown title={"Repository Activity"} {...{ ...DrilldownConfigRepository, ...props }} /> + }, + 'data.github.action': { + length: () => DrilldownConfigAction.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => <Drilldown title={"Action Activity"} {...{ ...DrilldownConfigAction, ...props }} /> + }, }; diff --git a/public/components/overview/github-panel/views/drilldown.tsx b/public/components/overview/github-panel/views/drilldown.tsx index 840ca592fb..4f1d9d443a 100644 --- a/public/components/overview/github-panel/views/drilldown.tsx +++ b/public/components/overview/github-panel/views/drilldown.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; import { VisConfigLayout } from '../../../common/modules/panel'; -export const Drilldown = ({ changeView, rows = [], toggleFilter, selectedFilter = { field: '', value: '' }, ...props }) => { +export const Drilldown = ({ changeView, title='', rows = [], toggleFilter, selectedFilter = { field: '', value: '' }, ...props }) => { const rowClickHandler = () => { toggleFilter(selectedFilter.field); @@ -28,7 +28,7 @@ export const Drilldown = ({ changeView, rows = [], toggleFilter, selectedFilter </EuiFlexItem> <EuiFlexItem grow={false}> <EuiTitle size='s'> - <h3>User Activity</h3> + <h3>{title}</h3> </EuiTitle> <p>{selectedFilter.value}</p> </EuiFlexItem> diff --git a/public/components/overview/github-panel/views/main.tsx b/public/components/overview/github-panel/views/main.tsx index e3aaa4704d..f2552d3893 100644 --- a/public/components/overview/github-panel/views/main.tsx +++ b/public/components/overview/github-panel/views/main.tsx @@ -18,7 +18,7 @@ export const Main = ({ changeView, toggleFilter, rows = [] }) => { const rowClickHandler = (field, value) => { toggleFilter(field, value); - changeView('drilldown'); + changeView(field); } return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler}/> diff --git a/server/integration-files/visualizations/overview/overview-github.ts b/server/integration-files/visualizations/overview/overview-github.ts index 82eb13c856..6b0195144e 100644 --- a/server/integration-files/visualizations/overview/overview-github.ts +++ b/server/integration-files/visualizations/overview/overview-github.ts @@ -15,138 +15,139 @@ export default [ _source: { title: 'Alerts evolution by organization', visState: JSON.stringify({ - "title": "Alerts evolution by organization", - "type": "area", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "date_histogram", - "params": { - "field": "timestamp", - "timeRange": { - "from": "now-7d", - "to": "now" + title: 'Alerts evolution by organization', + type: 'area', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-7d', + to: 'now', }, - "useNormalizedEsInterval": true, - "scaleMetricValues": false, - "interval": "auto", - "drop_partials": false, - "min_doc_count": 1, - "extended_bounds": {}, - "customLabel": "" - }, - "schema": "segment" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.org", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "group" - } + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + customLabel: '', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, ], - "params": { - "type": "area", - "grid": { - "categoryLines": false + params: { + type: 'area', + grid: { + categoryLines: false, }, - "categoryAxes": [ + categoryAxes: [ { - "id": "CategoryAxis-1", - "type": "category", - "position": "bottom", - "show": true, - "style": {}, - "scale": { - "type": "linear" + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', }, - "labels": { - "show": true, - "filter": true, - "truncate": 100, - "rotate": 0 + labels: { + show: true, + filter: true, + truncate: 100, + rotate: 0, }, - "title": {} - } + title: {}, + }, ], - "valueAxes": [ + valueAxes: [ { - "id": "ValueAxis-1", - "name": "LeftAxis-1", - "type": "value", - "position": "left", - "show": true, - "style": {}, - "scale": { - "type": "linear", - "mode": "normal" + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', }, - "labels": { - "show": true, - "rotate": 0, - "filter": false, - "truncate": 100 + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, }, - "title": { - "text": "Count" - } - } + title: { + text: 'Count', + }, + }, ], - "seriesParams": [ + seriesParams: [ { - "show": true, - "type": "line", - "mode": "normal", - "data": { - "label": "Count", - "id": "1" + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Count', + id: '1', }, - "drawLinesBetweenPoints": true, - "lineWidth": 2, - "showCircles": true, - "interpolate": "linear", - "valueAxis": "ValueAxis-1" - } + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'linear', + valueAxis: 'ValueAxis-1', + }, ], - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "times": [], - "addTimeMarker": false, - "thresholdLine": { - "show": false, - "value": 10, - "width": 1, - "style": "full", - "color": "#E7664C" - }, - "labels": {}, - "orderBucketsBySum": false - } + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + orderBucketsBySum: false, + }, }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - } + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, _type: 'visualization', }, @@ -155,53 +156,54 @@ export default [ _source: { title: 'Top 5 organizations by alerts', visState: JSON.stringify({ - "title": "Top 5 organizations by alerts", - "type": "pie", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.org", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - } + title: 'Top 5 organizations by alerts', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, ], - "params": { - "type": "pie", - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "isDonut": false, - "labels": { - "show": false, - "values": true, - "last_level": true, - "truncate": 100 - } - } + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - } + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, _type: 'visualization', }, @@ -210,132 +212,133 @@ export default [ _source: { title: 'Users with more alerts', visState: JSON.stringify({ - "title": "Users with more alerts", - "type": "line", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "4", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.org", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.actor", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "group" - } + title: 'Users with more alerts', + type: 'line', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.github.actor', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, ], - "params": { - "type": "line", - "grid": { - "categoryLines": false + params: { + type: 'line', + grid: { + categoryLines: false, }, - "categoryAxes": [ + categoryAxes: [ { - "id": "CategoryAxis-1", - "type": "category", - "position": "bottom", - "show": true, - "style": {}, - "scale": { - "type": "linear" + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', }, - "labels": { - "show": true, - "filter": true, - "truncate": 100 + labels: { + show: true, + filter: true, + truncate: 100, }, - "title": {} - } + title: {}, + }, ], - "valueAxes": [ + valueAxes: [ { - "id": "ValueAxis-1", - "name": "LeftAxis-1", - "type": "value", - "position": "left", - "show": true, - "style": {}, - "scale": { - "type": "linear", - "mode": "normal" + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, }, - "labels": { - "show": true, - "rotate": 0, - "filter": false, - "truncate": 100 + title: { + text: 'Count', }, - "title": { - "text": "Count" - } - } + }, ], - "seriesParams": [ + seriesParams: [ { - "show": true, - "type": "histogram", - "mode": "stacked", - "data": { - "label": "Count", - "id": "1" + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', }, - "valueAxis": "ValueAxis-1", - "drawLinesBetweenPoints": true, - "lineWidth": 2, - "interpolate": "linear", - "showCircles": true - } + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, ], - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "times": [], - "addTimeMarker": false, - "labels": {}, - "thresholdLine": { - "show": false, - "value": 10, - "width": 1, - "style": "full", - "color": "#E7664C" - } - } + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - } + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, _type: 'visualization', }, @@ -344,69 +347,70 @@ export default [ _source: { title: 'Top alerts by alert action type and organization', visState: JSON.stringify({ - "title": "Top alerts by alert action type and organization", - "type": "pie", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.org", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.action", - "orderBy": "1", - "order": "desc", - "size": 3, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - } + title: 'Top alerts by alert action type and organization', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.action', + orderBy: '1', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, ], - "params": { - "type": "pie", - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "isDonut": true, - "labels": { - "show": false, - "values": true, - "last_level": true, - "truncate": 100 - } - } + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, }), uiStateJSON: '', description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - } + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, _type: 'visualization', }, @@ -415,77 +419,649 @@ export default [ _source: { title: 'Alert summary', visState: JSON.stringify({ - "title": "Alert summary", - "type": "table", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "agent.name", - "orderBy": "1", - "order": "desc", - "size": 50, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "bucket" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.github.org", - "orderBy": "1", - "order": "desc", - "size": 10, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "bucket" - }, - { - "id": "4", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.description", - "orderBy": "1", - "order": "desc", - "size": 10, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "bucket" - } + title: 'Alert summary', + type: 'table', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'agent.name', + orderBy: '1', + order: 'desc', + size: 50, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'bucket', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'bucket', + }, + ], + params: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Ten-Organizations', + _source: { + title: 'Top 10 organizations', + visState: JSON.stringify({ + title: 'Top 10 Organizations', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Organizations', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Countries', + _source: { + title: 'Countries', + visState: JSON.stringify({ + title: 'Top github actors countries', + type: 'tagcloud', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.actor_location.country_code', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Top countries ', + }, + schema: 'segment', + }, + ], + params: { + scale: 'linear', + orientation: 'single', + minFontSize: 18, + maxFontSize: 72, + showLabel: true, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Events', + _source: { + title: 'GitHub top events', + visState: JSON.stringify({ + title: 'Github Top Events', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.action', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Github Actions', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Stats', + _source: { + title: 'GitHub Stats', + visState: JSON.stringify({ + title: 'Github Stats', + type: 'metric', + aggs: [ + { + id: '2', + enabled: true, + type: 'count', + params: { + customLabel: 'Total Alerts', + }, + schema: 'metric', + }, + { + id: '1', + enabled: true, + type: 'top_hits', + params: { + field: 'rule.level', + aggregate: 'concat', + size: 1, + sortField: 'rule.level', + sortOrder: 'desc', + customLabel: 'Max rule level detected', + }, + schema: 'metric', + }, + ], + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [ + { + from: 0, + to: 10000, + }, + ], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Organization-Heatmap', + _source: { + title: 'GitHub Organization Heatmap', + visState: JSON.stringify({ + title: 'GitHub Organization Heatmap', + type: 'heatmap', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.github.action', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + params: { + type: 'heatmap', + addTooltip: true, + addLegend: true, + enableHover: false, + legendPosition: 'right', + times: [], + colorsNumber: 4, + colorSchema: 'Blues', + setColorRange: false, + colorsRange: [], + invertColors: false, + percentageMode: false, + valueAxes: [ + { + show: false, + id: 'ValueAxis-1', + type: 'value', + scale: { + type: 'linear', + defaultYExtents: false, + }, + labels: { + show: false, + rotate: 0, + overwriteColor: false, + color: 'black', + }, + }, + ], + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Ten-Organizations', + _source: { + title: 'GitHub top 10 organizations', + visState: JSON.stringify({ + title: 'Top 10 Organizations', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.org', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Organizations', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Ten-Actors', + _source: { + title: 'Top 10 actors', + visState: JSON.stringify({ + title: 'Top 10 Actors', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.actor', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Actors', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Ten-Repositories', + _source: { + title: 'Top 10 repositories', + visState: JSON.stringify({ + title: 'Top 10 Repositories', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.repo', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Repositories', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, + { + _id: 'Wazuh-App-Overview-GitHub-Top-Ten-Actions', + _source: { + title: 'Top 10 actions', + visState: JSON.stringify({ + title: 'Top 10 Actions', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.github.action', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Actions', + }, + schema: 'segment', + }, ], - "params": { - "perPage": 10, - "showPartialRows": false, - "showMetricsAtAllLevels": false, - "sort": { - "columnIndex": null, - "direction": null - }, - "showTotal": false, - "totalFunc": "sum", - "percentageCol": "" - } + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, }), uiStateJSON: JSON.stringify({ vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, @@ -493,9 +1069,150 @@ export default [ description: '', version: 1, kibanaSavedObjectMeta: { - searchSourceJSON: '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - } + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, }, _type: 'visualization', - } + }, + { + _id: 'Wazuh-App-Overview-GitHub-Alert-Level-Evolution', + _source: { + title: 'Alert level evolution', + visState: JSON.stringify({ + title: 'Rule Level Over Time', + type: 'area', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-30d', + to: 'now', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'd', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + params: { + type: 'area', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'area', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: 'step-after', + valueAxis: 'ValueAxis-1', + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + labels: {}, + }, + }), + uiStateJSON: JSON.stringify({ + vis: { params: { sort: { columnIndex: 3, direction: 'desc' } } }, + }), + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', + }, + }, + _type: 'visualization', + }, ]; From 75d7e2298c7d085e44258d1eda76c600c5484995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 28 Jul 2021 15:59:15 +0200 Subject: [PATCH 162/493] Defined return type for hook --- public/components/common/hooks/use-value-suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts index e417237487..3f0d2b9a60 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -29,7 +29,7 @@ export interface IValueSuggestiions { setQuery: React.Dispatch<React.SetStateAction<string>>; } -export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') => { +export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') : IValueSuggestiions => { const [suggestedValues, setSuggestedValues] = useState<string[] | boolean[]>([]); const [query, setQuery] = useState<string>(''); const [isLoading, setIsLoading] = useState(true); From 606f97c3d55f0683ed647136e864851956b2984a Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 16:08:33 +0200 Subject: [PATCH 163/493] Added error handling to office 365 --- .../overview/office-panel/office-panel.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index ba5783166d..1f137d181e 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -14,6 +14,9 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { CustomSearchBar } from '../../common/custom-search-bar'; import { OfficeStats } from './views'; import { queryConfig } from '../../../react-services/query-config'; @@ -24,20 +27,35 @@ export const OfficePanel = withErrorBoundary(() => { const [moduleStatsList, setModuleStatsList] = useState([]); /** Get Office 365 Side Panel Module info **/ + const getModuleConfig = async () => { + const modulesConfig = await queryConfig( + '000', + [{ component: 'wmodules', configuration: 'wmodules' }] + ); + const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules + .filter((module) => { return Object.keys(module)[0] == 'office365' })[0]['office365']).map((configProp) => { + const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; + return { title: configProp[0], description } + }) + setModuleStatsList(config); + } + useEffect(() => { (async () => { try { - const modulesConfig = await queryConfig( - '000', - [{ component: 'wmodules', configuration: 'wmodules' }] - ); - const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules - .filter((module) => { return Object.keys(module)[0] == 'office365' })[0]['office365']).map((configProp) => { - const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; - return { title: configProp[0], description } - }) - setModuleStatsList(config); + await getModuleConfig(); } catch (error) { + const options = { + context: `${OfficePanel.name}.getModuleConfig`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Module Unavailable', + }, + }; + getErrorOrchestrator().handleError(options); setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); } } From efc4e8aace402a418af8580fc3d178dc1385df8e Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 28 Jul 2021 17:00:01 +0200 Subject: [PATCH 164/493] fix warnings --- .../custom-search-bar/custom-search-bar.tsx | 40 +++++++------------ .../office-panel/config/search-bar-config.ts | 18 ++++----- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 3ec6223cd9..c144e4fa13 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -16,8 +16,9 @@ import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { ModulesHelper } from '../modules/modules-helper'; +import { useValueSuggestions } from '../hooks/use-value-suggestions' -export const CustomSearchBar = ({ ...props }) => { +export const CustomSearchBar = ({ filtersValues, ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; @@ -37,8 +38,6 @@ export const CustomSearchBar = ({ ...props }) => { return array } const [isLoading, setLoading] = useState(false); - const [customFilters, setCustomFilters] = useState(props.filtersValues) - const [currentSelectName, setCurrentSelectName] = useState(''); const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); @@ -103,21 +102,14 @@ export const CustomSearchBar = ({ ...props }) => { const setKibanaFilters = (values) => { setLoading(true) const newFilters = [] - if(!values.length){ - const currentFilters = filterManager.getFilters().filter(item => item.meta.key != currentSelectName) - filterManager.removeAll() - filterManager.addFilters(currentFilters) - - }else{ - const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].key) - filterManager.removeAll() - filterManager.addFilters(currentFilters) - values.forEach(element => { - const customFilter = buildCustomFilter(false,indexPattern.title,element.label,element.key) - newFilters.push(customFilter); - }); - filterManager.addFilters(newFilters) - } + const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) + filterManager.removeAll() + filterManager.addFilters(currentFilters) + values.forEach(element => { + const customFilter = buildCustomFilter(false,indexPattern.title,element.label,element.value) + newFilters.push(customFilter); + }); + filterManager.addFilters(newFilters) } const refreshCustomSelectedFilter = () => { @@ -125,16 +117,16 @@ export const CustomSearchBar = ({ ...props }) => { const filters = filterManager.getFilters() const filterCustom = filters.map(item => { return { - key: item.meta.key, + value: item.meta.key, label: item.meta.params.query, } - }).filter(element => Object.keys(selectedOptions).includes(element.key)) + }).filter(element => Object.keys(selectedOptions).includes(element.value)) if(filterCustom.length != 0){ filterCustom.forEach(item => { setSelectedOptions(prevState => ({ ...prevState, - [item.key]: [...prevState[item.key],item], + [item.value]: [...prevState[item.value],item], })); }) @@ -153,23 +145,21 @@ export const CustomSearchBar = ({ ...props }) => { 'combobox': <EuiComboBox className={'filters-custom-combobox'} placeholder={'Select '+item.key} - options={item.values} + options={} selectedOptions={selectedOptions[item.key] || []} onChange={onChange} - onClick={() => setCurrentSelectName(item.key)} isClearable={false} /> }; return types[item.type] || types['default']; } - return ( <> <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> { avancedFiltersState === false ? - customFilters.map((item, key) => ( + filtersValues.map((item, key) => ( <EuiFlexItem grow={2} key={key}> {getComponent(item)} </EuiFlexItem> diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 351d606598..72e8ebbeb5 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -19,23 +19,23 @@ export const filtersValues = [ key: 'agent.id', values: [ { - key: 'agent.id', + value: 'agent.id', label: '001', }, { - key: 'agent.id', + value: 'agent.id', label: '002', }, { - key: 'agent.id', + value: 'agent.id', label: '003', }, { - key: 'agent.id', + value: 'agent.id', label: '004', }, { - key: 'agent.id', + value: 'agent.id', label: '006', }, ], @@ -45,11 +45,11 @@ export const filtersValues = [ key: 'agent.name', values: [ { - key: 'agent.name', + value: 'agent.name', label: 'Amazon', }, { - key: 'agent.name', + value: 'agent.name', label: 'Centos', } ], @@ -59,11 +59,11 @@ export const filtersValues = [ key: 'agent.ip', values: [ { - key: 'agent.ip', + value: 'agent.ip', label: '24.273.97.14', }, { - key: 'agent.ip', + value: 'agent.ip', label: '197.17.1.4', }, ], From 88944807b8871d33efc03f02dbfc1af8964a711f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 28 Jul 2021 17:05:33 +0200 Subject: [PATCH 165/493] remove selected filter on view destruction --- .../custom-search-bar/custom-search-bar.tsx | 5 ----- .../common/modules/panel/main-panel.tsx | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index c144e4fa13..958ddff0c2 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -69,11 +69,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { setAvancedFiltersState(state => !state); } - useEffect(() => { - if(avancedFiltersState){ - setTimeout(() => ModulesHelper.hideCloseButtons(), 10); - }; - }, [avancedFiltersState]); const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?:any, key?:any): Filter => { const meta: FilterMeta = { diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 8e90d04a37..7bb69d15c3 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -38,17 +38,22 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { /** * When a filter is toggled applies de selection */ - useEffect(() => { + const applyFilter = (clearOnly = false) => { const appliedFilters = filterManager.getAppFilters(); - const filters = appliedFilters.filter((filter) => { return filter.meta.key != selectedFilter.field; }); - if (selectedFilter.value) { + if (!clearOnly && selectedFilter.value) { const customFilter = buildCustomFilter(selectedFilter); filters.push(customFilter); } filterManager.setFilters(filters); + } + + useEffect(() => { + applyFilter(); + + return () => applyFilter(true); }, [selectedFilter]) @@ -69,11 +74,13 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { }; const $state: FilterState = { store: FilterStateStore.APP_STATE, + isImplicit: true }; const query = { - match_phrase: { + match: { [field]: { - query: value + query: value, + type: 'phrase' } } } From 1786116499ec40d07bd466507f5d90437f8df411 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 28 Jul 2021 17:46:29 +0200 Subject: [PATCH 166/493] add suggestions values in custom search bar --- .../custom-search-bar/components/combobox.tsx | 27 +++ .../custom-search-bar/components/index.ts | 13 ++ .../custom-search-bar/custom-search-bar.tsx | 163 +++++++++--------- .../office-panel/config/search-bar-config.ts | 44 +---- 4 files changed, 121 insertions(+), 126 deletions(-) create mode 100644 public/components/common/custom-search-bar/components/combobox.tsx create mode 100644 public/components/common/custom-search-bar/components/index.ts diff --git a/public/components/common/custom-search-bar/components/combobox.tsx b/public/components/common/custom-search-bar/components/combobox.tsx new file mode 100644 index 0000000000..e5fbf486cd --- /dev/null +++ b/public/components/common/custom-search-bar/components/combobox.tsx @@ -0,0 +1,27 @@ +import React, { useState, useEffect, useLayoutEffect } from 'react'; + +import { + EuiComboBox, +} from '@elastic/eui'; + +import { useValueSuggestions } from '../../hooks/use-value-suggestions' + +export const Combobox = ({ item, ...props }) => { + + const { suggestedValues, isLoading, setQuery } = useValueSuggestions(item.key) + + const comboOptions = suggestedValues.map((value,key) => ({ key:key, label:value, value:item.key})) + + return ( + <EuiComboBox + data-test-subj={`combobox-${item.key}`} + placeholder={`Select ${item.key}`} + className={'filters-custom-combobox'} + options={comboOptions} + isClearable={false} + isLoading={isLoading} + onSearchChange={(searchValue) => { setQuery(searchValue) }} + {...props} + /> + ) +}; \ No newline at end of file diff --git a/public/components/common/custom-search-bar/components/index.ts b/public/components/common/custom-search-bar/components/index.ts new file mode 100644 index 0000000000..bcbbe26655 --- /dev/null +++ b/public/components/common/custom-search-bar/components/index.ts @@ -0,0 +1,13 @@ +/* + * Wazuh app - React component to integrate Custom search bar + * Copyright (C) 2015-2021 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 { Combobox } from './combobox'; \ No newline at end of file diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index c144e4fa13..e0c52967b7 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -9,29 +9,29 @@ import { EuiFlexItem, EuiComboBox, EuiSwitch, - } from '@elastic/eui'; +} from '@elastic/eui'; //@ts-ignore import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { ModulesHelper } from '../modules/modules-helper'; -import { useValueSuggestions } from '../hooks/use-value-suggestions' +import { Combobox } from './components' export const CustomSearchBar = ({ filtersValues, ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; const timefilter = KibanaServices.timefilter.timefilter; - const indexPattern = getIndexPattern(); + const indexPattern = getIndexPattern(); const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters().map(({meta: {removable, ...restMeta}, ...rest}) => ({...rest,meta: restMeta})) || [], + filters: filterManager.getFilters().map(({ meta: { removable, ...restMeta }, ...rest }) => ({ ...rest, meta: restMeta })) || [], query: { language: 'kuery', query: '' }, time: timefilter.getTime(), }); const defaultSelectedOptions = () => { const array = [] - props.filtersValues.forEach(item =>{ + filtersValues.forEach(item => { array[item.key] = [] }) @@ -43,14 +43,14 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { useEffect(() => { let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - onFiltersUpdated(newFilters) - return () => { - filterSubscriber.unsubscribe(); - }; + const newFilters = filterManager.getFilters(); + onFiltersUpdated(newFilters) + return () => { + filterSubscriber.unsubscribe(); + }; }); }, []); - + const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; const filters = { query, time: dateRange, filters: filterParams.filters }; @@ -70,32 +70,32 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { } useEffect(() => { - if(avancedFiltersState){ + if (avancedFiltersState) { setTimeout(() => ModulesHelper.hideCloseButtons(), 10); }; }, [avancedFiltersState]); - const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?:any, key?:any): Filter => { + const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?: any, key?: any): Filter => { const meta: FilterMeta = { - disabled: false, - negate: false, - key:key, - params: {query:querySearch}, - alias: null, - type: "phrase", - index, + disabled: false, + negate: false, + key: key, + params: { query: querySearch }, + alias: null, + type: "phrase", + index, }; const $state: FilterState = { - store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, }; const query = { match_phrase: { - [key] : { + [key]: { query: querySearch } } } - + return { meta, $state, query }; }; @@ -106,7 +106,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { filterManager.removeAll() filterManager.addFilters(currentFilters) values.forEach(element => { - const customFilter = buildCustomFilter(false,indexPattern.title,element.label,element.value) + const customFilter = buildCustomFilter(false, indexPattern.title, element.label, element.value) newFilters.push(customFilter); }); filterManager.addFilters(newFilters) @@ -116,85 +116,82 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { setSelectedOptions(defaultSelectedOptions) const filters = filterManager.getFilters() const filterCustom = filters.map(item => { - return { - value: item.meta.key, - label: item.meta.params.query, - } + return { + value: item.meta.key, + label: item.meta.params.query, + } }).filter(element => Object.keys(selectedOptions).includes(element.value)) - if(filterCustom.length != 0){ + if (filterCustom.length != 0) { filterCustom.forEach(item => { setSelectedOptions(prevState => ({ - ...prevState, - [item.value]: [...prevState[item.value],item], + ...prevState, + [item.value]: [...prevState[item.value], item], })); - + }) - } - setLoading(false) + } + setLoading(false) }; const onChange = (values) => { setKibanaFilters(values) - refreshCustomSelectedFilter(); + refreshCustomSelectedFilter(); }; const getComponent = (item) => { var types = { 'default': <></>, - 'combobox': <EuiComboBox - className={'filters-custom-combobox'} - placeholder={'Select '+item.key} - options={} - selectedOptions={selectedOptions[item.key] || []} - onChange={onChange} - isClearable={false} - /> + 'combobox': <Combobox + item={item} + selectedOptions={selectedOptions[item.key] || []} + onChange={onChange} + /> }; return types[item.type] || types['default']; } return ( <> - <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> - { - avancedFiltersState === false ? - filtersValues.map((item, key) => ( - <EuiFlexItem grow={2} key={key}> - {getComponent(item)} - </EuiFlexItem> - )) - : - '' - } - <EuiFlexItem> - <KbnSearchBar - showFilterBar={false} - showQueryInput={avancedFiltersState} - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> - <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> - <KbnSearchBar - showDatePicker={false} - showQueryInput={false} - onQuerySubmit={onQuerySubmit} - onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSwitch - label="Advanced filters" - checked={avancedFiltersState} - onChange={() => changeSwitch()} - /> - </EuiFlexItem> - </EuiFlexGroup> - </> + <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> + { + avancedFiltersState === false ? + filtersValues.map((item, key) => ( + <EuiFlexItem grow={2} key={key}> + {getComponent(item)} + </EuiFlexItem> + )) + : + '' + } + <EuiFlexItem> + <KbnSearchBar + showFilterBar={false} + showQueryInput={avancedFiltersState} + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> + <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> + <KbnSearchBar + showDatePicker={false} + showQueryInput={false} + onQuerySubmit={onQuerySubmit} + onFiltersUpdated={onFiltersUpdated} + isLoading={isLoading} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSwitch + label="Advanced filters" + checked={avancedFiltersState} + onChange={() => changeSwitch()} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> ) }; \ No newline at end of file diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 72e8ebbeb5..d03698b4ee 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -17,55 +17,13 @@ export const filtersValues = [ { type: 'combobox', key: 'agent.id', - values: [ - { - value: 'agent.id', - label: '001', - }, - { - value: 'agent.id', - label: '002', - }, - { - value: 'agent.id', - label: '003', - }, - { - value: 'agent.id', - label: '004', - }, - { - value: 'agent.id', - label: '006', - }, - ], }, { type: 'combobox', key: 'agent.name', - values: [ - { - value: 'agent.name', - label: 'Amazon', - }, - { - value: 'agent.name', - label: 'Centos', - } - ], }, { type: 'combobox', key: 'agent.ip', - values: [ - { - value: 'agent.ip', - label: '24.273.97.14', - }, - { - value: 'agent.ip', - label: '197.17.1.4', - }, - ], - }, + } ]; From 240a6143f9e900df5ca409e32e709d2cafd8571e Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Wed, 28 Jul 2021 17:10:10 -0300 Subject: [PATCH 167/493] feat(office365): Added errorOrchestrator service, copyright sections on all files, prettier applied. --- .../modules/panel/components/agg-table.tsx | 27 +++- .../common/modules/panel/components/index.ts | 15 ++- .../panel/components/module-side-panel.scss | 26 ++-- .../panel/components/module-side-panel.tsx | 39 ++++-- .../modules/panel/components/vis-card.tsx | 87 ++++++++----- .../panel/components/vis-config-layout.tsx | 54 +++++--- .../components/common/modules/panel/index.ts | 1 - .../common/modules/panel/main-panel.tsx | 120 +++++++++++------- .../config/drilldown-ip-config.tsx | 49 +++++-- .../config/drilldown-user-config.tsx | 49 +++++-- .../overview/office-panel/config/index.ts | 13 ++ .../office-panel/config/main-view-config.tsx | 43 +++++-- .../office-panel/config/module-config.tsx | 24 +++- .../overview/office-panel/index.tsx | 2 +- .../overview/office-panel/office-panel.tsx | 50 +++++--- .../overview/office-panel/views/index.ts | 6 +- .../office-panel/views/office-body.tsx | 20 ++- .../office-panel/views/office-drilldown.tsx | 62 ++++++--- .../office-panel/views/office-stats.tsx | 53 +++++--- 19 files changed, 517 insertions(+), 223 deletions(-) diff --git a/public/components/common/modules/panel/components/agg-table.tsx b/public/components/common/modules/panel/components/agg-table.tsx index a859b1179b..230cdf0eab 100644 --- a/public/components/common/modules/panel/components/agg-table.tsx +++ b/public/components/common/modules/panel/components/agg-table.tsx @@ -1,3 +1,16 @@ +/* + * Wazuh app - React component Aggregations Table. + * + * Copyright (C) 2015-2021 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 { EuiBasicTable, EuiPanel, EuiTitle, EuiBasicTableColumn } from '@elastic/eui'; import { useEsSearch } from '../../../hooks'; import React from 'react'; @@ -9,7 +22,7 @@ export const AggTable = ({ maxRows, tableTitle, panelProps, - titleProps + titleProps, }) => { const preAppliedAggs = { buckets: { @@ -20,9 +33,9 @@ export const AggTable = ({ }, }, }; - const {esResults, isLoading, error} = useEsSearch({ preAppliedAggs }); + const { esResults, isLoading, error } = useEsSearch({ preAppliedAggs }); const buckets = ((esResults.aggregations || {}).buckets || {}).buckets || []; - const columns:EuiBasicTableColumn<any>[] = [ + const columns: EuiBasicTableColumn<any>[] = [ { field: 'key', name: aggLabel, @@ -49,7 +62,13 @@ export const AggTable = ({ <EuiTitle {...titleProps}> <h2>{tableTitle}</h2> </EuiTitle> - <EuiBasicTable items={buckets} columns={columns} rowProps={getRowProps} loading={isLoading} error={error ? error.message : undefined} /> + <EuiBasicTable + items={buckets} + columns={columns} + rowProps={getRowProps} + loading={isLoading} + error={error ? error.message : undefined} + /> </EuiPanel> ); }; diff --git a/public/components/common/modules/panel/components/index.ts b/public/components/common/modules/panel/components/index.ts index 1ae8ddbdc6..34924ba181 100644 --- a/public/components/common/modules/panel/components/index.ts +++ b/public/components/common/modules/panel/components/index.ts @@ -1,4 +1,17 @@ +/* + * Wazuh app - Index panel components. + * + * Copyright (C) 2015-2021 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 { ModuleSidePanel } from './module-side-panel'; export { AggTable } from './agg-table'; export { VisCard } from './vis-card'; -export { VisConfigLayout } from './vis-config-layout'; \ No newline at end of file +export { VisConfigLayout } from './vis-config-layout'; diff --git a/public/components/common/modules/panel/components/module-side-panel.scss b/public/components/common/modules/panel/components/module-side-panel.scss index e3963d3f2d..51e05444e0 100644 --- a/public/components/common/modules/panel/components/module-side-panel.scss +++ b/public/components/common/modules/panel/components/module-side-panel.scss @@ -1,13 +1,13 @@ -.sidepanel-infoBtnStyle{ - border-radius: 0 5px 5px 0; - background: #ffffffab; - border: 1px solid #006bb459; - box-shadow: 1px 1px 3px -1px #000; - position: fixed; - left: 0; - z-index: 2001; - top: 50%; - width: 26px; - padding-left: 6px; - height: 55px; -} \ No newline at end of file +.sidepanel-infoBtnStyle { + border-radius: 0 5px 5px 0; + background: #ffffffab; + border: 1px solid #006bb459; + box-shadow: 1px 1px 3px -1px #000; + position: fixed; + left: 0; + z-index: 2001; + top: 50%; + width: 26px; + padding-left: 6px; + height: 55px; +} diff --git a/public/components/common/modules/panel/components/module-side-panel.tsx b/public/components/common/modules/panel/components/module-side-panel.tsx index e8b3885c2b..bb8c135d57 100644 --- a/public/components/common/modules/panel/components/module-side-panel.tsx +++ b/public/components/common/modules/panel/components/module-side-panel.tsx @@ -1,11 +1,23 @@ -import { EuiCollapsibleNav, EuiButtonEmpty } from '@elastic/eui'; +/* + * Wazuh app - React component ModuleSidePanel. + * + * Copyright (C) 2015-2021 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 { EuiButtonEmpty, EuiCollapsibleNav } from '@elastic/eui'; import React, { useState } from 'react'; import './module-side-panel.scss'; export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => { const [navIsOpen, setNavIsOpen] = useState(false); - return ( <EuiCollapsibleNav isOpen={navIsOpen} @@ -13,17 +25,22 @@ export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => showCloseButton={true} maskProps={{ headerZindexLocation: 'below', className: 'wz-no-display' }} button={ - <EuiButtonEmpty className={'sidepanel-infoBtnStyle'} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'iInCircle'}> - </EuiButtonEmpty> + <EuiButtonEmpty + className={'sidepanel-infoBtnStyle'} + onClick={() => setNavIsOpen(!navIsOpen)} + iconType={'iInCircle'} + /> } - onClose={() => setNavIsOpen(false)}> + onClose={() => setNavIsOpen(false)} + > <div> - <EuiButtonEmpty style={{ float: 'right' }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'}> - </EuiButtonEmpty> - <div className={'wz-padding-16'}> - {children} - </div> + <EuiButtonEmpty + style={{ float: 'right' }} + onClick={() => setNavIsOpen(!navIsOpen)} + iconType={'cross'} + /> + <div className={'wz-padding-16'}>{children}</div> </div> </EuiCollapsibleNav> ); -}; \ No newline at end of file +}; diff --git a/public/components/common/modules/panel/components/vis-card.tsx b/public/components/common/modules/panel/components/vis-card.tsx index 05b9a949d4..490df599df 100644 --- a/public/components/common/modules/panel/components/vis-card.tsx +++ b/public/components/common/modules/panel/components/vis-card.tsx @@ -1,44 +1,73 @@ +/* + * Wazuh app - React component VisCard. + * + * Copyright (C) 2015-2021 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, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { RawVisualizations } from '../../../../../factories/raw-visualizations'; import KibanaVis from '../../../../../kibana-integrations/kibana-vis'; -export const VisCard = ({ changeView = () => { }, id, width, tab, ...props }) => { - +export const VisCard = ({ changeView = () => {}, id, width, tab, ...props }) => { const [expandedVis, setExpandedVis] = useState(false); - + const title = (() => { const visList = new RawVisualizations().getList(); const rawVis = visList ? visList.filter((item) => item && item.id === id) : []; return rawVis.length && rawVis[0]?.attributes?.title; - })() + })(); - const toggleExpand = id => { + const toggleExpand = (id) => { setExpandedVis(expandedVis === id ? false : id); }; - return <> <EuiFlexItem grow={width}> - <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> - <EuiFlexGroup direction={'column'} className={'h-100'}> - <EuiFlexItem grow={false}> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem>{title && <EuiTitle size={'xxs'}><h4>{title}</h4></EuiTitle>}</EuiFlexItem> - <EuiFlexItem grow={false} > - <EuiButtonIcon - color="text" - style={{ padding: '0px 6px', height: 30 }} - onClick={() => toggleExpand(id)} - iconType="expand" - aria-label="Expand" - /> + return ( + <> + {' '} + <EuiFlexItem grow={width}> + <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> + <EuiFlexGroup direction={'column'} className={'h-100'}> + <EuiFlexItem grow={false}> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + {title && ( + <EuiTitle size={'xxs'}> + <h4>{title}</h4> + </EuiTitle> + )} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + color="text" + style={{ padding: '0px 6px', height: 30 }} + onClick={() => toggleExpand(id)} + iconType="expand" + aria-label="Expand" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <div className={'h-100'}> + <KibanaVis + visID={id} + tab={tab} + onRowClick={() => changeView('drilldown')} + {...props} + /> + </div> </EuiFlexItem> </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <div className={'h-100'}><KibanaVis visID={id} tab={tab} onRowClick={() => changeView('drilldown')} {...props} /></div> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </EuiFlexItem> - </> -} \ No newline at end of file + </EuiPanel> + </EuiFlexItem> + </> + ); +}; diff --git a/public/components/common/modules/panel/components/vis-config-layout.tsx b/public/components/common/modules/panel/components/vis-config-layout.tsx index 5b3114d49e..abdb8bb178 100644 --- a/public/components/common/modules/panel/components/vis-config-layout.tsx +++ b/public/components/common/modules/panel/components/vis-config-layout.tsx @@ -1,22 +1,40 @@ +/* + * Wazuh app - React component VisConfigLayout. + * + * Copyright (C) 2015-2021 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 from 'react'; import { EuiFlexGroup } from '@elastic/eui'; export const VisConfigLayout = ({ rowClickHandler, rows = [] }) => { - - return <> - { - rows.map((row, key) => { - return <EuiFlexGroup key={key} className={'wz-margin-0'} style={{ - height: row.height || ('unset') - }}> - { - row.columns.map((column, key) => { - const growthFactor = Math.max((column.width ? parseInt(column.width / 10) : 1), 1); - return <column.component width={growthFactor} key={key} onRowClick={rowClickHandler} /> - }) - } - </EuiFlexGroup> - }) - } - </> -} + return ( + <> + {rows.map((row, key) => { + return ( + <EuiFlexGroup + key={key} + className={'wz-margin-0'} + style={{ + height: row.height || 'unset', + }} + > + {row.columns.map((column, key) => { + const growthFactor = Math.max(column.width ? parseInt(column.width / 10) : 1, 1); + return ( + <column.component width={growthFactor} key={key} onRowClick={rowClickHandler} /> + ); + })} + </EuiFlexGroup> + ); + })} + </> + ); +}; diff --git a/public/components/common/modules/panel/index.ts b/public/components/common/modules/panel/index.ts index 40147da54d..66d4ded487 100644 --- a/public/components/common/modules/panel/index.ts +++ b/public/components/common/modules/panel/index.ts @@ -10,6 +10,5 @@ * Find more information about this on the LICENSE file. */ - export { MainPanel } from './main-panel'; export * from './components/'; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 7bb69d15c3..99ab3131cc 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -1,9 +1,17 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPageBody, -} from '@elastic/eui'; +/* + * Wazuh app - React components MainPanel + * Copyright (C) 2015-2021 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, { useCallback, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPageBody } from '@elastic/eui'; import { ModuleSidePanel } from './components/'; import WzReduxProvider from '../../../../redux/wz-redux-provider'; import { VisFactoryHandler } from '../../../../react-services/vis-factory-handler'; @@ -12,28 +20,56 @@ import { useFilterManager } from '../../hooks'; import { FilterHandler } from '../../../../utils/filter-handler'; import { TabVisualizations } from '../../../../factories/tab-visualizations'; import { Filter } from '../../../../../../../src/plugins/data/public/'; -import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../../src/plugins/data/common'; +import { + FilterMeta, + FilterState, + FilterStateStore, +} from '../../../../../../../src/plugins/data/common'; import { SampleDataWarning } from '../../../visualize/components'; - +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { - const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); const filterManager = useFilterManager(); + const buildOverviewVisualization = async () => { + const tabVisualizations = new TabVisualizations(); + tabVisualizations.removeAll(); + tabVisualizations.setTab(tab); + tabVisualizations.assign({ + [tab]: moduleConfig[viewId].length(), + }); + const filterHandler = new FilterHandler(AppState.getCurrentPattern()); + await VisFactoryHandler.buildOverviewVisualizations(filterHandler, tab, null, true); + }; + useEffect(() => { (async () => { - const tabVisualizations = new TabVisualizations(); - tabVisualizations.removeAll(); - tabVisualizations.setTab(tab); - tabVisualizations.assign({ - [tab]: moduleConfig[viewId].length(), - }); - const filterHandler = new FilterHandler(AppState.getCurrentPattern()); - await VisFactoryHandler.buildOverviewVisualizations(filterHandler, tab, null, true); - })() - }, [viewId]) + try { + await buildOverviewVisualization(); + } catch (error) { + const options: UIErrorLog = { + context: `${MainPanel.name}.buildOverviewVisualization`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + })(); + }, [viewId]); /** * When a filter is toggled applies de selection @@ -48,19 +84,18 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { filters.push(customFilter); } filterManager.setFilters(filters); - } + }; useEffect(() => { applyFilter(); return () => applyFilter(true); - }, [selectedFilter]) - + }, [selectedFilter]); /** * Builds selected filter structure - * @param value - * @param field + * @param value + * @param field */ const buildCustomFilter = ({ field, value }): Filter => { const meta: FilterMeta = { @@ -69,51 +104,51 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { key: field, params: { query: value }, alias: null, - type: "phrase", + type: 'phrase', index: AppState.getCurrentPattern(), }; const $state: FilterState = { store: FilterStateStore.APP_STATE, - isImplicit: true + isImplicit: true, }; const query = { match: { [field]: { query: value, - type: 'phrase' - } - } - } + type: 'phrase', + }, + }, + }; return { meta, $state, query }; - } + }; const toggleView = (id = 'main') => { - if (id != viewId) - setViewId(id); - } + if (id != viewId) setViewId(id); + }; const toggleFilter = (field = '', value = '') => { setSelectedFilter({ field, value }); - } + }; /** * Builds active view - * @param props + * @param props * @returns React.Component */ const ModuleContent = useCallback(() => { const View = moduleConfig[viewId].component; - return <WzReduxProvider><View selectedFilter={selectedFilter} toggleFilter={toggleFilter} changeView={toggleView} /></WzReduxProvider> - }, [viewId]) + return ( + <WzReduxProvider> + <View selectedFilter={selectedFilter} toggleFilter={toggleFilter} changeView={toggleView} /> + </WzReduxProvider> + ); + }, [viewId]); return ( <EuiFlexGroup style={{ margin: '0 8px' }}> <EuiFlexItem> - {sidePanelChildren && <ModuleSidePanel> - {sidePanelChildren} - </ModuleSidePanel > - } + {sidePanelChildren && <ModuleSidePanel>{sidePanelChildren}</ModuleSidePanel>} <EuiPageBody> <SampleDataWarning /> <ModuleContent /> @@ -122,4 +157,3 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { </EuiFlexGroup> ); }; - diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index a4b59bc544..d09fa59e8e 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -1,3 +1,16 @@ +/* + * Wazuh app - Office 365 DrilldownIPConfig. + * + * Copyright (C) 2015-2021 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 from 'react'; import { VisCard } from '../../../common/modules/panel'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; @@ -10,35 +23,53 @@ export const DrilldownIPConfig = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> + ), }, { width: 40, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-IPs-By-User-Table" tab="office" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 100, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" + tab="office" + {...props} + /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), }, - ] + ], }, - ] + ], }; diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx index 785052d114..218951789e 100644 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -1,3 +1,16 @@ +/* + * Wazuh app - Office 365 DrilldownUserConfig. + * + * Copyright (C) 2015-2021 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 from 'react'; import { VisCard } from '../../../common/modules/panel'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; @@ -10,35 +23,53 @@ export const DrilldownUserConfig = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Metric-Stats' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Top-Events-Pie' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> + ), }, { width: 40, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-IPs-By-User-Table' tab='office' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-IPs-By-User-Table" tab="office" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 100, - component: (props) => <VisCard id='Wazuh-App-Overview-Office-Alerts-Evolution-By-User' tab='office' {...props} /> + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" + tab="office" + {...props} + /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), }, - ] + ], }, - ] + ], }; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 529b681a5d..ec36aaacad 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -1,3 +1,16 @@ +/* + * Wazuh app - Office 365 Config index. + * + * Copyright (C) 2015-2021 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 { DrilldownUserConfig } from './drilldown-user-config'; export { DrilldownIPConfig } from './drilldown-ip-config'; export { MainViewConfig } from './main-view-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 5109a55dc3..032b826255 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -1,5 +1,18 @@ +/* + * Wazuh app - Office 365 MainViewConfig. + * + * Copyright (C) 2015-2021 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 from 'react'; -import { VisCard, AggTable } from '../../../common/modules/panel/'; +import { AggTable } from '../../../common/modules/panel/'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; @@ -15,8 +28,10 @@ export const MainViewConfig = { tableTitle={''} aggTerm={'data.office365.UserId'} aggLabel={'User'} - maxRows={'7'} - onRowClick={(field, value) => props.onRowClick(field, value)} />) + maxRows={'7'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + ), }, { width: 50, @@ -25,19 +40,27 @@ export const MainViewConfig = { tableTitle={''} aggTerm={'data.office365.ClientIP'} aggLabel={'Client IP'} - maxRows={'7'} - onRowClick={(field, value) => props.onRowClick(field, value)} />) + maxRows={'7'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), }, - ] + ], }, - ] -}; \ No newline at end of file + ], +}; diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 3bd23c35ae..5ed1e28043 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -1,19 +1,35 @@ +/* + * Wazuh app - Office 365 ModuleConfig. + * + * Copyright (C) 2015-2021 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 from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; import { MainViewConfig, DrilldownUserConfig, DrilldownIPConfig } from './'; - export const ModuleConfig = { main: { length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} /> + component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} />, }, 'data.office365.UserId': { length: () => DrilldownUserConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeDrilldown title={"User Activity"} {...{ ...DrilldownUserConfig, ...props }} /> + component: (props) => ( + <OfficeDrilldown title={'User Activity'} {...{ ...DrilldownUserConfig, ...props }} /> + ), }, 'data.office365.ClientIP': { length: () => DrilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => <OfficeDrilldown title={"Client IP"} {...{ ...DrilldownIPConfig, ...props }} /> + component: (props) => ( + <OfficeDrilldown title={'Client IP'} {...{ ...DrilldownIPConfig, ...props }} /> + ), }, }; diff --git a/public/components/overview/office-panel/index.tsx b/public/components/overview/office-panel/index.tsx index 2b4b39b6d7..2cf9409b40 100644 --- a/public/components/overview/office-panel/index.tsx +++ b/public/components/overview/office-panel/index.tsx @@ -11,4 +11,4 @@ * Find more information about this on the LICENSE file. */ -export * from './office-panel'; \ No newline at end of file +export * from './office-panel'; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 1f137d181e..b2b198a80a 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -15,7 +15,12 @@ import React, { useEffect, useState } from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { CustomSearchBar } from '../../common/custom-search-bar'; import { OfficeStats } from './views'; @@ -23,32 +28,33 @@ import { queryConfig } from '../../../react-services/query-config'; import { ModuleConfig, filtersValues } from './config'; export const OfficePanel = withErrorBoundary(() => { - const [moduleStatsList, setModuleStatsList] = useState([]); /** Get Office 365 Side Panel Module info **/ const getModuleConfig = async () => { - const modulesConfig = await queryConfig( - '000', - [{ component: 'wmodules', configuration: 'wmodules' }] - ); - const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules - .filter((module) => { return Object.keys(module)[0] == 'office365' })[0]['office365']).map((configProp) => { - const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; - return { title: configProp[0], description } - }) + const modulesConfig = await queryConfig('000', [ + { component: 'wmodules', configuration: 'wmodules' }, + ]); + const config = Object.entries( + modulesConfig['wmodules-wmodules'].affected_items[0].wmodules.filter((module) => { + return Object.keys(module)[0] == 'office365'; + })[0]['office365'] + ).map((configProp) => { + const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; + return { title: configProp[0], description }; + }); setModuleStatsList(config); - } + }; useEffect(() => { (async () => { try { await getModuleConfig(); } catch (error) { - const options = { + const options: UIErrorLog = { context: `${OfficePanel.name}.getModuleConfig`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, error: { error: error, message: error.message || error, @@ -58,15 +64,17 @@ export const OfficePanel = withErrorBoundary(() => { getErrorOrchestrator().handleError(options); setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); } - } - )(); - }, []) + })(); + }, []); return ( <> <CustomSearchBar filtersValues={filtersValues} /> - <MainPanel moduleConfig={ModuleConfig} tab={'office'} - sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} /> + <MainPanel + moduleConfig={ModuleConfig} + tab={'office'} + sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} + /> </> - ) + ); }); diff --git a/public/components/overview/office-panel/views/index.ts b/public/components/overview/office-panel/views/index.ts index 39563eccfe..13a12dbded 100644 --- a/public/components/overview/office-panel/views/index.ts +++ b/public/components/overview/office-panel/views/index.ts @@ -1,3 +1,3 @@ -export { OfficeStats } from './office-stats' -export { OfficeBody } from './office-body' -export { OfficeDrilldown } from './office-drilldown' \ No newline at end of file +export { OfficeStats } from './office-stats'; +export { OfficeBody } from './office-body'; +export { OfficeDrilldown } from './office-drilldown'; diff --git a/public/components/overview/office-panel/views/office-body.tsx b/public/components/overview/office-panel/views/office-body.tsx index 3d65ba69c9..eb1d9237ff 100644 --- a/public/components/overview/office-panel/views/office-body.tsx +++ b/public/components/overview/office-panel/views/office-body.tsx @@ -1,13 +1,25 @@ +/* + * Wazuh app - React View OfficeBody. + * + * Copyright (C) 2015-2021 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 from 'react'; import { VisConfigLayout } from '../../../common/modules/panel'; export const OfficeBody = ({ changeView, toggleFilter, rows = [] }) => { - const rowClickHandler = (field, value) => { toggleFilter(field, value); changeView(field); - } + }; - return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler}/> -} \ No newline at end of file + return <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler} />; +}; diff --git a/public/components/overview/office-panel/views/office-drilldown.tsx b/public/components/overview/office-panel/views/office-drilldown.tsx index c036f5e41e..aef4e22d5c 100644 --- a/public/components/overview/office-panel/views/office-drilldown.tsx +++ b/public/components/overview/office-panel/views/office-drilldown.tsx @@ -1,28 +1,48 @@ +/* + * Wazuh app - React View OfficeDrilldown. + * + * Copyright (C) 2015-2021 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 from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiTitle } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { VisConfigLayout } from '../../../common/modules/panel/components'; - -export const OfficeDrilldown = ({ title = '', changeView, toggleFilter, rows = [], selectedFilter = { field: '', value: '' } }) => { - +export const OfficeDrilldown = ({ + title = '', + changeView, + toggleFilter, + rows = [], + selectedFilter = { field: '', value: '' }, +}) => { const rowClickHandler = () => { toggleFilter(selectedFilter.field); changeView('main'); - } - - return <> - <EuiFlexGroup className={'wz-margin-0'}> - <EuiFlexItem grow={false}> - <div><EuiButtonEmpty onClick={() => rowClickHandler()} iconType={"sortLeft"}></EuiButtonEmpty></div> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiTitle size="s"> - <h3>{title}</h3> - </EuiTitle> - <p>{selectedFilter.value}</p> - </EuiFlexItem> - </EuiFlexGroup> - <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler} /> + }; - </> -} \ No newline at end of file + return ( + <> + <EuiFlexGroup className={'wz-margin-0'}> + <EuiFlexItem grow={false}> + <div> + <EuiButtonEmpty onClick={() => rowClickHandler()} iconType={'sortLeft'} /> + </div> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="s"> + <h3>{title}</h3> + </EuiTitle> + <p>{selectedFilter.value}</p> + </EuiFlexItem> + </EuiFlexGroup> + <VisConfigLayout rows={rows} rowClickHandler={rowClickHandler} /> + </> + ); +}; diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index a7b2f07b16..bf8b39f4c4 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -1,28 +1,39 @@ +/* + * Wazuh app - React View OfficeStats. + * + * Copyright (C) 2015-2021 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 { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; import moduleLogo from '../../../../assets/office365.svg'; import React from 'react'; - export const OfficeStats = ({ listItems = [] }) => { - const logoStyle = { width: 30 }; - return ( - <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> - <EuiFlexItem> - <EuiFlexGroup> - <EuiFlexItem> - <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size={"xs"}><h4>Office 365</h4></EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiDescriptionList - listItems={listItems} - compressed - /> - </EuiFlexItem> + const logoStyle = { width: 30 }; + return ( + <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem> + <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size={'xs'}> + <h4>Office 365</h4> + </EuiTitle> + </EuiFlexItem> </EuiFlexGroup> - ); + </EuiFlexItem> + <EuiFlexItem> + <EuiDescriptionList listItems={listItems} compressed /> + </EuiFlexItem> + </EuiFlexGroup> + ); }; From d296d6d8063b6f12aca0c59077f4870e332fa995 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 29 Jul 2021 11:29:21 +0200 Subject: [PATCH 168/493] Added drilldown tables --- .../config/drilldown-ip-config.tsx | 4 +- .../config/drilldown-user-config.tsx | 4 +- .../overview/overview-office.ts | 186 ++++++++++++++++++ 3 files changed, 190 insertions(+), 4 deletions(-) diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index d09fa59e8e..b5ede19606 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -19,7 +19,7 @@ import { SecurityAlerts } from '../../../visualize/components'; export const DrilldownIPConfig = { rows: [ { - height: 230, + height: 400, columns: [ { width: 30, @@ -36,7 +36,7 @@ export const DrilldownIPConfig = { { width: 40, component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-IPs-By-User-Table" tab="office" {...props} /> + <VisCard id="Wazuh-App-Overview-Office-User-Operation-Level-Table" tab="office" {...props} /> ), }, ], diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx index 218951789e..d6ce5251ef 100644 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -19,7 +19,7 @@ import { SecurityAlerts } from '../../../visualize/components'; export const DrilldownUserConfig = { rows: [ { - height: 230, + height: 400, columns: [ { width: 30, @@ -36,7 +36,7 @@ export const DrilldownUserConfig = { { width: 40, component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-IPs-By-User-Table" tab="office" {...props} /> + <VisCard id="Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table" tab="office" {...props} /> ), }, ], diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 9474bfd5d7..6ef04f5295 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -1051,6 +1051,192 @@ export default [ }, }, }, + { + _id: 'Wazuh-App-Overview-Office-User-Operation-Level-Table', + _type: 'visualization', + _source: { + title: 'User Operations', + visState: JSON.stringify({ + "title": "User Operation Level", + "type": "table", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.UserId", + "orderBy": "1", + "order": "desc", + "size": 500, + "otherBucket": true, + "otherBucketLabel": "Others", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Users" + }, + "schema": "bucket" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.Operation", + "orderBy": "1", + "order": "desc", + "size": 100, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Operation" + }, + "schema": "bucket" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.level", + "orderBy": "1", + "order": "desc", + "size": 20, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Rule level" + }, + "schema": "bucket" + } + ], + "params": { + "perPage": 5, + "showPartialRows": false, + "showMetricsAtAllLevels": false, + "sort": { + "columnIndex": null, + "direction": null + }, + "showTotal": false, + "totalFunc": "sum", + "percentageCol": "" + } + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table', + _type: 'visualization', + _source: { + title: 'Client IP Operations', + visState: JSON.stringify({ + "title": "Client IP Operation Level", + "type": "table", + "aggs": [ + { + "id": "1", + "enabled": true, + "type": "count", + "params": {}, + "schema": "metric" + }, + { + "id": "2", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.ClientIP", + "orderBy": "1", + "order": "desc", + "size": 500, + "otherBucket": true, + "otherBucketLabel": "Others", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Client IP" + }, + "schema": "bucket" + }, + { + "id": "3", + "enabled": true, + "type": "terms", + "params": { + "field": "data.office365.Operation", + "orderBy": "1", + "order": "desc", + "size": 100, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Operation" + }, + "schema": "bucket" + }, + { + "id": "4", + "enabled": true, + "type": "terms", + "params": { + "field": "rule.level", + "orderBy": "1", + "order": "desc", + "size": 20, + "otherBucket": false, + "otherBucketLabel": "Other", + "missingBucket": false, + "missingBucketLabel": "Missing", + "customLabel": "Rule level" + }, + "schema": "bucket" + } + ], + "params": { + "perPage": 5, + "showPartialRows": false, + "showMetricsAtAllLevels": false, + "sort": { + "columnIndex": null, + "direction": null + }, + "showTotal": false, + "totalFunc": "sum", + "percentageCol": "" + } + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, { _id: 'Wazuh-App-Overview-Office-Top-Events-Pie', _type: 'visualization', From 8dd021bab1bd9207009b526bf5d3c7c31cb8c02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 12:12:31 +0200 Subject: [PATCH 169/493] Changed items currently using useFilterManager to pick a property of an object Switched useState by useMemo in use-filter-manager --- public/components/common/hocs/withKibanaContext.tsx | 2 +- public/components/common/hooks/use-es-search.ts | 2 +- public/components/common/hooks/use-filter-manager.ts | 9 ++++----- public/components/common/modules/panel/main-panel.tsx | 2 +- .../components/visualize/components/security-alerts.tsx | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/public/components/common/hocs/withKibanaContext.tsx b/public/components/common/hocs/withKibanaContext.tsx index a9669d87f9..e4dbab3ee3 100644 --- a/public/components/common/hocs/withKibanaContext.tsx +++ b/public/components/common/hocs/withKibanaContext.tsx @@ -35,7 +35,7 @@ export interface withKibanaContextExtendsProps { export const withKibanaContext = <T extends object>(Component:React.FunctionComponent<T>) => { function hoc(props:T & withKibanaContextProps ):React.FunctionComponentElement<T & withKibanaContextExtendsProps> { const indexPattern = props.indexPattern ? props.indexPattern : useIndexPattern(); - const filterManager = props.filterManager ? props.filterManager : useFilterManager(); + const filterManager = props.filterManager ? props.filterManager : useFilterManager().filterManager; const [query, setQuery] = props.query ? useState(props.query) : useQueryManager(); const { timeFilter, timeHistory, setTimeFilter } = props.timeFilter ? props.timeFilter : useTimeFilter(); return <Component {...props} diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 06d69f3274..89ca0e1321 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -11,7 +11,7 @@ You can find more info on how to construct a filter object at https://www.elasti const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const [esResults, setEsResults] = useState({}); const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); const [error, setError] = useState(null); diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index ba06229b37..ca8a1032bd 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -10,10 +10,9 @@ * Find more information about this on the LICENSE file. */ import { getDataPlugin } from '../../../kibana-services'; -import { useState, useEffect} from 'react'; - +import { useState, useEffect, useMemo } from 'react'; export const useFilterManager = () => { - const [filterManager, setFilterManager] = useState(getDataPlugin().query.filterManager); - return filterManager; -} \ No newline at end of file + const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); + return { filterManager }; +}; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 99ab3131cc..8eeb884de0 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -38,7 +38,7 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const buildOverviewVisualization = async () => { const tabVisualizations = new TabVisualizations(); diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index bd2c67bb37..762f51aa61 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -16,7 +16,7 @@ import { useAllowedAgents } from '../../common/hooks/useAllowedAgents' export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']}) => { const [query] = useQuery(); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const copyOfFilterManager = filterManager const refreshAngularDiscover = useRefreshAngularDiscover(); From 5e8b73267462ea090902bf6a8675fa5f82f0c333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 29 Jul 2021 12:23:23 +0200 Subject: [PATCH 170/493] feat(github_module): Add the configuration viewer for the GitHub module --- .../configuration/configuration-settings.js | 6 + .../configuration/configuration-switch.js | 9 ++ .../configuration/github/github.tsx | 108 ++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 public/controllers/management/components/management/configuration/github/github.tsx diff --git a/public/controllers/management/components/management/configuration/configuration-settings.js b/public/controllers/management/components/management/configuration/configuration-settings.js index 052921f913..abd90e7887 100644 --- a/public/controllers/management/components/management/configuration/configuration-settings.js +++ b/public/controllers/management/components/management/configuration/configuration-settings.js @@ -110,6 +110,12 @@ export default [ description: 'Configuration assessment using CIS scanner and SCAP checks', goto: 'cis-cat' + }, + { + name: 'GitHub', + description: + 'Detect threats targeting GitHub organizations', + goto: 'github' } ] }, diff --git a/public/controllers/management/components/management/configuration/configuration-switch.js b/public/controllers/management/components/management/configuration/configuration-switch.js index 8540fd0073..4733b1a1b0 100644 --- a/public/controllers/management/components/management/configuration/configuration-switch.js +++ b/public/controllers/management/components/management/configuration/configuration-switch.js @@ -42,6 +42,7 @@ import WzConfigurationIntegrityAgentless from './agentless/agentless'; import WzConfigurationIntegrityAmazonS3 from './aws-s3/aws-s3'; import WzConfigurationAzureLogs from './azure-logs/azure-logs'; import WzConfigurationGoogleCloudPubSub from './google-cloud-pub-sub/google-cloud-pub-sub'; +import { WzConfigurationGitHub } from './github/github'; import WzViewSelector, { WzViewSelectorSwitch } from './util-components/view-selector'; @@ -426,6 +427,14 @@ class WzConfigurationSwitch extends Component { updateConfigurationSection={this.updateConfigurationSection} /> </WzViewSelectorSwitch> + <WzViewSelectorSwitch view="github"> + <WzConfigurationGitHub + clusterNodeSelected={this.props.clusterNodeSelected} + agent={agent} + updateBadge={this.updateBadge} + updateConfigurationSection={this.updateConfigurationSection} + /> + </WzViewSelectorSwitch> </WzViewSelector> )} </EuiPanel> diff --git a/public/controllers/management/components/management/configuration/github/github.tsx b/public/controllers/management/components/management/configuration/github/github.tsx new file mode 100644 index 0000000000..3e41b215a6 --- /dev/null +++ b/public/controllers/management/components/management/configuration/github/github.tsx @@ -0,0 +1,108 @@ +/* + * Wazuh app - React component for show configuration of GitHub. + * Copyright (C) 2015-2021 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, { useEffect, useMemo } from 'react'; +import { EuiBasicTable } from '@elastic/eui'; +import { compose } from 'redux'; +import WzConfigurationSettingsTabSelector from '../util-components/configuration-settings-tab-selector'; +import WzConfigurationSettingsGroup from '../util-components/configuration-settings-group'; +import WzTabSelector, { + WzTabSelectorTab +} from '../util-components/tab-selector'; +import WzNoConfig from '../util-components/no-config'; +import { isString, renderValueYesThenEnabled } from '../utils/utils'; +import { wodleBuilder } from '../utils/builders'; +import { withGuard } from '../../../../../../components/common/hocs'; +import withWzConfig from '../util-hocs/wz-config'; + +const sections = [{ component: 'wmodules', configuration: 'wmodules' }]; + +const mainSettings = [ + { field: 'enabled', label: 'Enabled', render: renderValueYesThenEnabled }, + { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started' }, + { field: 'time_delay', label: 'Time in seconds that each scan will monitor until that delay backwards' }, + { field: 'curl_max_size', label: 'Maximum size allowed for the GitHub API response' }, + { field: 'interval', label: 'Interval between GitHub wodle executions in seconds' }, + { field: 'event_type', label: 'Event type' }, +]; + +const columns = [ + { field: 'org_name', name: 'Name' } +]; + +const helpLinks = [ + { + text: 'Using Wazuh to audit GitHub organizations', + href: 'https://documentation.wazuh.com/current/github/index.html' + }, + { + text: 'GitHub module reference', + href: 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/github-module.html' + } +]; + +export const WzConfigurationGitHub = withWzConfig(sections)(({currentConfig, updateBadge, ...rest }) => { + const wodleConfiguration = useMemo(() => wodleBuilder(currentConfig, 'github'), [currentConfig]); + + useEffect(() => { + updateBadge(currentConfig && + wodleConfiguration && + wodleConfiguration['github'] && + wodleConfiguration['github'].enabled === 'yes'); + }, [currentConfig]); + + return ( + <WzTabSelector> + <WzTabSelectorTab label="General"> + <GeneralTab wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} {...rest}/> + </WzTabSelectorTab> + <WzTabSelectorTab label="Organizations"> + <OrganizationsTab wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} {...rest}/> + </WzTabSelectorTab> + </WzTabSelector> + ) +}); + + +const tabWrapper = compose( + withGuard(({currentConfig}) => currentConfig['wmodules-wmodules'] && isString(currentConfig['wmodules-wmodules']), ({currentConfig}) => <WzNoConfig error={currentConfig['wmodules-wmodules']} help={helpLinks}/>), + withGuard(({wodleConfiguration}) => !wodleConfiguration['github'], (props) => <WzNoConfig error='not-present' help={helpLinks}/>), +); + +const GeneralTab = tabWrapper(({agent, wodleConfiguration}) => ( + <WzConfigurationSettingsTabSelector + title="Main settings" + description="Configuration for the GitHub module" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 260 : 320} + helpLinks={helpLinks} + > + <WzConfigurationSettingsGroup + config={wodleConfiguration['github']} + items={mainSettings} + /> + </WzConfigurationSettingsTabSelector> +)); + +const OrganizationsTab = tabWrapper(({agent, wodleConfiguration}) => ( + <WzConfigurationSettingsTabSelector + title="List of organizations to auditing" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 260 : 320} + helpLinks={helpLinks} + > + <EuiBasicTable + columns={columns} + items={wodleConfiguration['github'].api_auth} + /> + </WzConfigurationSettingsTabSelector> +)); From 22d32b8724b4eaf65b8a7e0d958b5ff73772c39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 29 Jul 2021 12:31:24 +0200 Subject: [PATCH 171/493] fix(github_module): Move to Github configuration viewer to `Cloud security monitoring` category --- .../configuration/configuration-settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/controllers/management/components/management/configuration/configuration-settings.js b/public/controllers/management/components/management/configuration/configuration-settings.js index abd90e7887..38880eb590 100644 --- a/public/controllers/management/components/management/configuration/configuration-settings.js +++ b/public/controllers/management/components/management/configuration/configuration-settings.js @@ -110,12 +110,6 @@ export default [ description: 'Configuration assessment using CIS scanner and SCAP checks', goto: 'cis-cat' - }, - { - name: 'GitHub', - description: - 'Detect threats targeting GitHub organizations', - goto: 'github' } ] }, @@ -213,6 +207,12 @@ export default [ name: 'Google Cloud Pub/Sub', description: 'Configuration options of the Google Cloud Pub/Sub module', goto: 'gcp-pubsub' + }, + { + name: 'GitHub', + description: + 'Detect threats targeting GitHub organizations', + goto: 'github' } ] } From 083cc6d68a84155975ea35754ad77255807e570f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 12:33:44 +0200 Subject: [PATCH 172/493] Set useFilterManager to handle its own subscription Removed subscription handling from useEsSearch --- .../components/common/hooks/use-es-search.ts | 22 ++++--------------- .../common/hooks/use-filter-manager.ts | 17 +++++++++++++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 1858ba24e9..b5fc92afe8 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -13,8 +13,7 @@ import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useFilterManager, useIndexPattern, useQuery } from '.'; -import _ from 'lodash'; -import { Filter, IndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { UI_ERROR_SEVERITIES, UIErrorLog, @@ -44,10 +43,9 @@ interface IUseEsSearch { const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }): IUseEsSearch => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const {filterManager} = useFilterManager(); + const {filters} = useFilterManager(); const [query] = useQuery(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); - const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); const [page, setPage] = useState<number>(0); @@ -74,25 +72,13 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setIsLoading(false); } })(); - }, [indexPattern, query, managedFilters, page]); - - useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - if (!_.isEqual(managedFilters, newFilters)) { - setManagedFilters(newFilters); - } - return () => { - filterSubscriber.unsubscribe(); - }; - }); - }, []); + }, [indexPattern, query, filters, page]); const search = async (): Promise<SearchResponse> => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); - const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; + const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...filters]; return await searchSource .setParent(undefined) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index ca8a1032bd..1102ba09b6 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -11,8 +11,23 @@ */ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect, useMemo } from 'react'; +import { Filter } from 'src/plugins/data/public'; +import _ from 'lodash'; export const useFilterManager = () => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - return { filterManager }; + const [filters, setFilters] = useState<Filter[]>([]) + + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const newFilters = filterManager.getFilters(); + if (!_.isEqual(filters, newFilters)) { + setFilters(newFilters); + } + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }, []); + return { filterManager, filters }; }; From 4c88d9b204aba602b7044f5edebf685f3c2b9442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 12:42:55 +0200 Subject: [PATCH 173/493] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49128be0ae..96b36af0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +- Moved the filterManager subscription to the hook useFilterManager [#3513](https://github.com/wazuh/wazuh-kibana-app/pull/3513) ### Fixed From 658596df93b1df38b4a574f504cc68eab455b7b9 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 29 Jul 2021 12:44:17 +0200 Subject: [PATCH 174/493] fix ts and sort suggested values --- .../custom-search-bar/components/combobox.tsx | 2 +- .../custom-search-bar.test.tsx | 41 ------------------- .../custom-search-bar/custom-search-bar.tsx | 8 ++-- 3 files changed, 4 insertions(+), 47 deletions(-) delete mode 100644 public/components/common/custom-search-bar/custom-search-bar.test.tsx diff --git a/public/components/common/custom-search-bar/components/combobox.tsx b/public/components/common/custom-search-bar/components/combobox.tsx index e5fbf486cd..47f3e1aadf 100644 --- a/public/components/common/custom-search-bar/components/combobox.tsx +++ b/public/components/common/custom-search-bar/components/combobox.tsx @@ -10,7 +10,7 @@ export const Combobox = ({ item, ...props }) => { const { suggestedValues, isLoading, setQuery } = useValueSuggestions(item.key) - const comboOptions = suggestedValues.map((value,key) => ({ key:key, label:value, value:item.key})) + const comboOptions = suggestedValues.map((value,key) => ({ key:key, label:value, value:item.key})).sort((a, b) => a.label - b.label) return ( <EuiComboBox diff --git a/public/components/common/custom-search-bar/custom-search-bar.test.tsx b/public/components/common/custom-search-bar/custom-search-bar.test.tsx deleted file mode 100644 index cdb4574d33..0000000000 --- a/public/components/common/custom-search-bar/custom-search-bar.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ - -/* - * Wazuh app - Custom Search Bar Component - Test - * - * Copyright (C) 2015-2021 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 from 'react'; -import { shallow } from 'enzyme'; -import { CustomSearchBar } from '../custom-search-bar'; - -describe('Search Bar container', () => { - const mock = [{ - type: 'combobox', - key: 'agent.name', - values: [ - { - key: 'agent.name', - label: 'Amazon', - }, - { - key: 'agent.name', - label: 'Centos', - } - ], - },] - - test('should render the component', () => { - const component = shallow(<CustomSearchBar filtersValues={mock}/>); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 9ed97d61f2..d5b5d71863 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -7,7 +7,6 @@ import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../src import { EuiFlexGroup, EuiFlexItem, - EuiComboBox, EuiSwitch, } from '@elastic/eui'; @@ -15,7 +14,6 @@ import { import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; -import { ModulesHelper } from '../modules/modules-helper'; import { Combobox } from './components' export const CustomSearchBar = ({ filtersValues, ...props }) => { @@ -94,7 +92,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { return { meta, $state, query }; }; - const setKibanaFilters = (values) => { + const setKibanaFilters = (values: any[]) => { setLoading(true) const newFilters = [] const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) @@ -129,12 +127,12 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { setLoading(false) }; - const onChange = (values) => { + const onChange = (values: any[]) => { setKibanaFilters(values) refreshCustomSelectedFilter(); }; - const getComponent = (item) => { + const getComponent = (item: any) => { var types = { 'default': <></>, 'combobox': <Combobox From 69e367259429a3813fb0d6bd64b45a650d74d735 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 29 Jul 2021 13:03:39 +0200 Subject: [PATCH 175/493] improved sidebar panel styles and error handling --- .../panel/components/module-side-panel.tsx | 2 +- .../overview/office-panel/office-panel.tsx | 2 +- .../office-panel/views/office-stats.scss | 20 ++++++++++++++++++ .../office-panel/views/office-stats.tsx | 21 ++++++++++++++----- 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 public/components/overview/office-panel/views/office-stats.scss diff --git a/public/components/common/modules/panel/components/module-side-panel.tsx b/public/components/common/modules/panel/components/module-side-panel.tsx index bb8c135d57..64f5a5c75e 100644 --- a/public/components/common/modules/panel/components/module-side-panel.tsx +++ b/public/components/common/modules/panel/components/module-side-panel.tsx @@ -35,7 +35,7 @@ export const ModuleSidePanel = ({ navIsDocked = false, children, ...props }) => > <div> <EuiButtonEmpty - style={{ float: 'right' }} + style={{ position: 'absolute', right: 0 }} onClick={() => setNavIsOpen(!navIsOpen)} iconType={'cross'} /> diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index b2b198a80a..1d7fdd8f20 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -62,7 +62,7 @@ export const OfficePanel = withErrorBoundary(() => { }, }; getErrorOrchestrator().handleError(options); - setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); + setModuleStatsList([]); } })(); }, []); diff --git a/public/components/overview/office-panel/views/office-stats.scss b/public/components/overview/office-panel/views/office-stats.scss new file mode 100644 index 0000000000..63fe17d180 --- /dev/null +++ b/public/components/overview/office-panel/views/office-stats.scss @@ -0,0 +1,20 @@ +.euiTitle.office-stats-title{ + white-space: nowrap; +} +h5.euiTitle.office-stats-subtitle{ + white-space: nowrap; + margin: 0; + font-weight: normal; +} +.office-stats-callout-warning{ + margin: 0 -16px; + padding: 16px 38px; +} + +element.style { +} + +dt.euiDescriptionList__title, dd.euiDescriptionList__description { + border-left: 2px solid rgba(0, 0, 0, 0.3); + padding-left: 10px; +} \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index bf8b39f4c4..62a0447f05 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -11,28 +11,39 @@ * Find more information about this on the LICENSE file. */ -import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle } from '@elastic/eui'; +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiCallOut } from '@elastic/eui'; import moduleLogo from '../../../../assets/office365.svg'; import React from 'react'; +import './office-stats.scss'; export const OfficeStats = ({ listItems = [] }) => { const logoStyle = { width: 30 }; return ( <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> <EuiFlexItem> - <EuiFlexGroup> - <EuiFlexItem> + <EuiFlexGroup justifyContent={'center'}> + <EuiFlexItem > <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> </EuiFlexItem> <EuiFlexItem> + <EuiTitle size={'s'}> + <h4 className={'office-stats-title'}>Office 365</h4> + </EuiTitle> <EuiTitle size={'xs'}> - <h4>Office 365</h4> + <h5 className={'office-stats-subtitle'}>Module configuration</h5> </EuiTitle> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem> - <EuiDescriptionList listItems={listItems} compressed /> + { + listItems.length ? ( + <EuiDescriptionList listItems={listItems} compressed />) : ( + <EuiCallOut className={'office-stats-callout-warning'} + title="Module configuration unavailable" + color="warning" + iconType="warning" />) + } </EuiFlexItem> </EuiFlexGroup> ); From 97280ae2161413dd89881dbffc34cdc7567397be Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 29 Jul 2021 13:12:17 +0200 Subject: [PATCH 176/493] Fixed side panel logo styles --- .../components/overview/office-panel/views/office-stats.scss | 3 ++- .../components/overview/office-panel/views/office-stats.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/public/components/overview/office-panel/views/office-stats.scss b/public/components/overview/office-panel/views/office-stats.scss index 63fe17d180..18c81783ff 100644 --- a/public/components/overview/office-panel/views/office-stats.scss +++ b/public/components/overview/office-panel/views/office-stats.scss @@ -11,7 +11,8 @@ h5.euiTitle.office-stats-subtitle{ padding: 16px 38px; } -element.style { +.wz-justify-center { + justify-content: center; } dt.euiDescriptionList__title, dd.euiDescriptionList__description { diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index 62a0447f05..952b0af247 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -21,8 +21,8 @@ export const OfficeStats = ({ listItems = [] }) => { return ( <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> <EuiFlexItem> - <EuiFlexGroup justifyContent={'center'}> - <EuiFlexItem > + <EuiFlexGroup> + <EuiFlexItem className={'wz-justify-center'}> <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> </EuiFlexItem> <EuiFlexItem> From d2686416292c7be47a5a4e2413c1d8adc8fef0b3 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 29 Jul 2021 16:02:33 +0200 Subject: [PATCH 177/493] fix aesthetic changes --- .../common/custom-search-bar/components/combobox.tsx | 2 +- .../common/custom-search-bar/custom-search-bar.tsx | 4 ++-- public/styles/common.scss | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/public/components/common/custom-search-bar/components/combobox.tsx b/public/components/common/custom-search-bar/components/combobox.tsx index 47f3e1aadf..8924d831f4 100644 --- a/public/components/common/custom-search-bar/components/combobox.tsx +++ b/public/components/common/custom-search-bar/components/combobox.tsx @@ -15,7 +15,7 @@ export const Combobox = ({ item, ...props }) => { return ( <EuiComboBox data-test-subj={`combobox-${item.key}`} - placeholder={`Select ${item.key}`} + placeholder={item.key} className={'filters-custom-combobox'} options={comboOptions} isClearable={false} diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index d5b5d71863..fed3bb9090 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -146,11 +146,11 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { return ( <> - <EuiFlexGroup alignItems='center' style={{ margin: '0 8px' }}> + <EuiFlexGroup className='custom-kbn-search-bar' alignItems='center' style={{ margin: '0 8px' }}> { avancedFiltersState === false ? filtersValues.map((item, key) => ( - <EuiFlexItem grow={2} key={key}> + <EuiFlexItem key={key}> {getComponent(item)} </EuiFlexItem> )) diff --git a/public/styles/common.scss b/public/styles/common.scss index 80155fb8b6..57de97d8da 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1750,4 +1750,14 @@ iframe.width-changed { .application .filters-search-bar .globalQueryBar, .app-container .filters-search-bar .globalQueryBar{ padding: 0 !important; +} + +@media only screen and (max-width: 960px){ + .custom-kbn-search-bar .euiSuperUpdateButton .euiSuperUpdateButton__text { + display: none; + } + + .custom-kbn-search-bar .euiSuperUpdateButton { + min-width: 0; + } } \ No newline at end of file From f221239b50276f2d976ef4b26847f95134a3bc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 16:19:31 +0200 Subject: [PATCH 178/493] Tweaked the unsubscription method --- public/components/common/hooks/use-filter-manager.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index 1102ba09b6..d9b03540a6 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -16,17 +16,15 @@ import _ from 'lodash'; export const useFilterManager = () => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - const [filters, setFilters] = useState<Filter[]>([]) + const [filters, setFilters] = useState<Filter[]>([]); useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const { unsubscribe } = filterManager.getUpdates$().subscribe(() => { const newFilters = filterManager.getFilters(); if (!_.isEqual(filters, newFilters)) { setFilters(newFilters); } - return () => { - filterSubscriber.unsubscribe(); - }; + return unsubscribe; }); }, []); return { filterManager, filters }; From 55864c89787f691c7d7165f4cb46b53b8d8bcebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 17:08:12 +0200 Subject: [PATCH 179/493] Fixed unsubscription typo and set it to return of useEffect --- public/components/common/hooks/use-filter-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index d9b03540a6..7e4d000d41 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -24,8 +24,8 @@ export const useFilterManager = () => { if (!_.isEqual(filters, newFilters)) { setFilters(newFilters); } - return unsubscribe; }); + return unsubscribe; }, []); return { filterManager, filters }; }; From 08e480ec5b97d879a95cb2874aed1f7a18f5fa7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 29 Jul 2021 17:20:05 +0200 Subject: [PATCH 180/493] fix(github_module): Recover stats in GitHub Dashboard --- public/components/overview/metrics/metrics.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index 4a2a84ebd1..def528e905 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -278,6 +278,11 @@ export const Metrics = withAllowedAgents( { name: "Full Access Permissions", type: "phrase", value: "91725", field: "rule.id" }, { name: "Phishing and Malware", type: "phrases", values: ["91556", "91575", "91700"], field: "rule.id", color: "danger" }, ], + github: [ + { name: "Organizations", type: "unique-count", field: "data.github.org"}, + { name: "Repositories", type: "unique-count", field: "data.github.repo", color: "secondary"}, + { name: "Actors", type: "unique-count", field: "data.github.actor", color: "danger"}, + ] }; } From 5067093d287d0458823ae4bfaff2452a2383dc10 Mon Sep 17 00:00:00 2001 From: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Date: Thu, 29 Jul 2021 17:29:09 +0200 Subject: [PATCH 181/493] Revert "Moved filter subscription to useFilterManager" --- CHANGELOG.md | 1 - .../common/hocs/withKibanaContext.tsx | 2 +- .../components/common/hooks/use-es-search.ts | 22 +++++++++++++++---- .../common/hooks/use-filter-manager.ts | 22 +++++-------------- .../common/modules/panel/main-panel.tsx | 2 +- .../visualize/components/security-alerts.tsx | 2 +- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b36af0cd..49128be0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) -- Moved the filterManager subscription to the hook useFilterManager [#3513](https://github.com/wazuh/wazuh-kibana-app/pull/3513) ### Fixed diff --git a/public/components/common/hocs/withKibanaContext.tsx b/public/components/common/hocs/withKibanaContext.tsx index e4dbab3ee3..a9669d87f9 100644 --- a/public/components/common/hocs/withKibanaContext.tsx +++ b/public/components/common/hocs/withKibanaContext.tsx @@ -35,7 +35,7 @@ export interface withKibanaContextExtendsProps { export const withKibanaContext = <T extends object>(Component:React.FunctionComponent<T>) => { function hoc(props:T & withKibanaContextProps ):React.FunctionComponentElement<T & withKibanaContextExtendsProps> { const indexPattern = props.indexPattern ? props.indexPattern : useIndexPattern(); - const filterManager = props.filterManager ? props.filterManager : useFilterManager().filterManager; + const filterManager = props.filterManager ? props.filterManager : useFilterManager(); const [query, setQuery] = props.query ? useState(props.query) : useQueryManager(); const { timeFilter, timeHistory, setTimeFilter } = props.timeFilter ? props.timeFilter : useTimeFilter(); return <Component {...props} diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index b5fc92afe8..ef7caf85a0 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -13,7 +13,8 @@ import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useFilterManager, useIndexPattern, useQuery } from '.'; -import { IndexPattern } from 'src/plugins/data/public'; +import _ from 'lodash'; +import { Filter, IndexPattern } from 'src/plugins/data/public'; import { UI_ERROR_SEVERITIES, UIErrorLog, @@ -43,9 +44,10 @@ interface IUseEsSearch { const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }): IUseEsSearch => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const {filters} = useFilterManager(); + const filterManager = useFilterManager(); const [query] = useQuery(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); + const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); const [page, setPage] = useState<number>(0); @@ -72,13 +74,25 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setIsLoading(false); } })(); - }, [indexPattern, query, filters, page]); + }, [indexPattern, query, managedFilters, page]); + + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const newFilters = filterManager.getFilters(); + if (!_.isEqual(managedFilters, newFilters)) { + setManagedFilters(newFilters); + } + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }, []); const search = async (): Promise<SearchResponse> => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); - const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...filters]; + const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; return await searchSource .setParent(undefined) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index 7e4d000d41..ba06229b37 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -10,22 +10,10 @@ * Find more information about this on the LICENSE file. */ import { getDataPlugin } from '../../../kibana-services'; -import { useState, useEffect, useMemo } from 'react'; -import { Filter } from 'src/plugins/data/public'; -import _ from 'lodash'; +import { useState, useEffect} from 'react'; -export const useFilterManager = () => { - const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - const [filters, setFilters] = useState<Filter[]>([]); - useEffect(() => { - const { unsubscribe } = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - if (!_.isEqual(filters, newFilters)) { - setFilters(newFilters); - } - }); - return unsubscribe; - }, []); - return { filterManager, filters }; -}; +export const useFilterManager = () => { + const [filterManager, setFilterManager] = useState(getDataPlugin().query.filterManager); + return filterManager; +} \ No newline at end of file diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 8eeb884de0..99ab3131cc 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -38,7 +38,7 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); - const {filterManager} = useFilterManager(); + const filterManager = useFilterManager(); const buildOverviewVisualization = async () => { const tabVisualizations = new TabVisualizations(); diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index 762f51aa61..bd2c67bb37 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -16,7 +16,7 @@ import { useAllowedAgents } from '../../common/hooks/useAllowedAgents' export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']}) => { const [query] = useQuery(); - const {filterManager} = useFilterManager(); + const filterManager = useFilterManager(); const copyOfFilterManager = filterManager const refreshAngularDiscover = useRefreshAngularDiscover(); From ede6eb8a023dbd468eee332a7be98aa376194cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 29 Jul 2021 17:54:42 +0200 Subject: [PATCH 182/493] fix(github_module): Replace field of aggregation table to `data.github.repository` --- public/components/overview/github-panel/config/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/overview/github-panel/config/main.tsx b/public/components/overview/github-panel/config/main.tsx index 740a6c14fe..ebbec8dfe6 100644 --- a/public/components/overview/github-panel/config/main.tsx +++ b/public/components/overview/github-panel/config/main.tsx @@ -54,7 +54,7 @@ export const MainViewConfig = { <EuiFlexItem grow={props.width}> <AggTable tableTitle='Repositories' - aggTerm='data.github.repo' + aggTerm='data.github.repository' aggLabel='Respository' maxRows={5} onRowClick={props.onRowClick} /> From d194cbf7ca080c0276dcd102e65d6bdf04fb9de4 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 29 Jul 2021 19:15:10 +0200 Subject: [PATCH 183/493] fix panel config and styles --- .../panel/components/module-side-panel.scss | 4 +++ .../office-panel/config/main-view-config.tsx | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/public/components/common/modules/panel/components/module-side-panel.scss b/public/components/common/modules/panel/components/module-side-panel.scss index 51e05444e0..7867067b62 100644 --- a/public/components/common/modules/panel/components/module-side-panel.scss +++ b/public/components/common/modules/panel/components/module-side-panel.scss @@ -2,6 +2,7 @@ border-radius: 0 5px 5px 0; background: #ffffffab; border: 1px solid #006bb459; + border-left: 0; box-shadow: 1px 1px 3px -1px #000; position: fixed; left: 0; @@ -11,3 +12,6 @@ padding-left: 6px; height: 55px; } +body.euiBody--collapsibleNavIsDocked .sidepanel-infoBtnStyle { + left: 320px; +} \ No newline at end of file diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 032b826255..37dd700b41 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -19,30 +19,33 @@ import { SecurityAlerts } from '../../../visualize/components'; export const MainViewConfig = { rows: [ { - // height: 300, columns: [ { width: 50, component: (props) => ( - <AggTable - tableTitle={''} - aggTerm={'data.office365.UserId'} - aggLabel={'User'} - maxRows={'7'} - onRowClick={(field, value) => props.onRowClick(field, value)} - /> + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle={'Users'} + aggTerm={'data.office365.UserId'} + aggLabel={'User'} + maxRows={'5'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + </EuiFlexItem> ), }, { width: 50, component: (props) => ( - <AggTable - tableTitle={''} - aggTerm={'data.office365.ClientIP'} - aggLabel={'Client IP'} - maxRows={'7'} - onRowClick={(field, value) => props.onRowClick(field, value)} - /> + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle={'Client IP'} + aggTerm={'data.office365.ClientIP'} + aggLabel={'Client IP'} + maxRows={'5'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + </EuiFlexItem> ), }, ], From d21b1473608056ab9e70338f16d3e1b93fbf9236 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Thu, 29 Jul 2021 14:19:14 -0300 Subject: [PATCH 184/493] feat(officePanel): Reduced drilldown-config to one file with dynamic drilldownConfig by id. Applied TS on files. --- .../config/drilldown-ip-config.tsx | 123 +++++++++--------- .../config/drilldown-user-config.tsx | 75 ----------- .../overview/office-panel/config/index.ts | 3 +- .../office-panel/config/module-config.tsx | 16 ++- .../office-panel/config/search-bar-config.ts | 4 +- 5 files changed, 79 insertions(+), 142 deletions(-) delete mode 100644 public/components/overview/office-panel/config/drilldown-user-config.tsx diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index b5ede19606..68070a14e5 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -1,5 +1,6 @@ +'use strict'; /* - * Wazuh app - Office 365 DrilldownIPConfig. + * Wazuh app - Office 365 DrilldownConfig. * * Copyright (C) 2015-2021 Wazuh, Inc. * @@ -11,65 +12,71 @@ * Find more information about this on the LICENSE file. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { VisCard } from '../../../common/modules/panel'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; -export const DrilldownIPConfig = { - rows: [ - { - height: 400, - columns: [ - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> - ), - }, - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> - ), - }, - { - width: 40, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-User-Operation-Level-Table" tab="office" {...props} /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: (props) => ( - <VisCard - id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" - tab="office" - {...props} - /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: () => ( - <EuiFlexItem> - <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> - </EuiPanel> - </EuiFlexItem> - ), - }, - ], - }, - ], +export const drilldownConfig = (visCardId: string): ReactNode => { + return { + rows: [ + { + height: 400, + columns: [ + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> + ), + }, + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> + ), + }, + { + width: 40, + component: (props) => ( + <VisCard + id={visCardId} //<--- el id + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), + }, + ], + }, + ], + }; }; diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx deleted file mode 100644 index d6ce5251ef..0000000000 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Wazuh app - Office 365 DrilldownUserConfig. - * - * Copyright (C) 2015-2021 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 from 'react'; -import { VisCard } from '../../../common/modules/panel'; -import { EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { SecurityAlerts } from '../../../visualize/components'; - -export const DrilldownUserConfig = { - rows: [ - { - height: 400, - columns: [ - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> - ), - }, - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> - ), - }, - { - width: 40, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table" tab="office" {...props} /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: (props) => ( - <VisCard - id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" - tab="office" - {...props} - /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: () => ( - <EuiFlexItem> - <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> - </EuiPanel> - </EuiFlexItem> - ), - }, - ], - }, - ], -}; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index ec36aaacad..d9c378b5a8 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -11,8 +11,7 @@ * Find more information about this on the LICENSE file. */ -export { DrilldownUserConfig } from './drilldown-user-config'; -export { DrilldownIPConfig } from './drilldown-ip-config'; +export { drilldownConfig } from './drilldown-ip-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 5ed1e28043..b9988aecd8 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -13,7 +13,13 @@ import React from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; -import { MainViewConfig, DrilldownUserConfig, DrilldownIPConfig } from './'; +import { MainViewConfig } from './'; +import { drilldownConfig } from './drilldown-ip-config'; + +const drilldownUserConfig = drilldownConfig('Wazuh-App-Overview-Office-User-Operation-Level-Table'); +const drilldownIPConfig = drilldownConfig( + 'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table' +); export const ModuleConfig = { main: { @@ -21,15 +27,15 @@ export const ModuleConfig = { component: (props) => <OfficeBody {...{ ...MainViewConfig, ...props }} />, }, 'data.office365.UserId': { - length: () => DrilldownUserConfig.rows.reduce((total, row) => total + row.columns.length, 0), + length: () => drilldownUserConfig.rows.reduce((total, row) => total + row.columns.length, 0), component: (props) => ( - <OfficeDrilldown title={'User Activity'} {...{ ...DrilldownUserConfig, ...props }} /> + <OfficeDrilldown title={'User Activity'} {...{ ...drilldownUserConfig, ...props }} /> ), }, 'data.office365.ClientIP': { - length: () => DrilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), + length: () => drilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), component: (props) => ( - <OfficeDrilldown title={'Client IP'} {...{ ...DrilldownIPConfig, ...props }} /> + <OfficeDrilldown title={'Client IP'} {...{ ...drilldownIPConfig, ...props }} /> ), }, }; diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index d03698b4ee..91d41f5f23 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -export const filtersValues = [ +export const filtersValues: { type: string; key: string }[] = [ { type: 'combobox', key: 'agent.id', @@ -25,5 +25,5 @@ export const filtersValues = [ { type: 'combobox', key: 'agent.ip', - } + }, ]; From cc615b75dc85b8bd0e51f87c864346b6438d3304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 30 Jul 2021 08:47:15 +0200 Subject: [PATCH 185/493] fix(github_module): Removed the height of SecurityAlerts component on the drilldown views --- .../components/overview/github-panel/config/drilldown-action.tsx | 1 - .../components/overview/github-panel/config/drilldown-actor.tsx | 1 - .../overview/github-panel/config/drilldown-organization.tsx | 1 - .../overview/github-panel/config/drilldown-repository.tsx | 1 - 4 files changed, 4 deletions(-) diff --git a/public/components/overview/github-panel/config/drilldown-action.tsx b/public/components/overview/github-panel/config/drilldown-action.tsx index a5c86e0a86..776a348639 100644 --- a/public/components/overview/github-panel/config/drilldown-action.tsx +++ b/public/components/overview/github-panel/config/drilldown-action.tsx @@ -49,7 +49,6 @@ export const DrilldownConfigAction = { ] }, { - height: 800, columns: [ { width: 100, diff --git a/public/components/overview/github-panel/config/drilldown-actor.tsx b/public/components/overview/github-panel/config/drilldown-actor.tsx index 5d96c6864e..697ff7f69e 100644 --- a/public/components/overview/github-panel/config/drilldown-actor.tsx +++ b/public/components/overview/github-panel/config/drilldown-actor.tsx @@ -49,7 +49,6 @@ export const DrilldownConfigActor = { ] }, { - height: 800, columns: [ { width: 100, diff --git a/public/components/overview/github-panel/config/drilldown-organization.tsx b/public/components/overview/github-panel/config/drilldown-organization.tsx index c29e7e02b8..637c23ebc2 100644 --- a/public/components/overview/github-panel/config/drilldown-organization.tsx +++ b/public/components/overview/github-panel/config/drilldown-organization.tsx @@ -49,7 +49,6 @@ export const DrilldownConfigOrganization = { ] }, { - height: 800, columns: [ { width: 100, diff --git a/public/components/overview/github-panel/config/drilldown-repository.tsx b/public/components/overview/github-panel/config/drilldown-repository.tsx index 78e1aa8505..8fe5a5f53f 100644 --- a/public/components/overview/github-panel/config/drilldown-repository.tsx +++ b/public/components/overview/github-panel/config/drilldown-repository.tsx @@ -49,7 +49,6 @@ export const DrilldownConfigRepository = { ] }, { - height: 800, columns: [ { width: 100, From 4ab58a336d73579b6197d1550bacf98343204662 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 30 Jul 2021 09:10:42 +0200 Subject: [PATCH 186/493] fix styles in stats module --- public/components/overview/office-panel/views/office-stats.scss | 2 +- public/components/overview/office-panel/views/office-stats.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/overview/office-panel/views/office-stats.scss b/public/components/overview/office-panel/views/office-stats.scss index 18c81783ff..572935358a 100644 --- a/public/components/overview/office-panel/views/office-stats.scss +++ b/public/components/overview/office-panel/views/office-stats.scss @@ -15,7 +15,7 @@ h5.euiTitle.office-stats-subtitle{ justify-content: center; } -dt.euiDescriptionList__title, dd.euiDescriptionList__description { +.office-description-list dt.euiDescriptionList__title, .office-description-list dd.euiDescriptionList__description { border-left: 2px solid rgba(0, 0, 0, 0.3); padding-left: 10px; } \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index 952b0af247..1eef592a33 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -38,7 +38,7 @@ export const OfficeStats = ({ listItems = [] }) => { <EuiFlexItem> { listItems.length ? ( - <EuiDescriptionList listItems={listItems} compressed />) : ( + <EuiDescriptionList className={'office-description-list'} listItems={listItems} compressed />) : ( <EuiCallOut className={'office-stats-callout-warning'} title="Module configuration unavailable" color="warning" From 95b05620768d79f13e19624a6dd006deaf6de131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 30 Jul 2021 09:14:31 +0200 Subject: [PATCH 187/493] fix(github_module): Fix the aggregation table for the Repositories - Now it uses `data.github.repo` --- public/components/overview/github-panel/config/main.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/overview/github-panel/config/main.tsx b/public/components/overview/github-panel/config/main.tsx index ebbec8dfe6..36d1b2e70e 100644 --- a/public/components/overview/github-panel/config/main.tsx +++ b/public/components/overview/github-panel/config/main.tsx @@ -54,8 +54,8 @@ export const MainViewConfig = { <EuiFlexItem grow={props.width}> <AggTable tableTitle='Repositories' - aggTerm='data.github.repository' - aggLabel='Respository' + aggTerm='data.github.repo' + aggLabel='Repository' maxRows={5} onRowClick={props.onRowClick} /> </EuiFlexItem>) From e70d20f29c69b486bf855bc35edad23c4a5c00fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 30 Jul 2021 09:15:33 +0200 Subject: [PATCH 188/493] feat(github_module): Added specific columsns to the `SecurityAlerts` component for each drilldown view --- public/components/common/modules/discover/discover.tsx | 4 ++++ .../overview/github-panel/config/drilldown-action.tsx | 10 +++++++++- .../overview/github-panel/config/drilldown-actor.tsx | 10 +++++++++- .../github-panel/config/drilldown-organization.tsx | 10 +++++++++- .../github-panel/config/drilldown-repository.tsx | 10 +++++++++- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 4f7daec002..c5dd85231b 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -151,6 +151,10 @@ export const Discover = compose( 'rule.nist_800_53': 'NIST 800-53', 'rule.tsc': 'TSC', 'rule.hipaa': 'HIPAA', + 'data.github.action': 'Action', + 'data.github.actor': 'Actor', + 'data.github.org': 'Organization', + 'data.github.repo': 'Repository' }; this.hideCreateCustomLabel.bind(this); diff --git a/public/components/overview/github-panel/config/drilldown-action.tsx b/public/components/overview/github-panel/config/drilldown-action.tsx index 776a348639..be52e018aa 100644 --- a/public/components/overview/github-panel/config/drilldown-action.tsx +++ b/public/components/overview/github-panel/config/drilldown-action.tsx @@ -52,7 +52,15 @@ export const DrilldownConfigAction = { columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.repo', 'data.github.actor', 'rule.level', 'rule.id']} + /> + </EuiPanel> + </EuiFlexItem> + ) }, ] }, diff --git a/public/components/overview/github-panel/config/drilldown-actor.tsx b/public/components/overview/github-panel/config/drilldown-actor.tsx index 697ff7f69e..4505d20b29 100644 --- a/public/components/overview/github-panel/config/drilldown-actor.tsx +++ b/public/components/overview/github-panel/config/drilldown-actor.tsx @@ -52,7 +52,15 @@ export const DrilldownConfigActor = { columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.repo', 'data.github.action', 'rule.level', 'rule.id']} + /> + </EuiPanel> + </EuiFlexItem> + ) }, ] }, diff --git a/public/components/overview/github-panel/config/drilldown-organization.tsx b/public/components/overview/github-panel/config/drilldown-organization.tsx index 637c23ebc2..0557675d03 100644 --- a/public/components/overview/github-panel/config/drilldown-organization.tsx +++ b/public/components/overview/github-panel/config/drilldown-organization.tsx @@ -52,7 +52,15 @@ export const DrilldownConfigOrganization = { columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={["icon", "timestamp", 'rule.description', 'data.github.repo', 'data.github.actor', 'data.github.action', 'rule.level', 'rule.id']} + /> + </EuiPanel> + </EuiFlexItem> + ) }, ] }, diff --git a/public/components/overview/github-panel/config/drilldown-repository.tsx b/public/components/overview/github-panel/config/drilldown-repository.tsx index 8fe5a5f53f..bccf57e449 100644 --- a/public/components/overview/github-panel/config/drilldown-repository.tsx +++ b/public/components/overview/github-panel/config/drilldown-repository.tsx @@ -52,7 +52,15 @@ export const DrilldownConfigRepository = { columns: [ { width: 100, - component: () => <EuiFlexItem><EuiPanel paddingSize={'s'} ><SecurityAlerts /></EuiPanel></EuiFlexItem> + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.actor', 'data.github.action', 'rule.level', 'rule.id']} + /> + </EuiPanel> + </EuiFlexItem> + ) }, ] }, From db63d302fa9fde0d92d45237e257a5dff2e9662a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 30 Jul 2021 10:34:58 +0200 Subject: [PATCH 189/493] Changed items currently using useFilterManager to pick a property of an object Switched useState by useMemo in use-filter-manager --- public/components/common/hocs/withKibanaContext.tsx | 2 +- public/components/common/hooks/use-es-search.ts | 2 +- public/components/common/hooks/use-filter-manager.ts | 9 ++++----- public/components/common/modules/panel/main-panel.tsx | 2 +- .../components/visualize/components/security-alerts.tsx | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/public/components/common/hocs/withKibanaContext.tsx b/public/components/common/hocs/withKibanaContext.tsx index a9669d87f9..e4dbab3ee3 100644 --- a/public/components/common/hocs/withKibanaContext.tsx +++ b/public/components/common/hocs/withKibanaContext.tsx @@ -35,7 +35,7 @@ export interface withKibanaContextExtendsProps { export const withKibanaContext = <T extends object>(Component:React.FunctionComponent<T>) => { function hoc(props:T & withKibanaContextProps ):React.FunctionComponentElement<T & withKibanaContextExtendsProps> { const indexPattern = props.indexPattern ? props.indexPattern : useIndexPattern(); - const filterManager = props.filterManager ? props.filterManager : useFilterManager(); + const filterManager = props.filterManager ? props.filterManager : useFilterManager().filterManager; const [query, setQuery] = props.query ? useState(props.query) : useQueryManager(); const { timeFilter, timeHistory, setTimeFilter } = props.timeFilter ? props.timeFilter : useTimeFilter(); return <Component {...props} diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index ef7caf85a0..1858ba24e9 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -44,7 +44,7 @@ interface IUseEsSearch { const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }): IUseEsSearch => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const [query] = useQuery(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index ba06229b37..ca8a1032bd 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -10,10 +10,9 @@ * Find more information about this on the LICENSE file. */ import { getDataPlugin } from '../../../kibana-services'; -import { useState, useEffect} from 'react'; - +import { useState, useEffect, useMemo } from 'react'; export const useFilterManager = () => { - const [filterManager, setFilterManager] = useState(getDataPlugin().query.filterManager); - return filterManager; -} \ No newline at end of file + const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); + return { filterManager }; +}; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 99ab3131cc..8eeb884de0 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -38,7 +38,7 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const buildOverviewVisualization = async () => { const tabVisualizations = new TabVisualizations(); diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index bd2c67bb37..762f51aa61 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -16,7 +16,7 @@ import { useAllowedAgents } from '../../common/hooks/useAllowedAgents' export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']}) => { const [query] = useQuery(); - const filterManager = useFilterManager(); + const {filterManager} = useFilterManager(); const copyOfFilterManager = filterManager const refreshAngularDiscover = useRefreshAngularDiscover(); From 3c2b218b4054d7f762654bc4f910f00d6251026c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 12:33:44 +0200 Subject: [PATCH 190/493] Set useFilterManager to handle its own subscription Removed subscription handling from useEsSearch --- .../components/common/hooks/use-es-search.ts | 22 ++++--------------- .../common/hooks/use-filter-manager.ts | 17 +++++++++++++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 1858ba24e9..b5fc92afe8 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -13,8 +13,7 @@ import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useFilterManager, useIndexPattern, useQuery } from '.'; -import _ from 'lodash'; -import { Filter, IndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { UI_ERROR_SEVERITIES, UIErrorLog, @@ -44,10 +43,9 @@ interface IUseEsSearch { const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }): IUseEsSearch => { const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const {filterManager} = useFilterManager(); + const {filters} = useFilterManager(); const [query] = useQuery(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); - const [managedFilters, setManagedFilters] = useState<Filter[] | []>([]); const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); const [page, setPage] = useState<number>(0); @@ -74,25 +72,13 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setIsLoading(false); } })(); - }, [indexPattern, query, managedFilters, page]); - - useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - if (!_.isEqual(managedFilters, newFilters)) { - setManagedFilters(newFilters); - } - return () => { - filterSubscriber.unsubscribe(); - }; - }); - }, []); + }, [indexPattern, query, filters, page]); const search = async (): Promise<SearchResponse> => { if (indexPattern) { const esQuery = await data.query.getEsQuery(indexPattern as IndexPattern); const searchSource = await data.search.searchSource.create(); - const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...managedFilters]; + const combined = [...esQuery.bool.filter, ...preAppliedFilters, ...filters]; return await searchSource .setParent(undefined) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index ca8a1032bd..1102ba09b6 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -11,8 +11,23 @@ */ import { getDataPlugin } from '../../../kibana-services'; import { useState, useEffect, useMemo } from 'react'; +import { Filter } from 'src/plugins/data/public'; +import _ from 'lodash'; export const useFilterManager = () => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - return { filterManager }; + const [filters, setFilters] = useState<Filter[]>([]) + + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const newFilters = filterManager.getFilters(); + if (!_.isEqual(filters, newFilters)) { + setFilters(newFilters); + } + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }, []); + return { filterManager, filters }; }; From 0e2323ea9302ad3b27d1b38c886839b4270e511d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 12:42:55 +0200 Subject: [PATCH 191/493] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49128be0ae..96b36af0cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +- Moved the filterManager subscription to the hook useFilterManager [#3513](https://github.com/wazuh/wazuh-kibana-app/pull/3513) ### Fixed From aa84f6b5a42ce125bf97ec58471d6c9a1c1a619a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 16:19:31 +0200 Subject: [PATCH 192/493] Tweaked the unsubscription method --- public/components/common/hooks/use-filter-manager.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index 1102ba09b6..d9b03540a6 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -16,17 +16,15 @@ import _ from 'lodash'; export const useFilterManager = () => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - const [filters, setFilters] = useState<Filter[]>([]) + const [filters, setFilters] = useState<Filter[]>([]); useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + const { unsubscribe } = filterManager.getUpdates$().subscribe(() => { const newFilters = filterManager.getFilters(); if (!_.isEqual(filters, newFilters)) { setFilters(newFilters); } - return () => { - filterSubscriber.unsubscribe(); - }; + return unsubscribe; }); }, []); return { filterManager, filters }; From 72619fb46503faad007c6cac94161d7aba2b7c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 29 Jul 2021 17:08:12 +0200 Subject: [PATCH 193/493] Fixed unsubscription typo and set it to return of useEffect --- public/components/common/hooks/use-filter-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index d9b03540a6..7e4d000d41 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -24,8 +24,8 @@ export const useFilterManager = () => { if (!_.isEqual(filters, newFilters)) { setFilters(newFilters); } - return unsubscribe; }); + return unsubscribe; }, []); return { filterManager, filters }; }; From ddc1a99d948f3100157775a4e83b62146a7623dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 30 Jul 2021 10:00:49 +0200 Subject: [PATCH 194/493] Fixed issue with cleanup method --- public/components/common/hooks/use-filter-manager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index 7e4d000d41..c18f1a7a00 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -19,13 +19,15 @@ export const useFilterManager = () => { const [filters, setFilters] = useState<Filter[]>([]); useEffect(() => { - const { unsubscribe } = filterManager.getUpdates$().subscribe(() => { + const subscription = filterManager.getUpdates$().subscribe(() => { const newFilters = filterManager.getFilters(); if (!_.isEqual(filters, newFilters)) { setFilters(newFilters); } }); - return unsubscribe; + return () => { + subscription.unsubscribe(); + }; }, []); return { filterManager, filters }; }; From ee2c9c7684190dbdd20a94b01268fd561b2542c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 30 Jul 2021 10:42:13 +0200 Subject: [PATCH 195/493] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b36af0cd..36adadce7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) -- Moved the filterManager subscription to the hook useFilterManager [#3513](https://github.com/wazuh/wazuh-kibana-app/pull/3513) +- Moved the filterManager subscription to the hook useFilterManager [#3517](https://github.com/wazuh/wazuh-kibana-app/pull/3517) ### Fixed From 51c24ca0e93d6653e49cb110b82ce94b55fb384b Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 10:48:23 +0200 Subject: [PATCH 196/493] Add rule description agg table --- .../office-panel/config/main-view-config.tsx | 18 ++++++++++++++++-- .../office-panel/config/module-config.tsx | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 37dd700b41..969bbef5e3 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -21,7 +21,7 @@ export const MainViewConfig = { { columns: [ { - width: 50, + width: 33, component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable @@ -35,7 +35,21 @@ export const MainViewConfig = { ), }, { - width: 50, + width: 33, + component: (props) => ( + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle={'Rules'} + aggTerm={'rule.description'} + aggLabel={'Rule'} + maxRows={'5'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + </EuiFlexItem> + ), + }, + { + width: 33, component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index b9988aecd8..228c3b600e 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -38,4 +38,10 @@ export const ModuleConfig = { <OfficeDrilldown title={'Client IP'} {...{ ...drilldownIPConfig, ...props }} /> ), }, + 'rule.description': { + length: () => drilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => ( + <OfficeDrilldown title={'Rule Description'} {...{ ...drilldownIPConfig, ...props }} /> + ), + }, }; From 78c3d0c0ed5d4faadf4a65a1b287d17beb1eccd9 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 12:14:08 +0200 Subject: [PATCH 197/493] Removed Security Alerts Added Agg tables --- .../office-panel/config/main-view-config.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 969bbef5e3..0e2c7b3d37 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -13,15 +13,14 @@ import React from 'react'; import { AggTable } from '../../../common/modules/panel/'; -import { EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { SecurityAlerts } from '../../../visualize/components'; +import { EuiFlexItem } from '@elastic/eui'; export const MainViewConfig = { rows: [ { columns: [ { - width: 33, + width: 50, component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable @@ -35,7 +34,7 @@ export const MainViewConfig = { ), }, { - width: 33, + width: 50, component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable @@ -48,8 +47,12 @@ export const MainViewConfig = { </EuiFlexItem> ), }, + ], + }, + { + columns: [ { - width: 33, + width: 50, component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable @@ -62,18 +65,17 @@ export const MainViewConfig = { </EuiFlexItem> ), }, - ], - }, - { - height: 300, - columns: [ { - width: 100, - component: () => ( - <EuiFlexItem> - <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> - </EuiPanel> + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle={'Subscriptions'} + aggTerm={'data.office365.Subscription'} + aggLabel={'Subscriptions'} + maxRows={'5'} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> </EuiFlexItem> ), }, From f5594aa169e51a620fd3ccf12b76989b1a50650b Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 12:53:08 +0200 Subject: [PATCH 198/493] Rolled back configuration change --- .../config/drilldown-ip-config.tsx | 126 +++++++++--------- .../overview/office-panel/config/index.ts | 3 +- .../office-panel/config/main-view-config.tsx | 32 ----- .../office-panel/config/module-config.tsx | 14 +- 4 files changed, 66 insertions(+), 109 deletions(-) diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index 68070a14e5..e74a4a8c22 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -1,6 +1,6 @@ 'use strict'; /* - * Wazuh app - Office 365 DrilldownConfig. + * Wazuh app - Office 365 Drilldown IP field Config. * * Copyright (C) 2015-2021 Wazuh, Inc. * @@ -12,71 +12,69 @@ * Find more information about this on the LICENSE file. */ -import React, { ReactNode } from 'react'; +import React from 'react'; import { VisCard } from '../../../common/modules/panel'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; -export const drilldownConfig = (visCardId: string): ReactNode => { - return { - rows: [ - { - height: 400, - columns: [ - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> - ), - }, - { - width: 30, - component: (props) => ( - <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> - ), - }, - { - width: 40, - component: (props) => ( - <VisCard - id={visCardId} //<--- el id - tab="office" - {...props} - /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: (props) => ( - <VisCard - id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" - tab="office" - {...props} - /> - ), - }, - ], - }, - { - height: 300, - columns: [ - { - width: 100, - component: () => ( - <EuiFlexItem> - <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> - </EuiPanel> - </EuiFlexItem> - ), - }, - ], - }, - ], - }; +export const drilldownIPConfig = { + rows: [ + { + height: 400, + columns: [ + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> + ), + }, + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> + ), + }, + { + width: 40, + component: (props) => ( + <VisCard + id={'Wazuh-App-Overview-Office-User-Operation-Level-Table'} + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), + }, + ], + }, + ], }; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index d9c378b5a8..529b8aee93 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -11,7 +11,8 @@ * Find more information about this on the LICENSE file. */ -export { drilldownConfig } from './drilldown-ip-config'; +export { drilldownIPConfig } from './drilldown-ip-config'; +export { drilldownUserConfig } from './drilldown-user-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 0e2c7b3d37..4bef4aabe8 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -33,24 +33,6 @@ export const MainViewConfig = { </EuiFlexItem> ), }, - { - width: 50, - component: (props) => ( - <EuiFlexItem grow={props.grow}> - <AggTable - tableTitle={'Rules'} - aggTerm={'rule.description'} - aggLabel={'Rule'} - maxRows={'5'} - onRowClick={(field, value) => props.onRowClick(field, value)} - /> - </EuiFlexItem> - ), - }, - ], - }, - { - columns: [ { width: 50, component: (props) => ( @@ -65,20 +47,6 @@ export const MainViewConfig = { </EuiFlexItem> ), }, - { - width: 50, - component: (props) => ( - <EuiFlexItem grow={props.grow}> - <AggTable - tableTitle={'Subscriptions'} - aggTerm={'data.office365.Subscription'} - aggLabel={'Subscriptions'} - maxRows={'5'} - onRowClick={(field, value) => props.onRowClick(field, value)} - /> - </EuiFlexItem> - ), - }, ], }, ], diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 228c3b600e..4d30440425 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -13,13 +13,8 @@ import React from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; -import { MainViewConfig } from './'; -import { drilldownConfig } from './drilldown-ip-config'; +import { MainViewConfig, drilldownIPConfig, drilldownUserConfig } from './'; -const drilldownUserConfig = drilldownConfig('Wazuh-App-Overview-Office-User-Operation-Level-Table'); -const drilldownIPConfig = drilldownConfig( - 'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table' -); export const ModuleConfig = { main: { @@ -38,10 +33,5 @@ export const ModuleConfig = { <OfficeDrilldown title={'Client IP'} {...{ ...drilldownIPConfig, ...props }} /> ), }, - 'rule.description': { - length: () => drilldownIPConfig.rows.reduce((total, row) => total + row.columns.length, 0), - component: (props) => ( - <OfficeDrilldown title={'Rule Description'} {...{ ...drilldownIPConfig, ...props }} /> - ), - }, + }; From ac1fbc3aa675aae865e9e206b6ebf09cb5457240 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 13:04:35 +0200 Subject: [PATCH 199/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36adadce7f..519b72f97a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) +- Added base Module Panel view with Office365 setup [3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) ### Changed From 079d0c757ae8cd26f765db419316f2dbf248c5e2 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 30 Jul 2021 15:49:09 +0200 Subject: [PATCH 200/493] fix filter params state and delete obselete code --- .../custom-search-bar/custom-search-bar.tsx | 20 +++++++++---------- .../overview/office-panel/config/index.ts | 1 - 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index fed3bb9090..006994df4c 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -35,7 +35,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { return array } - const [isLoading, setLoading] = useState(false); const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); @@ -51,15 +50,18 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { const { query, dateRange } = payload; - const filters = { query, time: dateRange, filters: filterParams.filters }; - setFilterParams(filters); - + setFilterParams(prevState => ({ + ...prevState, + time: dateRange, + query: query, + })); } const onFiltersUpdated = (filters: Filter[]) => { - const { query, time } = filterParams; - const updatedFilterParams = { query, time, filters }; - setFilterParams(updatedFilterParams); + setFilterParams(prevState => ({ + ...prevState, + filters: filters + })); refreshCustomSelectedFilter() } @@ -93,7 +95,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const setKibanaFilters = (values: any[]) => { - setLoading(true) const newFilters = [] const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) filterManager.removeAll() @@ -124,7 +125,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }) } - setLoading(false) }; const onChange = (values: any[]) => { @@ -163,7 +163,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { showQueryInput={avancedFiltersState} onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> </EuiFlexItem> </EuiFlexGroup> @@ -174,7 +173,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { showQueryInput={false} onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} - isLoading={isLoading} /> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 529b8aee93..da09d3e138 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -12,7 +12,6 @@ */ export { drilldownIPConfig } from './drilldown-ip-config'; -export { drilldownUserConfig } from './drilldown-user-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; From fc02277f4a237074d1eec03b372f6a6ec1627e72 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 16:47:21 +0200 Subject: [PATCH 201/493] Fix vis-card issues --- .../modules/panel/components/vis-card.tsx | 10 +-- .../config/drilldown-user-config.tsx | 80 +++++++++++++++++++ .../overview/office-panel/config/index.ts | 1 + .../office-panel/config/module-config.tsx | 4 +- 4 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 public/components/overview/office-panel/config/drilldown-user-config.tsx diff --git a/public/components/common/modules/panel/components/vis-card.tsx b/public/components/common/modules/panel/components/vis-card.tsx index 490df599df..1af4e1d446 100644 --- a/public/components/common/modules/panel/components/vis-card.tsx +++ b/public/components/common/modules/panel/components/vis-card.tsx @@ -25,15 +25,14 @@ export const VisCard = ({ changeView = () => {}, id, width, tab, ...props }) => return rawVis.length && rawVis[0]?.attributes?.title; })(); - const toggleExpand = (id) => { - setExpandedVis(expandedVis === id ? false : id); + const toggleExpand = () => { + setExpandedVis(!expandedVis); }; return ( <> - {' '} <EuiFlexItem grow={width}> - <EuiPanel paddingSize={'s'} className={expandedVis === id ? 'fullscreen h-100' : 'h-100'}> + <EuiPanel paddingSize={'s'} className={expandedVis ? 'fullscreen h-100' : 'h-100'}> <EuiFlexGroup direction={'column'} className={'h-100'}> <EuiFlexItem grow={false}> <EuiFlexGroup justifyContent="spaceBetween"> @@ -48,7 +47,7 @@ export const VisCard = ({ changeView = () => {}, id, width, tab, ...props }) => <EuiButtonIcon color="text" style={{ padding: '0px 6px', height: 30 }} - onClick={() => toggleExpand(id)} + onClick={() => toggleExpand()} iconType="expand" aria-label="Expand" /> @@ -60,7 +59,6 @@ export const VisCard = ({ changeView = () => {}, id, width, tab, ...props }) => <KibanaVis visID={id} tab={tab} - onRowClick={() => changeView('drilldown')} {...props} /> </div> diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx new file mode 100644 index 0000000000..04c0cfeabf --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -0,0 +1,80 @@ +'use strict'; +/* + * Wazuh app - Office 365 Drilldown UserId field Config. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const drilldownUserConfig = { + rows: [ + { + height: 400, + columns: [ + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Metric-Stats" tab="office" {...props} /> + ), + }, + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Events-Pie" tab="office" {...props} /> + ), + }, + { + width: 40, + component: (props) => ( + <VisCard + id={'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table'} + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-User" + tab="office" + {...props} + /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts /> + </EuiPanel> + </EuiFlexItem> + ), + }, + ], + }, + ], +}; diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index da09d3e138..529b8aee93 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -12,6 +12,7 @@ */ export { drilldownIPConfig } from './drilldown-ip-config'; +export { drilldownUserConfig } from './drilldown-user-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 4d30440425..7653aabcb7 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -15,7 +15,9 @@ import React from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; import { MainViewConfig, drilldownIPConfig, drilldownUserConfig } from './'; - +/** + * The length method has to count Kibana Visualizations for TabVisualizations class + */ export const ModuleConfig = { main: { length: () => MainViewConfig.rows.reduce((total, row) => total + row.columns.length, 0), From adfffb3def91d2bcd7fd55f6e52385a47244298f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 30 Jul 2021 16:51:14 +0200 Subject: [PATCH 202/493] fix search-bar-config copyright --- .../office-panel/config/search-bar-config.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 91d41f5f23..0d3641be40 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -1,16 +1,14 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Wazuh app - Office 365 Custom Search Bar Config. * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at + * Copyright (C) 2015-2021 Wazuh, Inc. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. + * Find more information about this on the LICENSE file. */ export const filtersValues: { type: string; key: string }[] = [ From ee6d15646567084f139a8fbfe557bf9dde1edb89 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 30 Jul 2021 16:52:36 +0200 Subject: [PATCH 203/493] delete filter state --- .../custom-search-bar/custom-search-bar.tsx | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 006994df4c..7ae637ce49 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -13,20 +13,13 @@ import { //@ts-ignore import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; -import { TimeRange, Query } from '../../../../../../src/plugins/data/common'; import { Combobox } from './components' export const CustomSearchBar = ({ filtersValues, ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; - const timefilter = KibanaServices.timefilter.timefilter; const indexPattern = getIndexPattern(); - const [filterParams, setFilterParams] = useState({ - filters: filterManager.getFilters().map(({ meta: { removable, ...restMeta }, ...rest }) => ({ ...rest, meta: restMeta })) || [], - query: { language: 'kuery', query: '' }, - time: timefilter.getTime(), - }); const defaultSelectedOptions = () => { const array = [] filtersValues.forEach(item => { @@ -40,28 +33,14 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { useEffect(() => { let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - const newFilters = filterManager.getFilters(); - onFiltersUpdated(newFilters) + onFiltersUpdated() return () => { filterSubscriber.unsubscribe(); }; }); }, []); - const onQuerySubmit = (payload: { dateRange: TimeRange, query: Query }) => { - const { query, dateRange } = payload; - setFilterParams(prevState => ({ - ...prevState, - time: dateRange, - query: query, - })); - } - - const onFiltersUpdated = (filters: Filter[]) => { - setFilterParams(prevState => ({ - ...prevState, - filters: filters - })); + const onFiltersUpdated = () => { refreshCustomSelectedFilter() } @@ -161,7 +140,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { <KbnSearchBar showFilterBar={false} showQueryInput={avancedFiltersState} - onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} /> </EuiFlexItem> @@ -171,7 +149,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { <KbnSearchBar showDatePicker={false} showQueryInput={false} - onQuerySubmit={onQuerySubmit} onFiltersUpdated={onFiltersUpdated} /> </EuiFlexItem> From 553785fbe0deacb6939f8453444a99181c9bb56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Fri, 30 Jul 2021 17:17:11 +0200 Subject: [PATCH 204/493] Fixed unsubscription on useQuery --- public/components/common/hooks/use-query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/common/hooks/use-query.ts b/public/components/common/hooks/use-query.ts index 9e969ac425..872cf7fcd2 100644 --- a/public/components/common/hooks/use-query.ts +++ b/public/components/common/hooks/use-query.ts @@ -47,10 +47,10 @@ export function useQuery(): [ export const useQueryManager = () => { const [query, setQuery] = useState(getDataPlugin().query.queryString.getQuery()); useEffect(() => { - const { unsubscribe } = getDataPlugin().query.queryString.getUpdates$().subscribe((q) => { + const subscriber = getDataPlugin().query.queryString.getUpdates$().subscribe((q) => { setQuery(q); }); - return () => unsubscribe(); + return () => subscriber.unsubscribe(); },[]); return [query, getDataPlugin().query.queryString.setQuery]; } From cee12465618f5a1f926a8e05f1e7bb97f585f986 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 14:40:11 -0300 Subject: [PATCH 205/493] feat(configuration-view): Added configuration view for Office 365 module. --- .../configuration/configuration-settings.js | 6 ++ .../configuration/configuration-switch.js | 15 ++- .../SubscriptionTab/SubscriptionTab.tsx | 39 ++++++++ .../components/api-auth-tab/api-auth-tab.tsx | 43 ++++++++ .../components/general-tab/general-tab.tsx | 46 +++++++++ .../configuration/office365/constants.tsx | 25 +++++ .../configuration/office365/office365.tsx | 99 +++++++++++++++++++ 7 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/constants.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/office365.tsx diff --git a/public/controllers/management/components/management/configuration/configuration-settings.js b/public/controllers/management/components/management/configuration/configuration-settings.js index 052921f913..1cb3769778 100644 --- a/public/controllers/management/components/management/configuration/configuration-settings.js +++ b/public/controllers/management/components/management/configuration/configuration-settings.js @@ -207,6 +207,12 @@ export default [ name: 'Google Cloud Pub/Sub', description: 'Configuration options of the Google Cloud Pub/Sub module', goto: 'gcp-pubsub' + }, + { + name: 'Office 365', + description: + 'Configuration options of the Office 365 module', + goto: 'office365' } ] } diff --git a/public/controllers/management/components/management/configuration/configuration-switch.js b/public/controllers/management/components/management/configuration/configuration-switch.js index 8540fd0073..ff32410251 100644 --- a/public/controllers/management/components/management/configuration/configuration-switch.js +++ b/public/controllers/management/components/management/configuration/configuration-switch.js @@ -74,6 +74,7 @@ import { WzRequest } from '../../../../../react-services/wz-request'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { WzConfigurationOffice365 } from './office365/office365'; class WzConfigurationSwitch extends Component { constructor(props) { @@ -156,7 +157,7 @@ class WzConfigurationSwitch extends Component { this.setState({ loadingOverview: true }); const masterNodeInfo = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=000'}}); this.setState({ - masterNodeInfo: masterNodeInfo.data.affected_items[0] + masterNodeInfo: masterNodeInfo.data.data.affected_items[0] }); this.setState({ loadingOverview: false }); }catch(error){ @@ -426,6 +427,14 @@ class WzConfigurationSwitch extends Component { updateConfigurationSection={this.updateConfigurationSection} /> </WzViewSelectorSwitch> + <WzViewSelectorSwitch view="office365"> + <WzConfigurationOffice365 + clusterNodeSelected={this.props.clusterNodeSelected} + agent={agent} + updateBadge={this.updateBadge} + updateConfigurationSection={this.updateConfigurationSection} + /> + </WzViewSelectorSwitch> </WzViewSelector> )} </EuiPanel> @@ -448,8 +457,8 @@ const mapDispatchToProps = dispatch => ({ }); export default compose( - withUserAuthorizationPrompt((props) => [props.agent.id === '000' ? - {action: 'manager:read', resource: '*:*:*'} : + withUserAuthorizationPrompt((props) => [props.agent.id === '000' ? + {action: 'manager:read', resource: '*:*:*'} : [ {action: 'agent:read', resource: `agent:id:${props.agent.id}`}, ...(props.agent.group || []).map(group => ({ action: 'agent:read', resource: `agent:group:${group}` })) diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx new file mode 100644 index 0000000000..0f17c90b2f --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx @@ -0,0 +1,39 @@ +/* + * Wazuh app - React component SubscriptionTab + * Copyright (C) 2015-2021 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 from 'react'; +import { EuiBasicTable } from '@elastic/eui'; +import WzConfigurationSettingsTabSelector from '../../../util-components/configuration-settings-tab-selector'; +import { HELP_LINKS, OFFICE_365 } from '../../constants'; + +export type SubscriptionTabProps = { + agent: { id: string }; + wodleConfiguration: any; +}; + +export const SubscriptionTab = ({ agent, wodleConfiguration }: SubscriptionTabProps) => { + const columns = [{ field: 'subscription', name: 'Name' }]; + + return ( + <WzConfigurationSettingsTabSelector + title="List of subscriptions" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 260 : 320} + helpLinks={HELP_LINKS} + > + <EuiBasicTable + columns={columns} + items={wodleConfiguration[OFFICE_365].subscriptions.map((item) => ({ subscription: item }))} + /> + </WzConfigurationSettingsTabSelector> + ); +}; diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx new file mode 100644 index 0000000000..faffb2916d --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx @@ -0,0 +1,43 @@ +/* + * Wazuh app - React component ApiAuthTab + * Copyright (C) 2015-2021 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 from 'react'; +import WzConfigurationSettingsTabSelector from '../../../util-components/configuration-settings-tab-selector'; +import WzConfigurationSettingsGroup from '../../../util-components/configuration-settings-group'; +import { HELP_LINKS, OFFICE_365 } from '../../constants'; + +export type ApiAuthProps = { + agent: { id: string }; + wodleConfiguration: any; +}; + +export const ApiAuthTab = ({ agent, wodleConfiguration }: ApiAuthProps) => { + const columns = [ + { field: 'tenant_id', label: 'Tenant Id' }, + { field: 'client_id', label: 'Client Id' }, + { field: 'client_secret', label: 'Client Secret' }, + ]; + + return ( + <WzConfigurationSettingsTabSelector + title="List of Api Auth" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 260 : 320} + helpLinks={HELP_LINKS} + > + <WzConfigurationSettingsGroup + config={wodleConfiguration[OFFICE_365].api_auth[0]} + items={columns} + /> + </WzConfigurationSettingsTabSelector> + ); +}; diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx new file mode 100644 index 0000000000..8bfa4d35f2 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -0,0 +1,46 @@ +/* + * Wazuh app - React component GeneralTab + * Copyright (C) 2015-2021 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 from 'react'; +import { renderValueYesThenEnabled } from '../../../utils/utils'; +import WzConfigurationSettingsTabSelector from '../../../util-components/configuration-settings-tab-selector'; +import WzConfigurationSettingsGroup from '../../../util-components/configuration-settings-group'; +import { HELP_LINKS, OFFICE_365 } from '../../constants'; + +export type GeneralTableProps = { + agent: { id: string }; + wodleConfiguration: any; +}; + +export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => { + const mainSettings = [ + { field: 'enabled', label: 'Enabled', render: renderValueYesThenEnabled }, + { + field: 'only_future_events', + label: 'Collect events generated since Wazuh agent was started', + }, + { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds' }, + { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, + ]; + + return ( + <WzConfigurationSettingsTabSelector + title="Main settings" + description="Configuration for the Office 365 module" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 260 : 320} + helpLinks={HELP_LINKS} + > + <WzConfigurationSettingsGroup config={wodleConfiguration[OFFICE_365]} items={mainSettings} /> + </WzConfigurationSettingsTabSelector> + ); +}; diff --git a/public/controllers/management/components/management/configuration/office365/constants.tsx b/public/controllers/management/components/management/configuration/office365/constants.tsx new file mode 100644 index 0000000000..96492ef51f --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/constants.tsx @@ -0,0 +1,25 @@ +/* + * Wazuh app - Constants of Office365 configuration + * Copyright (C) 2015-2021 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 OFFICE_365 = 'office365'; +export const WMODULES_WMODULES = 'wmodules-wmodules'; +export const HELP_LINKS = [ + { + text: 'Using Wazuh to monitor Office 365', + href: 'https://documentation.wazuh.com/current/office365/index.html', + }, + { + text: 'Configuration options of the Office 365 module reference', + href: + 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html', + }, +]; diff --git a/public/controllers/management/components/management/configuration/office365/office365.tsx b/public/controllers/management/components/management/configuration/office365/office365.tsx new file mode 100644 index 0000000000..ca6bed236b --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/office365.tsx @@ -0,0 +1,99 @@ +/* + * Wazuh app - React component for show configuration of Office 365 module. + * Copyright (C) 2015-2021 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, { useEffect, useMemo } from 'react'; +import WzNoConfig from '../util-components/no-config'; +import withWzConfig from '../util-hocs/wz-config'; +import { wodleBuilder } from '../utils/builders'; +import { compose } from 'redux'; +import WzTabSelector, { WzTabSelectorTab } from '../util-components/tab-selector'; +import { isString } from '../utils/utils'; +import { withGuard } from '../../../../../../components/common/hocs'; +import { HELP_LINKS, OFFICE_365, WMODULES_WMODULES } from './constants'; +import { GeneralTab } from './components/general-tab/general-tab'; +import { ApiAuthTab } from './components/api-auth-tab/api-auth-tab'; +import { SubscriptionTab } from './components/SubscriptionTab/SubscriptionTab'; + +interface IWzConfigOffice365 { + currentConfig: {}; + agent: { id: string | number }; + updateBadge: () => void; + updateConfigurationSection: () => void; +} +const sections = [{ component: 'wmodules', configuration: 'wmodules' }]; + +export const WzConfigurationOffice365: React.FunctionComponent<IWzConfigOffice365> = withWzConfig( + sections +)(({ currentConfig, updateBadge, ...rest }) => { + const wodleConfiguration = useMemo(() => wodleBuilder(currentConfig, OFFICE_365), [ + currentConfig, + ]); + + useEffect(() => { + updateBadge( + currentConfig && + wodleConfiguration && + wodleConfiguration[OFFICE_365] && + wodleConfiguration[OFFICE_365].enabled === 'yes' + ); + }, [currentConfig]); + + return ( + <WzTabSelector> + <WzTabSelectorTab label="General"> + <GeneralTabWrapped + wodleConfiguration={wodleConfiguration} + currentConfig={currentConfig} + {...rest} + /> + </WzTabSelectorTab> + <WzTabSelectorTab label="Api Auth"> + <ApiAuthTabWrapped + wodleConfiguration={wodleConfiguration} + currentConfig={currentConfig} + {...rest} + /> + </WzTabSelectorTab> + <WzTabSelectorTab label="Subscriptions"> + <SubscriptionTabWrapped + wodleConfiguration={wodleConfiguration} + currentConfig={currentConfig} + {...rest} + /> + </WzTabSelectorTab> + </WzTabSelector> + ); +}); + +const tabWrapper = compose( + withGuard( + ({ currentConfig }) => + currentConfig[WMODULES_WMODULES] && isString(currentConfig[WMODULES_WMODULES]), + ({ currentConfig }) => <WzNoConfig error={currentConfig[WMODULES_WMODULES]} help={HELP_LINKS} /> + ), + withGuard( + ({ wodleConfiguration }) => !wodleConfiguration[OFFICE_365], + (props) => <WzNoConfig error="not-present" help={HELP_LINKS} /> + ) +); + +const GeneralTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { + return <GeneralTab wodleConfiguration={wodleConfiguration} agent={agent} />; +}); + +const ApiAuthTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { + return <ApiAuthTab wodleConfiguration={wodleConfiguration} agent={agent} />; +}); + +const SubscriptionTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { + return <SubscriptionTab wodleConfiguration={wodleConfiguration} agent={agent} />; +}); From 57e551a7c1fa1da79bb7a93f331a71d64d85f2ff Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 14:40:43 -0300 Subject: [PATCH 206/493] feat(configuration-view): Added basic unit test for components --- .../SubscriptionTab/SubscriptionTab.test.tsx | 49 ++ .../SubscriptionTab.test.tsx.snap | 739 ++++++++++++++++ .../__snapshots__/api-auth-tab.test.tsx.snap | 713 +++++++++++++++ .../api-auth-tab/api-auth-tab.test.tsx | 47 + .../__snapshots__/general-tab.test.tsx.snap | 831 ++++++++++++++++++ .../general-tab/general-tab.test.tsx | 47 + 6 files changed, 2426 insertions(+) create mode 100644 public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap create mode 100644 public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap create mode 100644 public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx create mode 100644 public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap create mode 100644 public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.test.tsx diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx new file mode 100644 index 0000000000..a1979e37b6 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx @@ -0,0 +1,49 @@ +/* + * Wazuh app - React Test component SubscriptionTab + * Copyright (C) 2015-2021 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 from 'react'; +import { SubscriptionTab } from './SubscriptionTab'; +import { mount } from 'enzyme'; + +describe('SubscriptionTab component', () => { + it('renders correctly to match the snapshot', () => { + const wodleConfiguration = { + office365: { + enabled: 'yes', + only_future_events: 'yes', + interval: 600, + curl_max_size: 1024, + api_auth: [ + { + tenant_id: 'your_tenant_id_test', + client_id: 'your_client_id_test', + client_secret: 'your_secret_test', + }, + ], + subscriptions: [ + 'Audit.AzureActiveDirectory', + 'Audit.Exchange', + 'Audit.SharePoint', + 'Audit.General', + 'DLP.All', + ], + }, + }; + const agent = { id: '000' }; + + const wrapper = mount( + <SubscriptionTab wodleConfiguration={wodleConfiguration} agent={agent} /> + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap new file mode 100644 index 0000000000..53b87b8386 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap @@ -0,0 +1,739 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = ` +<SubscriptionTab + agent={ + Object { + "id": "000", + } + } + wodleConfiguration={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } +> + <WzConfigurationSettingsTabSelector + currentConfig={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } + helpLinks={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + minusHeight={260} + title="List of subscriptions" + > + <WzConfigurationSettingsHeader + help={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + json={[Function]} + settings={[Function]} + title="List of subscriptions" + viewSelected="" + xml={[Function]} + > + <EuiFlexGroup + alignItems="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + > + List of subscriptions + </h2> + </EuiTitle> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="SETTINGS" + view="" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + type="button" + > + SETTINGS + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="JSON" + view="json" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + JSON + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <ButtonLink + onClick={[Function]} + text="XML" + view="xml" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + XML + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <WzHelpButtonPopover + links={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + > + <EuiPopover + anchorPosition="downCenter" + button={ + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + /> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="show-help" + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownCenter" + id="show-help" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="questionInCircle" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="questionInCircle" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + /> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </WzHelpButtonPopover> + </span> + </div> + </EuiFlexGroup> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + <EuiHorizontalRule + margin="none" + style={ + Object { + "marginBottom": 16, + } + } + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + style={ + Object { + "marginBottom": 16, + } + } + /> + </EuiHorizontalRule> + </WzConfigurationSettingsHeader> + <WzViewSelector + view="" + > + <WzViewSelectorSwitch + default={true} + > + <EuiBasicTable + columns={ + Array [ + Object { + "field": "subscription", + "name": "Name", + }, + ] + } + items={ + Array [ + Object { + "subscription": "Audit.AzureActiveDirectory", + }, + Object { + "subscription": "Audit.Exchange", + }, + Object { + "subscription": "Audit.SharePoint", + }, + Object { + "subscription": "Audit.General", + }, + Object { + "subscription": "DLP.All", + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="fixed" + > + <div + className="euiBasicTable" + > + <div> + <EuiTableHeaderMobile> + <div + className="euiTableHeaderMobile" + > + <EuiFlexGroup + alignItems="baseline" + justifyContent="spaceBetween" + responsive={false} + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + /> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </div> + </EuiTableHeaderMobile> + <EuiTable + id="__table_45cd9dc1-f3b7-11eb-a3bd-df5b4b424cb0" + responsive={true} + tableLayout="fixed" + > + <table + className="euiTable euiTable--responsive" + id="__table_45cd9dc1-f3b7-11eb-a3bd-df5b4b424cb0" + tabIndex={-1} + > + <EuiScreenReaderOnly> + <caption + className="euiScreenReaderOnly euiTableCaption" + > + <EuiDelayRender + delay={500} + /> + </caption> + </EuiScreenReaderOnly> + <EuiTableHeader> + <thead> + <tr> + <EuiTableHeaderCell + align="left" + data-test-subj="tableHeaderCell_subscription_0" + key="_data_h_subscription_0" + > + <th + className="euiTableHeaderCell" + data-test-subj="tableHeaderCell_subscription_0" + role="columnheader" + scope="col" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableCellContent" + > + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Name" + > + Name + </span> + </EuiInnerText> + </div> + </th> + </EuiTableHeaderCell> + </tr> + </thead> + </EuiTableHeader> + <EuiTableBody + bodyRef={[Function]} + > + <tbody> + <EuiTableRow + isSelected={false} + > + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="left" + key="_data_column_subscription_0_0" + mobileOptions={ + Object { + "header": "Name", + "render": undefined, + } + } + setScopeRow={false} + textOnly={true} + > + <td + className="euiTableRowCell" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + className="euiTableCellContent" + > + <span + className="euiTableCellContent__text" + > + Audit.AzureActiveDirectory + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + <EuiTableRow + isSelected={false} + > + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="left" + key="_data_column_subscription_1_0" + mobileOptions={ + Object { + "header": "Name", + "render": undefined, + } + } + setScopeRow={false} + textOnly={true} + > + <td + className="euiTableRowCell" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + className="euiTableCellContent" + > + <span + className="euiTableCellContent__text" + > + Audit.Exchange + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + <EuiTableRow + isSelected={false} + > + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="left" + key="_data_column_subscription_2_0" + mobileOptions={ + Object { + "header": "Name", + "render": undefined, + } + } + setScopeRow={false} + textOnly={true} + > + <td + className="euiTableRowCell" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + className="euiTableCellContent" + > + <span + className="euiTableCellContent__text" + > + Audit.SharePoint + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + <EuiTableRow + isSelected={false} + > + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="left" + key="_data_column_subscription_3_0" + mobileOptions={ + Object { + "header": "Name", + "render": undefined, + } + } + setScopeRow={false} + textOnly={true} + > + <td + className="euiTableRowCell" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + className="euiTableCellContent" + > + <span + className="euiTableCellContent__text" + > + Audit.General + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + <EuiTableRow + isSelected={false} + > + <tr + className="euiTableRow" + > + <EuiTableRowCell + align="left" + key="_data_column_subscription_4_0" + mobileOptions={ + Object { + "header": "Name", + "render": undefined, + } + } + setScopeRow={false} + textOnly={true} + > + <td + className="euiTableRowCell" + style={ + Object { + "width": undefined, + } + } + > + <div + className="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + className="euiTableCellContent" + > + <span + className="euiTableCellContent__text" + > + DLP.All + </span> + </div> + </td> + </EuiTableRowCell> + </tr> + </EuiTableRow> + </tbody> + </EuiTableBody> + </table> + </EuiTable> + </div> + </div> + </EuiBasicTable> + </WzViewSelectorSwitch> + </WzViewSelector> + </WzConfigurationSettingsTabSelector> +</SubscriptionTab> +`; diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap new file mode 100644 index 0000000000..31abc70a72 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -0,0 +1,713 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` +<ApiAuthTab + agent={ + Object { + "id": "000", + } + } + wodleConfiguration={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } +> + <WzConfigurationSettingsTabSelector + currentConfig={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } + helpLinks={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + minusHeight={260} + title="List of Api Auth" + > + <WzConfigurationSettingsHeader + help={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + json={[Function]} + settings={[Function]} + title="List of Api Auth" + viewSelected="" + xml={[Function]} + > + <EuiFlexGroup + alignItems="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + > + List of Api Auth + </h2> + </EuiTitle> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="SETTINGS" + view="" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + type="button" + > + SETTINGS + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="JSON" + view="json" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + JSON + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <ButtonLink + onClick={[Function]} + text="XML" + view="xml" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + XML + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <WzHelpButtonPopover + links={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + > + <EuiPopover + anchorPosition="downCenter" + button={ + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + /> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="show-help" + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownCenter" + id="show-help" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="questionInCircle" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="questionInCircle" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + /> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </WzHelpButtonPopover> + </span> + </div> + </EuiFlexGroup> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + <EuiHorizontalRule + margin="none" + style={ + Object { + "marginBottom": 16, + } + } + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + style={ + Object { + "marginBottom": 16, + } + } + /> + </EuiHorizontalRule> + </WzConfigurationSettingsHeader> + <WzViewSelector + view="" + > + <WzViewSelectorSwitch + default={true} + > + <WzSettingsGroup + config={ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + } + } + items={ + Array [ + Object { + "field": "tenant_id", + "label": "Tenant Id", + }, + Object { + "field": "client_id", + "label": "Client Id", + }, + Object { + "field": "client_secret", + "label": "Client Secret", + }, + ] + } + > + <WzConfigurationSettingsHeader> + <EuiFlexGroup + alignItems="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + /> + </EuiTitle> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + /> + </EuiFlexGroup> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + </WzConfigurationSettingsHeader> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <WzConfigurationSetting + key="-Tenant Id-undefined-0" + keyItem="-Tenant Id-undefined-0" + label="Tenant Id" + value="your_tenant_id_test" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Tenant Id + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_tenant_id_test" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_tenant_id_test" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Id-undefined-1" + keyItem="-Client Id-undefined-1" + label="Client Id" + value="your_client_id_test" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Client Id + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_client_id_test" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_client_id_test" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Secret-undefined-2" + keyItem="-Client Secret-undefined-2" + label="Client Secret" + value="your_secret_test" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Client Secret + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_secret_test" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_secret_test" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </WzSettingsGroup> + </WzViewSelectorSwitch> + </WzViewSelector> + </WzConfigurationSettingsTabSelector> +</ApiAuthTab> +`; diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx new file mode 100644 index 0000000000..2aab23eca6 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx @@ -0,0 +1,47 @@ +/* + * Wazuh app - React Test component ApiAuthTab + * Copyright (C) 2015-2021 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 from 'react'; +import { ApiAuthTab } from './api-auth-tab'; +import { mount } from 'enzyme'; + +describe('ApiAuthTab component', () => { + it('renders correctly to match the snapshot', () => { + const wodleConfiguration = { + office365: { + enabled: 'yes', + only_future_events: 'yes', + interval: 600, + curl_max_size: 1024, + api_auth: [ + { + tenant_id: 'your_tenant_id_test', + client_id: 'your_client_id_test', + client_secret: 'your_secret_test', + }, + ], + subscriptions: [ + 'Audit.AzureActiveDirectory', + 'Audit.Exchange', + 'Audit.SharePoint', + 'Audit.General', + 'DLP.All', + ], + }, + }; + const agent = { id: '000' }; + + const wrapper = mount(<ApiAuthTab wodleConfiguration={wodleConfiguration} agent={agent} />); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap new file mode 100644 index 0000000000..6d36e08b10 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -0,0 +1,831 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` +<GeneralTab + agent={ + Object { + "id": "000", + } + } + wodleConfiguration={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } +> + <WzConfigurationSettingsTabSelector + currentConfig={ + Object { + "office365": Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + }, + } + } + description="Configuration for the Office 365 module" + helpLinks={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + minusHeight={260} + title="Main settings" + > + <WzConfigurationSettingsHeader + description="Configuration for the Office 365 module" + help={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + json={[Function]} + settings={[Function]} + title="Main settings" + viewSelected="" + xml={[Function]} + > + <EuiFlexGroup + alignItems="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + > + Main settings + </h2> + </EuiTitle> + <EuiText + color="subdued" + > + <div + className="euiText euiText--medium" + > + <EuiTextColor + color="subdued" + component="div" + > + <div + className="euiTextColor euiTextColor--subdued" + > + Configuration for the Office 365 module + </div> + </EuiTextColor> + </div> + </EuiText> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="SETTINGS" + view="" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + type="button" + > + SETTINGS + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span + style={ + Object { + "marginRight": "6px", + } + } + > + <ButtonLink + onClick={[Function]} + text="JSON" + view="json" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + JSON + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <ButtonLink + onClick={[Function]} + text="XML" + view="xml" + viewSelected="" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiLink + onClick={[Function]} + style={Object {}} + > + <button + className="euiLink euiLink--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" + > + XML + </button> + </EuiLink> + </div> + </EuiFlexItem> + </ButtonLink> + </span> + <span> + <WzHelpButtonPopover + links={ + Array [ + Object { + "href": "https://documentation.wazuh.com/current/office365/index.html", + "text": "Using Wazuh to monitor Office 365", + }, + Object { + "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", + "text": "Configuration options of the Office 365 module reference", + }, + ] + } + > + <EuiPopover + anchorPosition="downCenter" + button={ + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + /> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="show-help" + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + <EuiOutsideClickDetector + isDisabled={true} + onOutsideClick={[Function]} + > + <div + className="euiPopover euiPopover--anchorDownCenter" + id="show-help" + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <EuiButtonEmpty + iconSide="left" + iconType="questionInCircle" + onClick={[Function]} + > + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + iconType="questionInCircle" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <EuiIcon + className="euiButtonContent__icon" + size="m" + type="questionInCircle" + > + <EuiIconEmpty + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + role="img" + style={null} + > + <svg + aria-hidden={true} + className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonContent__icon" + focusable="false" + height={16} + role="img" + style={null} + viewBox="0 0 16 16" + width={16} + xmlns="http://www.w3.org/2000/svg" + /> + </EuiIconEmpty> + </EuiIcon> + <span + className="euiButtonEmpty__text" + /> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </div> + </div> + </EuiOutsideClickDetector> + </EuiPopover> + </WzHelpButtonPopover> + </span> + </div> + </EuiFlexGroup> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + <EuiHorizontalRule + margin="none" + style={ + Object { + "marginBottom": 16, + } + } + > + <hr + className="euiHorizontalRule euiHorizontalRule--full" + style={ + Object { + "marginBottom": 16, + } + } + /> + </EuiHorizontalRule> + </WzConfigurationSettingsHeader> + <WzViewSelector + view="" + > + <WzViewSelectorSwitch + default={true} + > + <WzSettingsGroup + config={ + Object { + "api_auth": Array [ + Object { + "client_id": "your_client_id_test", + "client_secret": "your_secret_test", + "tenant_id": "your_tenant_id_test", + }, + ], + "curl_max_size": 1024, + "enabled": "yes", + "interval": 600, + "only_future_events": "yes", + "subscriptions": Array [ + "Audit.AzureActiveDirectory", + "Audit.Exchange", + "Audit.SharePoint", + "Audit.General", + "DLP.All", + ], + } + } + items={ + Array [ + Object { + "field": "enabled", + "label": "Enabled", + "render": [Function], + }, + Object { + "field": "only_future_events", + "label": "Collect events generated since Wazuh agent was started", + }, + Object { + "field": "interval", + "label": "Interval between Office 365 wodle executions in seconds", + }, + Object { + "field": "curl_max_size", + "label": "Maximum size allowed for the Office 365 API response", + }, + ] + } + > + <WzConfigurationSettingsHeader> + <EuiFlexGroup + alignItems="center" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + /> + </EuiTitle> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + /> + </EuiFlexGroup> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + <EuiSpacer + size="xs" + > + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + </WzConfigurationSettingsHeader> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <EuiFlexItem> + <div + className="euiFlexItem" + > + <WzConfigurationSetting + key="-Enabled-undefined-0" + keyItem="-Enabled-undefined-0" + label="Enabled" + value="enabled" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Enabled + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="enabled" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="enabled" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Collect events generated since Wazuh agent was started-undefined-1" + keyItem="-Collect events generated since Wazuh agent was started-undefined-1" + label="Collect events generated since Wazuh agent was started" + value="yes" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Collect events generated since Wazuh agent was started + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="yes" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="yes" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Interval between Office 365 wodle executions in seconds-undefined-2" + keyItem="-Interval between Office 365 wodle executions in seconds-undefined-2" + label="Interval between Office 365 wodle executions in seconds" + value={600} + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Interval between Office 365 wodle executions in seconds + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="600" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="600" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Maximum size allowed for the Office 365 API response-undefined-3" + keyItem="-Maximum size allowed for the Office 365 API response-undefined-3" + label="Maximum size allowed for the Office 365 API response" + value={1024} + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Maximum size allowed for the Office 365 API response + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="1024" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="1024" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </WzSettingsGroup> + </WzViewSelectorSwitch> + </WzViewSelector> + </WzConfigurationSettingsTabSelector> +</GeneralTab> +`; diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.test.tsx new file mode 100644 index 0000000000..e6cfc1ca5c --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.test.tsx @@ -0,0 +1,47 @@ +/* + * Wazuh app - React Test component GeneralTab + * Copyright (C) 2015-2021 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 from 'react'; +import { GeneralTab } from './general-tab'; +import { mount } from 'enzyme'; + +describe('GeneralTab component', () => { + it('renders correctly to match the snapshot', () => { + const wodleConfiguration = { + office365: { + enabled: 'yes', + only_future_events: 'yes', + interval: 600, + curl_max_size: 1024, + api_auth: [ + { + tenant_id: 'your_tenant_id_test', + client_id: 'your_client_id_test', + client_secret: 'your_secret_test', + }, + ], + subscriptions: [ + 'Audit.AzureActiveDirectory', + 'Audit.Exchange', + 'Audit.SharePoint', + 'Audit.General', + 'DLP.All', + ], + }, + }; + const agent = { id: '000' }; + + const wrapper = mount(<GeneralTab wodleConfiguration={wodleConfiguration} agent={agent} />); + + expect(wrapper).toMatchSnapshot(); + }); +}); From 55d9d106b3245e0548951bddab6c691a84b7488d Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 14:58:53 -0300 Subject: [PATCH 207/493] feat(configuration-view): Added snapshot test for office365 --- .../__snapshots__/office365.test.tsx.snap | 46 +++++++++++++++++++ .../office365/office365.test.tsx | 38 +++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap create mode 100644 public/controllers/management/components/management/configuration/office365/office365.test.tsx diff --git a/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap new file mode 100644 index 0000000000..4ec6ef8b30 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = ` +<ContextProvider + value={ + Object { + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "subscription": Subscription { + "handleChangeWrapper": [Function], + "listeners": Object { + "notify": [Function], + }, + "onStateChange": [Function], + "parentSub": undefined, + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "unsubscribe": null, + }, + } + } +> + <Connect(WithLoading(Component)) + agent={ + Object { + "id": "000", + } + } + currentConfig="master-test-node" + updateBadge={[Function]} + updateConfigurationSection={[Function]} + /> +</ContextProvider> +`; diff --git a/public/controllers/management/components/management/configuration/office365/office365.test.tsx b/public/controllers/management/components/management/configuration/office365/office365.test.tsx new file mode 100644 index 0000000000..229cf296f2 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/office365.test.tsx @@ -0,0 +1,38 @@ +/* + * Wazuh app - React Test component SubscriptionTab + * Copyright (C) 2015-2021 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 from 'react'; +import { shallow } from 'enzyme'; +import { WzConfigurationOffice365 } from './office365'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('SubscriptionTab component', () => { + it('renders correctly to match the snapshot', () => { + const agent = { id: '000' }; + + const wrapper = shallow( + <Provider store={store}> + <WzConfigurationOffice365 + currentConfig={'master-test-node'} + agent={agent} + updateBadge={() => {}} + updateConfigurationSection={() => {}} + /> + </Provider> + ); + expect(wrapper).toMatchSnapshot(); + }); +}); From 1629e9ba36dc347f08a1bb70ac290f62da15f24d Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 14:59:26 -0300 Subject: [PATCH 208/493] feat(configuration-view): Added snapshot test for office365 --- .../management/configuration/office365/office365.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/office365.test.tsx b/public/controllers/management/components/management/configuration/office365/office365.test.tsx index 229cf296f2..709b6fa96f 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.test.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React Test component SubscriptionTab + * Wazuh app - React Test component WzConfigurationOffice365 * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -19,7 +19,7 @@ import configureMockStore from 'redux-mock-store'; const mockStore = configureMockStore(); const store = mockStore({}); -describe('SubscriptionTab component', () => { +describe('WzConfigurationOffice365 component', () => { it('renders correctly to match the snapshot', () => { const agent = { id: '000' }; From 6bd376f12824b2349e9fbffc5b8dff5280627d66 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 15:00:27 -0300 Subject: [PATCH 209/493] feat(configuration-view): Added snapshot test for office365 --- .../__snapshots__/office365.test.tsx.snap | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap diff --git a/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap deleted file mode 100644 index 4ec6ef8b30..0000000000 --- a/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = ` -<ContextProvider - value={ - Object { - "store": Object { - "clearActions": [Function], - "dispatch": [Function], - "getActions": [Function], - "getState": [Function], - "replaceReducer": [Function], - "subscribe": [Function], - }, - "subscription": Subscription { - "handleChangeWrapper": [Function], - "listeners": Object { - "notify": [Function], - }, - "onStateChange": [Function], - "parentSub": undefined, - "store": Object { - "clearActions": [Function], - "dispatch": [Function], - "getActions": [Function], - "getState": [Function], - "replaceReducer": [Function], - "subscribe": [Function], - }, - "unsubscribe": null, - }, - } - } -> - <Connect(WithLoading(Component)) - agent={ - Object { - "id": "000", - } - } - currentConfig="master-test-node" - updateBadge={[Function]} - updateConfigurationSection={[Function]} - /> -</ContextProvider> -`; From 41d26d9318aabf0ef2f8bbe19044717a5c78dfc5 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 15:10:56 -0300 Subject: [PATCH 210/493] doc(changelog): Update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 519b72f97a..b231c7546b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) - Added base Module Panel view with Office365 setup [3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) +- Added configuration viewer for Module Office365 on Management > Configuration [3524](https://github.com/wazuh/wazuh-kibana-app/pull/3524) ### Changed From bca5f8c9f17503f8d675679ded5788dc1047e3dc Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Mon, 2 Aug 2021 17:06:29 -0300 Subject: [PATCH 211/493] Added new fields in Inventory table and Flyout Details --- .../agents/vuls/inventory/detail.tsx | 16 ++++++++- .../agents/vuls/inventory/flyout.tsx | 17 +++++++-- .../agents/vuls/inventory/table.tsx | 35 +++++++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 68d3d8b532..2247e6461d 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -34,7 +34,7 @@ import { AppNavigate } from '../../../../react-services/app-navigate'; import { TruncateHorizontalComponents } from '../../../common/util'; import { getDataPlugin,getUiSettings } from '../../../../kibana-services'; import { FilterManager } from '../../../../../../../src/plugins/data/public/'; - +import { formatUIDate } from '../../../../react-services/time-service'; export class Details extends Component { props!: { currentItem: { @@ -112,6 +112,20 @@ export class Details extends Component { name: 'Architecture', icon: 'node', link: false, + }, + { + field: 'last_full_scan', + name: 'Last Full Scan', + icon: 'clock', + link: false, + transformValue: formatUIDate + }, + { + field: 'last_partial_scan', + name: 'Last Partial Scan', + icon: 'clock', + link: false, + transformValue: formatUIDate } ]; } diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 10410cd5a7..1ad1b18456 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -29,6 +29,7 @@ import { } from '../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzRequest } from '../../../../react-services/wz-request'; export class FlyoutDetail extends Component { state: { @@ -55,6 +56,11 @@ export class FlyoutDetail extends Component { }; } + async getLastScan(){ + const response = await WzRequest.apiReq('GET', `/vulnerability/${this.props.agentId}/last_scan`, {}); + return ((response.data || {}).data || {}).affected_items[0] || {}; + } + async componentDidMount() { try { const isCluster = (AppState.getClusterInfo() || {}).status === 'enabled'; @@ -62,11 +68,15 @@ export class FlyoutDetail extends Component { ? { 'cluster.name': AppState.getClusterInfo().cluster } : { 'manager.name': AppState.getClusterInfo().manager }; this.setState({ clusterFilter }); - const currentItem = this.props.item; + let currentItem = this.props.item; if (!currentItem) { throw false; } + + let lastScan = await this.getLastScan(); + currentItem = { ...currentItem, ...lastScan}; + this.setState({ currentItem, isLoading: false }); } catch (error) { const options: UIErrorLog = { @@ -88,10 +98,14 @@ export class FlyoutDetail extends Component { } } + + + render() { const { currentItem } = this.state; const title = `${currentItem.cve}`; const id = title.replace(/ /g, '_'); + return ( <EuiFlyout onClose={() => this.props.closeFlyout()} @@ -121,7 +135,6 @@ export class FlyoutDetail extends Component { { 'rule.groups': 'vulnerability-detector' }, { 'data.vulnerability.package.name': currentItem.name }, { 'data.vulnerability.cve': currentItem.cve }, - { 'data.vulnerability.type': currentItem.type }, { 'data.vulnerability.package.architecture': currentItem.architecture }, { 'data.vulnerability.package.version': currentItem.version }, { 'agent.id': this.props.agentId }, diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 420100846d..f3c8a89166 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -20,6 +20,7 @@ import { FlyoutDetail } from './flyout'; import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; import { TableWzAPI } from '../../../../components/common/tables'; import { getFilterValues } from './lib'; +import { formatUIDate } from '../../../../react-services/time-service'; export class InventoryTable extends Component { state: { @@ -38,6 +39,10 @@ export class InventoryTable extends Component { {type: 'q', label: 'cve', description:"Filter by CVE ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('cve', value, this.props.agent.id)}, {type: 'q', label: 'version', description:"Filter by CVE version", operators:['=','!=', '~'], values: async (value) => getFilterValues('version', value, this.props.agent.id)}, {type: 'q', label: 'architecture', description:"Filter by architecture", operators:['=','!=', '~'], values: async (value) => getFilterValues('architecture', value, this.props.agent.id)}, + {type: 'q', label: 'severity', description:"Filter by Severity", operators:['=','!=', '~'], values: async (value) => getFilterValues('severity', value, this.props.agent.id)}, + {type: 'q', label: 'cvss2_score', description:"Filter by CVSS2", operators:['=','!=', '~'], values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id)}, + {type: 'q', label: 'cvss3_score', description:"Filter by CVSS3", operators:['=','!=', '~'], values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id)}, + {type: 'q', label: 'detection_time', description:"Filter by Detection Time", operators:['=','!=', '~'], values: async (value) => getFilterValues('detection_time', value, this.props.agent.id)} ] props!: { @@ -125,13 +130,38 @@ export class InventoryTable extends Component { name: 'Name', sortable: true, width: '100px' + }, + { + field: 'severity', + name: 'Severity', + sortable: true, + width: `${width}` + }, + { + field: 'cvss2_score', + name: 'CVSS2', + sortable: true, + width: `${width}` + }, + { + field: 'cvss3_score', + name: 'CVSS3', + sortable: true, + width: `${width}` + }, + { + field: 'detection_time', + name: 'Detection Time', + sortable: true, + width: `100px`, + render: formatUIDate } ] } renderTable() { const getRowProps = item => { - const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}`; + const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}-${item.severity}-${item.cvss2_score}-${item.cvss3_score}-${item.detection_time}`; return { 'data-test-subj': `row-${id}`, onClick: () => this.showFlyout(item), @@ -140,7 +170,7 @@ export class InventoryTable extends Component { const { error } = this.state; const columns = this.columns(); - const selectFields = 'select=cve,architecture,version,name' + const selectFields = 'select=cve,architecture,version,name,severity,cvss2_score,cvss3_score,detection_time' return ( <TableWzAPI title='Vulnerabilities' @@ -159,7 +189,6 @@ export class InventoryTable extends Component { render() { const table = this.renderTable(); - return ( <div className='wz-inventory'> {table} From b7df9c698bd3fd7799ecb6eed03cd592fc24b373 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Mon, 2 Aug 2021 18:02:17 -0300 Subject: [PATCH 212/493] feat(configuration-view): PR comments --- .../__snapshots__/office365.test.tsx.snap | 46 +++++++++++++++++++ .../SubscriptionTab/SubscriptionTab.tsx | 2 +- .../SubscriptionTab.test.tsx.snap | 16 +++---- .../__snapshots__/api-auth-tab.test.tsx.snap | 6 +-- .../__snapshots__/general-tab.test.tsx.snap | 6 +-- .../configuration/office365/constants.tsx | 2 +- .../configuration/office365/office365.tsx | 8 ++-- 7 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap diff --git a/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap new file mode 100644 index 0000000000..48beb24a36 --- /dev/null +++ b/public/controllers/management/components/management/configuration/office365/__snapshots__/office365.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WzConfigurationOffice365 component renders correctly to match the snapshot 1`] = ` +<ContextProvider + value={ + Object { + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "subscription": Subscription { + "handleChangeWrapper": [Function], + "listeners": Object { + "notify": [Function], + }, + "onStateChange": [Function], + "parentSub": undefined, + "store": Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + }, + "unsubscribe": null, + }, + } + } +> + <Connect(WithLoading(Component)) + agent={ + Object { + "id": "000", + } + } + currentConfig="master-test-node" + updateBadge={[Function]} + updateConfigurationSection={[Function]} + /> +</ContextProvider> +`; diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx index 0f17c90b2f..0fdca9965e 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx @@ -25,7 +25,7 @@ export const SubscriptionTab = ({ agent, wodleConfiguration }: SubscriptionTabPr return ( <WzConfigurationSettingsTabSelector - title="List of subscriptions" + title="Subscriptions list" currentConfig={wodleConfiguration} minusHeight={agent.id === '000' ? 260 : 320} helpLinks={HELP_LINKS} diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap index 53b87b8386..bd0f41a886 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap @@ -65,12 +65,12 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } minusHeight={260} - title="List of subscriptions" + title="Subscriptions list" > <WzConfigurationSettingsHeader help={ @@ -81,13 +81,13 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } json={[Function]} settings={[Function]} - title="List of subscriptions" + title="Subscriptions list" viewSelected="" xml={[Function]} > @@ -107,7 +107,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = <h2 className="euiTitle euiTitle--small" > - List of subscriptions + Subscriptions list </h2> </EuiTitle> </div> @@ -243,7 +243,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } @@ -447,13 +447,13 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = </div> </EuiTableHeaderMobile> <EuiTable - id="__table_45cd9dc1-f3b7-11eb-a3bd-df5b4b424cb0" + id="__table_23ff43c1-f3d4-11eb-bdee-2dceba245eef" responsive={true} tableLayout="fixed" > <table className="euiTable euiTable--responsive" - id="__table_45cd9dc1-f3b7-11eb-a3bd-df5b4b424cb0" + id="__table_23ff43c1-f3d4-11eb-bdee-2dceba245eef" tabIndex={-1} > <EuiScreenReaderOnly> diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index 31abc70a72..18da1eab01 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -65,7 +65,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } @@ -81,7 +81,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } @@ -243,7 +243,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index 6d36e08b10..d93a4d5aeb 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -66,7 +66,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } @@ -83,7 +83,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } @@ -263,7 +263,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options of the Office 365 module reference", + "text": "Configuration options for the Office 365 module", }, ] } diff --git a/public/controllers/management/components/management/configuration/office365/constants.tsx b/public/controllers/management/components/management/configuration/office365/constants.tsx index 96492ef51f..7f2a265ad4 100644 --- a/public/controllers/management/components/management/configuration/office365/constants.tsx +++ b/public/controllers/management/components/management/configuration/office365/constants.tsx @@ -18,7 +18,7 @@ export const HELP_LINKS = [ href: 'https://documentation.wazuh.com/current/office365/index.html', }, { - text: 'Configuration options of the Office 365 module reference', + text: 'Configuration options for the Office 365 module', href: 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html', }, diff --git a/public/controllers/management/components/management/configuration/office365/office365.tsx b/public/controllers/management/components/management/configuration/office365/office365.tsx index ca6bed236b..dff09d08a5 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.tsx @@ -33,7 +33,7 @@ const sections = [{ component: 'wmodules', configuration: 'wmodules' }]; export const WzConfigurationOffice365: React.FunctionComponent<IWzConfigOffice365> = withWzConfig( sections -)(({ currentConfig, updateBadge, ...rest }) => { +)(({ currentConfig, updateBadge, ...props }) => { const wodleConfiguration = useMemo(() => wodleBuilder(currentConfig, OFFICE_365), [ currentConfig, ]); @@ -53,21 +53,21 @@ export const WzConfigurationOffice365: React.FunctionComponent<IWzConfigOffice36 <GeneralTabWrapped wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} - {...rest} + {...props} /> </WzTabSelectorTab> <WzTabSelectorTab label="Api Auth"> <ApiAuthTabWrapped wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} - {...rest} + {...props} /> </WzTabSelectorTab> <WzTabSelectorTab label="Subscriptions"> <SubscriptionTabWrapped wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} - {...rest} + {...props} /> </WzTabSelectorTab> </WzTabSelector> From 6631330bcaab44626b9d5fc5e7d6126d19e2445f Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 3 Aug 2021 10:44:40 +0200 Subject: [PATCH 213/493] change filter from is to is one of --- .../custom-search-bar/custom-search-bar.tsx | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 7ae637ce49..79fff91384 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect, useLayoutEffect } from 'react'; +import React, { useState, useEffect } from 'react'; -import { getIndexPattern } from '../../overview/mitre/lib' +import { getIndexPattern } from '../../overview/mitre/lib'; import { Filter } from '../../../../../../src/plugins/data/public/'; import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../src/plugins/data/common'; +import { AppState } from '../../../react-services/app-state'; import { EuiFlexGroup, @@ -13,7 +14,7 @@ import { //@ts-ignore import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; -import { Combobox } from './components' +import { Combobox } from './components'; export const CustomSearchBar = ({ filtersValues, ...props }) => { @@ -49,24 +50,34 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { } - const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?: any, key?: any): Filter => { + const buildCustomFilter = (isPinned: boolean, values?: any): Filter => { + const newFilters = [] + values.forEach(element => { + const filter = { + match_phrase: { + [element.value]: element.label + } + } + newFilters.push(filter); + }); + const params = values.map(item => item.label) const meta: FilterMeta = { disabled: false, negate: false, - key: key, - params: { query: querySearch }, + key: values[0].value, + params: params, alias: null, - type: "phrase", - index, + type: "phrases", + value: params.join(","), + index: AppState.getCurrentPattern(), }; const $state: FilterState = { store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, }; const query = { - match_phrase: { - [key]: { - query: querySearch - } + bool: { + minimum_should_match: 1, + should: newFilters } } @@ -74,26 +85,22 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const setKibanaFilters = (values: any[]) => { - const newFilters = [] const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) filterManager.removeAll() filterManager.addFilters(currentFilters) - values.forEach(element => { - const customFilter = buildCustomFilter(false, indexPattern.title, element.label, element.value) - newFilters.push(customFilter); - }); - filterManager.addFilters(newFilters) + const customFilter = buildCustomFilter(false, values) + filterManager.addFilters(customFilter) } const refreshCustomSelectedFilter = () => { setSelectedOptions(defaultSelectedOptions) - const filters = filterManager.getFilters() - const filterCustom = filters.map(item => { - return { - value: item.meta.key, - label: item.meta.params.query, - } - }).filter(element => Object.keys(selectedOptions).includes(element.value)) + const filterCustom = [] + const filters = filterManager.getFilters().filter(item => item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key)).map(element => ({ params: element.meta.params, key: element.meta.key })) || []; + filters.forEach(item => { + item.params.forEach(element => { + filterCustom.push({ label: element, value: item.key }) + }) + }) if (filterCustom.length != 0) { filterCustom.forEach(item => { From 62d7a45a673f92d4cb0263b3e97ea3ced165be70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 3 Aug 2021 13:42:38 +0200 Subject: [PATCH 214/493] feat(fronted_module): Add the configuration viewer to the `Panel` tab - Load the module configuration when opening the info button or when pin/unpin the agent - The configuration to display depends on if there is a pinned agent or not. - Pinned agent: agent configuration - No pinned agent: check if the cluster mode is enabled - Cluster mode: nodes configurations - Manager mode: manager - Callouts when: - No available configuration - There is some error fetching the information - Loading status --- .../overview/github-panel/github-panel.tsx | 20 -- .../overview/github-panel/views/stats.tsx | 201 +++++++++++++++--- 2 files changed, 176 insertions(+), 45 deletions(-) diff --git a/public/components/overview/github-panel/github-panel.tsx b/public/components/overview/github-panel/github-panel.tsx index 092bf0559e..45a76c9303 100644 --- a/public/components/overview/github-panel/github-panel.tsx +++ b/public/components/overview/github-panel/github-panel.tsx @@ -23,26 +23,6 @@ export const GitHubPanel = withErrorBoundary(() => { const [moduleStatsList, setModuleStatsList] = useState([]); - /** Get Office 365 Side Panel Module info **/ - useEffect(() => { - (async () => { - try { - const modulesConfig = await queryConfig( - '000', - [{ component: 'wmodules', configuration: 'wmodules' }] - ); - const config = Object.entries(modulesConfig["wmodules-wmodules"].affected_items[0].wmodules - .filter((module) => { return Object.keys(module)[0] == 'sca' })[0]['sca']).map((configProp) => { //<-- change module name - const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; - return { title: configProp[0], description } - }) - setModuleStatsList(config); - } catch (error) { - setModuleStatsList([{ title: 'Module Unavailable', description: '' }]); - } - } - )(); - }, []) return ( <> diff --git a/public/components/overview/github-panel/views/stats.tsx b/public/components/overview/github-panel/views/stats.tsx index 5e36cc6f93..956512b22b 100644 --- a/public/components/overview/github-panel/views/stats.tsx +++ b/public/components/overview/github-panel/views/stats.tsx @@ -10,29 +10,180 @@ * Find more information about this on the LICENSE file. */ -import React from 'react'; -import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiIcon } from '@elastic/eui'; - -export const Stats = ({ listItems = [] }) => { - const logoStyle = { width: 30 }; - return ( - <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> - <EuiFlexItem> - <EuiFlexGroup> - <EuiFlexItem> - <EuiIcon type='logoGithub' style={logoStyle}/> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size={"xs"}><h4>GitHub</h4></EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiDescriptionList - listItems={listItems} - compressed - /> - </EuiFlexItem> - </EuiFlexGroup> - ); +import React, { useEffect, useMemo } from 'react'; +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiCallOut, EuiIcon, EuiSpacer, EuiProgress, EuiAccordion, EuiText } from '@elastic/eui'; +import { WzRequest } from '../../../../react-services'; +import { connect } from 'react-redux'; +import { useAsyncAction } from '../../../common/hooks'; +import { withGuard } from '../../../common/hocs'; +import { compose } from 'redux'; +import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; + +const mapStateToProps = state => ({ + agent: state.appStateReducers.currentAgentData +}); + +export const Stats = connect(mapStateToProps)( +({ agent }) => { + const asyncAction = useAsyncAction((async () => { + try{ + if(agent.id){ + // Get module configuration for the pinned agent + const agentConfigurationResponse = await WzRequest.apiReq('GET', `/agents/${agent.id}/config/wmodules/wmodules`, {}); + const configuration = mapWModuleConfigurationToRenderProperties(agentConfigurationResponse.data.data.wmodules, 'github', 'Agent', agent.name); + return configuration ? [configuration] : null; + }else{ + const custerStatusResponse = await WzRequest.apiReq('GET', '/cluster/status', {}); + if (custerStatusResponse.data.data.enabled === 'yes' && custerStatusResponse.data.data.running === 'yes') { + // Cluster mode + // Get cluster nodes + const nodesResponse = await WzRequest.apiReq('GET', `/cluster/nodes`, {}); + // Get module configuration for each node + const nodesConfigurationResponses = await Promise.all(nodesResponse.data.data.affected_items.map(async node => await WzRequest.apiReq('GET', `/cluster/${node.name}/configuration/wmodules/wmodules`, {}))) + const nodeConfigurations = nodesConfigurationResponses.map((response, index) => { + const configuration = mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'github', 'Manager', nodesResponse.data.data.affected_items[index].name); + return configuration || null; + }).filter(nodeConfiguration => nodeConfiguration); + return nodeConfigurations.length ? nodeConfigurations : null; + } else { + // Manager mode + const managerConfigurationResponse = await WzRequest.apiReq('GET', `/manager/configuration/wmodules/wmodules`, {}); + const configuration = mapWModuleConfigurationToRenderProperties(managerConfigurationResponse.data.data.affected_items[0].wmodules, 'github', 'Manager'); + return configuration ? [configuration] : null; + }; + } + }catch(error){ + const options: UIErrorLog = { + context: `${Stats.name}.getModuleConfig`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: 'Module Unavailable', + }, + }; + getErrorOrchestrator().handleError(options); + throw error; // This lets to populate the 'asyncAction.error' property + } + }), [agent]); + + useEffect(() => { + asyncAction.run(); + },[asyncAction.run]); + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem className='wz-justify-center' grow={false}> + <EuiIcon type='logoGithub' /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size='s'> + <h4 className='office-stats-title'>GitHub</h4> + </EuiTitle> + <EuiTitle size='xs'> + <h5 className='office-stats-subtitle'>Module configuration</h5> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <ConfigurationWrapper configurations={asyncAction.data} mappers={configurationMapToDescriptionList} loading={asyncAction.running} error={asyncAction.error}/> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}); + +const ConfigurationWrapper = compose( + withGuard(({loading}) => loading, () => <EuiProgress size='xs' color='primary' />), + withGuard(({error}) => error, ({error}) => ( + <EuiCallOut className='office-stats-callout-warning' + title="Error fetching the module configuration" + color="danger" + iconType="alert" + > + {error && error.message || String(error)} + </EuiCallOut> + )), + withGuard(({configurations}) => !configurations, () => ( + <EuiCallOut className='office-stats-callout-warning' + title='Module configuration unavailable' + color='warning' + iconType='alert' + /> + )) +)(({configurations, mappers}) => { + return ( + <div style={{height: 'calc(100vh - 175px)', overflow: 'scroll'}}> + {configurations.length === 1 + ? <> + <EuiText>{configurations[0].entity}{configurations[0].name ? ` - ${configurations[0].name}` : ''}</EuiText> + <EuiSpacer size='s'/> + <Configuration configuration={configurations[0].configuration} mappers={mappers}/> + </> + : configurations.map((configuration) => ( + <EuiAccordion id={`module_configuration_${configuration.name}`} key={`module_configuration_${configuration.name}`} buttonContent={`${configuration.entity}${configuration.name ? ` - ${configuration.name}` : ''}`}> + <Configuration {...configuration} mappers={mappers}/> + </EuiAccordion> + )).reduce((prev, cur) => [prev, <div style={{marginTop: '8px'}} /> , cur]) + } + </div> + ) +}); + +const Configuration = ({ configuration, mappers }) => { + const listItems = useMemo(() => { + return Object.entries(configuration) + .map(([key, value]) => ({title: mappers?.[key]?.title || key, description: mappers?.[key]?.description ? mappers[key].description(value) : value})); + }, [configuration]); + return ( + <> + <EuiDescriptionList className='office-description-list' listItems={listItems} compressed /> + </> + ) +} + +const configurationMapToDescriptionList: {[key: string]: {title?: string, description?: (value) => any}} = { + enabled: { + title: 'Enabled' + }, + only_future_events: { + title: 'Collect events generated since Wazuh agent was started', + }, + time_delay: { + title: 'Time in seconds that each scan will monitor until that delay backwards' + }, + curl_max_size: { + title: 'Maximum size allowed for the GitHub API response' + }, + interval: { + title: 'Interval between GitHub wodle executions in seconds' + }, + event_type: { + title: 'Event type' + }, + api_auth: { + title: 'Organizations', + description: (value) => ( + <> + {value.map(v => <EuiDescriptionList key={`api_auth_${v.org_name}`}>{v.org_name}</EuiDescriptionList>)} + </> + ) + } +}; + +const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any}[], wmoduleID: string, entity: string, name = '') => { + const configuration = wmodules.find(wmodule => Object.keys(wmodule)[0] === wmoduleID); + return configuration + ? {entity, name, configuration: configuration[Object.keys(configuration)[0]]} + : null; }; From ac38912acb297dda88bef0f90ad8b61989902a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 3 Aug 2021 13:44:08 +0200 Subject: [PATCH 215/493] fix(): Fix useAsyncAction hook which doesn't remove the `error` state when it was used after an action failed - Reset the `error` state to null --- public/components/common/hooks/use_async_action.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/components/common/hooks/use_async_action.ts b/public/components/common/hooks/use_async_action.ts index d71167255e..3af453e8f2 100644 --- a/public/components/common/hooks/use_async_action.ts +++ b/public/components/common/hooks/use_async_action.ts @@ -19,6 +19,7 @@ export function useAsyncAction(action, dependencies = []){ const run = useCallback(async (...params) => { try{ setRunning(true); + setError(null); const data = await action(...params); setData(data); }catch(error){ From a2a331cf4ef323662fd86bb48d917a97d5ae9008 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 3 Aug 2021 08:57:40 -0300 Subject: [PATCH 216/493] feat(configuration-view): Fixed mock dynamic table id. Updated snapshot. Fix label of only_future_events --- .../SubscriptionTab/SubscriptionTab.test.tsx | 7 +++++++ .../__snapshots__/SubscriptionTab.test.tsx.snap | 4 ++-- .../__snapshots__/general-tab.test.tsx.snap | 10 +++++----- .../office365/components/general-tab/general-tab.tsx | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx index a1979e37b6..e4d477d1f6 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.test.tsx @@ -14,6 +14,13 @@ import React from 'react'; import { SubscriptionTab } from './SubscriptionTab'; import { mount } from 'enzyme'; +jest.mock( + '../../../../../../../../../../../../kibana/node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + describe('SubscriptionTab component', () => { it('renders correctly to match the snapshot', () => { const wodleConfiguration = { diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap index bd0f41a886..12699b396b 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap @@ -447,13 +447,13 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = </div> </EuiTableHeaderMobile> <EuiTable - id="__table_23ff43c1-f3d4-11eb-bdee-2dceba245eef" + id="htmlId" responsive={true} tableLayout="fixed" > <table className="euiTable euiTable--responsive" - id="__table_23ff43c1-f3d4-11eb-bdee-2dceba245eef" + id="htmlId" tabIndex={-1} > <EuiScreenReaderOnly> diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index d93a4d5aeb..864a1a0674 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -432,7 +432,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "field": "only_future_events", - "label": "Collect events generated since Wazuh agent was started", + "label": "Collect events generated since Wazuh is initialized", }, Object { "field": "interval", @@ -586,9 +586,9 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` </EuiSpacer> </WzConfigurationSetting> <WzConfigurationSetting - key="-Collect events generated since Wazuh agent was started-undefined-1" - keyItem="-Collect events generated since Wazuh agent was started-undefined-1" - label="Collect events generated since Wazuh agent was started" + key="-Collect events generated since Wazuh is initialized-undefined-1" + keyItem="-Collect events generated since Wazuh is initialized-undefined-1" + label="Collect events generated since Wazuh is initialized" value="yes" > <div @@ -615,7 +615,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` <div className="euiTextAlign euiTextAlign--right" > - Collect events generated since Wazuh agent was started + Collect events generated since Wazuh is initialized </div> </EuiTextAlign> </div> diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index 8bfa4d35f2..89b1dfdf4d 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -26,7 +26,7 @@ export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => { field: 'enabled', label: 'Enabled', render: renderValueYesThenEnabled }, { field: 'only_future_events', - label: 'Collect events generated since Wazuh agent was started', + label: 'Collect events generated since Wazuh is initialized', }, { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds' }, { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, From 5fb97f4a579262112d7bf838f6532b34d9748819 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Tue, 3 Aug 2021 14:41:15 +0200 Subject: [PATCH 217/493] Fix/office agent (#3528) * Base branch for Office365 * remove all office features * Office365 sample data * Office365 sample data Edit some texts adding office keywords * replace office365 sample data ID * replace office365 sample data ID * change agent id to default id instead of random * check params for agent.name * check params for agent.name --- server/lib/generate-alerts/generate-alerts-script.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index a4e4108e88..f13f8e0483 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -313,6 +313,11 @@ function generateAlert(params) { } if (params.office) { + + if (params.manager && params.manager.name) { + alert.agent.name = params.manager.name; + }; + const beforeDate = new Date(new Date(alert.timestamp) - 3 * 24 * 60 * 60 * 1000); const IntraID = randomArrayItem(Office.arrayUuidOffice); const OrgID = randomArrayItem(Office.arrayUuidOffice); @@ -324,6 +329,7 @@ function generateAlert(params) { const log = randomArrayItem(Office.arrayLogs); const ruleData = Office.officeRules[log.RecordType]; + alert.agent.id = '000' alert.rule = ruleData.rule; alert.decoder = randomArrayItem(Office.arrayDecoderOffice); alert.GeoLocation = randomArrayItem(GeoLocation); From ee526a4740acc42d4bec939fbe35268cd33727c7 Mon Sep 17 00:00:00 2001 From: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Date: Tue, 3 Aug 2021 15:07:23 +0200 Subject: [PATCH 218/493] Added 2 new aggtables to office365 with their drilldowns (#3526) * Added 2 new aggtables to office365 with their drilldowns * Updated changelog and removed redundant visualization for Operations * Updated columns for drilldown IP and drilldown user * Updated changelog * Applied prettier * Tweaked main view config * Removed height limitation for SecurityAlerts --- CHANGELOG.md | 2 +- .../config/drilldown-ip-config.tsx | 11 +- .../config/drilldown-operations-config.tsx | 76 + .../config/drilldown-rules-config.tsx | 82 + .../config/drilldown-user-config.tsx | 11 +- .../overview/office-panel/config/index.ts | 2 + .../office-panel/config/main-view-config.tsx | 49 +- .../office-panel/config/module-config.tsx | 23 +- .../overview/overview-office.ts | 1735 ++++++++++------- 9 files changed, 1267 insertions(+), 724 deletions(-) create mode 100644 public/components/overview/office-panel/config/drilldown-operations-config.tsx create mode 100644 public/components/overview/office-panel/config/drilldown-rules-config.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 519b72f97a..8988ccc135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) -- Added base Module Panel view with Office365 setup [3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) +- Added base Module Panel view with Office365 setup [#3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) ### Changed diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index e74a4a8c22..d017031c3c 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -62,14 +62,21 @@ export const drilldownIPConfig = { ], }, { - height: 300, columns: [ { width: 100, component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> + <SecurityAlerts + initialColumns={[ + 'icon', + 'timestamp', + 'data.office365.UserId', + 'rule.description', + 'data.office365.Operation', + ]} + /> </EuiPanel> </EuiFlexItem> ), diff --git a/public/components/overview/office-panel/config/drilldown-operations-config.tsx b/public/components/overview/office-panel/config/drilldown-operations-config.tsx new file mode 100644 index 0000000000..48f66c3b62 --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-operations-config.tsx @@ -0,0 +1,76 @@ +/* + * Wazuh app - Office 365 Drilldown Operations field Config. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const drilldownOperationsConfig = { + rows: [ + { + height: 400, + columns: [ + { + width: 40, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Users" tab="office" {...props} /> + ), + }, + { + width: 60, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Country-Tag-Cloud" tab="office" {...props} /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-UserID" + tab="office" + {...props} + /> + ), + }, + ], + }, + { + columns: [ + { + width: 100, + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={[ + 'icon', + 'timestamp', + 'data.office365.UserId', + 'data.office365.ClientIP', + 'rule.description', + ]} + /> + </EuiPanel> + </EuiFlexItem> + ), + }, + ], + }, + ], +}; diff --git a/public/components/overview/office-panel/config/drilldown-rules-config.tsx b/public/components/overview/office-panel/config/drilldown-rules-config.tsx new file mode 100644 index 0000000000..d29be7e15a --- /dev/null +++ b/public/components/overview/office-panel/config/drilldown-rules-config.tsx @@ -0,0 +1,82 @@ +/* + * Wazuh app - Office 365 Drilldown Rules field Config. + * + * Copyright (C) 2015-2021 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 from 'react'; +import { VisCard } from '../../../common/modules/panel'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; + +export const drilldownRulesConfig = { + rows: [ + { + height: 400, + columns: [ + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Operations" tab="office" {...props} /> + ), + }, + { + width: 30, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Top-Users" tab="office" {...props} /> + ), + }, + { + width: 40, + component: (props) => ( + <VisCard id="Wazuh-App-Overview-Office-Country-Tag-Cloud" tab="office" {...props} /> + ), + }, + ], + }, + { + height: 300, + columns: [ + { + width: 100, + component: (props) => ( + <VisCard + id="Wazuh-App-Overview-Office-Alerts-Evolution-By-UserID" + tab="office" + {...props} + /> + ), + }, + ], + }, + { + columns: [ + { + width: 100, + component: () => ( + <EuiFlexItem> + <EuiPanel paddingSize={'s'}> + <SecurityAlerts + initialColumns={[ + 'icon', + 'timestamp', + 'data.office365.UserId', + 'data.office365.ClientIP', + 'data.office365.Operation', + ]} + /> + </EuiPanel> + </EuiFlexItem> + ), + }, + ], + }, + ], +}; diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx index 04c0cfeabf..cebdccf6c8 100644 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -62,14 +62,21 @@ export const drilldownUserConfig = { ], }, { - height: 300, columns: [ { width: 100, component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts /> + <SecurityAlerts + initialColumns={[ + 'icon', + 'timestamp', + 'data.office365.ClientIP', + 'rule.description', + 'data.office365.Operation', + ]} + /> </EuiPanel> </EuiFlexItem> ), diff --git a/public/components/overview/office-panel/config/index.ts b/public/components/overview/office-panel/config/index.ts index 529b8aee93..2e3ee7a5a4 100644 --- a/public/components/overview/office-panel/config/index.ts +++ b/public/components/overview/office-panel/config/index.ts @@ -13,6 +13,8 @@ export { drilldownIPConfig } from './drilldown-ip-config'; export { drilldownUserConfig } from './drilldown-user-config'; +export { drilldownOperationsConfig } from './drilldown-operations-config'; +export { drilldownRulesConfig } from './drilldown-rules-config'; export { MainViewConfig } from './main-view-config'; export { ModuleConfig } from './module-config'; export { filtersValues } from './search-bar-config'; diff --git a/public/components/overview/office-panel/config/main-view-config.tsx b/public/components/overview/office-panel/config/main-view-config.tsx index 4bef4aabe8..f8baf3b028 100644 --- a/public/components/overview/office-panel/config/main-view-config.tsx +++ b/public/components/overview/office-panel/config/main-view-config.tsx @@ -14,6 +14,7 @@ import React from 'react'; import { AggTable } from '../../../common/modules/panel/'; import { EuiFlexItem } from '@elastic/eui'; +import { SecurityAlerts } from '../../../visualize/components'; export const MainViewConfig = { rows: [ @@ -24,10 +25,10 @@ export const MainViewConfig = { component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable - tableTitle={'Users'} - aggTerm={'data.office365.UserId'} - aggLabel={'User'} - maxRows={'5'} + tableTitle="Top users" + aggTerm="data.office365.UserId" + aggLabel="User" + maxRows={5} onRowClick={(field, value) => props.onRowClick(field, value)} /> </EuiFlexItem> @@ -38,10 +39,42 @@ export const MainViewConfig = { component: (props) => ( <EuiFlexItem grow={props.grow}> <AggTable - tableTitle={'Client IP'} - aggTerm={'data.office365.ClientIP'} - aggLabel={'Client IP'} - maxRows={'5'} + tableTitle="Top client IP" + aggTerm="data.office365.ClientIP" + aggLabel="Client IP" + maxRows={5} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + </EuiFlexItem> + ), + }, + ], + }, + { + columns: [ + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle="Top rules" + aggTerm="rule.description" + aggLabel="Rule" + maxRows={5} + onRowClick={(field, value) => props.onRowClick(field, value)} + /> + </EuiFlexItem> + ), + }, + { + width: 50, + component: (props) => ( + <EuiFlexItem grow={props.grow}> + <AggTable + tableTitle="Top operations" + aggTerm="data.office365.Operation" + aggLabel="Operation" + maxRows={5} onRowClick={(field, value) => props.onRowClick(field, value)} /> </EuiFlexItem> diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index 7653aabcb7..ce63ef5807 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -13,7 +13,13 @@ import React from 'react'; import { OfficeBody, OfficeDrilldown } from '../views'; -import { MainViewConfig, drilldownIPConfig, drilldownUserConfig } from './'; +import { + MainViewConfig, + drilldownIPConfig, + drilldownUserConfig, + drilldownOperationsConfig, + drilldownRulesConfig, +} from './'; /** * The length method has to count Kibana Visualizations for TabVisualizations class @@ -35,5 +41,18 @@ export const ModuleConfig = { <OfficeDrilldown title={'Client IP'} {...{ ...drilldownIPConfig, ...props }} /> ), }, - + 'data.office365.Operation': { + length: () => + drilldownOperationsConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => ( + <OfficeDrilldown title={'Operation'} {...{ ...drilldownOperationsConfig, ...props }} /> + ), + }, + 'rule.description': { + length: () => + drilldownRulesConfig.rows.reduce((total, row) => total + row.columns.length, 0), + component: (props) => ( + <OfficeDrilldown title={'Rule'} {...{ ...drilldownRulesConfig, ...props }} /> + ), + }, }; diff --git a/server/integration-files/visualizations/overview/overview-office.ts b/server/integration-files/visualizations/overview/overview-office.ts index 6ef04f5295..59b140c45c 100644 --- a/server/integration-files/visualizations/overview/overview-office.ts +++ b/server/integration-files/visualizations/overview/overview-office.ts @@ -165,56 +165,56 @@ export default [ _source: { title: 'Max Rule Level', visState: JSON.stringify({ - "title": "Max Rule Level", - "type": "metric", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "max", - "params": { - "field": "rule.level", - "customLabel": "Max Rule Level" - }, - "schema": "metric" - } + title: 'Max Rule Level', + type: 'metric', + aggs: [ + { + id: '1', + enabled: true, + type: 'max', + params: { + field: 'rule.level', + customLabel: 'Max Rule Level', + }, + schema: 'metric', + }, ], - "params": { - "addTooltip": true, - "addLegend": false, - "type": "metric", - "metric": { - "percentageMode": false, - "useRanges": false, - "colorSchema": "Green to Red", - "metricColorMode": "Labels", - "colorsRange": [ + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'Labels', + colorsRange: [ { - "from": 0, - "to": 7 + from: 0, + to: 7, }, { - "from": 7, - "to": 10 + from: 7, + to: 10, }, { - "from": 10, - "to": 20 - } + from: 10, + to: 20, + }, ], - "labels": { - "show": true - }, - "invertColors": false, - "style": { - "bgFill": "#000", - "bgColor": false, - "labelColor": false, - "subText": "", - "fontSize": 26 - } - } - } + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 26, + }, + }, + }, }), uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', @@ -231,62 +231,62 @@ export default [ _source: { title: 'Suspicious Downloads', visState: JSON.stringify({ - "title": "Suspicious Downloads Count", - "type": "metric", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "filters", - "params": { - "filters": [ + title: 'Suspicious Downloads Count', + type: 'metric', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ { - "input": { - "query": "rule.id: \"91724\"", - "language": "kuery" + input: { + query: 'rule.id: "91724"', + language: 'kuery', }, - "label": "Suspicious Downloads" - } - ] + label: 'Suspicious Downloads', + }, + ], }, - "schema": "group" - } + schema: 'group', + }, ], - "params": { - "addTooltip": true, - "addLegend": false, - "type": "metric", - "metric": { - "percentageMode": false, - "useRanges": false, - "colorSchema": "Green to Red", - "metricColorMode": "Labels", - "colorsRange": [ + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'Labels', + colorsRange: [ { - "from": 0, - "to": 1 - } + from: 0, + to: 1, + }, ], - "labels": { - "show": true - }, - "invertColors": false, - "style": { - "bgFill": "#000", - "bgColor": false, - "labelColor": false, - "subText": "", - "fontSize": 26 - } - } - } + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 26, + }, + }, + }, }), uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', @@ -303,62 +303,62 @@ export default [ _source: { title: 'Malware Alerts', visState: JSON.stringify({ - "title": "Malware Alerts Count", - "type": "metric", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "filters", - "params": { - "filters": [ + title: 'Malware Alerts Count', + type: 'metric', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ { - "input": { - "query": "rule.id: \"91556\" or rule.id: \"91575\" or rule.id: \"91700\" ", - "language": "kuery" + input: { + query: 'rule.id: "91556" or rule.id: "91575" or rule.id: "91700" ', + language: 'kuery', }, - "label": "Malware Alerts" - } - ] + label: 'Malware Alerts', + }, + ], }, - "schema": "group" - } + schema: 'group', + }, ], - "params": { - "addTooltip": true, - "addLegend": false, - "type": "metric", - "metric": { - "percentageMode": false, - "useRanges": false, - "colorSchema": "Green to Red", - "metricColorMode": "None", - "colorsRange": [ + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [ { - "from": 0, - "to": 10000 - } + from: 0, + to: 10000, + }, ], - "labels": { - "show": true - }, - "invertColors": false, - "style": { - "bgFill": "#000", - "bgColor": false, - "labelColor": false, - "subText": "", - "fontSize": 26 - } - } - } + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 26, + }, + }, + }, }), uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', @@ -375,62 +375,62 @@ export default [ _source: { title: 'Full Access Permissions', visState: JSON.stringify({ - "title": "Full Access Permission Count", - "type": "metric", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "filters", - "params": { - "filters": [ + title: 'Full Access Permission Count', + type: 'metric', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'filters', + params: { + filters: [ { - "input": { - "query": "rule.id: \"91725\"", - "language": "kuery" + input: { + query: 'rule.id: "91725"', + language: 'kuery', }, - "label": "Full Access Permissions" - } - ] + label: 'Full Access Permissions', + }, + ], }, - "schema": "group" - } + schema: 'group', + }, ], - "params": { - "addTooltip": true, - "addLegend": false, - "type": "metric", - "metric": { - "percentageMode": false, - "useRanges": false, - "colorSchema": "Green to Red", - "metricColorMode": "None", - "colorsRange": [ + params: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [ { - "from": 0, - "to": 10000 - } + from: 0, + to: 10000, + }, ], - "labels": { - "show": true - }, - "invertColors": false, - "style": { - "bgFill": "#000", - "bgColor": false, - "labelColor": false, - "subText": "", - "fontSize": 26 - } - } - } + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 26, + }, + }, + }, }), uiStateJSON: JSON.stringify({ vis: { defaultColors: { '0 - 100': 'rgb(0,104,55)' } } }), description: '', @@ -1057,194 +1057,8 @@ export default [ _source: { title: 'User Operations', visState: JSON.stringify({ - "title": "User Operation Level", - "type": "table", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.UserId", - "orderBy": "1", - "order": "desc", - "size": 500, - "otherBucket": true, - "otherBucketLabel": "Others", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Users" - }, - "schema": "bucket" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.Operation", - "orderBy": "1", - "order": "desc", - "size": 100, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Operation" - }, - "schema": "bucket" - }, - { - "id": "4", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.level", - "orderBy": "1", - "order": "desc", - "size": 20, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Rule level" - }, - "schema": "bucket" - } - ], - "params": { - "perPage": 5, - "showPartialRows": false, - "showMetricsAtAllLevels": false, - "sort": { - "columnIndex": null, - "direction": null - }, - "showTotal": false, - "totalFunc": "sum", - "percentageCol": "" - } - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table', - _type: 'visualization', - _source: { - title: 'Client IP Operations', - visState: JSON.stringify({ - "title": "Client IP Operation Level", - "type": "table", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.ClientIP", - "orderBy": "1", - "order": "desc", - "size": 500, - "otherBucket": true, - "otherBucketLabel": "Others", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Client IP" - }, - "schema": "bucket" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.Operation", - "orderBy": "1", - "order": "desc", - "size": 100, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Operation" - }, - "schema": "bucket" - }, - { - "id": "4", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.level", - "orderBy": "1", - "order": "desc", - "size": 20, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Rule level" - }, - "schema": "bucket" - } - ], - "params": { - "perPage": 5, - "showPartialRows": false, - "showMetricsAtAllLevels": false, - "sort": { - "columnIndex": null, - "direction": null - }, - "showTotal": false, - "totalFunc": "sum", - "percentageCol": "" - } - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-Office-Top-Events-Pie', - _type: 'visualization', - _source: { - title: 'Top Events', - visState: JSON.stringify({ - title: 'Cake', - type: 'pie', + title: 'User Operation Level', + type: 'table', aggs: [ { id: '1', @@ -1255,50 +1069,67 @@ export default [ }, { id: '2', - enabled: false, + enabled: true, type: 'terms', params: { - field: 'rule.level', + field: 'data.office365.UserId', orderBy: '1', order: 'desc', - size: 5, + size: 500, + otherBucket: true, + otherBucketLabel: 'Others', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Users', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Operation', + orderBy: '1', + order: 'desc', + size: 100, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', + customLabel: 'Operation', }, - schema: 'segment', + schema: 'bucket', }, { - id: '3', + id: '4', enabled: true, type: 'terms', params: { - field: 'rule.description', + field: 'rule.level', orderBy: '1', order: 'desc', - size: 10, + size: 20, otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', + customLabel: 'Rule level', }, - schema: 'segment', + schema: 'bucket', }, ], params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: false, - labels: { - show: false, - values: true, - last_level: true, - truncate: 100, + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, }, - row: true, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', }, }), uiStateJSON: '{}', @@ -1314,13 +1145,13 @@ export default [ }, }, { - _id: 'Wazuh-App-Overview-Office-Alerts-Evolution-By-User', + _id: 'Wazuh-App-Overview-Office-Client-IP-Operation-Level-Table', _type: 'visualization', _source: { - title: 'Alerts evolution over time', + title: 'Client IP Operations', visState: JSON.stringify({ - title: 'Alerts evolution over time', - type: 'line', + title: 'Client IP Operation Level', + type: 'table', aggs: [ { id: '1', @@ -1332,9 +1163,178 @@ export default [ { id: '2', enabled: true, - type: 'date_histogram', + type: 'terms', params: { - field: 'timestamp', + field: 'data.office365.ClientIP', + orderBy: '1', + order: 'desc', + size: 500, + otherBucket: true, + otherBucketLabel: 'Others', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Client IP', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Operation', + orderBy: '1', + order: 'desc', + size: 100, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Operation', + }, + schema: 'bucket', + }, + { + id: '4', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule level', + }, + schema: 'bucket', + }, + ], + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Top-Events-Pie', + _type: 'visualization', + _source: { + title: 'Top Events', + visState: JSON.stringify({ + title: 'Cake', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: false, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 10, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: false, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + row: true, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Alerts-Evolution-By-User', + _type: 'visualization', + _source: { + title: 'Alerts evolution over time', + visState: JSON.stringify({ + title: 'Alerts evolution over time', + type: 'line', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', timeRange: { from: 'now-1y', to: 'now', @@ -1553,63 +1553,63 @@ export default [ _source: { title: 'Rule Description by Level', visState: JSON.stringify({ - "title": "Rule Description Level Table", - "type": "table", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.description", - "orderBy": "1", - "order": "desc", - "size": 500, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Rule Description" - }, - "schema": "bucket" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.level", - "orderBy": "1", - "order": "desc", - "size": 20, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing", - "customLabel": "Rule Level" - }, - "schema": "bucket" - } + title: 'Rule Description Level Table', + type: 'table', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'rule.description', + orderBy: '1', + order: 'desc', + size: 500, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule Description', + }, + schema: 'bucket', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Rule Level', + }, + schema: 'bucket', + }, ], - "params": { - "perPage": 5, - "showPartialRows": false, - "showMetricsAtAllLevels": false, - "sort": { - "columnIndex": null, - "direction": null - }, - "showTotal": false, - "totalFunc": "sum", - "percentageCol": "" - } + params: { + perPage: 5, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, }), uiStateJSON: '{}', description: '', @@ -2034,266 +2034,266 @@ export default [ _source: { title: 'Severity by user', visState: JSON.stringify({ - "title": "Severity By User Barchart", - "type": "histogram", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "rule.level", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": true, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.UserId", - "orderBy": "1", - "order": "desc", - "size": 20, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "group" - } - ], - "params": { - "type": "histogram", - "grid": { - "categoryLines": false + title: 'Severity By User Barchart', + type: 'histogram', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', }, - "categoryAxes": [ - { - "id": "CategoryAxis-1", - "type": "category", - "position": "bottom", - "show": true, - "style": {}, - "scale": { - "type": "linear" - }, - "labels": { - "show": true, - "filter": true, - "truncate": 100 - }, - "title": {} - } - ], - "valueAxes": [ - { - "id": "ValueAxis-1", - "name": "LeftAxis-1", - "type": "value", - "position": "left", - "show": true, - "style": {}, - "scale": { - "type": "linear", - "mode": "normal" - }, - "labels": { - "show": true, - "rotate": 0, - "filter": false, - "truncate": 100 - }, - "title": { - "text": "Count" - } - } - ], - "seriesParams": [ - { - "show": true, - "type": "histogram", - "mode": "stacked", - "data": { - "label": "Count", - "id": "1" - }, - "valueAxis": "ValueAxis-1", - "drawLinesBetweenPoints": true, - "lineWidth": 2, - "showCircles": true - } - ], - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "times": [], - "addTimeMarker": false, - "labels": { - "show": false - }, - "thresholdLine": { - "show": false, - "value": 10, - "width": 1, - "style": "full", - "color": "#E7664C" - } - } - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-Office-Top-Users-By-Subscription-Barchart', - _type: 'visualization', - _source: { - title: 'Top User By Subscription', - visState: JSON.stringify({ - "title": "Top User By Subscription", - "type": "histogram", - "aggs": [ - { - "id": "1", - "enabled": true, - "type": "count", - "params": {}, - "schema": "metric" - }, - { - "id": "2", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.UserId", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "segment" - }, - { - "id": "3", - "enabled": true, - "type": "terms", - "params": { - "field": "data.office365.Subscription", - "orderBy": "1", - "order": "desc", - "size": 5, - "otherBucket": false, - "otherBucketLabel": "Other", - "missingBucket": false, - "missingBucketLabel": "Missing" - }, - "schema": "group" - } - ], - "params": { - "type": "histogram", - "grid": { - "categoryLines": false + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'rule.level', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: true, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 20, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + params: { + type: 'histogram', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { + show: false, + }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Top-Users-By-Subscription-Barchart', + _type: 'visualization', + _source: { + title: 'Top User By Subscription', + visState: JSON.stringify({ + title: 'Top User By Subscription', + type: 'histogram', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Subscription', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'group', + }, + ], + params: { + type: 'histogram', + grid: { + categoryLines: false, }, - "categoryAxes": [ + categoryAxes: [ { - "id": "CategoryAxis-1", - "type": "category", - "position": "bottom", - "show": true, - "style": {}, - "scale": { - "type": "linear" + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', }, - "labels": { - "show": true, - "filter": true, - "truncate": 100, - "rotate": 0 + labels: { + show: true, + filter: true, + truncate: 100, + rotate: 0, }, - "title": {} - } + title: {}, + }, ], - "valueAxes": [ + valueAxes: [ { - "id": "ValueAxis-1", - "name": "LeftAxis-1", - "type": "value", - "position": "left", - "show": true, - "style": {}, - "scale": { - "type": "linear", - "mode": "normal" + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, }, - "labels": { - "show": true, - "rotate": 0, - "filter": false, - "truncate": 100 + title: { + text: 'Count', }, - "title": { - "text": "Count" - } - } + }, ], - "seriesParams": [ + seriesParams: [ { - "show": true, - "type": "histogram", - "mode": "stacked", - "data": { - "label": "Count", - "id": "1" + show: true, + type: 'histogram', + mode: 'stacked', + data: { + label: 'Count', + id: '1', }, - "valueAxis": "ValueAxis-1", - "drawLinesBetweenPoints": true, - "lineWidth": 2, - "showCircles": true - } + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + }, ], - "addTooltip": true, - "addLegend": true, - "legendPosition": "right", - "times": [], - "addTimeMarker": false, - "labels": { - "show": false - }, - "thresholdLine": { - "show": false, - "value": 10, - "width": 1, - "style": "full", - "color": "#E7664C" - } - } + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: { + show: false, + }, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + }, }), uiStateJSON: '{}', description: '', @@ -2375,4 +2375,321 @@ export default [ }, }, }, + { + _id: 'Wazuh-App-Overview-Office-Country-Tag-Cloud', + _type: 'visualization', + _source: { + title: 'Country of origin', + visState: JSON.stringify({ + title: 'Country tag cloud', + type: 'tagcloud', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'GeoLocation.country_name', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + ], + params: { + scale: 'linear', + orientation: 'right angled', + minFontSize: 18, + maxFontSize: 72, + showLabel: false, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Alerts-Evolution-By-UserID', + _type: 'visualization', + _source: { + title: 'Alerts by user', + visState: JSON.stringify({ + title: 'Alerts evolution over time', + type: 'line', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: { + customLabel: 'Alerts', + }, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'date_histogram', + params: { + field: 'timestamp', + timeRange: { + from: 'now-1w', + to: 'now', + }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: 'auto', + drop_partials: false, + min_doc_count: 1, + extended_bounds: {}, + }, + schema: 'segment', + }, + { + id: '3', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'User ID', + }, + schema: 'group', + }, + ], + params: { + type: 'line', + grid: { + categoryLines: false, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + position: 'bottom', + show: true, + style: {}, + scale: { + type: 'linear', + }, + labels: { + show: true, + filter: true, + truncate: 100, + }, + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Alerts', + }, + }, + ], + seriesParams: [ + { + show: true, + type: 'line', + mode: 'normal', + data: { + label: 'Alerts', + id: '1', + }, + valueAxis: 'ValueAxis-1', + drawLinesBetweenPoints: true, + lineWidth: 2, + interpolate: 'linear', + showCircles: true, + }, + ], + addTooltip: true, + addLegend: true, + legendPosition: 'right', + times: [], + addTimeMarker: false, + labels: {}, + thresholdLine: { + show: false, + value: 10, + width: 1, + style: 'full', + color: '#E7664C', + }, + row: true, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Top-Users', + _type: 'visualization', + _source: { + title: 'Top Office Users', + visState: JSON.stringify({ + title: 'Alerts by user', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.UserId', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, + { + _id: 'Wazuh-App-Overview-Office-Top-Operations', + _type: 'visualization', + _source: { + title: 'Top Operations', + visState: JSON.stringify({ + title: 'Top Operations', + type: 'pie', + aggs: [ + { + id: '1', + enabled: true, + type: 'count', + params: {}, + schema: 'metric', + }, + { + id: '2', + enabled: true, + type: 'terms', + params: { + field: 'data.office365.Operation', + orderBy: '1', + order: 'desc', + size: 5, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Operation', + }, + schema: 'segment', + }, + ], + params: { + type: 'pie', + addTooltip: true, + addLegend: true, + legendPosition: 'right', + isDonut: true, + labels: { + show: false, + values: true, + last_level: true, + truncate: 100, + }, + }, + }), + uiStateJSON: '{}', + description: '', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + index: 'wazuh-alerts', + filter: [], + query: { query: '', language: 'lucene' }, + }), + }, + }, + }, ]; From e8b276b5f98ac8c7dcc5c336dd01f103c6eff948 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 3 Aug 2021 15:31:04 +0200 Subject: [PATCH 219/493] add changelog and fix comments --- CHANGELOG.md | 1 + .../custom-search-bar/custom-search-bar.tsx | 25 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 519b72f97a..1fbada1338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) - Moved the filterManager subscription to the hook useFilterManager [#3517](https://github.com/wazuh/wazuh-kibana-app/pull/3517) +- Change filter from is to is one of in custom searchbar [#3529](https://github.com/wazuh/wazuh-kibana-app/pull/3529) ### Fixed diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 79fff91384..491e205f01 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -20,7 +20,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const KibanaServices = getDataPlugin().query; const filterManager = KibanaServices.filterManager; - const indexPattern = getIndexPattern(); const defaultSelectedOptions = () => { const array = [] filtersValues.forEach(item => { @@ -51,15 +50,13 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const buildCustomFilter = (isPinned: boolean, values?: any): Filter => { - const newFilters = [] - values.forEach(element => { - const filter = { - match_phrase: { - [element.value]: element.label - } + const newFilters = values.map(element => ({ + match_phrase: { + [element.value]: { + query: element.label + } } - newFilters.push(filter); - }); + })); const params = values.map(item => item.label) const meta: FilterMeta = { disabled: false, @@ -94,13 +91,11 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const refreshCustomSelectedFilter = () => { setSelectedOptions(defaultSelectedOptions) - const filterCustom = [] const filters = filterManager.getFilters().filter(item => item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key)).map(element => ({ params: element.meta.params, key: element.meta.key })) || []; - filters.forEach(item => { - item.params.forEach(element => { - filterCustom.push({ label: element, value: item.key }) - }) - }) + const getFilterCustom = (item) => { + return item.params.map(element => ({ label: element, value: item.key })) + } + const filterCustom = filters.map((item) => getFilterCustom(item))[0] || [] if (filterCustom.length != 0) { filterCustom.forEach(item => { From f127f61432d85f603752874360c5c337817d2feb Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 3 Aug 2021 15:44:57 +0200 Subject: [PATCH 220/493] fix prettier --- .../custom-search-bar/custom-search-bar.tsx | 303 +++++++++--------- 1 file changed, 155 insertions(+), 148 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 491e205f01..a2673070b2 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect } from 'react'; import { getIndexPattern } from '../../overview/mitre/lib'; import { Filter } from '../../../../../../src/plugins/data/public/'; -import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../src/plugins/data/common'; +import { + FilterMeta, + FilterState, + FilterStateStore, +} from '../../../../../../src/plugins/data/common'; import { AppState } from '../../../react-services/app-state'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore import { getDataPlugin } from '../../../kibana-services'; @@ -17,151 +17,158 @@ import { KbnSearchBar } from '../../kbn-search-bar'; import { Combobox } from './components'; export const CustomSearchBar = ({ filtersValues, ...props }) => { - - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; - const defaultSelectedOptions = () => { - const array = [] - filtersValues.forEach(item => { - array[item.key] = [] - }) - - return array - } - const [avancedFiltersState, setAvancedFiltersState] = useState(false); - const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); - - useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - onFiltersUpdated() - return () => { - filterSubscriber.unsubscribe(); - }; - }); - }, []); - - const onFiltersUpdated = () => { - refreshCustomSelectedFilter() - } - - const changeSwitch = () => { - setAvancedFiltersState(state => !state); - } - - - const buildCustomFilter = (isPinned: boolean, values?: any): Filter => { - const newFilters = values.map(element => ({ - match_phrase: { - [element.value]: { - query: element.label - } - } - })); - const params = values.map(item => item.label) - const meta: FilterMeta = { - disabled: false, - negate: false, - key: values[0].value, - params: params, - alias: null, - type: "phrases", - value: params.join(","), - index: AppState.getCurrentPattern(), - }; - const $state: FilterState = { - store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, - }; - const query = { - bool: { - minimum_should_match: 1, - should: newFilters - } - } - - return { meta, $state, query }; + const KibanaServices = getDataPlugin().query; + const filterManager = KibanaServices.filterManager; + const defaultSelectedOptions = () => { + const array = []; + filtersValues.forEach((item) => { + array[item.key] = []; + }); + + return array; + }; + const [avancedFiltersState, setAvancedFiltersState] = useState(false); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + + useEffect(() => { + let filterSubscriber = filterManager.getUpdates$().subscribe(() => { + onFiltersUpdated(); + return () => { + filterSubscriber.unsubscribe(); + }; + }); + }, []); + + const onFiltersUpdated = () => { + refreshCustomSelectedFilter(); + }; + + const changeSwitch = () => { + setAvancedFiltersState((state) => !state); + }; + + const buildCustomFilter = (isPinned: boolean, values?: any): Filter => { + const newFilters = values.map((element) => ({ + match_phrase: { + [element.value]: { + query: element.label, + }, + }, + })); + const params = values.map((item) => item.label); + const meta: FilterMeta = { + disabled: false, + negate: false, + key: values[0].value, + params: params, + alias: null, + type: 'phrases', + value: params.join(','), + index: AppState.getCurrentPattern(), }; - - const setKibanaFilters = (values: any[]) => { - const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) - filterManager.removeAll() - filterManager.addFilters(currentFilters) - const customFilter = buildCustomFilter(false, values) - filterManager.addFilters(customFilter) - } - - const refreshCustomSelectedFilter = () => { - setSelectedOptions(defaultSelectedOptions) - const filters = filterManager.getFilters().filter(item => item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key)).map(element => ({ params: element.meta.params, key: element.meta.key })) || []; - const getFilterCustom = (item) => { - return item.params.map(element => ({ label: element, value: item.key })) - } - const filterCustom = filters.map((item) => getFilterCustom(item))[0] || [] - - if (filterCustom.length != 0) { - filterCustom.forEach(item => { - setSelectedOptions(prevState => ({ - ...prevState, - [item.value]: [...prevState[item.value], item], - })); - - }) - } + const $state: FilterState = { + store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + }; + const query = { + bool: { + minimum_should_match: 1, + should: newFilters, + }, }; - const onChange = (values: any[]) => { - setKibanaFilters(values) - refreshCustomSelectedFilter(); + return { meta, $state, query }; + }; + + const setKibanaFilters = (values: any[]) => { + const currentFilters = filterManager + .getFilters() + .filter((item) => item.meta.key != values[0].value); + filterManager.removeAll(); + filterManager.addFilters(currentFilters); + const customFilter = buildCustomFilter(false, values); + filterManager.addFilters(customFilter); + }; + + const refreshCustomSelectedFilter = () => { + setSelectedOptions(defaultSelectedOptions); + const filters = + filterManager + .getFilters() + .filter( + (item) => + item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key) + ) + .map((element) => ({ params: element.meta.params, key: element.meta.key })) || []; + const getFilterCustom = (item) => { + return item.params.map((element) => ({ label: element, value: item.key })); }; + const filterCustom = filters.map((item) => getFilterCustom(item))[0] || []; - const getComponent = (item: any) => { - var types = { - 'default': <></>, - 'combobox': <Combobox - item={item} - selectedOptions={selectedOptions[item.key] || []} - onChange={onChange} - /> - }; - return types[item.type] || types['default']; + if (filterCustom.length != 0) { + filterCustom.forEach((item) => { + setSelectedOptions((prevState) => ({ + ...prevState, + [item.value]: [...prevState[item.value], item], + })); + }); } - - return ( - <> - <EuiFlexGroup className='custom-kbn-search-bar' alignItems='center' style={{ margin: '0 8px' }}> - { - avancedFiltersState === false ? - filtersValues.map((item, key) => ( - <EuiFlexItem key={key}> - {getComponent(item)} - </EuiFlexItem> - )) - : - '' - } - <EuiFlexItem> - <KbnSearchBar - showFilterBar={false} - showQueryInput={avancedFiltersState} - onFiltersUpdated={onFiltersUpdated} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> - <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> - <KbnSearchBar - showDatePicker={false} - showQueryInput={false} - onFiltersUpdated={onFiltersUpdated} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSwitch - label="Advanced filters" - checked={avancedFiltersState} - onChange={() => changeSwitch()} - /> - </EuiFlexItem> - </EuiFlexGroup> - </> - ) -}; \ No newline at end of file + }; + + const onChange = (values: any[]) => { + setKibanaFilters(values); + refreshCustomSelectedFilter(); + }; + + const getComponent = (item: any) => { + var types = { + default: <></>, + combobox: ( + <Combobox + item={item} + selectedOptions={selectedOptions[item.key] || []} + onChange={onChange} + /> + ), + }; + return types[item.type] || types['default']; + }; + + return ( + <> + <EuiFlexGroup + className="custom-kbn-search-bar" + alignItems="center" + style={{ margin: '0 8px' }} + > + {avancedFiltersState === false + ? filtersValues.map((item, key) => ( + <EuiFlexItem key={key}>{getComponent(item)}</EuiFlexItem> + )) + : ''} + <EuiFlexItem> + <KbnSearchBar + showFilterBar={false} + showQueryInput={avancedFiltersState} + onFiltersUpdated={onFiltersUpdated} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup justifyContent="flexEnd" style={{ margin: '0 20px' }}> + <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> + <KbnSearchBar + showDatePicker={false} + showQueryInput={false} + onFiltersUpdated={onFiltersUpdated} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSwitch + label="Advanced filters" + checked={avancedFiltersState} + onChange={() => changeSwitch()} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; From 4ac9237f50bef9825a509b51e80db5c4cf1ac9fb Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Tue, 3 Aug 2021 10:56:46 -0300 Subject: [PATCH 221/493] Requested changes --- .../agents/vuls/inventory/flyout.tsx | 23 ++++++++++--------- .../agents/vuls/inventory/table.tsx | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 1ad1b18456..ea754b61ea 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -56,9 +56,13 @@ export class FlyoutDetail extends Component { }; } - async getLastScan(){ - const response = await WzRequest.apiReq('GET', `/vulnerability/${this.props.agentId}/last_scan`, {}); - return ((response.data || {}).data || {}).affected_items[0] || {}; + async getLastScan() { + const response = await WzRequest.apiReq( + 'GET', + `/vulnerability/${this.props.agentId}/last_scan`, + {} + ); + return ((response.data || {}).data || {}).affected_items[0] || {}; } async componentDidMount() { @@ -68,15 +72,15 @@ export class FlyoutDetail extends Component { ? { 'cluster.name': AppState.getClusterInfo().cluster } : { 'manager.name': AppState.getClusterInfo().manager }; this.setState({ clusterFilter }); - let currentItem = this.props.item; + const currentItem = this.props.item; if (!currentItem) { throw false; } - let lastScan = await this.getLastScan(); - currentItem = { ...currentItem, ...lastScan}; - + const lastScan = await this.getLastScan(); + Object.assign(currentItem, { ...lastScan }); + this.setState({ currentItem, isLoading: false }); } catch (error) { const options: UIErrorLog = { @@ -98,14 +102,11 @@ export class FlyoutDetail extends Component { } } - - - render() { const { currentItem } = this.state; const title = `${currentItem.cve}`; const id = title.replace(/ /g, '_'); - + return ( <EuiFlyout onClose={() => this.props.closeFlyout()} diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index f3c8a89166..7ef3ad36ed 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -139,13 +139,13 @@ export class InventoryTable extends Component { }, { field: 'cvss2_score', - name: 'CVSS2', + name: 'CVSS2 Score', sortable: true, width: `${width}` }, { field: 'cvss3_score', - name: 'CVSS3', + name: 'CVSS3 Score', sortable: true, width: `${width}` }, From 9d0b30971bbc9f80b64b02fb22b3747538898e55 Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Tue, 3 Aug 2021 10:57:40 -0300 Subject: [PATCH 222/493] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5591927a8b..a99e88cc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to the Wazuh app project will be documented in this file. [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added new fields in Inventory table and Flyout Details [#3525](https://github.com/wazuh/wazuh-kibana-app/pull/3525) ### Changed From e5bc91d138d6f7f3b6eb906dff221642fa6ace0e Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 3 Aug 2021 16:42:02 +0200 Subject: [PATCH 223/493] fix comments PR --- .../custom-search-bar/custom-search-bar.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index a2673070b2..71fec9fa32 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -15,6 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { Combobox } from './components'; +import { element } from 'angular'; export const CustomSearchBar = ({ filtersValues, ...props }) => { const KibanaServices = getDataPlugin().query; @@ -99,17 +100,20 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key) ) .map((element) => ({ params: element.meta.params, key: element.meta.key })) || []; + const getFilterCustom = (item) => { return item.params.map((element) => ({ label: element, value: item.key })); }; - const filterCustom = filters.map((item) => getFilterCustom(item))[0] || []; - + const filterCustom = filters.map((item) => getFilterCustom(item)) || []; + console.log(filters,filters.map((item) => getFilterCustom(item))) if (filterCustom.length != 0) { filterCustom.forEach((item) => { - setSelectedOptions((prevState) => ({ - ...prevState, - [item.value]: [...prevState[item.value], item], - })); + item.forEach(element =>{ + setSelectedOptions((prevState) => ({ + ...prevState, + [element.value]: [...prevState[element.value], element], + })); + }) }); } }; From 0d4665400442ba022ebefe6d9ff2eebc423d83ab Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 4 Aug 2021 10:31:35 +0200 Subject: [PATCH 224/493] delete console.lgo --- public/components/common/custom-search-bar/custom-search-bar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 71fec9fa32..ef6b949f61 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -105,7 +105,6 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { return item.params.map((element) => ({ label: element, value: item.key })); }; const filterCustom = filters.map((item) => getFilterCustom(item)) || []; - console.log(filters,filters.map((item) => getFilterCustom(item))) if (filterCustom.length != 0) { filterCustom.forEach((item) => { item.forEach(element =>{ From c080bcb94b452b4099d8a11fbd79ba05fbfafbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Wed, 4 Aug 2021 11:02:10 +0200 Subject: [PATCH 225/493] Integrating dynamic filters (#3531) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Integrating dynamic filters * Adding Changelog * fix(github_module): Replaced dynamic selector on GitHub panel to use `data.github.repo` field Co-authored-by: Antonio David Gutiérrez <antonio.gutierrez@wazuh.com> --- CHANGELOG.md | 1 + .../custom-search-bar/custom-search-bar.tsx | 302 +++++++++--------- .../common/hooks/use-kbn-loading-indicator.ts | 3 +- .../common/hooks/use-value-suggestions.ts | 29 +- .../github-panel/config/search-bar-config.ts | 101 ++---- 5 files changed, 214 insertions(+), 222 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 519b72f97a..68f9e85eaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) - Added base Module Panel view with Office365 setup [3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) +- Added dinamic simple filters and adding simple GitHub filters fields [3531](https://github.com/wazuh/wazuh-kibana-app/pull/3531) ### Changed diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 7ae637ce49..a178cd9816 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,165 +1,169 @@ import React, { useState, useEffect, useLayoutEffect } from 'react'; -import { getIndexPattern } from '../../overview/mitre/lib' +import { getIndexPattern } from '../../overview/mitre/lib'; import { Filter } from '../../../../../../src/plugins/data/public/'; -import { FilterMeta, FilterState, FilterStateStore } from '../../../../../../src/plugins/data/common'; - import { - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, -} from '@elastic/eui'; + FilterMeta, + FilterState, + FilterStateStore, +} from '../../../../../../src/plugins/data/common'; + +import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; -import { Combobox } from './components' +import { Combobox } from './components'; +import { useFilterManager } from '../hooks'; export const CustomSearchBar = ({ filtersValues, ...props }) => { - - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; - const indexPattern = getIndexPattern(); - const defaultSelectedOptions = () => { - const array = [] - filtersValues.forEach(item => { - array[item.key] = [] - }) - - return array - } - const [avancedFiltersState, setAvancedFiltersState] = useState(false); - const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); - - useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - onFiltersUpdated() - return () => { - filterSubscriber.unsubscribe(); - }; - }); - }, []); - - const onFiltersUpdated = () => { - refreshCustomSelectedFilter() - } - - const changeSwitch = () => { - setAvancedFiltersState(state => !state); - } - - - const buildCustomFilter = (isPinned: boolean, index?: any, querySearch?: any, key?: any): Filter => { - const meta: FilterMeta = { - disabled: false, - negate: false, - key: key, - params: { query: querySearch }, - alias: null, - type: "phrase", - index, - }; - const $state: FilterState = { - store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, - }; - const query = { - match_phrase: { - [key]: { - query: querySearch - } - } - } - - return { meta, $state, query }; + const { filterManager } = useFilterManager(); + const indexPattern = getIndexPattern(); + const defaultSelectedOptions = () => { + const array = []; + filtersValues.forEach((item) => { + array[item.key] = []; + }); + + return array; + }; + const [avancedFiltersState, setAvancedFiltersState] = useState(false); + const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + + const onFiltersUpdated = () => { + refreshCustomSelectedFilter(); + }; + + const changeSwitch = () => { + setAvancedFiltersState((state) => !state); + }; + + const buildCustomFilter = ( + isPinned: boolean, + index?: any, + querySearch?: any, + key?: any + ): Filter => { + const meta: FilterMeta = { + disabled: false, + negate: false, + key: key, + params: { query: querySearch }, + alias: null, + type: 'phrase', + index, }; - - const setKibanaFilters = (values: any[]) => { - const newFilters = [] - const currentFilters = filterManager.getFilters().filter(item => item.meta.key != values[0].value) - filterManager.removeAll() - filterManager.addFilters(currentFilters) - values.forEach(element => { - const customFilter = buildCustomFilter(false, indexPattern.title, element.label, element.value) - newFilters.push(customFilter); - }); - filterManager.addFilters(newFilters) - } - - const refreshCustomSelectedFilter = () => { - setSelectedOptions(defaultSelectedOptions) - const filters = filterManager.getFilters() - const filterCustom = filters.map(item => { - return { - value: item.meta.key, - label: item.meta.params.query, - } - }).filter(element => Object.keys(selectedOptions).includes(element.value)) - - if (filterCustom.length != 0) { - filterCustom.forEach(item => { - setSelectedOptions(prevState => ({ - ...prevState, - [item.value]: [...prevState[item.value], item], - })); - - }) - } + const $state: FilterState = { + store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, }; - - const onChange = (values: any[]) => { - setKibanaFilters(values) - refreshCustomSelectedFilter(); + const query = { + match: { + [key]: { + query: querySearch, + type: 'phrase', + }, + }, }; - const getComponent = (item: any) => { - var types = { - 'default': <></>, - 'combobox': <Combobox - item={item} - selectedOptions={selectedOptions[item.key] || []} - onChange={onChange} - /> + return { meta, $state, query }; + }; + + const setKibanaFilters = (values: any[]) => { + const newFilters = []; + const currentFilters = filterManager + .getFilters() + .filter((item) => item.meta.key != values[0].value); + filterManager.removeAll(); + filterManager.addFilters(currentFilters); + values.forEach((element) => { + const customFilter = buildCustomFilter( + false, + indexPattern.title, + element.label, + element.value + ); + newFilters.push(customFilter); + }); + filterManager.addFilters(newFilters); + }; + + const refreshCustomSelectedFilter = () => { + setSelectedOptions(defaultSelectedOptions); + const filters = filterManager.getFilters(); + const filterCustom = filters + .map((item) => { + return { + value: item.meta.key, + label: item.meta.params.query, }; - return types[item.type] || types['default']; + }) + .filter((element) => Object.keys(selectedOptions).includes(element.value)); + + if (filterCustom.length != 0) { + filterCustom.forEach((item) => { + setSelectedOptions((prevState) => ({ + ...prevState, + [item.value]: [...prevState[item.value], item], + })); + }); } - - return ( - <> - <EuiFlexGroup className='custom-kbn-search-bar' alignItems='center' style={{ margin: '0 8px' }}> - { - avancedFiltersState === false ? - filtersValues.map((item, key) => ( - <EuiFlexItem key={key}> - {getComponent(item)} - </EuiFlexItem> - )) - : - '' - } - <EuiFlexItem> - <KbnSearchBar - showFilterBar={false} - showQueryInput={avancedFiltersState} - onFiltersUpdated={onFiltersUpdated} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup justifyContent='flexEnd' style={{ margin: '0 20px' }}> - <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> - <KbnSearchBar - showDatePicker={false} - showQueryInput={false} - onFiltersUpdated={onFiltersUpdated} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSwitch - label="Advanced filters" - checked={avancedFiltersState} - onChange={() => changeSwitch()} - /> - </EuiFlexItem> - </EuiFlexGroup> - </> - ) -}; \ No newline at end of file + }; + + const onChange = (values: any[]) => { + setKibanaFilters(values); + refreshCustomSelectedFilter(); + }; + + const getComponent = (item: any) => { + var types = { + default: <></>, + combobox: ( + <Combobox + item={item} + selectedOptions={selectedOptions[item.key] || []} + onChange={onChange} + /> + ), + }; + return types[item.type] || types['default']; + }; + + return ( + <> + <EuiFlexGroup + className="custom-kbn-search-bar" + alignItems="center" + style={{ margin: '0 8px' }} + > + {avancedFiltersState === false + ? filtersValues.map((item, key) => ( + <EuiFlexItem key={key}>{getComponent(item)}</EuiFlexItem> + )) + : ''} + <EuiFlexItem> + <KbnSearchBar + showFilterBar={false} + showQueryInput={avancedFiltersState} + onFiltersUpdated={onFiltersUpdated} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup justifyContent="flexEnd" style={{ margin: '0 20px' }}> + <EuiFlexItem className={'filters-search-bar'} style={{ margin: '0px' }}> + <KbnSearchBar + showDatePicker={false} + showQueryInput={false} + onFiltersUpdated={onFiltersUpdated} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSwitch + label="Advanced filters" + checked={avancedFiltersState} + onChange={() => changeSwitch()} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; diff --git a/public/components/common/hooks/use-kbn-loading-indicator.ts b/public/components/common/hooks/use-kbn-loading-indicator.ts index 5ffbd5f34c..99899c6bad 100644 --- a/public/components/common/hooks/use-kbn-loading-indicator.ts +++ b/public/components/common/hooks/use-kbn-loading-indicator.ts @@ -26,12 +26,13 @@ export const useKbnLoadingIndicator = (): [ useEffect(() => { getHttp().addLoadingCountSource(loadingCount$); - getHttp() + const { unsubscribe } = getHttp() .getLoadingCount$() .subscribe((count) => { setVisible(count); !count && setFlag(false); }); + return unsubscribe; }, []); useEffect(() => { diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestions.ts index 3f0d2b9a60..c0d9634a21 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestions.ts @@ -12,7 +12,7 @@ import { useState, useEffect } from 'react'; import { getDataPlugin } from '../../../kibana-services'; import { useIndexPattern } from '.'; -import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { IFieldType, IIndexPattern, Filter } from 'src/plugins/data/public'; import React from 'react'; import { UI_ERROR_SEVERITIES, @@ -22,6 +22,7 @@ import { } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { useFilterManager } from '.'; export interface IValueSuggestiions { suggestedValues: string[] | boolean[]; @@ -29,14 +30,35 @@ export interface IValueSuggestiions { setQuery: React.Dispatch<React.SetStateAction<string>>; } -export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') : IValueSuggestiions => { +export const useValueSuggestions = ( + filterField: string, + type: 'string' | 'boolean' = 'string' +): IValueSuggestiions => { const [suggestedValues, setSuggestedValues] = useState<string[] | boolean[]>([]); const [query, setQuery] = useState<string>(''); const [isLoading, setIsLoading] = useState(true); const data = getDataPlugin(); const indexPattern = useIndexPattern(); + const { filters } = useFilterManager(); useEffect(() => { + const boolFilter = filters + .filter( + (managedFilter) => + managedFilter && + managedFilter.query && + managedFilter.query.match && + Object.keys(managedFilter.query.match)[0] !== filterField + ) + .map((managedFilter) => { + return { + term: { + [Object.keys(managedFilter.query.match)[0]]: + managedFilter.query.match[Object.keys(managedFilter.query.match)[0]].query, + }, + }; + }); + if (indexPattern) { setIsLoading(true); (async () => { @@ -51,6 +73,7 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole query, indexPattern: indexPattern as IIndexPattern, field, + boolFilter, }) ); } catch (error) { @@ -70,7 +93,7 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole } })(); } - }, [indexPattern, query, filterField, type]); + }, [indexPattern, query, filterField, type, filters]); return { suggestedValues, isLoading, setQuery }; }; diff --git a/public/components/overview/github-panel/config/search-bar-config.ts b/public/components/overview/github-panel/config/search-bar-config.ts index 3c1de9e8c2..5aaf19f202 100644 --- a/public/components/overview/github-panel/config/search-bar-config.ts +++ b/public/components/overview/github-panel/config/search-bar-config.ts @@ -1,70 +1,33 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + export const filtersValues = [ - { - type: 'combobox', - key: 'agent.id', - values:[ - { - key:'agent.id', - label: '001', - - }, - { - key:'agent.id', - label: '002', - }, - { - key:'agent.id', - label: '003', - }, - { - key:'agent.id', - label: '004', - }, - { - key:'agent.id', - label: '006', - } - ] - }, - { - type: 'combobox', - key: 'agent.name', - values:[ - { - key:'agent.name', - label: 'Amazon', - - }, - { - key:'agent.name', - label: 'Centos', - }, - { - key:'agent.name', - label: '003', - }, - { - key:'agent.name', - label: '004', - }, - { - key:'agent.name', - label: '006', - } - ] - }, - { - type: 'combobox', - key: 'agent.ip', - values:[ - { - key:'agent.ip', - label: '24.273.97.14', - }, - { - key:'agent.ip', - label: '197.17.1.4', - }, - ] - } -] \ No newline at end of file + { + type: 'combobox', + key: 'data.github.actor', + }, + { + type: 'combobox', + key: 'data.github.org', + }, + { + type: 'combobox', + key: 'data.github.repo', + }, + { + type: 'combobox', + key: 'data.github.action', + }, +]; From 02f941ef98fb8ac42939fcc433e4fce4ab11d723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 4 Aug 2021 09:43:19 +0200 Subject: [PATCH 226/493] Applied prettier to files that will be used --- .../agents/fim/inventory/fileDetail.tsx | 59 +-- .../agents/vuls/inventory/detail.tsx | 22 +- .../common/modules/discover/discover.tsx | 172 ++++---- .../requirement-flyout/requirement-flyout.tsx | 411 +++++++++--------- .../flyout-technique/flyout-technique.tsx | 379 +++++++++------- .../visualize/components/security-alerts.tsx | 78 ++-- 6 files changed, 603 insertions(+), 518 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 6d846532fb..f4f2ebb35a 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -95,7 +95,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'mtime', @@ -103,7 +103,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'uname', @@ -183,15 +183,15 @@ export class FileDetails extends Component { name: 'Last analysis', grow: 2, icon: 'clock', - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'mtime', name: 'Last modified', grow: 2, icon: 'clock', - transformValue: formatUIDate - } + transformValue: formatUIDate, + }, ]; } @@ -264,7 +264,10 @@ export class FileDetails extends Component { getDetails() { const { view } = this.props; - const columns = this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' ? this.registryDetails() : this.details(); + const columns = + this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' + ? this.registryDetails() + : this.details(); const generalDetails = columns.map((item, idx) => { var value = this.props.currentFile[item.field] || '-'; if (item.transformValue) { @@ -418,29 +421,27 @@ export class FileDetails extends Component { > <div className="flyout-row details-row">{this.getDetails()}</div> </EuiAccordion> - { (type === 'registry_key' || currentFile.type === 'registry_key') && <> - <EuiSpacer size="s" /> - <EuiAccordion - id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} - buttonContent={ - <EuiTitle size="s"> - <h3> - Registry values - </h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true} - > - <EuiFlexGroup className="flyout-row"> - <EuiFlexItem> - <RegistryValues - currentFile={currentFile} - agent={agent} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiAccordion> </>} + {(type === 'registry_key' || currentFile.type === 'registry_key') && ( + <> + <EuiSpacer size="s" /> + <EuiAccordion + id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} + buttonContent={ + <EuiTitle size="s"> + <h3>Registry values</h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > + <EuiFlexGroup className="flyout-row"> + <EuiFlexItem> + <RegistryValues currentFile={currentFile} agent={agent} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiAccordion>{' '} + </> + )} <EuiSpacer /> <EuiAccordion id={fileName === undefined ? Math.random().toString() : `${fileName}_events`} diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 68d3d8b532..7c8483373b 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -32,7 +32,7 @@ import { getIndexPattern } from '../../../overview/mitre/lib'; import moment from 'moment-timezone'; import { AppNavigate } from '../../../../react-services/app-navigate'; import { TruncateHorizontalComponents } from '../../../common/util'; -import { getDataPlugin,getUiSettings } from '../../../../kibana-services'; +import { getDataPlugin, getUiSettings } from '../../../../kibana-services'; import { FilterManager } from '../../../../../../../src/plugins/data/public/'; export class Details extends Component { @@ -112,7 +112,7 @@ export class Details extends Component { name: 'Architecture', icon: 'node', link: false, - } + }, ]; } @@ -138,7 +138,11 @@ export class Details extends Component { const { cve } = this.props.currentItem; const filters = [ { - ...buildPhraseFilter({ name: 'data.vulnerability.cve', type: 'text' }, cve, this.indexPattern), + ...buildPhraseFilter( + { name: 'data.vulnerability.cve', type: 'text' }, + cve, + this.indexPattern + ), $state: { store: 'appState' }, }, ]; @@ -151,7 +155,6 @@ export class Details extends Component { const { filterManager } = getDataPlugin().query; const _filters = filterManager.getFilters(); if (_filters && _filters.length) { - filterManager.addFilters([filters]); const scope = await ModulesHelper.getDiscoverScope(); scope.updateQueryAndFetch && scope.updateQueryAndFetch({ query: null }); @@ -188,7 +191,7 @@ export class Details extends Component { } var link = (item.link && !['events', 'extern'].includes(view)) || false; const agentPlatform = ((this.props.agent || {}).os || {}).platform; - + if (!item.onlyLinux || (item.onlyLinux && this.props.agent && agentPlatform !== 'windows')) { let className = item.checksum ? 'detail-value detail-value-checksum' : 'detail-value'; className += item.field === 'perm' ? ' detail-value-perm' : ''; @@ -303,7 +306,6 @@ export class Details extends Component { return value; } - render() { const { type, implicitFilters, view, currentItem, agent } = this.props; const id = `${currentItem.name}-${currentItem.cve}-${currentItem.architecture}-${currentItem.version}`; @@ -358,13 +360,7 @@ export class Details extends Component { <Discover kbnSearchBar shareFilterManager={this.discoverFilterManager} - initialColumns={[ - 'icon', - 'timestamp', - 'rule.description', - 'rule.level', - 'rule.id', - ]} + initialColumns={['icon', 'timestamp', 'rule.description', 'rule.level', 'rule.id']} includeFilters="vulnerability" implicitFilters={implicitFilters} initialFilters={[]} diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 4f7daec002..b09d81aeb3 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -30,20 +30,20 @@ import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrat import { getErrorOrchestrator } from '../../../../react-services/common-services'; import { - EuiBasicTable, - EuiLoadingContent, - EuiTableSortingType, - EuiFlexItem, - EuiFlexGroup, - Direction, - EuiOverlayMask, - EuiOutsideClickDetector, - EuiSpacer, - EuiCallOut, - EuiIcon, - EuiButtonIcon, - EuiButtonEmpty, - EuiToolTip + EuiBasicTable, + EuiLoadingContent, + EuiTableSortingType, + EuiFlexItem, + EuiFlexGroup, + Direction, + EuiOverlayMask, + EuiOutsideClickDetector, + EuiSpacer, + EuiCallOut, + EuiIcon, + EuiButtonIcon, + EuiButtonEmpty, + EuiToolTip, } from '@elastic/eui'; import { IIndexPattern, @@ -738,71 +738,83 @@ export const Discover = compose( const columns = this.columns(); - const sorting: EuiTableSortingType<{}> = { - sort: { - //@ts-ignore - field: this.state.sortField, - direction: this.state.sortDirection, - } - }; - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: this.state.pageSize, - totalItemCount: this.state.total > 10000 ? 10000 : this.state.total, - pageSizeOptions: [10, 25, 50], - }; - const noResultsText = `No results match for this search criteria`; - let flyout = this.state.showMitreFlyout ? <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} /> + const sorting: EuiTableSortingType<{}> = { + sort: { + //@ts-ignore + field: this.state.sortField, + direction: this.state.sortDirection, + }, + }; + const pagination = { + pageIndex: this.state.pageIndex, + pageSize: this.state.pageSize, + totalItemCount: this.state.total > 10000 ? 10000 : this.state.total, + pageSizeOptions: [10, 25, 50], + }; + const noResultsText = `No results match for this search criteria`; + let flyout = this.state.showMitreFlyout ? ( + <EuiOverlayMask headerZindexLocation="below"> + <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> + <div> + {/* EuiOutsideClickDetector needs a static first child */} + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + onChangeFlyout={this.onMitreChangeFlyout} + currentTechnique={this.state.selectedTechnique} + /> + </div> + </EuiOutsideClickDetector> + </EuiOverlayMask> + ) : ( + <></> + ); + return ( + <div className="wz-discover hide-filter-control wz-inventory"> + {this.props.kbnSearchBar && ( + <KbnSearchBar + indexPattern={this.indexPattern} + filterManager={this.props.shareFilterManager} + timeFilter={{ + timeFilter: this.state.dateRange, + timeHistory: this.state.dateRangeHistory, + setTimeFilter: (dateRange) => this.setState({ dateRange }), + }} + onQuerySubmit={this.onQuerySubmit} + onFiltersUpdated={this.onFiltersUpdated} + query={query} + /> + )} + {total ? ( + <EuiFlexGroup> + <EuiFlexItem> + {this.state.alerts.length && ( + <EuiBasicTable + items={this.state.alerts.map((alert) => ({ ...alert._source, _id: alert._id }))} + className="module-discover-table" + itemId="_id" + itemIdToExpandedRowMap={itemIdToExpandedRowMap} + isExpandable={true} + columns={columns} + rowProps={getRowProps} + pagination={pagination} + sorting={sorting} + onChange={this.onTableChange} + /> + )} + </EuiFlexItem> + </EuiFlexGroup> + ) : ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiSpacer size="s" /> + <EuiCallOut title={noResultsText} color="warning" iconType="alert" /> + </EuiFlexItem> + </EuiFlexGroup> + )} + {flyout} </div> - </EuiOutsideClickDetector> - </EuiOverlayMask> : <></>; - return ( - <div - className='wz-discover hide-filter-control wz-inventory' > - {this.props.kbnSearchBar && <KbnSearchBar - indexPattern={this.indexPattern} - filterManager={this.props.shareFilterManager} - timeFilter={{timeFilter:this.state.dateRange, - timeHistory:this.state.dateRangeHistory, - setTimeFilter:(dateRange)=> this.setState({dateRange})}} - onQuerySubmit={this.onQuerySubmit} - onFiltersUpdated={this.onFiltersUpdated} - query={query} /> - } - {total - ? <EuiFlexGroup> - <EuiFlexItem> - {this.state.alerts.length && ( - <EuiBasicTable - items={this.state.alerts.map(alert => ({...alert._source, _id: alert._id}))} - className="module-discover-table" - itemId="_id" - itemIdToExpandedRowMap={itemIdToExpandedRowMap} - isExpandable={true} - columns={columns} - rowProps={getRowProps} - pagination={pagination} - sorting={sorting} - onChange={this.onTableChange} - /> - )} - </EuiFlexItem> - </EuiFlexGroup> - : <EuiFlexGroup> - <EuiFlexItem> - <EuiSpacer size="s" /> - <EuiCallOut title={noResultsText} color="warning" iconType="alert" /> - </EuiFlexItem> - </EuiFlexGroup> - } - {flyout} - </div>); + ); + } } -}); +); diff --git a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx index f2b655ad94..e5612e6800 100644 --- a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx +++ b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx @@ -11,21 +11,21 @@ */ import React, { Component } from 'react'; import { - EuiFlyout, - EuiFlyoutHeader, - EuiLoadingContent, - EuiTitle, - EuiToolTip, - EuiIcon, - EuiFlyoutBody, - EuiAccordion, - EuiFlexGroup, - EuiText, - EuiFlexItem, - EuiLink, - EuiStat, - EuiDescriptionList, - EuiSpacer + EuiFlyout, + EuiFlyoutHeader, + EuiLoadingContent, + EuiTitle, + EuiToolTip, + EuiIcon, + EuiFlyoutBody, + EuiAccordion, + EuiFlexGroup, + EuiText, + EuiFlexItem, + EuiLink, + EuiStat, + EuiDescriptionList, + EuiSpacer, } from '@elastic/eui'; import { Discover } from '../../../../common/modules/discover'; import { AppState } from '../../../../../react-services/app-state'; @@ -33,185 +33,208 @@ import { requirementGoal } from '../../requirement-goal'; import { getUiSettings } from '../../../../../kibana-services'; import { FilterManager } from '../../../../../../../../src/plugins/data/public/'; - - export class RequirementFlyout extends Component { - _isMount = false; - state: { - } - - props!: { - }; - - filterManager: FilterManager; - - constructor(props) { - super(props); - this.state = { - } - this.filterManager = new FilterManager(getUiSettings()); - } - - componentDidMount() { - this._isMount = true; - } - - renderHeader() { - const { currentRequirement } = this.props; - return ( - <EuiFlyoutHeader hasBorder style={{ padding: "12px 16px" }}> - {(!currentRequirement && ( - <div> - <EuiLoadingContent lines={1} /> - </div> - )) || ( - <EuiTitle size="m"> - <h2 id="flyoutSmallTitle"> - Requirement {currentRequirement} - </h2> - </EuiTitle> - )} - </EuiFlyoutHeader> - ) - } - - updateTotalHits = (total) => { - this.setState({ totalHits: total }); - } - - - renderBody() { - const { currentRequirement } = this.props; - const requirementImplicitFilter = {}; - const isCluster = (AppState.getClusterInfo() || {}).status === "enabled"; - const clusterFilter = isCluster - ? { "cluster.name": AppState.getClusterInfo().cluster } - : { "manager.name": AppState.getClusterInfo().manager }; - this.clusterFilter = clusterFilter; - requirementImplicitFilter[this.props.getRequirementKey()] = currentRequirement; - - const implicitFilters = [requirementImplicitFilter, this.clusterFilter]; - if (this.props.implicitFilters) { - this.props.implicitFilters.forEach(item => - implicitFilters.push(item)) - } - //Goal for PCI - const currentReq = this.props.currentRequirement.split(".")[0]; - - return ( - <EuiFlyoutBody className="flyout-body" > - <EuiAccordion - id={"details"} - buttonContent={ - <EuiTitle size="s"> - <h3>Details</h3> - </EuiTitle> - } - paddingSize="xs" - initialIsOpen={true}> - <div className='flyout-row details-row'> - <EuiSpacer size="xs" /> - {requirementGoal[currentReq] && <EuiFlexGroup style={{marginBottom: 10}}> - <EuiFlexItem grow={false}> - <EuiIcon size="l" type={"bullseye"} color='primary' style={{marginTop: 8}} /> - </EuiFlexItem> - <EuiFlexItem style={{marginLeft: 2}} grow={true}> - <EuiText style={{marginLeft: 8, fontSize: 14}}> - <p style={{fontWeight: 500, marginBottom: 2}}>Goals</p> - - <p>{requirementGoal[currentReq]}</p> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup>} - - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiIcon size="l" type={"filebeatApp"} color='primary' style={{marginTop: 8}} /> - </EuiFlexItem> - <EuiFlexItem style={{marginLeft: 2}} grow={true}> - <EuiText style={{marginLeft: 8, fontSize: 14}}> - <p style={{fontWeight: 500, marginBottom: 2}}>Requirement description</p> - - <p>{this.props.description}</p> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xs" /> - </div> - </EuiAccordion> - - - <EuiSpacer size='s' /> - <EuiAccordion - style={{ textDecoration: 'none' }} - id={"recent_events"} - className='events-accordion' - extraAction={<div style={{ marginBottom: 5 }}><strong>{this.state.totalHits || 0}</strong> hits</div>} - buttonContent={ - <EuiTitle size="s"> - <h3> - Recent events{this.props.view !== 'events' && ( - <span style={{ marginLeft: 16 }}> - <span> - <EuiToolTip position="top" content={"Show " + currentRequirement + " in Dashboard"}> - <EuiIcon onMouseDown={(e) => { this.props.openDashboard(e, currentRequirement); e.stopPropagation() }} color="primary" type="visualizeApp" style={{ marginRight: '10px' }}></EuiIcon> - </EuiToolTip> - <EuiToolTip position="top" content={"Inspect " + currentRequirement + " in Events"} > - <EuiIcon onMouseDown={(e) => { this.props.openDiscover(e, currentRequirement); e.stopPropagation() }} color="primary" type="discoverApp"></EuiIcon> - </EuiToolTip> - </span> - </span> - )} - </h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true}> - <EuiFlexGroup className="flyout-row"> - <EuiFlexItem> - <Discover kbnSearchBar shareFilterManager={this.filterManager} initialColumns={["icon", "timestamp", this.props.getRequirementKey(), 'rule.level', 'rule.id', 'rule.description']} implicitFilters={implicitFilters} initialFilters={[]} updateTotalHits={(total) => this.updateTotalHits(total)} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiAccordion> - - </EuiFlyoutBody> - ); - } - - - renderLoading() { - return ( - <EuiFlyoutBody> - <EuiLoadingContent lines={2} /> - <EuiLoadingContent lines={3} /> - </EuiFlyoutBody> - ) - } - - - - - render() { - const { currentRequirement } = this.props; - const { onChangeFlyout } = this.props; - return ( - <EuiFlyout - onClose={() => onChangeFlyout(false)} - maxWidth="60%" - size="l" - className="flyout-no-overlap wz-inventory wzApp" - aria-labelledby="flyoutSmallTitle" - > - {currentRequirement && - this.renderHeader() - } - { - this.renderBody() - } - {this.state.loading && - this.renderLoading() - } - </EuiFlyout> - ); + _isMount = false; + state: {}; + + props!: {}; + + filterManager: FilterManager; + + constructor(props) { + super(props); + this.state = {}; + this.filterManager = new FilterManager(getUiSettings()); + } + + componentDidMount() { + this._isMount = true; + } + + renderHeader() { + const { currentRequirement } = this.props; + return ( + <EuiFlyoutHeader hasBorder style={{ padding: '12px 16px' }}> + {(!currentRequirement && ( + <div> + <EuiLoadingContent lines={1} /> + </div> + )) || ( + <EuiTitle size="m"> + <h2 id="flyoutSmallTitle">Requirement {currentRequirement}</h2> + </EuiTitle> + )} + </EuiFlyoutHeader> + ); + } + + updateTotalHits = (total) => { + this.setState({ totalHits: total }); + }; + + renderBody() { + const { currentRequirement } = this.props; + const requirementImplicitFilter = {}; + const isCluster = (AppState.getClusterInfo() || {}).status === 'enabled'; + const clusterFilter = isCluster + ? { 'cluster.name': AppState.getClusterInfo().cluster } + : { 'manager.name': AppState.getClusterInfo().manager }; + this.clusterFilter = clusterFilter; + requirementImplicitFilter[this.props.getRequirementKey()] = currentRequirement; + + const implicitFilters = [requirementImplicitFilter, this.clusterFilter]; + if (this.props.implicitFilters) { + this.props.implicitFilters.forEach((item) => implicitFilters.push(item)); } + //Goal for PCI + const currentReq = this.props.currentRequirement.split('.')[0]; + + return ( + <EuiFlyoutBody className="flyout-body"> + <EuiAccordion + id={'details'} + buttonContent={ + <EuiTitle size="s"> + <h3>Details</h3> + </EuiTitle> + } + paddingSize="xs" + initialIsOpen={true} + > + <div className="flyout-row details-row"> + <EuiSpacer size="xs" /> + {requirementGoal[currentReq] && ( + <EuiFlexGroup style={{ marginBottom: 10 }}> + <EuiFlexItem grow={false}> + <EuiIcon size="l" type={'bullseye'} color="primary" style={{ marginTop: 8 }} /> + </EuiFlexItem> + <EuiFlexItem style={{ marginLeft: 2 }} grow={true}> + <EuiText style={{ marginLeft: 8, fontSize: 14 }}> + <p style={{ fontWeight: 500, marginBottom: 2 }}>Goals</p> + + <p>{requirementGoal[currentReq]}</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + )} + + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiIcon size="l" type={'filebeatApp'} color="primary" style={{ marginTop: 8 }} /> + </EuiFlexItem> + <EuiFlexItem style={{ marginLeft: 2 }} grow={true}> + <EuiText style={{ marginLeft: 8, fontSize: 14 }}> + <p style={{ fontWeight: 500, marginBottom: 2 }}>Requirement description</p> + + <p>{this.props.description}</p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xs" /> + </div> + </EuiAccordion> + + <EuiSpacer size="s" /> + <EuiAccordion + style={{ textDecoration: 'none' }} + id={'recent_events'} + className="events-accordion" + extraAction={ + <div style={{ marginBottom: 5 }}> + <strong>{this.state.totalHits || 0}</strong> hits + </div> + } + buttonContent={ + <EuiTitle size="s"> + <h3> + Recent events + {this.props.view !== 'events' && ( + <span style={{ marginLeft: 16 }}> + <span> + <EuiToolTip + position="top" + content={'Show ' + currentRequirement + ' in Dashboard'} + > + <EuiIcon + onMouseDown={(e) => { + this.props.openDashboard(e, currentRequirement); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + style={{ marginRight: '10px' }} + ></EuiIcon> + </EuiToolTip> + <EuiToolTip + position="top" + content={'Inspect ' + currentRequirement + ' in Events'} + > + <EuiIcon + onMouseDown={(e) => { + this.props.openDiscover(e, currentRequirement); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + ></EuiIcon> + </EuiToolTip> + </span> + </span> + )} + </h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > + <EuiFlexGroup className="flyout-row"> + <EuiFlexItem> + <Discover + kbnSearchBar + shareFilterManager={this.filterManager} + initialColumns={[ + 'icon', + 'timestamp', + this.props.getRequirementKey(), + 'rule.level', + 'rule.id', + 'rule.description', + ]} + implicitFilters={implicitFilters} + initialFilters={[]} + updateTotalHits={(total) => this.updateTotalHits(total)} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiAccordion> + </EuiFlyoutBody> + ); + } + + renderLoading() { + return ( + <EuiFlyoutBody> + <EuiLoadingContent lines={2} /> + <EuiLoadingContent lines={3} /> + </EuiFlyoutBody> + ); + } + + render() { + const { currentRequirement } = this.props; + const { onChangeFlyout } = this.props; + return ( + <EuiFlyout + onClose={() => onChangeFlyout(false)} + maxWidth="60%" + size="l" + className="flyout-no-overlap wz-inventory wzApp" + aria-labelledby="flyoutSmallTitle" + > + {currentRequirement && this.renderHeader()} + {this.renderBody()} + {this.state.loading && this.renderLoading()} + </EuiFlyout> + ); + } } diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 6fa1b41b1b..b605d26101 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -17,7 +17,7 @@ const md = new MarkdownIt({ html: true, linkify: true, breaks: true, - typographer: true + typographer: true, }); import { @@ -33,7 +33,7 @@ import { EuiLink, EuiAccordion, EuiToolTip, - EuiIcon + EuiIcon, } from '@elastic/eui'; import { WzRequest } from '../../../../../../../react-services/wz-request'; import { AppState } from '../../../../../../../react-services/app-state'; @@ -51,16 +51,16 @@ export class FlyoutTechnique extends Component { state: { techniqueData: { - [key:string]: any, - } - loading: boolean - } + [key: string]: any; + }; + loading: boolean; + }; props!: { - currentTechniqueData: any - currentTechnique: string - tacticsObject: any - } + currentTechniqueData: any; + currentTechnique: string; + tacticsObject: any; + }; filterManager: FilterManager; @@ -70,66 +70,76 @@ export class FlyoutTechnique extends Component { techniqueData: { // description: '' }, - loading: false - } + loading: false, + }; this.filterManager = new FilterManager(getUiSettings()); } async componentDidMount() { this._isMount = true; - const isCluster = (AppState.getClusterInfo() || {}).status === "enabled"; - const clusterFilter = isCluster - ? { "cluster.name": AppState.getClusterInfo().cluster } - : { "manager.name": AppState.getClusterInfo().manager }; - this.clusterFilter = clusterFilter ; + const isCluster = (AppState.getClusterInfo() || {}).status === 'enabled'; + const clusterFilter = isCluster + ? { 'cluster.name': AppState.getClusterInfo().cluster } + : { 'manager.name': AppState.getClusterInfo().manager }; + this.clusterFilter = clusterFilter; await this.getTechniqueData(); this.addListenersToCitations(); } async componentDidUpdate(prevProps) { const { currentTechnique } = this.props; - if (prevProps.currentTechnique !== currentTechnique ){ + if (prevProps.currentTechnique !== currentTechnique) { await this.getTechniqueData(); } this.addListenersToCitations(); } - componentWillUnmount(){ + componentWillUnmount() { // remove listeners of citations if these exist - if(this.state.techniqueData && this.state.techniqueData.replaced_external_references && this.state.techniqueData.replaced_external_references.length > 0){ - this.state.techniqueData.replaced_external_references.forEach(reference => { - $(`.technique-reference-${reference.index}`).each(function(){ + if ( + this.state.techniqueData && + this.state.techniqueData.replaced_external_references && + this.state.techniqueData.replaced_external_references.length > 0 + ) { + this.state.techniqueData.replaced_external_references.forEach((reference) => { + $(`.technique-reference-${reference.index}`).each(function () { $(this).off(); }); - }) + }); } } - addListenersToCitations(){ - if(this.state.techniqueData && this.state.techniqueData.replaced_external_references && this.state.techniqueData.replaced_external_references.length > 0){ - this.state.techniqueData.replaced_external_references.forEach(reference => { - $(`.technique-reference-citation-${reference.index}`).each(function(){ + addListenersToCitations() { + if ( + this.state.techniqueData && + this.state.techniqueData.replaced_external_references && + this.state.techniqueData.replaced_external_references.length > 0 + ) { + this.state.techniqueData.replaced_external_references.forEach((reference) => { + $(`.technique-reference-citation-${reference.index}`).each(function () { $(this).off(); $(this).click(() => { - $(`.euiFlyoutBody__overflow`).scrollTop($(`#technique-reference-${reference.index}`).position().top - 150); + $(`.euiFlyoutBody__overflow`).scrollTop( + $(`#technique-reference-${reference.index}`).position().top - 150 + ); }); - }) - }) + }); + }); } } async getTechniqueData() { - try{ - this.setState({loading: true, techniqueData: {}}); + try { + this.setState({ loading: true, techniqueData: {} }); const { currentTechnique } = this.props; const result = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `references.external_id=${currentTechnique}` - } + q: `references.external_id=${currentTechnique}`, + }, }); - const rawData = (((result || {}).data || {}).data || {}).affected_items + const rawData = (((result || {}).data || {}).data || {}).affected_items; !!rawData && this.formatTechniqueData(rawData[0]); - }catch(error){ + } catch (error) { const options = { context: `${FlyoutTechnique.name}.getTechniqueData`, level: UI_LOGGER_LEVELS.ERROR, @@ -143,208 +153,241 @@ export class FlyoutTechnique extends Component { }, }; getErrorOrchestrator().handleError(options); - this.setState({loading: false}); + this.setState({ loading: false }); } } - findTacticName(tactics){ + findTacticName(tactics) { const { tacticsObject } = this.props; return tactics.map((element) => { - const tactic = Object.values(tacticsObject).find(obj => obj.id === element); - return { id:tactic.references[0].external_id, name: tactic.name}; + const tactic = Object.values(tacticsObject).find((obj) => obj.id === element); + return { id: tactic.references[0].external_id, name: tactic.name }; }); } - formatTechniqueData (rawData) { + formatTechniqueData(rawData) { const { tactics, name, mitre_version } = rawData; - const tacticsObj = this.findTacticName(tactics) - this.setState({techniqueData: { name, mitre_version, tacticsObj }, loading: false }); + const tacticsObj = this.findTacticName(tactics); + this.setState({ techniqueData: { name, mitre_version, tacticsObj }, loading: false }); } - + renderHeader() { const { techniqueData } = this.state; - return( - <EuiFlyoutHeader hasBorder style={{padding:"12px 16px"}}> + return ( + <EuiFlyoutHeader hasBorder style={{ padding: '12px 16px' }}> {(Object.keys(techniqueData).length === 0 && ( <div> <EuiLoadingContent lines={1} /> </div> )) || ( <EuiTitle size="m"> - <h2 id="flyoutSmallTitle"> - {techniqueData.name} - </h2> + <h2 id="flyoutSmallTitle">{techniqueData.name}</h2> </EuiTitle> )} </EuiFlyoutHeader> - ) + ); } renderBody() { const { currentTechnique } = this.props; const { techniqueData } = this.state; - const implicitFilters=[{ 'rule.mitre.id': currentTechnique}, this.clusterFilter ]; - if(this.props.implicitFilters){ - this.props.implicitFilters.forEach( item => - implicitFilters.push(item)) + const implicitFilters = [{ 'rule.mitre.id': currentTechnique }, this.clusterFilter]; + if (this.props.implicitFilters) { + this.props.implicitFilters.forEach((item) => implicitFilters.push(item)); } const link = `https://attack.mitre.org/techniques/${currentTechnique}/`; - const formattedDescription = techniqueData.description - ? ( - <div - className="wz-markdown-margin wz-markdown-wrapper" - dangerouslySetInnerHTML={{__html: md.render(techniqueData.description)}}> - </div> - ) - : techniqueData.description; + const formattedDescription = techniqueData.description ? ( + <div + className="wz-markdown-margin wz-markdown-wrapper" + dangerouslySetInnerHTML={{ __html: md.render(techniqueData.description) }} + ></div> + ) : ( + techniqueData.description + ); const data = [ { title: 'ID', - description: ( <EuiToolTip - position="top" - content={`Open ${currentTechnique} details in the Intelligence section`}> - <EuiLink onClick={(e) => {this.props.openIntelligence(e,'techniques',currentTechnique);e.stopPropagation()}}> - {currentTechnique} - </EuiLink> - </EuiToolTip>) + description: ( + <EuiToolTip + position="top" + content={`Open ${currentTechnique} details in the Intelligence section`} + > + <EuiLink + onClick={(e) => { + this.props.openIntelligence(e, 'techniques', currentTechnique); + e.stopPropagation(); + }} + > + {currentTechnique} + </EuiLink> + </EuiToolTip> + ), }, { title: 'Tactics', description: techniqueData.tacticsObj - ? techniqueData.tacticsObj.map((tactic) => { - return ( - <> - <EuiToolTip - position="top" - content={`Open ${tactic.name} details in the Intelligence section`} - > - <EuiLink - onClick={(e) => { - this.props.openIntelligence(e, "tactics", tactic.id); - e.stopPropagation(); - }} + ? techniqueData.tacticsObj.map((tactic) => { + return ( + <> + <EuiToolTip + position="top" + content={`Open ${tactic.name} details in the Intelligence section`} > - {tactic.name} - </EuiLink> - </EuiToolTip> - <br /> - </> - ); - }) - : "" + <EuiLink + onClick={(e) => { + this.props.openIntelligence(e, 'tactics', tactic.id); + e.stopPropagation(); + }} + > + {tactic.name} + </EuiLink> + </EuiToolTip> + <br /> + </> + ); + }) + : '', }, { title: 'Version', - description: techniqueData.mitre_version - } - + description: techniqueData.mitre_version, + }, ]; return ( - <EuiFlyoutBody className="flyout-body" > + <EuiFlyoutBody className="flyout-body"> <EuiAccordion - id={"details"} - buttonContent={ - <EuiTitle size="s"> - <h3>Technique details</h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true}> - <div className='flyout-row details-row'> - + id={'details'} + buttonContent={ + <EuiTitle size="s"> + <h3>Technique details</h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > + <div className="flyout-row details-row"> {(Object.keys(techniqueData).length === 0 && ( - <div> - <EuiLoadingContent lines={2} /> - <EuiLoadingContent lines={3} /> - </div> - )) || ( - <div style={{marginBottom: 30}}> - <EuiDescriptionList listItems={data} /> - </div> - )} - </div> + <div> + <EuiLoadingContent lines={2} /> + <EuiLoadingContent lines={3} /> + </div> + )) || ( + <div style={{ marginBottom: 30 }}> + <EuiDescriptionList listItems={data} /> + </div> + )} + </div> </EuiAccordion> - - <EuiSpacer size='s' /> - <EuiAccordion - style={{textDecoration: 'none'}} - id={"recent_events"} - className='events-accordion' - extraAction={<div style={{marginBottom: 5}}><strong>{this.state.totalHits || 0}</strong> hits</div>} - buttonContent={ - <EuiTitle size="s"> - <h3> - Recent events{this.props.view !== 'events' && ( - <span style={{ marginLeft: 16 }}> - <span> - <EuiToolTip position="top" content={"Show " + currentTechnique+ " in Dashboard"}> - <EuiIcon onMouseDown={(e) => {this.props.openDashboard(e,currentTechnique);e.stopPropagation()}} color="primary" type="visualizeApp" style={{marginRight: '10px'}}></EuiIcon> - </EuiToolTip> - <EuiToolTip position="top" content={"Inspect " + currentTechnique + " in Events"} > - <EuiIcon onMouseDown={(e) => {this.props.openDiscover(e,currentTechnique);e.stopPropagation()}} color="primary" type="discoverApp"></EuiIcon> - </EuiToolTip> - </span> + <EuiSpacer size="s" /> + <EuiAccordion + style={{ textDecoration: 'none' }} + id={'recent_events'} + className="events-accordion" + extraAction={ + <div style={{ marginBottom: 5 }}> + <strong>{this.state.totalHits || 0}</strong> hits + </div> + } + buttonContent={ + <EuiTitle size="s"> + <h3> + Recent events + {this.props.view !== 'events' && ( + <span style={{ marginLeft: 16 }}> + <span> + <EuiToolTip + position="top" + content={'Show ' + currentTechnique + ' in Dashboard'} + > + <EuiIcon + onMouseDown={(e) => { + this.props.openDashboard(e, currentTechnique); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + style={{ marginRight: '10px' }} + ></EuiIcon> + </EuiToolTip> + <EuiToolTip + position="top" + content={'Inspect ' + currentTechnique + ' in Events'} + > + <EuiIcon + onMouseDown={(e) => { + this.props.openDiscover(e, currentTechnique); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + ></EuiIcon> + </EuiToolTip> </span> - )} - </h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true}> + </span> + )} + </h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > <EuiFlexGroup className="flyout-row"> <EuiFlexItem> <Discover kbnSearchBar shareFilterManager={this.filterManager} - initialColumns={["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.level', 'rule.id', 'rule.description']} + initialColumns={[ + 'icon', + 'timestamp', + 'rule.mitre.id', + 'rule.mitre.tactic', + 'rule.level', + 'rule.id', + 'rule.description', + ]} implicitFilters={implicitFilters} initialFilters={[]} updateTotalHits={(total) => this.updateTotalHits(total)} - openIntelligence={(e,redirectTo,itemId) => this.props.openIntelligence(e,redirectTo,itemId)} + openIntelligence={(e, redirectTo, itemId) => + this.props.openIntelligence(e, redirectTo, itemId) + } /> </EuiFlexItem> </EuiFlexGroup> </EuiAccordion> - </EuiFlyoutBody> ); } updateTotalHits = (total) => { - this.setState({totalHits : total}); - } + this.setState({ totalHits: total }); + }; - renderLoading(){ + renderLoading() { return ( - <EuiFlyoutBody> - <EuiLoadingContent lines={2} /> - <EuiLoadingContent lines={3} /> - </EuiFlyoutBody> - ) + <EuiFlyoutBody> + <EuiLoadingContent lines={2} /> + <EuiLoadingContent lines={3} /> + </EuiFlyoutBody> + ); } render() { const { techniqueData } = this.state; const { onChangeFlyout } = this.props; - return( - <EuiFlyout - onClose={() => onChangeFlyout(false)} - size="l" - className="flyout-no-overlap wz-inventory wzApp" - aria-labelledby="flyoutSmallTitle" - > - { techniqueData && - this.renderHeader() - } - { - this.renderBody() - } - { this.state.loading && - this.renderLoading() - } - </EuiFlyout> + return ( + <EuiFlyout + onClose={() => onChangeFlyout(false)} + size="l" + className="flyout-no-overlap wz-inventory wzApp" + aria-labelledby="flyoutSmallTitle" + > + {techniqueData && this.renderHeader()} + {this.renderBody()} + {this.state.loading && this.renderLoading()} + </EuiFlyout> ); } -} \ No newline at end of file +} diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index 762f51aa61..4c46b23da8 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -1,39 +1,49 @@ /* -* Wazuh app - React component for Visualize. -* Copyright (C) 2015-2021 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. -*/ + * Wazuh app - React component for Visualize. + * Copyright (C) 2015-2021 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 from 'react'; import { useFilterManager, useQuery, useRefreshAngularDiscover } from '../../common/hooks'; import { Discover } from '../../common/modules/discover'; -import { useAllowedAgents } from '../../common/hooks/useAllowedAgents' - -export const SecurityAlerts = ({initialColumns = ["icon", "timestamp", 'rule.mitre.id', 'rule.mitre.tactic', 'rule.description', 'rule.level', 'rule.id']}) => { - const [query] = useQuery(); - const {filterManager} = useFilterManager(); - const copyOfFilterManager = filterManager - const refreshAngularDiscover = useRefreshAngularDiscover(); - - const customFilterWithAllowedAgents = []; - const {allowedAgents, filterAllowedAgents} = useAllowedAgents(); - filterAllowedAgents && customFilterWithAllowedAgents.push(filterAllowedAgents); +import { useAllowedAgents } from '../../common/hooks/useAllowedAgents'; - return ( - <Discover - shareFilterManager={filterManager} - shareFilterManagerWithUserAuthorized={customFilterWithAllowedAgents} - query={query} - initialColumns={initialColumns} - implicitFilters={[]} - initialFilters={[]} - updateTotalHits={(total) => { }} - refreshAngularDiscover={refreshAngularDiscover} - /> - ) -} \ No newline at end of file +export const SecurityAlerts = ({ + initialColumns = [ + 'icon', + 'timestamp', + 'rule.mitre.id', + 'rule.mitre.tactic', + 'rule.description', + 'rule.level', + 'rule.id', + ], +}) => { + const [query] = useQuery(); + const { filterManager } = useFilterManager(); + const copyOfFilterManager = filterManager; + const refreshAngularDiscover = useRefreshAngularDiscover(); + + const customFilterWithAllowedAgents = []; + const { allowedAgents, filterAllowedAgents } = useAllowedAgents(); + filterAllowedAgents && customFilterWithAllowedAgents.push(filterAllowedAgents); + + return ( + <Discover + shareFilterManager={filterManager} + shareFilterManagerWithUserAuthorized={customFilterWithAllowedAgents} + query={query} + initialColumns={initialColumns} + implicitFilters={[]} + initialFilters={[]} + updateTotalHits={(total) => {}} + refreshAngularDiscover={refreshAngularDiscover} + /> + ); +}; From 5c5287e68a3194eaf8b4e673cc50e2af9169c329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 4 Aug 2021 15:41:39 +0200 Subject: [PATCH 227/493] Changed the type for innitialColumns in Discover --- .../agents/fim/inventory/fileDetail.tsx | 22 ++++++--- .../agents/vuls/inventory/detail.tsx | 17 ++++++- .../common/modules/discover/discover.tsx | 48 +++++++++---------- .../requirement-flyout/requirement-flyout.tsx | 22 ++++++--- .../flyout-technique/flyout-technique.tsx | 25 +++++++--- .../config/drilldown-ip-config.tsx | 11 +++-- .../config/drilldown-operations-config.tsx | 11 +++-- .../config/drilldown-rules-config.tsx | 11 +++-- .../config/drilldown-user-config.tsx | 11 +++-- .../visualize/components/security-alerts.tsx | 30 ++++++++---- 10 files changed, 134 insertions(+), 74 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index f4f2ebb35a..f2519beff8 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -479,12 +479,22 @@ export class FileDetails extends Component { kbnSearchBar shareFilterManager={this.discoverFilterManager} initialColumns={[ - 'icon', - 'timestamp', - 'syscheck.event', - 'rule.description', - 'rule.level', - 'rule.id', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'agent.id', label: 'Agent' }, + { field: 'agent.name', label: 'Agent name' }, + { field: 'syscheck.event', label: 'Action' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + ]} + initialAgentColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'syscheck.event', label: 'Action' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} includeFilters="syscheck" implicitFilters={implicitFilters} diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 7c8483373b..c83ee7ba20 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -360,7 +360,22 @@ export class Details extends Component { <Discover kbnSearchBar shareFilterManager={this.discoverFilterManager} - initialColumns={['icon', 'timestamp', 'rule.description', 'rule.level', 'rule.id']} + initialColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'agent.id', label: 'Agent' }, + { field: 'agent.name', label: 'Agent name' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + ]} + initialAgentColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + ]} includeFilters="vulnerability" implicitFilters={implicitFilters} initialFilters={[]} diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index b09d81aeb3..75101899eb 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -60,6 +60,11 @@ const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); +interface ColumnDefinition { + field: string; + label?: string; +} + export const Discover = compose( withErrorBoundary, withReduxProvider, @@ -104,7 +109,8 @@ export const Discover = compose( updateTotalHits: Function; openIntelligence: Function; includeFilters?: string; - initialColumns: string[]; + initialColumns: ColumnDefinition[]; + initialAgentColumns?: ColumnDefinition[]; shareFilterManager: FilterManager; shareFilterManagerWithUserAuthorized: Filter[]; refreshAngularDiscover?: number; @@ -137,21 +143,6 @@ export const Discover = compose( }; this.wazuhConfig = new WazuhConfig(); - this.nameEquivalences = { - 'agent.id': 'Agent', - 'agent.name': 'Agent name', - 'syscheck.event': 'Action', - 'rule.id': 'Rule ID', - 'rule.description': 'Description', - 'rule.level': 'Level', - 'rule.mitre.id': 'Technique(s)', - 'rule.mitre.tactic': 'Tactic(s)', - 'rule.pci_dss': 'PCI DSS', - 'rule.gdpr': 'GDPR', - 'rule.nist_800_53': 'NIST 800-53', - 'rule.tsc': 'TSC', - 'rule.hipaa': 'HIPAA', - }; this.hideCreateCustomLabel.bind(this); this.onQuerySubmit.bind(this); @@ -249,16 +240,23 @@ export const Discover = compose( } } - getColumns() { + getInnitialDefinitions() { if (this.props.currentAgentData.id) { - return this.props.initialColumns.filter( - (column) => !['agent.id', 'agent.name'].includes(column) - ); + return this.props.initialAgentColumns || this.props.initialColumns; + } else { + return this.props.initialColumns; + } + } + getColumns() { + //Extract array of terms from object + return this.getInnitialDefinitions().map((column) => column.field); + } + getLabel(field) { + const innitialLabels = this.getInnitialDefinitions().filter((value) => value.field === field); + if (innitialLabels.length) { + return innitialLabels[0].label || field; } else { - const columns = [...this.props.initialColumns]; - columns.splice(2, 0, 'agent.id'); - columns.splice(3, 0, 'agent.name'); - return columns; + return field; } } @@ -548,7 +546,7 @@ export const Discover = compose( }} style={{ display: 'inline-flex' }} > - {this.nameEquivalences[item] || item}{' '} + {this.getLabel(item)}{' '} {this.state.hover === item && ( <EuiToolTip position="top" content={`Remove column`}> <EuiButtonIcon diff --git a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx index e5612e6800..040c0418e7 100644 --- a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx +++ b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx @@ -193,12 +193,22 @@ export class RequirementFlyout extends Component { kbnSearchBar shareFilterManager={this.filterManager} initialColumns={[ - 'icon', - 'timestamp', - this.props.getRequirementKey(), - 'rule.level', - 'rule.id', - 'rule.description', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'agent.id', label: 'Agent' }, + { field: 'agent.name', label: 'Agent name' }, + { field: this.props.getRequirementKey() }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + ]} + initialAgentColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: this.props.getRequirementKey() }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} implicitFilters={implicitFilters} initialFilters={[]} diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index b605d26101..e264808cc5 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -339,13 +339,24 @@ export class FlyoutTechnique extends Component { kbnSearchBar shareFilterManager={this.filterManager} initialColumns={[ - 'icon', - 'timestamp', - 'rule.mitre.id', - 'rule.mitre.tactic', - 'rule.level', - 'rule.id', - 'rule.description', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'agent.id', label: 'Agent' }, + { field: 'agent.name', label: 'Agent Name' }, + { field: 'rule.mitre.id', label: 'Technique(s)' }, + { field: 'rule.mitre.tactic', label: 'Tactic(s)' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + { field: 'rule.description', label: 'Description' }, + ]} + initialAgentColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.mitre.id', label: 'Technique(s)' }, + { field: 'rule.mitre.tactic', label: 'Tactic(s)' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + { field: 'rule.description', label: 'Description' }, ]} implicitFilters={implicitFilters} initialFilters={[]} diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index d017031c3c..0ba03931f5 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -70,12 +70,13 @@ export const drilldownIPConfig = { <EuiPanel paddingSize={'s'}> <SecurityAlerts initialColumns={[ - 'icon', - 'timestamp', - 'data.office365.UserId', - 'rule.description', - 'data.office365.Operation', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'data.office365.UserId', label: 'User ID' }, + { field: 'rule.description', label: 'Description' }, + { field: 'data.office365.Operation', label: 'Operation' }, ]} + useAgentColumns={false} /> </EuiPanel> </EuiFlexItem> diff --git a/public/components/overview/office-panel/config/drilldown-operations-config.tsx b/public/components/overview/office-panel/config/drilldown-operations-config.tsx index 48f66c3b62..70a50efdb4 100644 --- a/public/components/overview/office-panel/config/drilldown-operations-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-operations-config.tsx @@ -59,12 +59,13 @@ export const drilldownOperationsConfig = { <EuiPanel paddingSize={'s'}> <SecurityAlerts initialColumns={[ - 'icon', - 'timestamp', - 'data.office365.UserId', - 'data.office365.ClientIP', - 'rule.description', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'data.office365.UserId', label: 'User ID' }, + { field: 'data.office365.ClientIP', label: 'Client IP' }, + { field: 'rule.description', label: 'Description' }, ]} + useAgentColumns={false} /> </EuiPanel> </EuiFlexItem> diff --git a/public/components/overview/office-panel/config/drilldown-rules-config.tsx b/public/components/overview/office-panel/config/drilldown-rules-config.tsx index d29be7e15a..0cc4aac3c9 100644 --- a/public/components/overview/office-panel/config/drilldown-rules-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-rules-config.tsx @@ -65,12 +65,13 @@ export const drilldownRulesConfig = { <EuiPanel paddingSize={'s'}> <SecurityAlerts initialColumns={[ - 'icon', - 'timestamp', - 'data.office365.UserId', - 'data.office365.ClientIP', - 'data.office365.Operation', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'data.office365.UserId', label: 'User ID' }, + { field: 'data.office365.ClientIP', label: 'Client IP' }, + { field: 'data.office365.Operation', label: 'Operation' }, ]} + useAgentColumns={false} /> </EuiPanel> </EuiFlexItem> diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx index cebdccf6c8..cc0ec9fd5a 100644 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -70,12 +70,13 @@ export const drilldownUserConfig = { <EuiPanel paddingSize={'s'}> <SecurityAlerts initialColumns={[ - 'icon', - 'timestamp', - 'data.office365.ClientIP', - 'rule.description', - 'data.office365.Operation', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'data.office365.ClientIP', label: 'Client IP' }, + { field: 'rule.description', label: 'Description' }, + { field: 'data.office365.Operation', label: 'Operation' }, ]} + useAgentColumns={false} /> </EuiPanel> </EuiFlexItem> diff --git a/public/components/visualize/components/security-alerts.tsx b/public/components/visualize/components/security-alerts.tsx index 4c46b23da8..dbf160030f 100644 --- a/public/components/visualize/components/security-alerts.tsx +++ b/public/components/visualize/components/security-alerts.tsx @@ -16,22 +16,33 @@ import { useAllowedAgents } from '../../common/hooks/useAllowedAgents'; export const SecurityAlerts = ({ initialColumns = [ - 'icon', - 'timestamp', - 'rule.mitre.id', - 'rule.mitre.tactic', - 'rule.description', - 'rule.level', - 'rule.id', + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'agent.id', label: 'Agent' }, + { field: 'agent.name', label: 'Agent name' }, + { field: 'rule.mitre.id', label: 'Technique(s)' }, + { field: 'rule.mitre.tactic', label: 'Tactic(s)' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ], + initialAgentColumns = [ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.mitre.id', label: 'Technique(s)' }, + { field: 'rule.mitre.tactic', label: 'Tactic(s)' }, + { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, + ], + useAgentColumns = true, }) => { const [query] = useQuery(); const { filterManager } = useFilterManager(); - const copyOfFilterManager = filterManager; const refreshAngularDiscover = useRefreshAngularDiscover(); const customFilterWithAllowedAgents = []; - const { allowedAgents, filterAllowedAgents } = useAllowedAgents(); + const { filterAllowedAgents } = useAllowedAgents(); filterAllowedAgents && customFilterWithAllowedAgents.push(filterAllowedAgents); return ( @@ -40,6 +51,7 @@ export const SecurityAlerts = ({ shareFilterManagerWithUserAuthorized={customFilterWithAllowedAgents} query={query} initialColumns={initialColumns} + initialAgentColumns={useAgentColumns ? initialAgentColumns : undefined} implicitFilters={[]} initialFilters={[]} updateTotalHits={(total) => {}} From 611eadadd8c77cd11ed8b1f43d07b75831b52fe7 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 4 Aug 2021 16:22:53 +0200 Subject: [PATCH 228/493] fix PR comments --- .../custom-search-bar/components/combobox.tsx | 1 - .../custom-search-bar/custom-search-bar.tsx | 54 ++++++++++--------- public/components/common/hooks/index.ts | 1 + .../components/common/hooks/use_previous.ts | 9 ++++ public/styles/common.scss | 4 -- 5 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 public/components/common/hooks/use_previous.ts diff --git a/public/components/common/custom-search-bar/components/combobox.tsx b/public/components/common/custom-search-bar/components/combobox.tsx index 8924d831f4..c268dae6ad 100644 --- a/public/components/common/custom-search-bar/components/combobox.tsx +++ b/public/components/common/custom-search-bar/components/combobox.tsx @@ -18,7 +18,6 @@ export const Combobox = ({ item, ...props }) => { placeholder={item.key} className={'filters-custom-combobox'} options={comboOptions} - isClearable={false} isLoading={isLoading} onSearchChange={(searchValue) => { setQuery(searchValue) }} {...props} diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index ef6b949f61..699c72753b 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { getIndexPattern } from '../../overview/mitre/lib'; import { Filter } from '../../../../../../src/plugins/data/public/'; import { FilterMeta, @@ -12,14 +11,12 @@ import { AppState } from '../../../react-services/app-state'; import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore -import { getDataPlugin } from '../../../kibana-services'; import { KbnSearchBar } from '../../kbn-search-bar'; import { Combobox } from './components'; -import { element } from 'angular'; +import { useFilterManager, usePrevious } from '../hooks'; export const CustomSearchBar = ({ filtersValues, ...props }) => { - const KibanaServices = getDataPlugin().query; - const filterManager = KibanaServices.filterManager; + const { filterManager, filters } = useFilterManager(); const defaultSelectedOptions = () => { const array = []; filtersValues.forEach((item) => { @@ -30,15 +27,21 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); + const [values, setValues] = useState([]); + const [selectReference, setSelectReference] = useState('') + const prevValues = usePrevious(values); useEffect(() => { - let filterSubscriber = filterManager.getUpdates$().subscribe(() => { - onFiltersUpdated(); - return () => { - filterSubscriber.unsubscribe(); - }; - }); - }, []); + if(prevValues != values){ + setKibanaFilters(values,selectReference); + refreshCustomSelectedFilter(); + } + }, [values,selectReference]); + + useEffect(() => { + onFiltersUpdated(); + }, [filters]); + const onFiltersUpdated = () => { refreshCustomSelectedFilter(); @@ -80,14 +83,16 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { return { meta, $state, query }; }; - const setKibanaFilters = (values: any[]) => { - const currentFilters = filterManager + const setKibanaFilters = (values: any[], selectReference: String) => { + const currentFilters = filterManager .getFilters() - .filter((item) => item.meta.key != values[0].value); - filterManager.removeAll(); - filterManager.addFilters(currentFilters); - const customFilter = buildCustomFilter(false, values); - filterManager.addFilters(customFilter); + .filter((item) => item.meta.key != selectReference); + filterManager.removeAll(); + filterManager.addFilters(currentFilters); + if(values.length != 0){ + const customFilter = buildCustomFilter(false, values); + filterManager.addFilters(customFilter); + } }; const refreshCustomSelectedFilter = () => { @@ -97,10 +102,10 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { .getFilters() .filter( (item) => - item.meta.type === 'phrases' || Object.keys(selectedOptions).includes(item.meta.key) + item.meta.type === 'phrases' && Object.keys(selectedOptions).includes(item.meta.key) ) .map((element) => ({ params: element.meta.params, key: element.meta.key })) || []; - + const getFilterCustom = (item) => { return item.params.map((element) => ({ label: element, value: item.key })); }; @@ -118,18 +123,19 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const onChange = (values: any[]) => { - setKibanaFilters(values); - refreshCustomSelectedFilter(); + setValues(values) }; const getComponent = (item: any) => { - var types = { + const types = { default: <></>, combobox: ( <Combobox + id={item.key} item={item} selectedOptions={selectedOptions[item.key] || []} onChange={onChange} + onClick={() => setSelectReference(item.key)} /> ), }; diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 64591f2071..26dad891a9 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,5 +24,6 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; +export * from './use_previous'; export { useEsSearch } from './use-es-search'; export { useValueSuggestions, IValueSuggestiions } from './use-value-suggestions'; diff --git a/public/components/common/hooks/use_previous.ts b/public/components/common/hooks/use_previous.ts new file mode 100644 index 0000000000..90d2e94112 --- /dev/null +++ b/public/components/common/hooks/use_previous.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from 'react'; + +export function usePrevious(value) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} diff --git a/public/styles/common.scss b/public/styles/common.scss index 57de97d8da..90ba0f5712 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1744,10 +1744,6 @@ iframe.width-changed { /* Custom Searchbar styles */ -.filters-custom-combobox .euiBadge__iconButton{ - display: none; -} - .application .filters-search-bar .globalQueryBar, .app-container .filters-search-bar .globalQueryBar{ padding: 0 !important; } From 626a0b675df4ea569bca3033e336e65956cb2dd2 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 4 Aug 2021 16:31:11 +0200 Subject: [PATCH 229/493] fix prettier --- .../custom-search-bar/custom-search-bar.tsx | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 699c72753b..2a3143a206 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -27,22 +27,21 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); - const [values, setValues] = useState([]); - const [selectReference, setSelectReference] = useState('') + const [values, setValues] = useState([]); + const [selectReference, setSelectReference] = useState(''); const prevValues = usePrevious(values); useEffect(() => { - if(prevValues != values){ - setKibanaFilters(values,selectReference); - refreshCustomSelectedFilter(); - } - }, [values,selectReference]); + if (prevValues != values) { + setKibanaFilters(values, selectReference); + refreshCustomSelectedFilter(); + } + }, [values, selectReference]); useEffect(() => { onFiltersUpdated(); }, [filters]); - const onFiltersUpdated = () => { refreshCustomSelectedFilter(); }; @@ -84,15 +83,15 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const setKibanaFilters = (values: any[], selectReference: String) => { - const currentFilters = filterManager + const currentFilters = filterManager .getFilters() .filter((item) => item.meta.key != selectReference); - filterManager.removeAll(); - filterManager.addFilters(currentFilters); - if(values.length != 0){ - const customFilter = buildCustomFilter(false, values); - filterManager.addFilters(customFilter); - } + filterManager.removeAll(); + filterManager.addFilters(currentFilters); + if (values.length != 0) { + const customFilter = buildCustomFilter(false, values); + filterManager.addFilters(customFilter); + } }; const refreshCustomSelectedFilter = () => { @@ -105,25 +104,25 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { item.meta.type === 'phrases' && Object.keys(selectedOptions).includes(item.meta.key) ) .map((element) => ({ params: element.meta.params, key: element.meta.key })) || []; - + const getFilterCustom = (item) => { return item.params.map((element) => ({ label: element, value: item.key })); }; const filterCustom = filters.map((item) => getFilterCustom(item)) || []; if (filterCustom.length != 0) { filterCustom.forEach((item) => { - item.forEach(element =>{ - setSelectedOptions((prevState) => ({ - ...prevState, - [element.value]: [...prevState[element.value], element], - })); - }) + item.forEach((element) => { + setSelectedOptions((prevState) => ({ + ...prevState, + [element.value]: [...prevState[element.value], element], + })); + }); }); } }; const onChange = (values: any[]) => { - setValues(values) + setValues(values); }; const getComponent = (item: any) => { From 63496304fb9e39f9e4c9749ac2e235c8ff2f27ae Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Wed, 4 Aug 2021 16:33:12 -0300 Subject: [PATCH 230/493] feat(configuration-view): PR comments + update snapshots --- .../__snapshots__/api-auth-tab.test.tsx.snap | 15 ++++++++++++--- .../components/api-auth-tab/api-auth-tab.tsx | 3 ++- .../__snapshots__/general-tab.test.tsx.snap | 10 +++++----- .../components/general-tab/general-tab.tsx | 6 +++++- .../configuration/office365/office365.tsx | 14 +++----------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index 18da1eab01..48d6122327 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -70,7 +70,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` ] } minusHeight={260} - title="List of Api Auth" + title="Credential for the authentication with the API" > <WzConfigurationSettingsHeader help={ @@ -87,7 +87,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` } json={[Function]} settings={[Function]} - title="List of Api Auth" + title="Credential for the authentication with the API" viewSelected="" xml={[Function]} > @@ -107,7 +107,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` <h2 className="euiTitle euiTitle--small" > - List of Api Auth + Credential for the authentication with the API </h2> </EuiTitle> </div> @@ -402,6 +402,10 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` "field": "client_secret", "label": "Client Secret", }, + Object { + "field": "client_secret_path", + "label": "Client Secret Path", + }, ] } > @@ -701,6 +705,11 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` /> </EuiSpacer> </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Secret Path-undefined-3" + keyItem="-Client Secret Path-undefined-3" + label="Client Secret Path" + /> </div> </EuiFlexItem> </div> diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx index faffb2916d..5c7caaba3e 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx @@ -25,11 +25,12 @@ export const ApiAuthTab = ({ agent, wodleConfiguration }: ApiAuthProps) => { { field: 'tenant_id', label: 'Tenant Id' }, { field: 'client_id', label: 'Client Id' }, { field: 'client_secret', label: 'Client Secret' }, + { field: 'client_secret_path', label: 'Client Secret Path' }, ]; return ( <WzConfigurationSettingsTabSelector - title="List of Api Auth" + title="Credential for the authentication with the API" currentConfig={wodleConfiguration} minusHeight={agent.id === '000' ? 260 : 320} helpLinks={HELP_LINKS} diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index 864a1a0674..b0791b6c63 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -436,7 +436,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "field": "interval", - "label": "Interval between Office 365 wodle executions in seconds", + "label": "Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)", }, Object { "field": "curl_max_size", @@ -664,9 +664,9 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` </EuiSpacer> </WzConfigurationSetting> <WzConfigurationSetting - key="-Interval between Office 365 wodle executions in seconds-undefined-2" - keyItem="-Interval between Office 365 wodle executions in seconds-undefined-2" - label="Interval between Office 365 wodle executions in seconds" + key="-Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)-undefined-2" + keyItem="-Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)-undefined-2" + label="Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)" value={600} > <div @@ -693,7 +693,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` <div className="euiTextAlign euiTextAlign--right" > - Interval between Office 365 wodle executions in seconds + Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days) </div> </EuiTextAlign> </div> diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index 89b1dfdf4d..6c028f2dad 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -28,7 +28,11 @@ export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => field: 'only_future_events', label: 'Collect events generated since Wazuh is initialized', }, - { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds' }, + { + field: 'interval', + label: + 'Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)', + }, { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, ]; diff --git a/public/controllers/management/components/management/configuration/office365/office365.tsx b/public/controllers/management/components/management/configuration/office365/office365.tsx index dff09d08a5..ca6ea2ba4b 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.tsx @@ -86,14 +86,6 @@ const tabWrapper = compose( ) ); -const GeneralTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { - return <GeneralTab wodleConfiguration={wodleConfiguration} agent={agent} />; -}); - -const ApiAuthTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { - return <ApiAuthTab wodleConfiguration={wodleConfiguration} agent={agent} />; -}); - -const SubscriptionTabWrapped = tabWrapper(({ agent, wodleConfiguration }) => { - return <SubscriptionTab wodleConfiguration={wodleConfiguration} agent={agent} />; -}); +const GeneralTabWrapped = tabWrapper(GeneralTab); +const ApiAuthTabWrapped = tabWrapper(ApiAuthTab); +const SubscriptionTabWrapped = tabWrapper(SubscriptionTab); From 1de40b85cbcef10043cf38ae09962fc5fe6b0602 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Wed, 4 Aug 2021 16:57:32 -0300 Subject: [PATCH 231/493] feat(configuration-view): Added multiple api_auth. Updated snapshot --- .../__snapshots__/api-auth-tab.test.tsx.snap | 36 +++++++++---------- .../api-auth-tab/api-auth-tab.test.tsx | 13 +++++-- .../components/api-auth-tab/api-auth-tab.tsx | 7 ++-- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index 48d6122327..2e78de741e 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -12,9 +12,9 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` "office365": Object { "api_auth": Array [ Object { - "client_id": "your_client_id_test", - "client_secret": "your_secret_test", - "tenant_id": "your_tenant_id_test", + "client_id": "your_client_id_test_2", + "client_secret": "your_secret_test_\\"", + "tenant_id": "your_tenant_id_test_2", }, ], "curl_max_size": 1024, @@ -38,9 +38,9 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` "office365": Object { "api_auth": Array [ Object { - "client_id": "your_client_id_test", - "client_secret": "your_secret_test", - "tenant_id": "your_tenant_id_test", + "client_id": "your_client_id_test_2", + "client_secret": "your_secret_test_\\"", + "tenant_id": "your_tenant_id_test_2", }, ], "curl_max_size": 1024, @@ -383,9 +383,9 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` <WzSettingsGroup config={ Object { - "client_id": "your_client_id_test", - "client_secret": "your_secret_test", - "tenant_id": "your_tenant_id_test", + "client_id": "your_client_id_test_2", + "client_secret": "your_secret_test_\\"", + "tenant_id": "your_tenant_id_test_2", } } items={ @@ -475,7 +475,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` key="-Tenant Id-undefined-0" keyItem="-Tenant Id-undefined-0" label="Tenant Id" - value="your_tenant_id_test" + value="your_tenant_id_test_2" > <div style={ @@ -514,7 +514,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` > <EuiFieldText readOnly={true} - value="your_tenant_id_test" + value="your_tenant_id_test_2" > <EuiFormControlLayout fullWidth={false} @@ -531,7 +531,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` className="euiFieldText" readOnly={true} type="text" - value="your_tenant_id_test" + value="your_tenant_id_test_2" /> </EuiValidatableControl> <EuiFormControlLayoutIcons /> @@ -553,7 +553,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` key="-Client Id-undefined-1" keyItem="-Client Id-undefined-1" label="Client Id" - value="your_client_id_test" + value="your_client_id_test_2" > <div style={ @@ -592,7 +592,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` > <EuiFieldText readOnly={true} - value="your_client_id_test" + value="your_client_id_test_2" > <EuiFormControlLayout fullWidth={false} @@ -609,7 +609,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` className="euiFieldText" readOnly={true} type="text" - value="your_client_id_test" + value="your_client_id_test_2" /> </EuiValidatableControl> <EuiFormControlLayoutIcons /> @@ -631,7 +631,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` key="-Client Secret-undefined-2" keyItem="-Client Secret-undefined-2" label="Client Secret" - value="your_secret_test" + value="your_secret_test_\\"" > <div style={ @@ -670,7 +670,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` > <EuiFieldText readOnly={true} - value="your_secret_test" + value="your_secret_test_\\"" > <EuiFormControlLayout fullWidth={false} @@ -687,7 +687,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` className="euiFieldText" readOnly={true} type="text" - value="your_secret_test" + value="your_secret_test_\\"" /> </EuiValidatableControl> <EuiFormControlLayoutIcons /> diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx index 2aab23eca6..e5f8a8f5d2 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx @@ -24,9 +24,16 @@ describe('ApiAuthTab component', () => { curl_max_size: 1024, api_auth: [ { - tenant_id: 'your_tenant_id_test', - client_id: 'your_client_id_test', - client_secret: 'your_secret_test', + tenant_id: 'your_tenant_id_test_1', + client_id: 'your_client_id_test_1', + client_secret: 'your_secret_test_1', + }, + ], + api_auth: [ + { + tenant_id: 'your_tenant_id_test_2', + client_id: 'your_client_id_test_2', + client_secret: 'your_secret_test_"', }, ], subscriptions: [ diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx index 5c7caaba3e..0385f05353 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx @@ -35,10 +35,9 @@ export const ApiAuthTab = ({ agent, wodleConfiguration }: ApiAuthProps) => { minusHeight={agent.id === '000' ? 260 : 320} helpLinks={HELP_LINKS} > - <WzConfigurationSettingsGroup - config={wodleConfiguration[OFFICE_365].api_auth[0]} - items={columns} - /> + {wodleConfiguration[OFFICE_365].api_auth.map((api_auth) => ( + <WzConfigurationSettingsGroup config={api_auth} items={columns} /> + ))} </WzConfigurationSettingsTabSelector> ); }; From 7fc4c1efe6fe8622f29df33e281ed907d832b0d4 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Wed, 4 Aug 2021 16:59:58 -0300 Subject: [PATCH 232/493] feat(configuration-view): Typo --- .../__snapshots__/api-auth-tab.test.tsx.snap | 12 ++++++------ .../components/api-auth-tab/api-auth-tab.test.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index 2e78de741e..2b9b49e2b5 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -13,7 +13,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` "api_auth": Array [ Object { "client_id": "your_client_id_test_2", - "client_secret": "your_secret_test_\\"", + "client_secret": "your_secret_test_2", "tenant_id": "your_tenant_id_test_2", }, ], @@ -39,7 +39,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` "api_auth": Array [ Object { "client_id": "your_client_id_test_2", - "client_secret": "your_secret_test_\\"", + "client_secret": "your_secret_test_2", "tenant_id": "your_tenant_id_test_2", }, ], @@ -384,7 +384,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` config={ Object { "client_id": "your_client_id_test_2", - "client_secret": "your_secret_test_\\"", + "client_secret": "your_secret_test_2", "tenant_id": "your_tenant_id_test_2", } } @@ -631,7 +631,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` key="-Client Secret-undefined-2" keyItem="-Client Secret-undefined-2" label="Client Secret" - value="your_secret_test_\\"" + value="your_secret_test_2" > <div style={ @@ -670,7 +670,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` > <EuiFieldText readOnly={true} - value="your_secret_test_\\"" + value="your_secret_test_2" > <EuiFormControlLayout fullWidth={false} @@ -687,7 +687,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` className="euiFieldText" readOnly={true} type="text" - value="your_secret_test_\\"" + value="your_secret_test_2" /> </EuiValidatableControl> <EuiFormControlLayoutIcons /> diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx index e5f8a8f5d2..3c1ce99bb7 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx @@ -33,7 +33,7 @@ describe('ApiAuthTab component', () => { { tenant_id: 'your_tenant_id_test_2', client_id: 'your_client_id_test_2', - client_secret: 'your_secret_test_"', + client_secret: 'your_secret_test_2', }, ], subscriptions: [ From 75f9a38a5581eec1010ea19de3d5ba843b98e76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 5 Aug 2021 11:53:12 +0200 Subject: [PATCH 233/493] Fixed innitial state of filterManager --- public/components/common/hooks/use-filter-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index c18f1a7a00..a0ad870674 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -16,7 +16,7 @@ import _ from 'lodash'; export const useFilterManager = () => { const filterManager = useMemo(() => getDataPlugin().query.filterManager, []); - const [filters, setFilters] = useState<Filter[]>([]); + const [filters, setFilters] = useState<Filter[]>(filterManager.getFilters()); useEffect(() => { const subscription = filterManager.getUpdates$().subscribe(() => { From 5139819f4a77a1f8a16a10b70d685eab150f0a67 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Thu, 5 Aug 2021 16:53:35 +0200 Subject: [PATCH 234/493] fix useState --- .../common/custom-search-bar/custom-search-bar.tsx | 5 +---- public/components/common/hooks/use_previous.ts | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 public/components/common/hooks/use_previous.ts diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 2a3143a206..9751ae47f2 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -29,14 +29,11 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); const [values, setValues] = useState([]); const [selectReference, setSelectReference] = useState(''); - const prevValues = usePrevious(values); useEffect(() => { - if (prevValues != values) { setKibanaFilters(values, selectReference); refreshCustomSelectedFilter(); - } - }, [values, selectReference]); + }, [values]); useEffect(() => { onFiltersUpdated(); diff --git a/public/components/common/hooks/use_previous.ts b/public/components/common/hooks/use_previous.ts deleted file mode 100644 index 90d2e94112..0000000000 --- a/public/components/common/hooks/use_previous.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useEffect, useRef } from 'react'; - -export function usePrevious(value) { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} From cb2c131b0c7cccba315ed2a885c8289e357fd9cb Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 9 Aug 2021 12:04:14 +0200 Subject: [PATCH 235/493] [FEAT] Refactor the layout of modules (#3494) * feat(frontend_module_tab_buttons): Refactor as the module tab buttons and view are redered. - Moved view to each tab in tab for each module - Now each module tab defines its buttons to display in the header - Adapted custom tab components adding the `withAgentSupportModule` - Created and refactor the logic of `Loader` component to a HOC `withModuleTabLoader` - Removed `Settings` and `Loader` components duw to are not used - Removed not used imports * feat(frontend_module_tab_buttons): Refactor `Explore agent` button - Refactor `Explore agent` button - Added `availabeFor` propery to `modules-defaults.json` used by `Explore agent` button. - Created `PromptModuleNotForAgent` component - Created `withModuleNotForAgent` HOC - Removed no used code and use component props intead use `store` in `public/components/common/modules/main.tsx` * changelog: Add PR to changelog * Fixing error generating report in security events * fix(frontend): Add license block to `with_module_not_for_agent.tsx` and `with_module_tab_loader.tsx` * fix(frontend): Fixed description in license for a prompt component file Co-authored-by: CPAlejandro <cuellarpeinado@gmail.com> --- CHANGELOG.md | 1 + public/components/agents/fim/main.tsx | 3 +- public/components/agents/prompts/index.ts | 11 +- .../prompts/prompt_module_not_for_agent.tsx | 52 +++++++ public/components/agents/sca/main.tsx | 3 +- public/components/agents/vuls/main.tsx | 3 +- public/components/common/hocs/index.ts | 1 + .../common/hocs/withAgentSupportModule.tsx | 14 +- .../common/hocs/with_module_not_for_agent.tsx | 29 ++++ .../common/hocs/with_module_tab_loader.tsx | 42 ++++++ .../components/common/hooks/useRootScope.ts | 8 +- .../modules/buttons/generate_report.tsx | 63 ++++++++ .../common/modules/buttons/index.ts | 13 ++ .../components/common/modules/dashboard.tsx | 13 +- public/components/common/modules/events.tsx | 9 +- public/components/common/modules/index.ts | 8 +- public/components/common/modules/loader.tsx | 53 ------- .../components/common/modules/main-agent.tsx | 82 ++-------- .../components/common/modules/main-mitre.tsx | 3 +- .../common/modules/main-overview.tsx | 98 ++++-------- public/components/common/modules/main.tsx | 140 +----------------- .../common/modules/modules-defaults.js | 107 +++++++++---- public/components/common/modules/settings.tsx | 70 --------- .../compliance-table/compliance-table.tsx | 6 +- .../overview-actions/overview-actions.js | 114 +++++++------- public/react-services/reporting.js | 14 +- 26 files changed, 448 insertions(+), 512 deletions(-) create mode 100644 public/components/agents/prompts/prompt_module_not_for_agent.tsx create mode 100644 public/components/common/hocs/with_module_not_for_agent.tsx create mode 100644 public/components/common/hocs/with_module_tab_loader.tsx create mode 100644 public/components/common/modules/buttons/generate_report.tsx create mode 100644 public/components/common/modules/buttons/index.ts delete mode 100644 public/components/common/modules/loader.tsx delete mode 100644 public/components/common/modules/settings.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5591927a8b..322c9d9119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) - Changed empty fields in FIM tables and `syscheck.value_name` in discovery now show an empty tag for visual clarity [#3279](https://github.com/wazuh/wazuh-kibana-app/pull/3279) - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) +- Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) ### Fixed diff --git a/public/components/agents/fim/main.tsx b/public/components/agents/fim/main.tsx index 99c593a792..e5d5f339a3 100644 --- a/public/components/agents/fim/main.tsx +++ b/public/components/agents/fim/main.tsx @@ -4,13 +4,14 @@ import '../../common/modules/module.scss'; import { connect } from 'react-redux'; import { PromptNoActiveAgent, PromptNoSelectedAgent } from '../prompts'; import { compose } from 'redux'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withAgentSupportModule, withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainFim = compose( + withAgentSupportModule, connect(mapStateToProps), withGuard( (props) => !(props.currentAgentData && props.currentAgentData.id && props.agent), diff --git a/public/components/agents/prompts/index.ts b/public/components/agents/prompts/index.ts index 1e567a6281..5519dfa4e7 100644 --- a/public/components/agents/prompts/index.ts +++ b/public/components/agents/prompts/index.ts @@ -9,8 +9,9 @@ * * Find more information about this on the LICENSE file. */ -export { PromptAgentNoSupportModule } from './prompt-agent-no-support-module'; -export { PromptNoActiveAgent, PromptNoActiveAgentWithoutSelect } from './prompt-no-active-agent'; -export { PromptNoSelectedAgent } from './prompt-no-selected-agent'; -export { PromptSelectAgent } from './prompt-select-agent'; -export { PromptAgentFeatureVersion } from './prompt-agent-feature-version'; +export * from './prompt-agent-no-support-module'; +export * from './prompt-no-active-agent'; +export * from './prompt-no-selected-agent'; +export * from './prompt-select-agent'; +export * from './prompt-agent-feature-version'; +export * from './prompt_module_not_for_agent'; diff --git a/public/components/agents/prompts/prompt_module_not_for_agent.tsx b/public/components/agents/prompts/prompt_module_not_for_agent.tsx new file mode 100644 index 0000000000..500b230c3d --- /dev/null +++ b/public/components/agents/prompts/prompt_module_not_for_agent.tsx @@ -0,0 +1,52 @@ +/* + * Wazuh app - Prompt when an agent doesn't support some module + * Copyright (C) 2015-2021 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 from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { updateCurrentAgentData } from '../../../redux/actions/appStateActions'; +import { useFilterManager } from '../../common/hooks'; + +type PromptSelectAgentProps = { + body?: string; + title: string; + agentSelectionProps: { + setAgent: (agent: boolean) => void + } +}; + +export const PromptModuleNotForAgent = ({ body, title, ...agentSelectionProps }: PromptSelectAgentProps) => { + const dispatch = useDispatch(); + const filterManager = useFilterManager(); + + const unpinAgent = async () => { + dispatch(updateCurrentAgentData({})); + await agentSelectionProps.setAgent(false); + const filters = filterManager.filters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + filterManager.setFilters(filters); + }; + + return ( + <EuiEmptyPrompt + iconType="watchesApp" + title={<h2>{title}</h2>} + body={body && <p>{body}</p>} + actions={ + <EuiButton color="primary" fill onClick={unpinAgent}> + Unpin agent + </EuiButton> + } + /> + ); +}; \ No newline at end of file diff --git a/public/components/agents/sca/main.tsx b/public/components/agents/sca/main.tsx index 8daf51de54..1307bfa734 100644 --- a/public/components/agents/sca/main.tsx +++ b/public/components/agents/sca/main.tsx @@ -15,13 +15,14 @@ import { Inventory } from './index'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { PromptSelectAgent, PromptNoSelectedAgent } from '../prompts'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withGuard, withUserAuthorizationPrompt, withAgentSupportModule } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainSca = compose( + withAgentSupportModule, withUserAuthorizationPrompt([ [ {action: 'agent:read', resource: 'agent:id:*'}, diff --git a/public/components/agents/vuls/main.tsx b/public/components/agents/vuls/main.tsx index d4aa148163..07affef2de 100644 --- a/public/components/agents/vuls/main.tsx +++ b/public/components/agents/vuls/main.tsx @@ -4,13 +4,14 @@ import '../../common/modules/module.scss'; import { connect } from 'react-redux'; import { PromptNoSelectedAgent, PromptNoActiveAgent } from '../prompts'; import { compose } from 'redux'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withGuard, withUserAuthorizationPrompt, withAgentSupportModule } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainVuls = compose( + withAgentSupportModule, connect(mapStateToProps), withGuard( (props) => !((props.currentAgentData && props.currentAgentData.id) && props.agent), diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index a08b1f9c64..4c84b317e5 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -21,3 +21,4 @@ export * from './withButtonOpenOnClick'; export * from './withAgentSupportModule'; export * from './withUserLogged'; export * from './error-boundary/with-error-boundary'; +export * from './with_module_tab_loader'; diff --git a/public/components/common/hocs/withAgentSupportModule.tsx b/public/components/common/hocs/withAgentSupportModule.tsx index a613f39539..f5c738d63b 100644 --- a/public/components/common/hocs/withAgentSupportModule.tsx +++ b/public/components/common/hocs/withAgentSupportModule.tsx @@ -12,9 +12,17 @@ import { PromptAgentNoSupportModule } from '../../agents/prompts'; import { withGuard } from '../../common/hocs'; import { hasAgentSupportModule } from '../../../react-services/wz-agents'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; -export const withAgentSupportModule = WrappedComponent => +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); + +export const withAgentSupportModule = WrappedComponent => compose( + connect(mapStateToProps), withGuard( - ({agent, component}) => Object.keys(agent).length && !hasAgentSupportModule(agent, component), + ({agent, moduleID}) => Object.keys(agent).length && !hasAgentSupportModule(agent, moduleID), PromptAgentNoSupportModule - )(WrappedComponent) + ) +)(WrappedComponent) diff --git a/public/components/common/hocs/with_module_not_for_agent.tsx b/public/components/common/hocs/with_module_not_for_agent.tsx new file mode 100644 index 0000000000..ca362f85a1 --- /dev/null +++ b/public/components/common/hocs/with_module_not_for_agent.tsx @@ -0,0 +1,29 @@ +/* + * Wazuh app - React HOC to show a prompt when a module is not available for agents and let to unpin the agent + * Copyright (C) 2015-2021 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 from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withGuard } from './withGuard'; +import { PromptModuleNotForAgent } from '../../agents/prompts'; + +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); + +export const withModuleNotForAgent = WrappedComponent => compose( + connect(mapStateToProps), + withGuard( + ({agent}) => agent?.id, + (props) => <PromptModuleNotForAgent title='Module not avaliable for agents' body='Remove the pinned agent.' {...props}/> + ) +)(WrappedComponent); diff --git a/public/components/common/hocs/with_module_tab_loader.tsx b/public/components/common/hocs/with_module_tab_loader.tsx new file mode 100644 index 0000000000..637dda21da --- /dev/null +++ b/public/components/common/hocs/with_module_tab_loader.tsx @@ -0,0 +1,42 @@ +/* + * Wazuh app - React HOC to show a loader used for Dashboard adn Events module tabs + * Copyright (C) 2015-2021 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, { useEffect, useState } from 'react'; +import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { useRootScope } from '../hooks'; + +export const withModuleTabLoader = WrappedComponent => props => { + const $rootScope = useRootScope(); + const [showTab, setShowTab] = useState(); + useEffect(() => { + if($rootScope){ + $rootScope.loadingDashboard = true; + $rootScope.$applyAsync(); + setTimeout(() => { + setShowTab(true); + }, 100); + return () => { + $rootScope.loadingDashboard = false; + $rootScope.$applyAsync(); + } + } + },[$rootScope]); + + return showTab ? <WrappedComponent {...props}/> : ( + <> + <EuiSpacer size='xl' /> + <div style={{ margin: '-8px auto', width: 32 }}> + <EuiLoadingSpinner size="xl" /> + </div> + </> + ) +} \ No newline at end of file diff --git a/public/components/common/hooks/useRootScope.ts b/public/components/common/hooks/useRootScope.ts index de6d0b9628..1f36e253ea 100644 --- a/public/components/common/hooks/useRootScope.ts +++ b/public/components/common/hooks/useRootScope.ts @@ -9,14 +9,14 @@ * * Find more information about this on the LICENSE file. */ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { getAngularModule } from '../../../kibana-services'; export function useRootScope(){ - const refRootScope = useRef(); + const [refRootScope,setRefRootScope] = useState(); useEffect(() => { const app = getAngularModule(); - refRootScope.current = app.$injector.get('$rootScope'); + setRefRootScope(app.$injector.get('$rootScope')); },[]); - return refRootScope.current; + return refRootScope; }; diff --git a/public/components/common/modules/buttons/generate_report.tsx b/public/components/common/modules/buttons/generate_report.tsx new file mode 100644 index 0000000000..f56622bab6 --- /dev/null +++ b/public/components/common/modules/buttons/generate_report.tsx @@ -0,0 +1,63 @@ +/* + * Wazuh app - Component for the module generate reports + * Copyright (C) 2015-2021 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 from 'react'; +import { useAsyncAction } from '../../hooks'; +import { getUiSettings } from '../../../../kibana-services'; +import { ReportingService } from '../../../../react-services'; +import $ from 'jquery'; +import { WzButton } from '../../../common/buttons'; + + +export const ButtonModuleGenerateReport = ({agent, moduleID, disabledReport}) => { + const action = useAsyncAction(async () => { + const reportingService = new ReportingService(); + const isDarkModeTheme = getUiSettings().get('theme:darkMode'); + if (isDarkModeTheme) { + + //Patch to fix white text in dark-mode pdf reports + const defaultTextColor = '#DFE5EF'; + + //Patch to fix dark backgrounds in visualizations dark-mode pdf reports + const $labels = $('.euiButtonEmpty__text, .echLegendItem'); + const $vizBackground = $('.echChartBackground'); + const defaultVizBackground = $vizBackground.css('background-color'); + + try { + $labels.css('color', 'black'); + $vizBackground.css('background-color', 'transparent'); + await reportingService.startVis2Png(moduleID, agent?.id || false) + $vizBackground.css('background-color', defaultVizBackground); + $labels.css('color', defaultTextColor); + } catch (e) { + $labels.css('color', defaultTextColor); + $vizBackground.css('background-color', defaultVizBackground); + } + } else { + await reportingService.startVis2Png(moduleID, agent?.id || false) + } + }); + + return ( + <WzButton + buttonType='empty' + iconType='document' + isLoading={action.running} + onClick={action.run} + isDisabled={disabledReport} + tooltip={disabledReport ? {position: 'top', content: 'No results match for this search criteria.'} : undefined} + > + Generate report + </WzButton> + ) +} + diff --git a/public/components/common/modules/buttons/index.ts b/public/components/common/modules/buttons/index.ts new file mode 100644 index 0000000000..76dedb877b --- /dev/null +++ b/public/components/common/modules/buttons/index.ts @@ -0,0 +1,13 @@ +/* + * Wazuh app - Module buttons components + * Copyright (C) 2015-2021 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 * from './generate_report'; \ No newline at end of file diff --git a/public/components/common/modules/dashboard.tsx b/public/components/common/modules/dashboard.tsx index 4827c26e28..4f07a74b7e 100644 --- a/public/components/common/modules/dashboard.tsx +++ b/public/components/common/modules/dashboard.tsx @@ -13,8 +13,14 @@ import { Component } from 'react'; import { ModulesHelper } from './modules-helper' import { getAngularModule } from '../../../kibana-services'; +import { withAgentSupportModule, withModuleTabLoader } from '../hocs'; +import { compose } from 'redux'; +import React from 'react'; -export class Dashboard extends Component { +export const Dashboard = compose( + withAgentSupportModule, + withModuleTabLoader +)(class Dashboard extends Component { _isMount = false; constructor(props) { super(props); @@ -43,6 +49,7 @@ export class Dashboard extends Component { } render() { - return false; + return null; } -} +}) + diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 55eef4103a..5e887aa082 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -19,11 +19,16 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiOverlayMask, EuiOutsideClickDe import { PatternHandler } from '../../../react-services/pattern-handler'; import { enhanceDiscoverEventsCell } from './events-enhance-discover-fields'; import { toMountPoint } from '../../../../../../src/plugins/kibana_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 class Events extends Component { +export const Events = compose( + withAgentSupportModule, + withModuleTabLoader +)(class Events extends Component { intervalCheckExistsDiscoverTableTime: number = 200; isMount: boolean; state: { @@ -292,4 +297,4 @@ export class Events extends Component { </Fragment> ) } -} +}) diff --git a/public/components/common/modules/index.ts b/public/components/common/modules/index.ts index 7b19779c33..bbf2232cd7 100644 --- a/public/components/common/modules/index.ts +++ b/public/components/common/modules/index.ts @@ -10,8 +10,6 @@ * Find more information about this on the LICENSE file. */ -export { Events } from './events'; -export { Dashboard } from './dashboard'; -export { Loader } from './loader'; -export { Settings } from './settings'; -export { ModulesHelper } from './modules-helper.js'; +export * from './dashboard'; +export * from './events'; +export * from './modules-helper.js'; diff --git a/public/components/common/modules/loader.tsx b/public/components/common/modules/loader.tsx deleted file mode 100644 index 98a069ec85..0000000000 --- a/public/components/common/modules/loader.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2021 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 } from '../../../kibana-services'; -import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; - -const app = getAngularModule(); - -export class Loader extends Component { - constructor(props) { - super(props); - } - - componentDidMount() { - this.$rootScope = app.$injector.get('$rootScope'); - this.$rootScope.loadingDashboard = true; - this.$rootScope.$applyAsync(); - } - - componentWillUnmount() { - this.$rootScope.loadingDashboard = false; - this.$rootScope.$applyAsync(); - } - - redirect() { - setTimeout(() => { - this.props.loadSection(this.props.redirect); - }, 100); - } - - render() { - const redirect = this.redirect(); - return ( - <Fragment> - <EuiSpacer size='xl' /> - <div style={{ margin: '-8px auto', width: 32 }}> - <EuiLoadingSpinner size="xl" /> - </div> - {redirect} - </Fragment> - ); - } -} diff --git a/public/components/common/modules/main-agent.tsx b/public/components/common/modules/main-agent.tsx index 8b5eb683ba..5a02871986 100644 --- a/public/components/common/modules/main-agent.tsx +++ b/public/components/common/modules/main-agent.tsx @@ -31,18 +31,8 @@ import { FilterHandler } from '../../../utils/filter-handler'; import { AppState } from '../../../react-services/app-state'; import { ReportingService } from '../../../react-services/reporting'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { Events, Dashboard, Loader, Settings } from '../../common/modules'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; import { AgentInfo } from '../../common/welcome/agents-info'; -import Overview from '../../wz-menu/wz-menu-overview'; -import { MainFim } from '../../agents/fim'; -import { MainSca } from '../../agents/sca'; -import { MainMitre } from '../modules/main-mitre'; import { getAngularModule } from '../../../kibana-services'; -import { withAgentSupportModule } from '../../../components/common/hocs'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; export class MainModuleAgent extends Component { props!: { @@ -248,22 +238,11 @@ export class MainModuleAgent extends Component { ); } - renderSettingsButton() { - return ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - fill={this.state.selectView === 'settings' || undefined} - iconType="wrench" - onClick={() => this.onSelectedTabChanged('settings')}> - Configuration - </EuiButtonEmpty> - </EuiFlexItem> - ); - } render() { const { agent, section, selectView } = this.props; const title = this.renderTitle(); + const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> <div className='wz-module-header-agent-wrapper'> @@ -287,29 +266,26 @@ export class MainModuleAgent extends Component { <div className="wz-welcome-page-agent-tabs"> <EuiFlexGroup> {this.props.renderTabs()} - {(selectView === 'dashboard') && - this.props.renderReportButton() - } - {(this.props.buttons || []).includes('dashboard') && - this.props.renderDashboardButton() - } - {(this.props.buttons || []).includes('settings') && - this.renderSettingsButton() - } + <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> + <EuiFlexGroup> + {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => + typeof ModuleViewButton !== 'string' ? <EuiFlexItem key={`module_button_${index}`}><ModuleViewButton {...{ ...this.props, ...this.props.agentsSelectionProps }} moduleID={section}/></EuiFlexItem> : null)} + </EuiFlexGroup> + </EuiFlexItem> </EuiFlexGroup> </div> } </div> </div> - {!['syscollector', 'configuration'].includes(this.props.section) && - <ModuleTabViewer component={section} {...this.props}/> + {!['syscollector', 'configuration'].includes(section) && + ModuleTabView && ModuleTabView.component && <ModuleTabView.component {...this.props} moduleID={section}/> } </Fragment> } {(!agent || !agent.os) && <EuiCallOut style={{ margin: '66px 16px 0 16px' }} - title=" This agent has never connected" + title="This agent has never connected" color="warning" iconType="alert"> </EuiCallOut> @@ -318,41 +294,3 @@ export class MainModuleAgent extends Component { ); } } - - - -const mapStateToProps = state => ({ - agent: state.appStateReducers.currentAgentData -}); - -const ModuleTabViewer = compose( - connect(mapStateToProps), - withAgentSupportModule -)((props) => { - const { section, selectView } = props; - return <> - {selectView === 'events' && - <Events {...props} /> - } - {selectView === 'loader' && - <Loader {...props} - loadSection={(section) => props.loadSection(section)} - redirect={props.afterLoad}> - </Loader>} - {selectView === 'dashboard' && - <Dashboard {...props} /> - } - {selectView === 'settings' && - <Settings {...props} /> - } - - - {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} - {section === 'fim' && selectView==='inventory' && <MainFim {...props} />} - {section === 'sca' && selectView==='inventory' && <MainSca {...props} />} - {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} - {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} - {/* {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && <ComplianceTable {...props} goToDiscover={(id) => props.onSelectedTabChanged(id)} />} */} - {/* -------------------------------------------------------------------------- */} - </> -}) \ No newline at end of file diff --git a/public/components/common/modules/main-mitre.tsx b/public/components/common/modules/main-mitre.tsx index 2cbaa7e2f5..1d8584658d 100644 --- a/public/components/common/modules/main-mitre.tsx +++ b/public/components/common/modules/main-mitre.tsx @@ -12,10 +12,11 @@ import React, { Component } from 'react'; import { Mitre } from '../../../components/overview/mitre/mitre'; -import { withUserAuthorizationPrompt } from '../hocs'; +import { withUserAuthorizationPrompt, withAgentSupportModule } from '../hocs'; import { compose } from 'redux'; export const MainMitre = compose( + withAgentSupportModule, withUserAuthorizationPrompt([ { action: 'mitre:read', resource: '*:*:*' }, ]) diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index 8cf7c4bf9b..a50955fb5b 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -26,27 +26,20 @@ import { } from '@elastic/eui'; import '../../common/modules/module.scss'; import { updateGlobalBreadcrumb } from '../../../redux/actions/globalBreadcrumbActions'; + import store from '../../../redux/store'; import { ReportingService } from '../../../react-services/reporting'; import { AppNavigate } from '../../../react-services/app-navigate'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { Events, Dashboard, Loader, Settings } from '../../common/modules'; -import OverviewActions from '../../../controllers/overview/components/overview-actions/overview-actions'; -import { MainFim } from '../../agents/fim'; - -import { MainVuls } from '../../agents/vuls'; -import { MainSca } from '../../agents/sca'; -import { MainMitre } from './main-mitre'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import { ComplianceTable } from '../../overview/compliance-table'; -import { withAgentSupportModule } from '../../../components/common/hocs'; import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { getDataPlugin } from '../../../kibana-services'; -import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); -export class MainModuleOverview extends Component { +export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverview extends Component { constructor(props) { super(props); this.reportingService = new ReportingService(); @@ -132,71 +125,32 @@ export class MainModuleOverview extends Component { } this.setGlobalBreadcrumb(); + const { filterManager } = getDataPlugin().query; + this.filterManager = filterManager; } render() { const { section, selectView } = this.props; + const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> - <Fragment> - <div className={this.props.tabs && this.props.tabs.length && 'wz-module-header-nav'}> - {this.props.tabs && this.props.tabs.length && ( - <div className="wz-welcome-page-agent-tabs"> - <EuiFlexGroup> - {this.props.renderTabs()} - <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> - <WzReduxProvider> - <OverviewActions {...{ ...this.props, ...this.props.agentsSelectionProps }} /> - </WzReduxProvider> - </EuiFlexItem> - {selectView === 'dashboard' && this.props.renderReportButton()} - {(this.props.buttons || []).includes('dashboard') && - this.props.renderDashboardButton()} - </EuiFlexGroup> - </div> - )} - </div> - <ModuleTabViewer component={section} {...this.props} /> - </Fragment> + <div className={this.props.tabs && this.props.tabs.length && 'wz-module-header-nav'}> + {this.props.tabs && this.props.tabs.length && ( + <div className="wz-welcome-page-agent-tabs"> + <EuiFlexGroup> + {this.props.renderTabs()} + <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> + <EuiFlexGroup> + {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => + typeof ModuleViewButton !== 'string' ? <EuiFlexItem key={`module_button_${index}`}><ModuleViewButton {...{ ...this.props, ...this.props.agentsSelectionProps }} moduleID={section} /></EuiFlexItem> : null)} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </div> + )} + </div> + {ModuleTabView && ModuleTabView.component && <ModuleTabView.component {...this.props} moduleID={section}/> } </div> ); } -} - -const mapStateToProps = (state) => ({ - agent: state.appStateReducers.currentAgentData, -}); - -const ModuleTabViewer = compose( - connect(mapStateToProps), - withAgentSupportModule -)((props) => { - const { section, selectView } = props; - return ( - <> - {selectView === 'events' && <Events {...props} />} - {selectView === 'loader' && ( - <Loader - {...props} - loadSection={(section) => props.loadSection(section)} - redirect={props.afterLoad} - ></Loader> - )} - {selectView === 'dashboard' && <Dashboard {...props} />} - {selectView === 'settings' && <Settings {...props} />} - - {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} - {section === 'fim' && selectView === 'inventory' && <MainFim {...props} />} - {section === 'sca' && selectView === 'inventory' && <MainSca {...props} />} - - {section === 'vuls' && selectView === 'inventory' && <MainVuls {...props} />} - - {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} - {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} - {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && ( - <ComplianceTable {...props} goToDiscover={(id) => props.onSelectedTabChanged(id)} /> - )} - {/* -------------------------------------------------------------------------- */} - </> - ); -}); \ No newline at end of file +}) diff --git a/public/components/common/modules/main.tsx b/public/components/common/modules/main.tsx index 9f0e853d2d..1793af4d17 100644 --- a/public/components/common/modules/main.tsx +++ b/public/components/common/modules/main.tsx @@ -21,12 +21,10 @@ import { } from '@elastic/eui'; import '../../common/modules/module.scss'; import { ReportingService } from '../../../react-services/reporting'; -import { AppNavigate } from '../../../react-services/app-navigate'; import { ModulesDefaults } from './modules-defaults'; -import { getAngularModule, getDataPlugin, getUiSettings } from '../../../kibana-services'; +import { getAngularModule, getDataPlugin } from '../../../kibana-services'; import { MainModuleAgent } from './main-agent' import { MainModuleOverview } from './main-overview'; -import store from '../../../redux/store'; import { compose } from 'redux'; import { withReduxProvider,withErrorBoundary } from '../hocs'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; @@ -49,18 +47,12 @@ export const MainModule = compose( }; const app = getAngularModule(); this.$rootScope = app.$injector.get('$rootScope'); - } - - async componentDidMount() { if (!(ModulesDefaults[this.props.section] || {}).notModule) { this.tabs = (ModulesDefaults[this.props.section] || {}).tabs || [ { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }, ]; - this.buttons = (ModulesDefaults[this.props.section] || {}).buttons || [ - 'reporting', - 'settings', - ]; + this.module = ModulesDefaults[this.props.section]; } } @@ -71,17 +63,6 @@ export const MainModule = compose( } } - canBeInit(tab) { - //checks if the init table can be set - let canInit = false; - this.tabs.forEach((element) => { - if (element.id === tab && (!element.onlyAgent || (element.onlyAgent && this.props.agent))) { - canInit = true; - } - }); - return canInit; - } - renderTabs(agent = false) { const { selectView } = this.state; if (!agent) { @@ -107,114 +88,6 @@ export const MainModule = compose( ); } - startVis2PngByAgent = async () => { - const agent = - (this.props.agent || store.getState().appStateReducers.currentAgentData || {}).id || false; - await this.reportingService.startVis2Png(this.props.section, agent); - }; - - async startReport() { - try { - this.setState({ loadingReport: true }); - const isDarkModeTheme = getUiSettings().get('theme:darkMode'); - if (isDarkModeTheme) { - - //Patch to fix white text in dark-mode pdf reports - const defaultTextColor = '#DFE5EF'; - - //Patch to fix dark backgrounds in visualizations dark-mode pdf reports - const $labels = $('.euiButtonEmpty__text, .echLegendItem'); - const $vizBackground = $('.echChartBackground'); - const defaultVizBackground = $vizBackground.css('background-color'); - - try { - $labels.css('color', 'black'); - $vizBackground.css('background-color', 'transparent'); - await this.startVis2PngByAgent(); - $vizBackground.css('background-color', defaultVizBackground); - $labels.css('color', defaultTextColor); - } catch (error) { - $labels.css('color', defaultTextColor); - $vizBackground.css('background-color', defaultVizBackground); - const options = { - context: `${MainModule.name}.startReport`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: 'Error generating the report', - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ loadingReport: false }); - } - } else { - await this.startVis2PngByAgent(); - } - } finally { - this.setState({ loadingReport: false }); - } - } - - renderReportButton() { - return ( - (this.props.disabledReport && ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiToolTip position="top" content="No results match for this search criteria."> - <EuiButtonEmpty - iconType="document" - isLoading={this.state.loadingReport} - isDisabled={true} - onClick={async () => this.startReport()} - > - Generate report - </EuiButtonEmpty> - </EuiToolTip> - </EuiFlexItem> - )) || ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - iconType="document" - isLoading={this.state.loadingReport} - onClick={async () => this.startReport()} - > - Generate report - </EuiButtonEmpty> - </EuiFlexItem> - ) - ); - } - - renderDashboardButton() { - const href = `#/overview?tab=${this.props.section}&agentId=${this.props.agent.id}`; - return ( - <EuiFlexItem grow={false} style={{ marginLeft: 0, marginTop: 6, marginBottom: 18 }}> - <EuiButton - fill={this.state.selectView === 'dashboard'} - iconType="visLine" - onClick={() => this.onSelectedTabChanged('dashboard')} - > - Dashboard - </EuiButton> - </EuiFlexItem> - ); - } - - renderSettingsButton() { - return ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - fill={this.state.selectView === 'settings'} - iconType="wrench" - onClick={() => this.onSelectedTabChanged('settings')} - > - Configuration - </EuiButtonEmpty> - </EuiFlexItem> - ); - } - loadSection(id) { this.setState({ selectView: id }); } @@ -231,8 +104,7 @@ export const MainModule = compose( new RegExp('tabView=' + '[^&]*'), `tabView=${id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels'}` ); - this.afterLoad = id; - this.loadSection('loader'); + this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); } else { this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); } @@ -244,13 +116,9 @@ export const MainModule = compose( const { selectView } = this.state; const mainProps = { selectView, - afterLoad: this.afterLoad, - buttons: this.buttons, tabs: this.tabs, + module: this.module, renderTabs: () => this.renderTabs(), - renderReportButton: () => this.renderReportButton(), - renderDashboardButton: () => this.renderDashboardButton(), - renderSettingsButton: () => this.renderSettingsButton(), loadSection: (id) => this.loadSection(id), onSelectedTabChanged: (id) => this.onSelectedTabChanged(id), }; diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 0a7232f2c2..d5783601dd 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -9,66 +9,123 @@ * * Find more information about this on the LICENSE file. */ +import { Dashboard } from './dashboard'; +import { Events } from './events'; +import { MainFim } from '../../agents/fim'; +import { MainSca } from '../../agents/sca'; +import { MainVuls } from '../../agents/vuls'; +import { MainMitre } from './main-mitre'; +import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; +import { ComplianceTable } from '../../overview/compliance-table'; +import ButtonModuleExploreAgent from '../../../controllers/overview/components/overview-actions/overview-actions'; +import { ButtonModuleGenerateReport } from '../modules/buttons'; + +const DashboardTab = { id: 'dashboard', name: 'Dashboard', buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], component: Dashboard}; +const EventsTab = { id: 'events', name: 'Events', buttons: [ButtonModuleExploreAgent], component: Events }; +const RegulatoryComplianceTabs = [{ id: 'inventory', name: 'Controls', buttons: [ButtonModuleExploreAgent], component: ComplianceTable }, DashboardTab, EventsTab]; + export const ModulesDefaults = { general: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, fim: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', onlyAgent: false }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting', 'settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainFim }, DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + aws: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'], }, gcp: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + pm: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + audit: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, sca: { init: 'inventory', - tabs: [{ id: 'inventory', name: 'Inventory' }, { id: 'events', name: 'Events' }], - buttons: ['settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainSca }, EventsTab], + buttons: ['settings'], + availableFor: ['manager','agent'] }, - mitre: { + ciscat: { init: 'dashboard', - tabs: [{id: 'intelligence', name: 'Intelligence'}, { id: 'inventory', name: 'Framework' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, vuls: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', onlyAgent: false }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting', 'settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainVuls }, DashboardTab, EventsTab], + buttons: ['settings'], + availableFor: ['manager','agent'] + }, + mitre: { + init: 'dashboard', + tabs: [{ id: 'intelligence', name: 'Intelligence', component: ModuleMitreAttackIntelligence }, { id: 'inventory', name: 'Framework', buttons: [ButtonModuleExploreAgent], component: MainMitre }, DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, virustotal: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + docker: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + pci: { + init: 'dashboard', + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] + }, + osquery: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + oscap: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, pci: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, hipaa: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, nist: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, gdpr: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, tsc: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, syscollector: { notModule: true diff --git a/public/components/common/modules/settings.tsx b/public/components/common/modules/settings.tsx deleted file mode 100644 index 26045f995c..0000000000 --- a/public/components/common/modules/settings.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2021 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React, { Component } from 'react'; -import { EuiSpacer, EuiTitle, EuiPanel, EuiPage } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import WzBadge from '../../../controllers/management/components/management/configuration/util-components/badge' -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import WzConfigurationIntegrityMonitoring from '../../../controllers/management/components/management/configuration/integrity-monitoring/integrity-monitoring'; -import WzConfigurationPolicyMonitoring from '../../../controllers/management/components/management/configuration/policy-monitoring/policy-monitoring'; -import WzConfigurationOpenSCAP from '../../../controllers/management/components/management/configuration/open-scap/open-scap'; -import WzConfigurationCisCat from '../../../controllers/management/components/management/configuration/cis-cat/cis-cat'; -import WzConfigurationVulnerabilities from '../../../controllers/management/components/management/configuration/vulnerabilities/vulnerabilities'; -import WzConfigurationOsquery from '../../../controllers/management/components/management/configuration/osquery/osquery'; -import WzConfigurationDockerListener from '../../../controllers/management/components/management/configuration/docker-listener/docker-listener'; - -type SettingsPropTypes = { - agent: { id: string }, - clusterNodeSelected?: string -} - -type SettingsState = { - badge: boolean | null -} -export class Settings extends Component<SettingsPropTypes, SettingsState> { - constructor(props) { - super(props); - this.state = { - badge: null - } - } - updateBadge(badge) { - this.setState({ badge }) - } - render() { - const { badge } = this.state; - const { section } = this.props; - return ( - <WzReduxProvider> - <EuiPage> - <EuiPanel> - <EuiTitle> - <span>{i18n.translate('wazuh.configuration', { defaultMessage: 'Configuration' })} {typeof badge === 'boolean' ? - <WzBadge enabled={badge} /> : null} - </span> - </EuiTitle> - <EuiSpacer size='m' /> - {section === 'fim' && <WzConfigurationIntegrityMonitoring {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {(section === 'pm' || section === 'sca' || section === 'audit') && - <WzConfigurationPolicyMonitoring {...this.props} updateBadge={(e) => this.updateBadge(e)} onlyShowTab={section === 'pm' ? 'Policy Monitoring' : section === 'audit' ? 'System audit' : section === 'sca' ? 'SCA': undefined}/>} - {section === 'oscap' && <WzConfigurationOpenSCAP {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'ciscat' && <WzConfigurationCisCat {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'vuls' && <WzConfigurationVulnerabilities {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'osquery' && <WzConfigurationOsquery {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'docker' && <WzConfigurationDockerListener {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - </EuiPanel> - </EuiPage> - </WzReduxProvider> - ) - } -} diff --git a/public/components/overview/compliance-table/compliance-table.tsx b/public/components/overview/compliance-table/compliance-table.tsx index 2f3430708e..ca95757668 100644 --- a/public/components/overview/compliance-table/compliance-table.tsx +++ b/public/components/overview/compliance-table/compliance-table.tsx @@ -13,7 +13,6 @@ import React, { Component } from 'react'; import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SearchBar, FilterManager } from '../../../../../../src/plugins/data/public/'; -import { I18nProvider } from '@kbn/i18n/react'; //@ts-ignore import { ComplianceRequirements } from './components/requirements'; import { ComplianceSubrequirements } from './components/subrequirements'; @@ -28,8 +27,9 @@ import { getDataPlugin } from '../../../kibana-services'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { withAgentSupportModule } from '../../common/hocs'; -export class ComplianceTable extends Component { +export const ComplianceTable = withAgentSupportModule(class ComplianceTable extends Component { _isMount = false; timefilter: { getTime(): any; @@ -331,4 +331,4 @@ export class ComplianceTable extends Component { </div> ); } -} +}) diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index b5c4a60127..c92715e2b8 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -10,22 +10,18 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import store from '../../../../redux/store'; import { connect } from 'react-redux'; import { showExploreAgentModal, updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexItem, - EuiIcon, EuiOverlayMask, EuiOutsideClickDetector, EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiToolTip, + EuiPopover, } from '@elastic/eui'; +import { WzButton } from '../../../../components/common/buttons'; import './agents-selector.scss'; import { AgentSelectionTable } from './agents-selection-table'; import { WAZUH_ALERTS_PATTERN } from '../../../../../common/constants'; @@ -53,21 +49,20 @@ class OverviewActions extends Component { } componentDidMount() { - const agentId = store.getState().appStateReducers.currentAgentData.id; + const { filterManager } = getDataPlugin().query; this.setState({ filterManager: filterManager }, () => { if (this.props.initialFilter) this.agentTableSearch([this.props.initialFilter]) - if (agentId) this.agentTableSearch([agentId]) + if (this.props.agent.id) this.agentTableSearch([this.props.agent.id]) }); } componentDidUpdate(){ - const agent = store.getState().appStateReducers.currentAgentData; - if(this.state.isAgent && !agent.id){ + if(this.state.isAgent && !this.props.agent.id){ this.setState({isAgent: false}) - }else if(agent.id && this.state.isAgent !== agent.id){ - this.setState({isAgent: agent.id}) + }else if(this.props.agent.id && this.state.isAgent !== this.props.agent.id){ + this.setState({isAgent: this.props.agent.id}) } } @@ -86,7 +81,7 @@ class OverviewActions extends Component { closeAgentModal() { this.setState({ isAgentModalVisible: false }); - store.dispatch(showExploreAgentModal(false)); + this.props.showExploreAgentModal(false); } showAgentModal() { @@ -169,43 +164,56 @@ class OverviewActions extends Component { </EuiOverlayMask> ); } - const agent = store.getState().appStateReducers.currentAgentData; + + const thereAgentSelected = (this.props.agent || {}).id + + const avaliableForAgent = this.props.module.availableFor && this.props.module.availableFor.includes('agent'); + + let buttonUnpinAgent, buttonExploreAgent; + if(thereAgentSelected){ + buttonUnpinAgent = ( + <WzButton + buttonType='icon' + className="wz-unpin-agent" + iconType='pinFilled' + onClick={() => { + this.props.updateCurrentAgentData({}); + this.removeAgentsFilter(); + }} + tooltip={{position: 'bottom', content: 'Unpin agent'}} + aria-label='Unpin agent' + /> + ); + }; + + buttonExploreAgent = ( + <WzButton + buttonType='empty' + isLoading={this.state.loadingReport} + color='primary' + isDisabled={!avaliableForAgent} + tooltip={{position: 'bottom', content: !avaliableForAgent ? 'This module is not supported for agents.' : (thereAgentSelected ? 'Change agent selected' : 'Select an agent to explore its modules') }} + style={thereAgentSelected ? {background: 'rgba(0, 107, 180, 0.1)'} : undefined} + iconType='watchesApp' + onClick={() => this.showAgentModal()}> + {thereAgentSelected ? `${this.props.agent.name} (${this.props.agent.id})` : 'Explore agent'} + </WzButton> + ) + return ( - <div> - <EuiFlexItem> - {!this.state.isAgent && ( - <EuiToolTip position='bottom' content='Select an agent to explore its modules' > - <EuiButtonEmpty - isLoading={this.state.loadingReport} - color='primary' - onClick={() => this.showAgentModal()}> - <EuiIcon type="watchesApp" color="primary" style={{ marginBottom: 3 }} />  Explore agent - </EuiButtonEmpty> - </EuiToolTip> - )} - {this.state.isAgent && ( - <div style={{ display: "inline-flex" }}> - <EuiToolTip position='bottom' content='Change agent selected' > - <EuiButtonEmpty - style={{background: 'rgba(0, 107, 180, 0.1)'}} - isLoading={this.state.loadingReport} - onClick={() => this.showAgentModal()}> - {agent.name} ({agent.id}) - </EuiButtonEmpty> - </EuiToolTip> - <EuiToolTip position='bottom' content='Unpin agent'> - <EuiButtonIcon - className="wz-unpin-agent" - iconType='pinFilled' - onClick={() => { - store.dispatch(updateCurrentAgentData({})); - this.removeAgentsFilter(); - }} - aria-label='Unpin agent' /> - </EuiToolTip> - </div> - )} - </EuiFlexItem> + <div style={{ display: "inline-flex" }}> + {buttonExploreAgent} + {thereAgentSelected && ( + !avaliableForAgent && ( + <EuiPopover + button={buttonUnpinAgent} + isOpen={thereAgentSelected} + closePopover={()=> {}}> + This module is not supported for agents. Remove the pinned agent. + </EuiPopover> + + ) || buttonUnpinAgent + )} {modal} </div> ); @@ -215,7 +223,13 @@ class OverviewActions extends Component { const mapStateToProps = state => { return { state: state.appStateReducers, + agent: state.appStateReducers.currentAgentData }; }; -export default connect(mapStateToProps, null)(OverviewActions); +const mapDispatchToProps = dispatch => ({ + updateCurrentAgentData: (agent) => dispatch(updateCurrentAgentData(agent)), + showExploreAgentModal: (data) => dispatch(showExploreAgentModal(data)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(OverviewActions); diff --git a/public/react-services/reporting.js b/public/react-services/reporting.js index 2508a7a7db..3c227159d5 100644 --- a/public/react-services/reporting.js +++ b/public/react-services/reporting.js @@ -79,17 +79,21 @@ export class ReportingService { idArray = rawVisualizations.map((item) => item.id); } + const visualizationIDList = []; for (const item of idArray) { const tmpHTMLElement = $(`#${item}`); - this.vis2png.assignHTMLItem(item, tmpHTMLElement); + if(tmpHTMLElement[0]){ + this.vis2png.assignHTMLItem(item, tmpHTMLElement); + visualizationIDList.push(item); + } } const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); - const array = await this.vis2png.checkArray(idArray); - const name = `wazuh-${agents ? `agent-${agents}` : 'overview'}-${tab}-${ - (Date.now() / 1000) | 0 - }.pdf`; + const array = await this.vis2png.checkArray(visualizationIDList); + const name = `wazuh-${ + agents ? `agent-${agents}` : 'overview' + }-${tab}-${(Date.now() / 1000) | 0}.pdf`; const browserTimezone = moment.tz.guess(true); From 996e1a9fc83b6fee8f920094949c95aeb5196a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Mon, 9 Aug 2021 12:35:28 +0200 Subject: [PATCH 236/493] Merge branch '4.3-7.10' into feature/Office365 --- CHANGELOG.md | 1 + public/components/agents/fim/main.tsx | 3 +- public/components/agents/prompts/index.ts | 11 +- .../prompts/prompt_module_not_for_agent.tsx | 52 +++++++ public/components/agents/sca/main.tsx | 3 +- public/components/agents/vuls/main.tsx | 3 +- public/components/common/hocs/index.ts | 1 + .../common/hocs/withAgentSupportModule.tsx | 14 +- .../common/hocs/with_module_not_for_agent.tsx | 29 ++++ .../common/hocs/with_module_tab_loader.tsx | 42 ++++++ public/components/common/hooks/index.ts | 1 - .../components/common/hooks/useRootScope.ts | 8 +- .../modules/buttons/generate_report.tsx | 63 ++++++++ .../components/common/modules/dashboard.tsx | 13 +- public/components/common/modules/events.tsx | 9 +- public/components/common/modules/index.ts | 8 +- public/components/common/modules/loader.tsx | 53 ------- .../components/common/modules/main-agent.tsx | 82 ++-------- .../components/common/modules/main-mitre.tsx | 3 +- .../common/modules/main-overview.tsx | 99 ++++--------- public/components/common/modules/main.tsx | 140 +----------------- .../common/modules/modules-defaults.js | 113 ++++++++++---- public/components/common/modules/settings.tsx | 70 --------- .../compliance-table/compliance-table.tsx | 6 +- .../overview-actions/overview-actions.js | 114 +++++++------- public/react-services/reporting.js | 14 +- 26 files changed, 439 insertions(+), 516 deletions(-) create mode 100644 public/components/agents/prompts/prompt_module_not_for_agent.tsx create mode 100644 public/components/common/hocs/with_module_not_for_agent.tsx create mode 100644 public/components/common/hocs/with_module_tab_loader.tsx create mode 100644 public/components/common/modules/buttons/generate_report.tsx delete mode 100644 public/components/common/modules/loader.tsx delete mode 100644 public/components/common/modules/settings.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c659471e..1a836c4e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Adapted the Mitre tactics and techniques resources to use the API endpoints [#3346](https://github.com/wazuh/wazuh-kibana-app/pull/3346) - Moved the filterManager subscription to the hook useFilterManager [#3517](https://github.com/wazuh/wazuh-kibana-app/pull/3517) - Change filter from is to is one of in custom searchbar [#3529](https://github.com/wazuh/wazuh-kibana-app/pull/3529) +- Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) ### Fixed diff --git a/public/components/agents/fim/main.tsx b/public/components/agents/fim/main.tsx index 99c593a792..e5d5f339a3 100644 --- a/public/components/agents/fim/main.tsx +++ b/public/components/agents/fim/main.tsx @@ -4,13 +4,14 @@ import '../../common/modules/module.scss'; import { connect } from 'react-redux'; import { PromptNoActiveAgent, PromptNoSelectedAgent } from '../prompts'; import { compose } from 'redux'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withAgentSupportModule, withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainFim = compose( + withAgentSupportModule, connect(mapStateToProps), withGuard( (props) => !(props.currentAgentData && props.currentAgentData.id && props.agent), diff --git a/public/components/agents/prompts/index.ts b/public/components/agents/prompts/index.ts index 1e567a6281..5519dfa4e7 100644 --- a/public/components/agents/prompts/index.ts +++ b/public/components/agents/prompts/index.ts @@ -9,8 +9,9 @@ * * Find more information about this on the LICENSE file. */ -export { PromptAgentNoSupportModule } from './prompt-agent-no-support-module'; -export { PromptNoActiveAgent, PromptNoActiveAgentWithoutSelect } from './prompt-no-active-agent'; -export { PromptNoSelectedAgent } from './prompt-no-selected-agent'; -export { PromptSelectAgent } from './prompt-select-agent'; -export { PromptAgentFeatureVersion } from './prompt-agent-feature-version'; +export * from './prompt-agent-no-support-module'; +export * from './prompt-no-active-agent'; +export * from './prompt-no-selected-agent'; +export * from './prompt-select-agent'; +export * from './prompt-agent-feature-version'; +export * from './prompt_module_not_for_agent'; diff --git a/public/components/agents/prompts/prompt_module_not_for_agent.tsx b/public/components/agents/prompts/prompt_module_not_for_agent.tsx new file mode 100644 index 0000000000..500b230c3d --- /dev/null +++ b/public/components/agents/prompts/prompt_module_not_for_agent.tsx @@ -0,0 +1,52 @@ +/* + * Wazuh app - Prompt when an agent doesn't support some module + * Copyright (C) 2015-2021 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 from 'react'; +import { useDispatch } from 'react-redux'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { updateCurrentAgentData } from '../../../redux/actions/appStateActions'; +import { useFilterManager } from '../../common/hooks'; + +type PromptSelectAgentProps = { + body?: string; + title: string; + agentSelectionProps: { + setAgent: (agent: boolean) => void + } +}; + +export const PromptModuleNotForAgent = ({ body, title, ...agentSelectionProps }: PromptSelectAgentProps) => { + const dispatch = useDispatch(); + const filterManager = useFilterManager(); + + const unpinAgent = async () => { + dispatch(updateCurrentAgentData({})); + await agentSelectionProps.setAgent(false); + const filters = filterManager.filters.filter(x => { + return x.meta.key !== 'agent.id'; + }); + filterManager.setFilters(filters); + }; + + return ( + <EuiEmptyPrompt + iconType="watchesApp" + title={<h2>{title}</h2>} + body={body && <p>{body}</p>} + actions={ + <EuiButton color="primary" fill onClick={unpinAgent}> + Unpin agent + </EuiButton> + } + /> + ); +}; \ No newline at end of file diff --git a/public/components/agents/sca/main.tsx b/public/components/agents/sca/main.tsx index 8daf51de54..1307bfa734 100644 --- a/public/components/agents/sca/main.tsx +++ b/public/components/agents/sca/main.tsx @@ -15,13 +15,14 @@ import { Inventory } from './index'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { PromptSelectAgent, PromptNoSelectedAgent } from '../prompts'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withGuard, withUserAuthorizationPrompt, withAgentSupportModule } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainSca = compose( + withAgentSupportModule, withUserAuthorizationPrompt([ [ {action: 'agent:read', resource: 'agent:id:*'}, diff --git a/public/components/agents/vuls/main.tsx b/public/components/agents/vuls/main.tsx index d4aa148163..07affef2de 100644 --- a/public/components/agents/vuls/main.tsx +++ b/public/components/agents/vuls/main.tsx @@ -4,13 +4,14 @@ import '../../common/modules/module.scss'; import { connect } from 'react-redux'; import { PromptNoSelectedAgent, PromptNoActiveAgent } from '../prompts'; import { compose } from 'redux'; -import { withGuard, withUserAuthorizationPrompt } from '../../common/hocs'; +import { withGuard, withUserAuthorizationPrompt, withAgentSupportModule } from '../../common/hocs'; const mapStateToProps = (state) => ({ currentAgentData: state.appStateReducers.currentAgentData, }); export const MainVuls = compose( + withAgentSupportModule, connect(mapStateToProps), withGuard( (props) => !((props.currentAgentData && props.currentAgentData.id) && props.agent), diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index a08b1f9c64..4c84b317e5 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -21,3 +21,4 @@ export * from './withButtonOpenOnClick'; export * from './withAgentSupportModule'; export * from './withUserLogged'; export * from './error-boundary/with-error-boundary'; +export * from './with_module_tab_loader'; diff --git a/public/components/common/hocs/withAgentSupportModule.tsx b/public/components/common/hocs/withAgentSupportModule.tsx index a613f39539..f5c738d63b 100644 --- a/public/components/common/hocs/withAgentSupportModule.tsx +++ b/public/components/common/hocs/withAgentSupportModule.tsx @@ -12,9 +12,17 @@ import { PromptAgentNoSupportModule } from '../../agents/prompts'; import { withGuard } from '../../common/hocs'; import { hasAgentSupportModule } from '../../../react-services/wz-agents'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; -export const withAgentSupportModule = WrappedComponent => +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); + +export const withAgentSupportModule = WrappedComponent => compose( + connect(mapStateToProps), withGuard( - ({agent, component}) => Object.keys(agent).length && !hasAgentSupportModule(agent, component), + ({agent, moduleID}) => Object.keys(agent).length && !hasAgentSupportModule(agent, moduleID), PromptAgentNoSupportModule - )(WrappedComponent) + ) +)(WrappedComponent) diff --git a/public/components/common/hocs/with_module_not_for_agent.tsx b/public/components/common/hocs/with_module_not_for_agent.tsx new file mode 100644 index 0000000000..ca362f85a1 --- /dev/null +++ b/public/components/common/hocs/with_module_not_for_agent.tsx @@ -0,0 +1,29 @@ +/* + * Wazuh app - React HOC to show a prompt when a module is not available for agents and let to unpin the agent + * Copyright (C) 2015-2021 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 from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { withGuard } from './withGuard'; +import { PromptModuleNotForAgent } from '../../agents/prompts'; + +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); + +export const withModuleNotForAgent = WrappedComponent => compose( + connect(mapStateToProps), + withGuard( + ({agent}) => agent?.id, + (props) => <PromptModuleNotForAgent title='Module not avaliable for agents' body='Remove the pinned agent.' {...props}/> + ) +)(WrappedComponent); diff --git a/public/components/common/hocs/with_module_tab_loader.tsx b/public/components/common/hocs/with_module_tab_loader.tsx new file mode 100644 index 0000000000..637dda21da --- /dev/null +++ b/public/components/common/hocs/with_module_tab_loader.tsx @@ -0,0 +1,42 @@ +/* + * Wazuh app - React HOC to show a loader used for Dashboard adn Events module tabs + * Copyright (C) 2015-2021 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, { useEffect, useState } from 'react'; +import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import { useRootScope } from '../hooks'; + +export const withModuleTabLoader = WrappedComponent => props => { + const $rootScope = useRootScope(); + const [showTab, setShowTab] = useState(); + useEffect(() => { + if($rootScope){ + $rootScope.loadingDashboard = true; + $rootScope.$applyAsync(); + setTimeout(() => { + setShowTab(true); + }, 100); + return () => { + $rootScope.loadingDashboard = false; + $rootScope.$applyAsync(); + } + } + },[$rootScope]); + + return showTab ? <WrappedComponent {...props}/> : ( + <> + <EuiSpacer size='xl' /> + <div style={{ margin: '-8px auto', width: 32 }}> + <EuiLoadingSpinner size="xl" /> + </div> + </> + ) +} \ No newline at end of file diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 26dad891a9..64591f2071 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,6 +24,5 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; -export * from './use_previous'; export { useEsSearch } from './use-es-search'; export { useValueSuggestions, IValueSuggestiions } from './use-value-suggestions'; diff --git a/public/components/common/hooks/useRootScope.ts b/public/components/common/hooks/useRootScope.ts index de6d0b9628..1f36e253ea 100644 --- a/public/components/common/hooks/useRootScope.ts +++ b/public/components/common/hooks/useRootScope.ts @@ -9,14 +9,14 @@ * * Find more information about this on the LICENSE file. */ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { getAngularModule } from '../../../kibana-services'; export function useRootScope(){ - const refRootScope = useRef(); + const [refRootScope,setRefRootScope] = useState(); useEffect(() => { const app = getAngularModule(); - refRootScope.current = app.$injector.get('$rootScope'); + setRefRootScope(app.$injector.get('$rootScope')); },[]); - return refRootScope.current; + return refRootScope; }; diff --git a/public/components/common/modules/buttons/generate_report.tsx b/public/components/common/modules/buttons/generate_report.tsx new file mode 100644 index 0000000000..f56622bab6 --- /dev/null +++ b/public/components/common/modules/buttons/generate_report.tsx @@ -0,0 +1,63 @@ +/* + * Wazuh app - Component for the module generate reports + * Copyright (C) 2015-2021 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 from 'react'; +import { useAsyncAction } from '../../hooks'; +import { getUiSettings } from '../../../../kibana-services'; +import { ReportingService } from '../../../../react-services'; +import $ from 'jquery'; +import { WzButton } from '../../../common/buttons'; + + +export const ButtonModuleGenerateReport = ({agent, moduleID, disabledReport}) => { + const action = useAsyncAction(async () => { + const reportingService = new ReportingService(); + const isDarkModeTheme = getUiSettings().get('theme:darkMode'); + if (isDarkModeTheme) { + + //Patch to fix white text in dark-mode pdf reports + const defaultTextColor = '#DFE5EF'; + + //Patch to fix dark backgrounds in visualizations dark-mode pdf reports + const $labels = $('.euiButtonEmpty__text, .echLegendItem'); + const $vizBackground = $('.echChartBackground'); + const defaultVizBackground = $vizBackground.css('background-color'); + + try { + $labels.css('color', 'black'); + $vizBackground.css('background-color', 'transparent'); + await reportingService.startVis2Png(moduleID, agent?.id || false) + $vizBackground.css('background-color', defaultVizBackground); + $labels.css('color', defaultTextColor); + } catch (e) { + $labels.css('color', defaultTextColor); + $vizBackground.css('background-color', defaultVizBackground); + } + } else { + await reportingService.startVis2Png(moduleID, agent?.id || false) + } + }); + + return ( + <WzButton + buttonType='empty' + iconType='document' + isLoading={action.running} + onClick={action.run} + isDisabled={disabledReport} + tooltip={disabledReport ? {position: 'top', content: 'No results match for this search criteria.'} : undefined} + > + Generate report + </WzButton> + ) +} + diff --git a/public/components/common/modules/dashboard.tsx b/public/components/common/modules/dashboard.tsx index 4827c26e28..4f07a74b7e 100644 --- a/public/components/common/modules/dashboard.tsx +++ b/public/components/common/modules/dashboard.tsx @@ -13,8 +13,14 @@ import { Component } from 'react'; import { ModulesHelper } from './modules-helper' import { getAngularModule } from '../../../kibana-services'; +import { withAgentSupportModule, withModuleTabLoader } from '../hocs'; +import { compose } from 'redux'; +import React from 'react'; -export class Dashboard extends Component { +export const Dashboard = compose( + withAgentSupportModule, + withModuleTabLoader +)(class Dashboard extends Component { _isMount = false; constructor(props) { super(props); @@ -43,6 +49,7 @@ export class Dashboard extends Component { } render() { - return false; + return null; } -} +}) + diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 55eef4103a..5e887aa082 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -19,11 +19,16 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiOverlayMask, EuiOutsideClickDe import { PatternHandler } from '../../../react-services/pattern-handler'; import { enhanceDiscoverEventsCell } from './events-enhance-discover-fields'; import { toMountPoint } from '../../../../../../src/plugins/kibana_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 class Events extends Component { +export const Events = compose( + withAgentSupportModule, + withModuleTabLoader +)(class Events extends Component { intervalCheckExistsDiscoverTableTime: number = 200; isMount: boolean; state: { @@ -292,4 +297,4 @@ export class Events extends Component { </Fragment> ) } -} +}) diff --git a/public/components/common/modules/index.ts b/public/components/common/modules/index.ts index 7b19779c33..bbf2232cd7 100644 --- a/public/components/common/modules/index.ts +++ b/public/components/common/modules/index.ts @@ -10,8 +10,6 @@ * Find more information about this on the LICENSE file. */ -export { Events } from './events'; -export { Dashboard } from './dashboard'; -export { Loader } from './loader'; -export { Settings } from './settings'; -export { ModulesHelper } from './modules-helper.js'; +export * from './dashboard'; +export * from './events'; +export * from './modules-helper.js'; diff --git a/public/components/common/modules/loader.tsx b/public/components/common/modules/loader.tsx deleted file mode 100644 index 98a069ec85..0000000000 --- a/public/components/common/modules/loader.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2021 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 } from '../../../kibana-services'; -import { EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; - -const app = getAngularModule(); - -export class Loader extends Component { - constructor(props) { - super(props); - } - - componentDidMount() { - this.$rootScope = app.$injector.get('$rootScope'); - this.$rootScope.loadingDashboard = true; - this.$rootScope.$applyAsync(); - } - - componentWillUnmount() { - this.$rootScope.loadingDashboard = false; - this.$rootScope.$applyAsync(); - } - - redirect() { - setTimeout(() => { - this.props.loadSection(this.props.redirect); - }, 100); - } - - render() { - const redirect = this.redirect(); - return ( - <Fragment> - <EuiSpacer size='xl' /> - <div style={{ margin: '-8px auto', width: 32 }}> - <EuiLoadingSpinner size="xl" /> - </div> - {redirect} - </Fragment> - ); - } -} diff --git a/public/components/common/modules/main-agent.tsx b/public/components/common/modules/main-agent.tsx index 8b5eb683ba..5a02871986 100644 --- a/public/components/common/modules/main-agent.tsx +++ b/public/components/common/modules/main-agent.tsx @@ -31,18 +31,8 @@ import { FilterHandler } from '../../../utils/filter-handler'; import { AppState } from '../../../react-services/app-state'; import { ReportingService } from '../../../react-services/reporting'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { Events, Dashboard, Loader, Settings } from '../../common/modules'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; import { AgentInfo } from '../../common/welcome/agents-info'; -import Overview from '../../wz-menu/wz-menu-overview'; -import { MainFim } from '../../agents/fim'; -import { MainSca } from '../../agents/sca'; -import { MainMitre } from '../modules/main-mitre'; import { getAngularModule } from '../../../kibana-services'; -import { withAgentSupportModule } from '../../../components/common/hocs'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; -import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; export class MainModuleAgent extends Component { props!: { @@ -248,22 +238,11 @@ export class MainModuleAgent extends Component { ); } - renderSettingsButton() { - return ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - fill={this.state.selectView === 'settings' || undefined} - iconType="wrench" - onClick={() => this.onSelectedTabChanged('settings')}> - Configuration - </EuiButtonEmpty> - </EuiFlexItem> - ); - } render() { const { agent, section, selectView } = this.props; const title = this.renderTitle(); + const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> <div className='wz-module-header-agent-wrapper'> @@ -287,29 +266,26 @@ export class MainModuleAgent extends Component { <div className="wz-welcome-page-agent-tabs"> <EuiFlexGroup> {this.props.renderTabs()} - {(selectView === 'dashboard') && - this.props.renderReportButton() - } - {(this.props.buttons || []).includes('dashboard') && - this.props.renderDashboardButton() - } - {(this.props.buttons || []).includes('settings') && - this.renderSettingsButton() - } + <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> + <EuiFlexGroup> + {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => + typeof ModuleViewButton !== 'string' ? <EuiFlexItem key={`module_button_${index}`}><ModuleViewButton {...{ ...this.props, ...this.props.agentsSelectionProps }} moduleID={section}/></EuiFlexItem> : null)} + </EuiFlexGroup> + </EuiFlexItem> </EuiFlexGroup> </div> } </div> </div> - {!['syscollector', 'configuration'].includes(this.props.section) && - <ModuleTabViewer component={section} {...this.props}/> + {!['syscollector', 'configuration'].includes(section) && + ModuleTabView && ModuleTabView.component && <ModuleTabView.component {...this.props} moduleID={section}/> } </Fragment> } {(!agent || !agent.os) && <EuiCallOut style={{ margin: '66px 16px 0 16px' }} - title=" This agent has never connected" + title="This agent has never connected" color="warning" iconType="alert"> </EuiCallOut> @@ -318,41 +294,3 @@ export class MainModuleAgent extends Component { ); } } - - - -const mapStateToProps = state => ({ - agent: state.appStateReducers.currentAgentData -}); - -const ModuleTabViewer = compose( - connect(mapStateToProps), - withAgentSupportModule -)((props) => { - const { section, selectView } = props; - return <> - {selectView === 'events' && - <Events {...props} /> - } - {selectView === 'loader' && - <Loader {...props} - loadSection={(section) => props.loadSection(section)} - redirect={props.afterLoad}> - </Loader>} - {selectView === 'dashboard' && - <Dashboard {...props} /> - } - {selectView === 'settings' && - <Settings {...props} /> - } - - - {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} - {section === 'fim' && selectView==='inventory' && <MainFim {...props} />} - {section === 'sca' && selectView==='inventory' && <MainSca {...props} />} - {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} - {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} - {/* {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && <ComplianceTable {...props} goToDiscover={(id) => props.onSelectedTabChanged(id)} />} */} - {/* -------------------------------------------------------------------------- */} - </> -}) \ No newline at end of file diff --git a/public/components/common/modules/main-mitre.tsx b/public/components/common/modules/main-mitre.tsx index 2cbaa7e2f5..1d8584658d 100644 --- a/public/components/common/modules/main-mitre.tsx +++ b/public/components/common/modules/main-mitre.tsx @@ -12,10 +12,11 @@ import React, { Component } from 'react'; import { Mitre } from '../../../components/overview/mitre/mitre'; -import { withUserAuthorizationPrompt } from '../hocs'; +import { withUserAuthorizationPrompt, withAgentSupportModule } from '../hocs'; import { compose } from 'redux'; export const MainMitre = compose( + withAgentSupportModule, withUserAuthorizationPrompt([ { action: 'mitre:read', resource: '*:*:*' }, ]) diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index f8651ab108..a50955fb5b 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -26,28 +26,20 @@ import { } from '@elastic/eui'; import '../../common/modules/module.scss'; import { updateGlobalBreadcrumb } from '../../../redux/actions/globalBreadcrumbActions'; + import store from '../../../redux/store'; import { ReportingService } from '../../../react-services/reporting'; import { AppNavigate } from '../../../react-services/app-navigate'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { Events, Dashboard, Loader, Settings } from '../../common/modules'; -import OverviewActions from '../../../controllers/overview/components/overview-actions/overview-actions'; -import { MainFim } from '../../agents/fim'; - -import { MainVuls } from '../../agents/vuls'; -import { MainSca } from '../../agents/sca'; -import { MainMitre } from './main-mitre'; -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import { ComplianceTable } from '../../overview/compliance-table'; -import { withAgentSupportModule } from '../../../components/common/hocs'; import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { getDataPlugin } from '../../../kibana-services'; -import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; -import { OfficePanel } from '../../overview/office-panel'; +const mapStateToProps = (state) => ({ + agent: state.appStateReducers.currentAgentData, +}); -export class MainModuleOverview extends Component { +export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverview extends Component { constructor(props) { super(props); this.reportingService = new ReportingService(); @@ -133,71 +125,32 @@ export class MainModuleOverview extends Component { } this.setGlobalBreadcrumb(); + const { filterManager } = getDataPlugin().query; + this.filterManager = filterManager; } render() { const { section, selectView } = this.props; + const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> - <Fragment> - <div className={this.props.tabs && this.props.tabs.length && 'wz-module-header-nav'}> - {this.props.tabs && this.props.tabs.length && ( - <div className="wz-welcome-page-agent-tabs"> - <EuiFlexGroup> - {this.props.renderTabs()} - <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> - <WzReduxProvider> - <OverviewActions {...{ ...this.props, ...this.props.agentsSelectionProps }} /> - </WzReduxProvider> - </EuiFlexItem> - {selectView === 'dashboard' && this.props.renderReportButton()} - {(this.props.buttons || []).includes('dashboard') && - this.props.renderDashboardButton()} - </EuiFlexGroup> - </div> - )} - </div> - <ModuleTabViewer component={section} {...this.props} /> - </Fragment> + <div className={this.props.tabs && this.props.tabs.length && 'wz-module-header-nav'}> + {this.props.tabs && this.props.tabs.length && ( + <div className="wz-welcome-page-agent-tabs"> + <EuiFlexGroup> + {this.props.renderTabs()} + <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> + <EuiFlexGroup> + {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => + typeof ModuleViewButton !== 'string' ? <EuiFlexItem key={`module_button_${index}`}><ModuleViewButton {...{ ...this.props, ...this.props.agentsSelectionProps }} moduleID={section} /></EuiFlexItem> : null)} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </div> + )} + </div> + {ModuleTabView && ModuleTabView.component && <ModuleTabView.component {...this.props} moduleID={section}/> } </div> ); } -} - -const mapStateToProps = (state) => ({ - agent: state.appStateReducers.currentAgentData, -}); - -const ModuleTabViewer = compose( - connect(mapStateToProps), - withAgentSupportModule -)((props) => { - const { section, selectView } = props; - return ( - <> - {selectView === 'events' && <Events {...props} />} - {selectView === 'loader' && ( - <Loader - {...props} - loadSection={(section) => props.loadSection(section)} - redirect={props.afterLoad} - ></Loader> - )} - {selectView === 'dashboard' && <Dashboard {...props} />} - {selectView === 'settings' && <Settings {...props} />} - - {/* ---------------------MODULES WITH CUSTOM PANELS--------------------------- */} - {section === 'fim' && selectView === 'inventory' && <MainFim {...props} />} - {section === 'sca' && selectView === 'inventory' && <MainSca {...props} />} - {section === 'vuls' && selectView === 'inventory' && <MainVuls {...props} />} - {section === 'office' && selectView === 'inventory' && <OfficePanel {...props} />} - - {section === 'mitre' && selectView === 'inventory' && <MainMitre {...props} />} - {section === 'mitre' && selectView === 'intelligence' && <ModuleMitreAttackIntelligence {...props} />} - {['pci', 'gdpr', 'hipaa', 'nist', 'tsc'].includes(section) && selectView === 'inventory' && ( - <ComplianceTable {...props} goToDiscover={(id) => props.onSelectedTabChanged(id)} /> - )} - {/* -------------------------------------------------------------------------- */} - </> - ); -}); \ No newline at end of file +}) diff --git a/public/components/common/modules/main.tsx b/public/components/common/modules/main.tsx index 9f0e853d2d..1793af4d17 100644 --- a/public/components/common/modules/main.tsx +++ b/public/components/common/modules/main.tsx @@ -21,12 +21,10 @@ import { } from '@elastic/eui'; import '../../common/modules/module.scss'; import { ReportingService } from '../../../react-services/reporting'; -import { AppNavigate } from '../../../react-services/app-navigate'; import { ModulesDefaults } from './modules-defaults'; -import { getAngularModule, getDataPlugin, getUiSettings } from '../../../kibana-services'; +import { getAngularModule, getDataPlugin } from '../../../kibana-services'; import { MainModuleAgent } from './main-agent' import { MainModuleOverview } from './main-overview'; -import store from '../../../redux/store'; import { compose } from 'redux'; import { withReduxProvider,withErrorBoundary } from '../hocs'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; @@ -49,18 +47,12 @@ export const MainModule = compose( }; const app = getAngularModule(); this.$rootScope = app.$injector.get('$rootScope'); - } - - async componentDidMount() { if (!(ModulesDefaults[this.props.section] || {}).notModule) { this.tabs = (ModulesDefaults[this.props.section] || {}).tabs || [ { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }, ]; - this.buttons = (ModulesDefaults[this.props.section] || {}).buttons || [ - 'reporting', - 'settings', - ]; + this.module = ModulesDefaults[this.props.section]; } } @@ -71,17 +63,6 @@ export const MainModule = compose( } } - canBeInit(tab) { - //checks if the init table can be set - let canInit = false; - this.tabs.forEach((element) => { - if (element.id === tab && (!element.onlyAgent || (element.onlyAgent && this.props.agent))) { - canInit = true; - } - }); - return canInit; - } - renderTabs(agent = false) { const { selectView } = this.state; if (!agent) { @@ -107,114 +88,6 @@ export const MainModule = compose( ); } - startVis2PngByAgent = async () => { - const agent = - (this.props.agent || store.getState().appStateReducers.currentAgentData || {}).id || false; - await this.reportingService.startVis2Png(this.props.section, agent); - }; - - async startReport() { - try { - this.setState({ loadingReport: true }); - const isDarkModeTheme = getUiSettings().get('theme:darkMode'); - if (isDarkModeTheme) { - - //Patch to fix white text in dark-mode pdf reports - const defaultTextColor = '#DFE5EF'; - - //Patch to fix dark backgrounds in visualizations dark-mode pdf reports - const $labels = $('.euiButtonEmpty__text, .echLegendItem'); - const $vizBackground = $('.echChartBackground'); - const defaultVizBackground = $vizBackground.css('background-color'); - - try { - $labels.css('color', 'black'); - $vizBackground.css('background-color', 'transparent'); - await this.startVis2PngByAgent(); - $vizBackground.css('background-color', defaultVizBackground); - $labels.css('color', defaultTextColor); - } catch (error) { - $labels.css('color', defaultTextColor); - $vizBackground.css('background-color', defaultVizBackground); - const options = { - context: `${MainModule.name}.startReport`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: 'Error generating the report', - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ loadingReport: false }); - } - } else { - await this.startVis2PngByAgent(); - } - } finally { - this.setState({ loadingReport: false }); - } - } - - renderReportButton() { - return ( - (this.props.disabledReport && ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiToolTip position="top" content="No results match for this search criteria."> - <EuiButtonEmpty - iconType="document" - isLoading={this.state.loadingReport} - isDisabled={true} - onClick={async () => this.startReport()} - > - Generate report - </EuiButtonEmpty> - </EuiToolTip> - </EuiFlexItem> - )) || ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - iconType="document" - isLoading={this.state.loadingReport} - onClick={async () => this.startReport()} - > - Generate report - </EuiButtonEmpty> - </EuiFlexItem> - ) - ); - } - - renderDashboardButton() { - const href = `#/overview?tab=${this.props.section}&agentId=${this.props.agent.id}`; - return ( - <EuiFlexItem grow={false} style={{ marginLeft: 0, marginTop: 6, marginBottom: 18 }}> - <EuiButton - fill={this.state.selectView === 'dashboard'} - iconType="visLine" - onClick={() => this.onSelectedTabChanged('dashboard')} - > - Dashboard - </EuiButton> - </EuiFlexItem> - ); - } - - renderSettingsButton() { - return ( - <EuiFlexItem grow={false} style={{ marginRight: 4, marginTop: 6 }}> - <EuiButtonEmpty - fill={this.state.selectView === 'settings'} - iconType="wrench" - onClick={() => this.onSelectedTabChanged('settings')} - > - Configuration - </EuiButtonEmpty> - </EuiFlexItem> - ); - } - loadSection(id) { this.setState({ selectView: id }); } @@ -231,8 +104,7 @@ export const MainModule = compose( new RegExp('tabView=' + '[^&]*'), `tabView=${id === 'events' ? 'discover' : id === 'inventory' ? 'inventory' : 'panels'}` ); - this.afterLoad = id; - this.loadSection('loader'); + this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); } else { this.loadSection(id === 'panels' ? 'dashboard' : id === 'discover' ? 'events' : id); } @@ -244,13 +116,9 @@ export const MainModule = compose( const { selectView } = this.state; const mainProps = { selectView, - afterLoad: this.afterLoad, - buttons: this.buttons, tabs: this.tabs, + module: this.module, renderTabs: () => this.renderTabs(), - renderReportButton: () => this.renderReportButton(), - renderDashboardButton: () => this.renderDashboardButton(), - renderSettingsButton: () => this.renderSettingsButton(), loadSection: (id) => this.loadSection(id), onSelectedTabChanged: (id) => this.onSelectedTabChanged(id), }; diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 9b83235b71..1b7de16433 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -9,71 +9,130 @@ * * Find more information about this on the LICENSE file. */ +import { Dashboard } from './dashboard'; +import { Events } from './events'; +import { MainFim } from '../../agents/fim'; +import { MainSca } from '../../agents/sca'; +import { MainVuls } from '../../agents/vuls'; +import { MainMitre } from './main-mitre'; +import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intelligence'; +import { ComplianceTable } from '../../overview/compliance-table'; +import ButtonModuleExploreAgent from '../../../controllers/overview/components/overview-actions/overview-actions'; +import { ButtonModuleGenerateReport } from '../modules/buttons'; +import { OfficePanel } from '../../overview/office-panel' + +const DashboardTab = { id: 'dashboard', name: 'Dashboard', buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], component: Dashboard}; +const EventsTab = { id: 'events', name: 'Events', buttons: [ButtonModuleExploreAgent], component: Events }; +const RegulatoryComplianceTabs = [{ id: 'inventory', name: 'Controls', buttons: [ButtonModuleExploreAgent], component: ComplianceTable }, DashboardTab, EventsTab]; + export const ModulesDefaults = { general: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, fim: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', onlyAgent: false }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting', 'settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainFim }, DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + aws: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'], }, gcp: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + pm: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + audit: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, sca: { init: 'inventory', - tabs: [{ id: 'inventory', name: 'Inventory' }, { id: 'events', name: 'Events' }], - buttons: ['settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainSca }, EventsTab], + buttons: ['settings'], + availableFor: ['manager','agent'] }, + office: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Panel' }, { id: 'dashboard', name: 'Dashboards' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [{ id: 'inventory', name: 'Panel', buttons: [ButtonModuleExploreAgent], component: OfficePanel }, DashboardTab, EventsTab], + availableFor: ['manager'] }, - mitre: { + ciscat: { init: 'dashboard', - tabs: [{id: 'intelligence', name: 'Intelligence'}, { id: 'inventory', name: 'Framework' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, vuls: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', onlyAgent: false }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting', 'settings'] + tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainVuls }, DashboardTab, EventsTab], + buttons: ['settings'], + availableFor: ['manager','agent'] + }, + mitre: { + init: 'dashboard', + tabs: [{ id: 'intelligence', name: 'Intelligence', component: ModuleMitreAttackIntelligence }, { id: 'inventory', name: 'Framework', buttons: [ButtonModuleExploreAgent], component: MainMitre }, DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, virustotal: { init: 'dashboard', - tabs: [{ id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + docker: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + pci: { + init: 'dashboard', + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] + }, + osquery: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] + }, + oscap: { + init: 'dashboard', + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, pci: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: [DashboardTab, EventsTab], + availableFor: ['manager','agent'] }, hipaa: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, nist: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, gdpr: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, tsc: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Controls' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] + tabs: RegulatoryComplianceTabs, + availableFor: ['manager','agent'] }, syscollector: { notModule: true diff --git a/public/components/common/modules/settings.tsx b/public/components/common/modules/settings.tsx deleted file mode 100644 index 26045f995c..0000000000 --- a/public/components/common/modules/settings.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Wazuh app - Integrity monitoring components - * Copyright (C) 2015-2021 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React, { Component } from 'react'; -import { EuiSpacer, EuiTitle, EuiPanel, EuiPage } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import WzBadge from '../../../controllers/management/components/management/configuration/util-components/badge' -import WzReduxProvider from '../../../redux/wz-redux-provider'; -import WzConfigurationIntegrityMonitoring from '../../../controllers/management/components/management/configuration/integrity-monitoring/integrity-monitoring'; -import WzConfigurationPolicyMonitoring from '../../../controllers/management/components/management/configuration/policy-monitoring/policy-monitoring'; -import WzConfigurationOpenSCAP from '../../../controllers/management/components/management/configuration/open-scap/open-scap'; -import WzConfigurationCisCat from '../../../controllers/management/components/management/configuration/cis-cat/cis-cat'; -import WzConfigurationVulnerabilities from '../../../controllers/management/components/management/configuration/vulnerabilities/vulnerabilities'; -import WzConfigurationOsquery from '../../../controllers/management/components/management/configuration/osquery/osquery'; -import WzConfigurationDockerListener from '../../../controllers/management/components/management/configuration/docker-listener/docker-listener'; - -type SettingsPropTypes = { - agent: { id: string }, - clusterNodeSelected?: string -} - -type SettingsState = { - badge: boolean | null -} -export class Settings extends Component<SettingsPropTypes, SettingsState> { - constructor(props) { - super(props); - this.state = { - badge: null - } - } - updateBadge(badge) { - this.setState({ badge }) - } - render() { - const { badge } = this.state; - const { section } = this.props; - return ( - <WzReduxProvider> - <EuiPage> - <EuiPanel> - <EuiTitle> - <span>{i18n.translate('wazuh.configuration', { defaultMessage: 'Configuration' })} {typeof badge === 'boolean' ? - <WzBadge enabled={badge} /> : null} - </span> - </EuiTitle> - <EuiSpacer size='m' /> - {section === 'fim' && <WzConfigurationIntegrityMonitoring {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {(section === 'pm' || section === 'sca' || section === 'audit') && - <WzConfigurationPolicyMonitoring {...this.props} updateBadge={(e) => this.updateBadge(e)} onlyShowTab={section === 'pm' ? 'Policy Monitoring' : section === 'audit' ? 'System audit' : section === 'sca' ? 'SCA': undefined}/>} - {section === 'oscap' && <WzConfigurationOpenSCAP {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'ciscat' && <WzConfigurationCisCat {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'vuls' && <WzConfigurationVulnerabilities {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'osquery' && <WzConfigurationOsquery {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - {section === 'docker' && <WzConfigurationDockerListener {...this.props} updateBadge={(e) => this.updateBadge(e)} />} - </EuiPanel> - </EuiPage> - </WzReduxProvider> - ) - } -} diff --git a/public/components/overview/compliance-table/compliance-table.tsx b/public/components/overview/compliance-table/compliance-table.tsx index 2f3430708e..ca95757668 100644 --- a/public/components/overview/compliance-table/compliance-table.tsx +++ b/public/components/overview/compliance-table/compliance-table.tsx @@ -13,7 +13,6 @@ import React, { Component } from 'react'; import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SearchBar, FilterManager } from '../../../../../../src/plugins/data/public/'; -import { I18nProvider } from '@kbn/i18n/react'; //@ts-ignore import { ComplianceRequirements } from './components/requirements'; import { ComplianceSubrequirements } from './components/subrequirements'; @@ -28,8 +27,9 @@ import { getDataPlugin } from '../../../kibana-services'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { withAgentSupportModule } from '../../common/hocs'; -export class ComplianceTable extends Component { +export const ComplianceTable = withAgentSupportModule(class ComplianceTable extends Component { _isMount = false; timefilter: { getTime(): any; @@ -331,4 +331,4 @@ export class ComplianceTable extends Component { </div> ); } -} +}) diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index b5c4a60127..c92715e2b8 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -10,22 +10,18 @@ * Find more information about this on the LICENSE file. */ import React, { Component } from 'react'; -import store from '../../../../redux/store'; import { connect } from 'react-redux'; import { showExploreAgentModal, updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; import { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexItem, - EuiIcon, EuiOverlayMask, EuiOutsideClickDetector, EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiToolTip, + EuiPopover, } from '@elastic/eui'; +import { WzButton } from '../../../../components/common/buttons'; import './agents-selector.scss'; import { AgentSelectionTable } from './agents-selection-table'; import { WAZUH_ALERTS_PATTERN } from '../../../../../common/constants'; @@ -53,21 +49,20 @@ class OverviewActions extends Component { } componentDidMount() { - const agentId = store.getState().appStateReducers.currentAgentData.id; + const { filterManager } = getDataPlugin().query; this.setState({ filterManager: filterManager }, () => { if (this.props.initialFilter) this.agentTableSearch([this.props.initialFilter]) - if (agentId) this.agentTableSearch([agentId]) + if (this.props.agent.id) this.agentTableSearch([this.props.agent.id]) }); } componentDidUpdate(){ - const agent = store.getState().appStateReducers.currentAgentData; - if(this.state.isAgent && !agent.id){ + if(this.state.isAgent && !this.props.agent.id){ this.setState({isAgent: false}) - }else if(agent.id && this.state.isAgent !== agent.id){ - this.setState({isAgent: agent.id}) + }else if(this.props.agent.id && this.state.isAgent !== this.props.agent.id){ + this.setState({isAgent: this.props.agent.id}) } } @@ -86,7 +81,7 @@ class OverviewActions extends Component { closeAgentModal() { this.setState({ isAgentModalVisible: false }); - store.dispatch(showExploreAgentModal(false)); + this.props.showExploreAgentModal(false); } showAgentModal() { @@ -169,43 +164,56 @@ class OverviewActions extends Component { </EuiOverlayMask> ); } - const agent = store.getState().appStateReducers.currentAgentData; + + const thereAgentSelected = (this.props.agent || {}).id + + const avaliableForAgent = this.props.module.availableFor && this.props.module.availableFor.includes('agent'); + + let buttonUnpinAgent, buttonExploreAgent; + if(thereAgentSelected){ + buttonUnpinAgent = ( + <WzButton + buttonType='icon' + className="wz-unpin-agent" + iconType='pinFilled' + onClick={() => { + this.props.updateCurrentAgentData({}); + this.removeAgentsFilter(); + }} + tooltip={{position: 'bottom', content: 'Unpin agent'}} + aria-label='Unpin agent' + /> + ); + }; + + buttonExploreAgent = ( + <WzButton + buttonType='empty' + isLoading={this.state.loadingReport} + color='primary' + isDisabled={!avaliableForAgent} + tooltip={{position: 'bottom', content: !avaliableForAgent ? 'This module is not supported for agents.' : (thereAgentSelected ? 'Change agent selected' : 'Select an agent to explore its modules') }} + style={thereAgentSelected ? {background: 'rgba(0, 107, 180, 0.1)'} : undefined} + iconType='watchesApp' + onClick={() => this.showAgentModal()}> + {thereAgentSelected ? `${this.props.agent.name} (${this.props.agent.id})` : 'Explore agent'} + </WzButton> + ) + return ( - <div> - <EuiFlexItem> - {!this.state.isAgent && ( - <EuiToolTip position='bottom' content='Select an agent to explore its modules' > - <EuiButtonEmpty - isLoading={this.state.loadingReport} - color='primary' - onClick={() => this.showAgentModal()}> - <EuiIcon type="watchesApp" color="primary" style={{ marginBottom: 3 }} />  Explore agent - </EuiButtonEmpty> - </EuiToolTip> - )} - {this.state.isAgent && ( - <div style={{ display: "inline-flex" }}> - <EuiToolTip position='bottom' content='Change agent selected' > - <EuiButtonEmpty - style={{background: 'rgba(0, 107, 180, 0.1)'}} - isLoading={this.state.loadingReport} - onClick={() => this.showAgentModal()}> - {agent.name} ({agent.id}) - </EuiButtonEmpty> - </EuiToolTip> - <EuiToolTip position='bottom' content='Unpin agent'> - <EuiButtonIcon - className="wz-unpin-agent" - iconType='pinFilled' - onClick={() => { - store.dispatch(updateCurrentAgentData({})); - this.removeAgentsFilter(); - }} - aria-label='Unpin agent' /> - </EuiToolTip> - </div> - )} - </EuiFlexItem> + <div style={{ display: "inline-flex" }}> + {buttonExploreAgent} + {thereAgentSelected && ( + !avaliableForAgent && ( + <EuiPopover + button={buttonUnpinAgent} + isOpen={thereAgentSelected} + closePopover={()=> {}}> + This module is not supported for agents. Remove the pinned agent. + </EuiPopover> + + ) || buttonUnpinAgent + )} {modal} </div> ); @@ -215,7 +223,13 @@ class OverviewActions extends Component { const mapStateToProps = state => { return { state: state.appStateReducers, + agent: state.appStateReducers.currentAgentData }; }; -export default connect(mapStateToProps, null)(OverviewActions); +const mapDispatchToProps = dispatch => ({ + updateCurrentAgentData: (agent) => dispatch(updateCurrentAgentData(agent)), + showExploreAgentModal: (data) => dispatch(showExploreAgentModal(data)) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(OverviewActions); diff --git a/public/react-services/reporting.js b/public/react-services/reporting.js index 2508a7a7db..3c227159d5 100644 --- a/public/react-services/reporting.js +++ b/public/react-services/reporting.js @@ -79,17 +79,21 @@ export class ReportingService { idArray = rawVisualizations.map((item) => item.id); } + const visualizationIDList = []; for (const item of idArray) { const tmpHTMLElement = $(`#${item}`); - this.vis2png.assignHTMLItem(item, tmpHTMLElement); + if(tmpHTMLElement[0]){ + this.vis2png.assignHTMLItem(item, tmpHTMLElement); + visualizationIDList.push(item); + } } const appliedFilters = await this.visHandlers.getAppliedFilters(syscollectorFilters); - const array = await this.vis2png.checkArray(idArray); - const name = `wazuh-${agents ? `agent-${agents}` : 'overview'}-${tab}-${ - (Date.now() / 1000) | 0 - }.pdf`; + const array = await this.vis2png.checkArray(visualizationIDList); + const name = `wazuh-${ + agents ? `agent-${agents}` : 'overview' + }-${tab}-${(Date.now() / 1000) | 0}.pdf`; const browserTimezone = moment.tz.guess(true); From 9abcc09d784d59ca744f5cc7656065b3d44f74b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Mon, 9 Aug 2021 12:35:49 +0200 Subject: [PATCH 237/493] Solved issue where use-previous was not removed from index --- public/components/common/modules/buttons/index.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 public/components/common/modules/buttons/index.ts diff --git a/public/components/common/modules/buttons/index.ts b/public/components/common/modules/buttons/index.ts new file mode 100644 index 0000000000..76dedb877b --- /dev/null +++ b/public/components/common/modules/buttons/index.ts @@ -0,0 +1,13 @@ +/* + * Wazuh app - Module buttons components + * Copyright (C) 2015-2021 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 * from './generate_report'; \ No newline at end of file From 1763581fe8575caa0ce974fa5dbd118b701e47bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Mon, 9 Aug 2021 12:41:52 +0200 Subject: [PATCH 238/493] Feature/pagination drilldown tables (#3544) * Pagination in Drilldown Tables * Adding key filter in aggs tables * Adding Changelog * Fixing some bugs * Fixing useSearch use --- CHANGELOG.md | 1 + .../components/common/hooks/use-es-search.ts | 2 +- .../modules/panel/components/agg-table.tsx | 52 ++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a836c4e64..92ea29decc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) - Added base Module Panel view with Office365 setup [#3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) +- Adding Pagination and filter to drilldown tables at Office pannel [#3544](https://github.com/wazuh/wazuh-kibana-app/issues/3544). ### Changed diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index b5fc92afe8..f9296a94d2 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -72,7 +72,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setIsLoading(false); } })(); - }, [indexPattern, query, filters, page]); + }, [indexPattern, query, filters, page, preAppliedAggs]); const search = async (): Promise<SearchResponse> => { if (indexPattern) { diff --git a/public/components/common/modules/panel/components/agg-table.tsx b/public/components/common/modules/panel/components/agg-table.tsx index 230cdf0eab..e58b7f577e 100644 --- a/public/components/common/modules/panel/components/agg-table.tsx +++ b/public/components/common/modules/panel/components/agg-table.tsx @@ -11,9 +11,9 @@ * Find more information about this on the LICENSE file. */ -import { EuiBasicTable, EuiPanel, EuiTitle, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiPanel, EuiTitle, EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; import { useEsSearch } from '../../../hooks'; -import React from 'react'; +import React, { useState, useMemo } from 'react'; export const AggTable = ({ onRowClick = (field, value) => {}, @@ -24,27 +24,33 @@ export const AggTable = ({ panelProps, titleProps, }) => { - const preAppliedAggs = { - buckets: { - terms: { - field: aggTerm, - size: maxRows, - order: { _count: 'desc' }, + const [order, setOrder] = useState({ _count: 'desc' }); + const preAppliedAggs = useMemo(() => { + return { + buckets: { + terms: { + field: aggTerm, + size: maxRows, + order, + }, }, - }, - }; + }; + }, [order, aggTerm, maxRows]); + const { esResults, isLoading, error } = useEsSearch({ preAppliedAggs }); const buckets = ((esResults.aggregations || {}).buckets || {}).buckets || []; const columns: EuiBasicTableColumn<any>[] = [ { field: 'key', name: aggLabel, + sortable: true, }, { field: 'doc_count', name: 'Count', isExpander: false, align: 'right', + sortable: true, }, ]; const getRowProps = (item) => { @@ -56,18 +62,36 @@ export const AggTable = ({ }, }; }; - + const pagination = { + hidePerPageOptions: true, + pageSize: 10, + }; + const sorting = { + sort: { + field: 'doc_count', + direction: 'desc', + }, + }; + const onTableChange = ({ sort = {} }) => { + if (sort.field) { + const field = { key: '_key', doc_count: '_count' }[sort.field]; + setOrder({ [field]: sort.direction }); + } + }; return ( <EuiPanel data-test-subj={`${aggTerm}-aggTable`} {...panelProps}> <EuiTitle {...titleProps}> <h2>{tableTitle}</h2> </EuiTitle> - <EuiBasicTable - items={buckets} + <EuiInMemoryTable columns={columns} - rowProps={getRowProps} + items={buckets} loading={isLoading} + rowProps={getRowProps} error={error ? error.message : undefined} + pagination={pagination} + onTableChange={onTableChange} + sorting={sorting} /> </EuiPanel> ); From 11903770c685fb6a29995333050cc80c455065ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 9 Aug 2021 13:02:42 +0200 Subject: [PATCH 239/493] fix(frontend): Removed import of not defined `usePrevious` hook --- .../components/common/custom-search-bar/custom-search-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 9751ae47f2..ea752de6af 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore import { KbnSearchBar } from '../../kbn-search-bar'; import { Combobox } from './components'; -import { useFilterManager, usePrevious } from '../hooks'; +import { useFilterManager } from '../hooks'; export const CustomSearchBar = ({ filtersValues, ...props }) => { const { filterManager, filters } = useFilterManager(); From 15f7f3f050e896004ce3aee5e0f770d0a96a8a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 9 Aug 2021 17:37:53 +0200 Subject: [PATCH 240/493] fix(module_office365): Fix some problems in the module configuration viewer - Module configuration only available for managers - Replaced as the list of credentials are rendered. Now it uses the `WzConfigurationSettingsListSelector` - Replaced some descriptions --- .../configuration/configuration-settings.js | 3 +- .../components/api-auth-tab/api-auth-tab.tsx | 32 ++++++++++++------- .../configuration/office365/constants.tsx | 2 +- .../configuration/office365/office365.tsx | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/public/controllers/management/components/management/configuration/configuration-settings.js b/public/controllers/management/components/management/configuration/configuration-settings.js index 1cb3769778..46edcac04c 100644 --- a/public/controllers/management/components/management/configuration/configuration-settings.js +++ b/public/controllers/management/components/management/configuration/configuration-settings.js @@ -212,7 +212,8 @@ export default [ name: 'Office 365', description: 'Configuration options of the Office 365 module', - goto: 'office365' + goto: 'office365', + when: 'manager' } ] } diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx index 0385f05353..04b6b5123e 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx @@ -10,9 +10,10 @@ * Find more information about this on the LICENSE file. */ -import React from 'react'; +import React, { useMemo } from 'react'; import WzConfigurationSettingsTabSelector from '../../../util-components/configuration-settings-tab-selector'; -import WzConfigurationSettingsGroup from '../../../util-components/configuration-settings-group'; +import WzConfigurationSettingsListSelector from '../../../util-components/configuration-settings-list-selector'; +import { settingsListBuilder } from '../../../utils/builders'; import { HELP_LINKS, OFFICE_365 } from '../../constants'; export type ApiAuthProps = { @@ -20,24 +21,31 @@ export type ApiAuthProps = { wodleConfiguration: any; }; +const columns = [ + { field: 'tenant_id', label: 'Tenant Id' }, + { field: 'client_id', label: 'Client Id' }, + { field: 'client_secret', label: 'Client Secret' }, + { field: 'client_secret_path', label: 'Client Secret Path' }, +]; + export const ApiAuthTab = ({ agent, wodleConfiguration }: ApiAuthProps) => { - const columns = [ - { field: 'tenant_id', label: 'Tenant Id' }, - { field: 'client_id', label: 'Client Id' }, - { field: 'client_secret', label: 'Client Secret' }, - { field: 'client_secret_path', label: 'Client Secret Path' }, - ]; + + const credentials = useMemo(() => settingsListBuilder( + wodleConfiguration[OFFICE_365].api_auth, + 'tenant_id' + ), [wodleConfiguration]); return ( <WzConfigurationSettingsTabSelector - title="Credential for the authentication with the API" + title="Credentials for the authentication with the API" currentConfig={wodleConfiguration} minusHeight={agent.id === '000' ? 260 : 320} helpLinks={HELP_LINKS} > - {wodleConfiguration[OFFICE_365].api_auth.map((api_auth) => ( - <WzConfigurationSettingsGroup config={api_auth} items={columns} /> - ))} + <WzConfigurationSettingsListSelector + items={credentials} + settings={columns} + /> </WzConfigurationSettingsTabSelector> ); }; diff --git a/public/controllers/management/components/management/configuration/office365/constants.tsx b/public/controllers/management/components/management/configuration/office365/constants.tsx index 7f2a265ad4..4841e0b7a1 100644 --- a/public/controllers/management/components/management/configuration/office365/constants.tsx +++ b/public/controllers/management/components/management/configuration/office365/constants.tsx @@ -18,7 +18,7 @@ export const HELP_LINKS = [ href: 'https://documentation.wazuh.com/current/office365/index.html', }, { - text: 'Configuration options for the Office 365 module', + text: 'Configuration options for the module', href: 'https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html', }, diff --git a/public/controllers/management/components/management/configuration/office365/office365.tsx b/public/controllers/management/components/management/configuration/office365/office365.tsx index ca6ea2ba4b..b3c94295bd 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.tsx @@ -56,7 +56,7 @@ export const WzConfigurationOffice365: React.FunctionComponent<IWzConfigOffice36 {...props} /> </WzTabSelectorTab> - <WzTabSelectorTab label="Api Auth"> + <WzTabSelectorTab label="Credentials"> <ApiAuthTabWrapped wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} From e96998c12be4bfafa899281a1bc1b61d7c708535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 9 Aug 2021 17:40:12 +0200 Subject: [PATCH 241/493] fix(module_office365): Update components tests and snapshot --- .../__snapshots__/api-auth-tab.test.tsx.snap | 747 +++++++++++------- .../api-auth-tab/api-auth-tab.test.tsx | 12 +- .../office365/office365.test.tsx | 10 + 3 files changed, 493 insertions(+), 276 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index 2b9b49e2b5..ef4df7e55d 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -11,6 +11,11 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` Object { "office365": Object { "api_auth": Array [ + Object { + "client_id": "your_client_id_test_1", + "client_secret": "your_secret_test_1", + "tenant_id": "your_tenant_id_test_1", + }, Object { "client_id": "your_client_id_test_2", "client_secret": "your_secret_test_2", @@ -37,6 +42,11 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` Object { "office365": Object { "api_auth": Array [ + Object { + "client_id": "your_client_id_test_1", + "client_secret": "your_secret_test_1", + "tenant_id": "your_tenant_id_test_1", + }, Object { "client_id": "your_client_id_test_2", "client_secret": "your_secret_test_2", @@ -65,12 +75,12 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } minusHeight={260} - title="Credential for the authentication with the API" + title="Credentials for the authentication with the API" > <WzConfigurationSettingsHeader help={ @@ -81,13 +91,13 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } json={[Function]} settings={[Function]} - title="Credential for the authentication with the API" + title="Credentials for the authentication with the API" viewSelected="" xml={[Function]} > @@ -107,7 +117,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` <h2 className="euiTitle euiTitle--small" > - Credential for the authentication with the API + Credentials for the authentication with the API </h2> </EuiTitle> </div> @@ -243,7 +253,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -380,15 +390,28 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` <WzViewSelectorSwitch default={true} > - <WzSettingsGroup - config={ - Object { - "client_id": "your_client_id_test_2", - "client_secret": "your_secret_test_2", - "tenant_id": "your_tenant_id_test_2", - } - } + <WzConfigurationSettingsListSelector items={ + Array [ + Object { + "data": Object { + "client_id": "your_client_id_test_1", + "client_secret": "your_secret_test_1", + "tenant_id": "your_tenant_id_test_1", + }, + "label": "your_tenant_id_test_1", + }, + Object { + "data": Object { + "client_id": "your_client_id_test_2", + "client_secret": "your_secret_test_2", + "tenant_id": "your_tenant_id_test_2", + }, + "label": "your_tenant_id_test_2", + }, + ] + } + settings={ Array [ Object { "field": "tenant_id", @@ -409,312 +432,488 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` ] } > - <WzConfigurationSettingsHeader> - <EuiFlexGroup - alignItems="center" - > - <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" - > - <EuiFlexItem> - <div - className="euiFlexItem" - > - <EuiTitle - size="s" - > - <h2 - className="euiTitle euiTitle--small" - /> - </EuiTitle> - </div> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <EuiFlexGroup - alignItems="center" - gutterSize="none" - justifyContent="flexEnd" - > - <div - className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" - /> - </EuiFlexGroup> - </div> - </EuiFlexItem> - </div> - </EuiFlexGroup> - <EuiSpacer - size="xs" - > - <div - className="euiSpacer euiSpacer--xs" - /> - </EuiSpacer> - </WzConfigurationSettingsHeader> <EuiSpacer - size="s" + size="m" > <div - className="euiSpacer euiSpacer--s" + className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <EuiFlexGroup> + <EuiFlexGroup + alignItems="flexStart" + > <div - className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsFlexStart euiFlexGroup--directionRow euiFlexGroup--responsive" > - <EuiFlexItem> + <EuiFlexItem + grow={false} + style={ + Object { + "maxWidth": 25, + "minWidth": 250, + } + } + > <div - className="euiFlexItem" + className="euiFlexItem euiFlexItem--flexGrowZero" + style={ + Object { + "maxWidth": 25, + "minWidth": 250, + } + } > - <WzConfigurationSetting - key="-Tenant Id-undefined-0" - keyItem="-Tenant Id-undefined-0" - label="Tenant Id" - value="your_tenant_id_test_2" + <EuiPanel + style={ + Object { + "background": "#fafbfd", + } + } > <div + className="euiPanel euiPanel--paddingMedium" style={ Object { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", + "background": "#fafbfd", } } > - <div - style={ - Object { - "justifySelf": "flex-end", - "margin": "1em", - "width": "35%", - } - } - > - <EuiTextAlign - textAlign="right" + <ul> + <li + key="undefined-0" > - <div - className="euiTextAlign euiTextAlign--right" + <EuiButtonEmpty + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } > - Tenant Id - </div> - </EuiTextAlign> - </div> - <div - style={ - Object { - "width": "65%", - } - } - > - <EuiFieldText - readOnly={true} - value="your_tenant_id_test_2" + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + style={ + Object { + "textDecoration": "underline", + } + } + type="button" + > + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } + > + <span + className="euiButtonContent euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + your_tenant_id_test_1 + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </li> + <li + key="undefined-1" > - <EuiFormControlLayout - fullWidth={false} - readOnly={true} + <EuiButtonEmpty + onClick={[Function]} + style={Object {}} > - <div - className="euiFormControlLayout euiFormControlLayout--readOnly" + <button + className="euiButtonEmpty euiButtonEmpty--primary" + disabled={false} + onClick={[Function]} + style={Object {}} + type="button" > - <div - className="euiFormControlLayout__childrenWrapper" + <EuiButtonContent + className="euiButtonEmpty__content" + iconSide="left" + textProps={ + Object { + "className": "euiButtonEmpty__text", + } + } > - <EuiValidatableControl> - <input - className="euiFieldText" - readOnly={true} - type="text" - value="your_tenant_id_test_2" - /> - </EuiValidatableControl> - <EuiFormControlLayoutIcons /> - </div> - </div> - </EuiFormControlLayout> - </EuiFieldText> - </div> + <span + className="euiButtonContent euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + your_tenant_id_test_2 + </span> + </span> + </EuiButtonContent> + </button> + </EuiButtonEmpty> + </li> + </ul> </div> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - </WzConfigurationSetting> - <WzConfigurationSetting - key="-Client Id-undefined-1" - keyItem="-Client Id-undefined-1" - label="Client Id" - value="your_client_id_test_2" - > + </EuiPanel> + </div> + </EuiFlexItem> + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiPanel> <div - style={ - Object { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - } - } + className="euiPanel euiPanel--paddingMedium" > - <div - style={ + <WzSettingsGroup + config={ Object { - "justifySelf": "flex-end", - "margin": "1em", - "width": "35%", + "client_id": "your_client_id_test_1", + "client_secret": "your_secret_test_1", + "tenant_id": "your_tenant_id_test_1", } } - > - <EuiTextAlign - textAlign="right" - > - <div - className="euiTextAlign euiTextAlign--right" - > - Client Id - </div> - </EuiTextAlign> - </div> - <div - style={ - Object { - "width": "65%", - } + items={ + Array [ + Object { + "field": "tenant_id", + "label": "Tenant Id", + }, + Object { + "field": "client_id", + "label": "Client Id", + }, + Object { + "field": "client_secret", + "label": "Client Secret", + }, + Object { + "field": "client_secret_path", + "label": "Client Secret Path", + }, + ] } > - <EuiFieldText - readOnly={true} - value="your_client_id_test_2" - > - <EuiFormControlLayout - fullWidth={false} - readOnly={true} + <WzConfigurationSettingsHeader> + <EuiFlexGroup + alignItems="center" > <div - className="euiFormControlLayout euiFormControlLayout--readOnly" + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" > - <div - className="euiFormControlLayout__childrenWrapper" + <EuiFlexItem> + <div + className="euiFlexItem" + > + <EuiTitle + size="s" + > + <h2 + className="euiTitle euiTitle--small" + /> + </EuiTitle> + </div> + </EuiFlexItem> + <EuiFlexItem + grow={false} > - <EuiValidatableControl> - <input - className="euiFieldText" - readOnly={true} - type="text" - value="your_client_id_test_2" - /> - </EuiValidatableControl> - <EuiFormControlLayoutIcons /> - </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="flexEnd" + > + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive" + /> + </EuiFlexGroup> + </div> + </EuiFlexItem> </div> - </EuiFormControlLayout> - </EuiFieldText> - </div> - </div> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - </WzConfigurationSetting> - <WzConfigurationSetting - key="-Client Secret-undefined-2" - keyItem="-Client Secret-undefined-2" - label="Client Secret" - value="your_secret_test_2" - > - <div - style={ - Object { - "alignItems": "center", - "display": "flex", - "flexDirection": "row", - } - } - > - <div - style={ - Object { - "justifySelf": "flex-end", - "margin": "1em", - "width": "35%", - } - } - > - <EuiTextAlign - textAlign="right" - > - <div - className="euiTextAlign euiTextAlign--right" + </EuiFlexGroup> + <EuiSpacer + size="xs" > - Client Secret - </div> - </EuiTextAlign> - </div> - <div - style={ - Object { - "width": "65%", - } - } - > - <EuiFieldText - readOnly={true} - value="your_secret_test_2" + <div + className="euiSpacer euiSpacer--xs" + /> + </EuiSpacer> + </WzConfigurationSettingsHeader> + <EuiSpacer + size="s" > - <EuiFormControlLayout - fullWidth={false} - readOnly={true} + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + <EuiFlexGroup> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > - <div - className="euiFormControlLayout euiFormControlLayout--readOnly" - > + <EuiFlexItem> <div - className="euiFormControlLayout__childrenWrapper" + className="euiFlexItem" > - <EuiValidatableControl> - <input - className="euiFieldText" - readOnly={true} - type="text" - value="your_secret_test_2" - /> - </EuiValidatableControl> - <EuiFormControlLayoutIcons /> + <WzConfigurationSetting + key="-Tenant Id-undefined-0" + keyItem="-Tenant Id-undefined-0" + label="Tenant Id" + value="your_tenant_id_test_1" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Tenant Id + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_tenant_id_test_1" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_tenant_id_test_1" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Id-undefined-1" + keyItem="-Client Id-undefined-1" + label="Client Id" + value="your_client_id_test_1" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Client Id + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_client_id_test_1" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_client_id_test_1" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Secret-undefined-2" + keyItem="-Client Secret-undefined-2" + label="Client Secret" + value="your_secret_test_1" + > + <div + style={ + Object { + "alignItems": "center", + "display": "flex", + "flexDirection": "row", + } + } + > + <div + style={ + Object { + "justifySelf": "flex-end", + "margin": "1em", + "width": "35%", + } + } + > + <EuiTextAlign + textAlign="right" + > + <div + className="euiTextAlign euiTextAlign--right" + > + Client Secret + </div> + </EuiTextAlign> + </div> + <div + style={ + Object { + "width": "65%", + } + } + > + <EuiFieldText + readOnly={true} + value="your_secret_test_1" + > + <EuiFormControlLayout + fullWidth={false} + readOnly={true} + > + <div + className="euiFormControlLayout euiFormControlLayout--readOnly" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <EuiValidatableControl> + <input + className="euiFieldText" + readOnly={true} + type="text" + value="your_secret_test_1" + /> + </EuiValidatableControl> + <EuiFormControlLayoutIcons /> + </div> + </div> + </EuiFormControlLayout> + </EuiFieldText> + </div> + </div> + <EuiSpacer + size="s" + > + <div + className="euiSpacer euiSpacer--s" + /> + </EuiSpacer> + </WzConfigurationSetting> + <WzConfigurationSetting + key="-Client Secret Path-undefined-3" + keyItem="-Client Secret Path-undefined-3" + label="Client Secret Path" + /> </div> - </div> - </EuiFormControlLayout> - </EuiFieldText> - </div> + </EuiFlexItem> + </div> + </EuiFlexGroup> + </WzSettingsGroup> </div> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - </WzConfigurationSetting> - <WzConfigurationSetting - key="-Client Secret Path-undefined-3" - keyItem="-Client Secret Path-undefined-3" - label="Client Secret Path" - /> + </EuiPanel> </div> </EuiFlexItem> </div> </EuiFlexGroup> - </WzSettingsGroup> + </WzConfigurationSettingsListSelector> </WzViewSelectorSwitch> </WzViewSelector> </WzConfigurationSettingsTabSelector> diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx index 3c1ce99bb7..2135f68497 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.test.tsx @@ -14,6 +14,16 @@ import React from 'react'; import { ApiAuthTab } from './api-auth-tab'; import { mount } from 'enzyme'; +jest.mock('../../../../../../../../kibana-services', () => ({ + getUiSettings: () => ({ + get: (uiSetting: string) => { + if (uiSetting === 'theme:darkMode') { + return false + } + } + }) +})); + describe('ApiAuthTab component', () => { it('renders correctly to match the snapshot', () => { const wodleConfiguration = { @@ -28,8 +38,6 @@ describe('ApiAuthTab component', () => { client_id: 'your_client_id_test_1', client_secret: 'your_secret_test_1', }, - ], - api_auth: [ { tenant_id: 'your_tenant_id_test_2', client_id: 'your_client_id_test_2', diff --git a/public/controllers/management/components/management/configuration/office365/office365.test.tsx b/public/controllers/management/components/management/configuration/office365/office365.test.tsx index 709b6fa96f..2aa496f64e 100644 --- a/public/controllers/management/components/management/configuration/office365/office365.test.tsx +++ b/public/controllers/management/components/management/configuration/office365/office365.test.tsx @@ -16,6 +16,16 @@ import { WzConfigurationOffice365 } from './office365'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; +jest.mock('../../../../../../kibana-services', () => ({ + getUiSettings: () => ({ + get: (uiSetting: string) => { + if (uiSetting === 'theme:darkMode') { + return false + } + } + }) +})); + const mockStore = configureMockStore(); const store = mockStore({}); From 1f01f71f2a48ec7f697756a3d31c51dfa5886f96 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 10 Aug 2021 08:13:19 +0200 Subject: [PATCH 242/493] [Module] [Office365] [Panel] Add the view for the module configuration (#3541) * feat(module_office365): Add the module module configuration view to the `Panel` tab - Created reusable component to display the module configuration - Adapt to the Office 365 use case * fix(frontend): Fix `useAsyncAction` which doesn't reset the `data` and `error` states when rerun the action after the first time * fix(frontend): Moved the styles for the `PanelModuleConfiguration` component - Renamed the component file - Adapted the imports - Removed `office-stats.scss` file and its import * fix(frontend): Add types to `office-stats.tsx` * fix(frontend): Apply the request changes for the PR - Removed empty line at the beginning of the file - Replaced as compare variable against undefined --- .../common/hooks/use_async_action.ts | 2 + .../common/modules/panel/components/index.ts | 1 + .../components/module_configuration.scss | 23 +++ .../panel/components/module_configuration.tsx | 173 ++++++++++++++++++ .../overview/office-panel/office-panel.tsx | 74 ++------ .../overview/office-panel/views/index.ts | 6 +- .../office-panel/views/office-stats.scss | 21 --- .../office-panel/views/office-stats.tsx | 77 ++++---- 8 files changed, 257 insertions(+), 120 deletions(-) create mode 100644 public/components/common/modules/panel/components/module_configuration.scss create mode 100644 public/components/common/modules/panel/components/module_configuration.tsx delete mode 100644 public/components/overview/office-panel/views/office-stats.scss diff --git a/public/components/common/hooks/use_async_action.ts b/public/components/common/hooks/use_async_action.ts index d71167255e..51a2d59410 100644 --- a/public/components/common/hooks/use_async_action.ts +++ b/public/components/common/hooks/use_async_action.ts @@ -19,6 +19,8 @@ export function useAsyncAction(action, dependencies = []){ const run = useCallback(async (...params) => { try{ setRunning(true); + setError(null); + setData(null); const data = await action(...params); setData(data); }catch(error){ diff --git a/public/components/common/modules/panel/components/index.ts b/public/components/common/modules/panel/components/index.ts index 34924ba181..4fb2c9c246 100644 --- a/public/components/common/modules/panel/components/index.ts +++ b/public/components/common/modules/panel/components/index.ts @@ -15,3 +15,4 @@ export { ModuleSidePanel } from './module-side-panel'; export { AggTable } from './agg-table'; export { VisCard } from './vis-card'; export { VisConfigLayout } from './vis-config-layout'; +export * from './module_configuration'; diff --git a/public/components/common/modules/panel/components/module_configuration.scss b/public/components/common/modules/panel/components/module_configuration.scss new file mode 100644 index 0000000000..cf26973a44 --- /dev/null +++ b/public/components/common/modules/panel/components/module_configuration.scss @@ -0,0 +1,23 @@ +.euiTitle.module-panel-configuration-title{ + white-space: nowrap; +} + +h5.euiTitle.module-panel-configuration-subtitle{ + white-space: nowrap; + margin: 0; + font-weight: normal; +} + +.module-panel-configuration-callout-warning{ + margin: 0 -16px; + padding: 16px 38px; +} + +.wz-justify-center { + justify-content: center; +} + +.module-panel-configuration-list dt.euiDescriptionList__title, .module-panel-configuration-list dd.euiDescriptionList__description { + border-left: 2px solid rgba(0, 0, 0, 0.3); + padding-left: 10px; +} diff --git a/public/components/common/modules/panel/components/module_configuration.tsx b/public/components/common/modules/panel/components/module_configuration.tsx new file mode 100644 index 0000000000..076273cc87 --- /dev/null +++ b/public/components/common/modules/panel/components/module_configuration.tsx @@ -0,0 +1,173 @@ +/* + * Wazuh app - Panel - Module configuration component + * Copyright (C) 2015-2021 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, { useEffect, useMemo, FunctionalComponent } from 'react'; +import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiCallOut, EuiIcon, EuiSpacer, EuiProgress, EuiAccordion, EuiText } from '@elastic/eui'; +import { WzRequest } from '../../../../../react-services'; +import { connect } from 'react-redux'; +import { useAsyncAction } from '../../../hooks'; +import { withGuard } from '../../../hocs'; +import { compose } from 'redux'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { + UI_ERROR_SEVERITIES, + UIErrorLog, + UIErrorSeverity, + UILogLevel, +} from '../../../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import './module_configuration.scss'; + +const mapStateToProps = state => ({ + agent: state.appStateReducers.currentAgentData +}); + +const getMapConfigurationToState = async (type: string, configurationAPIPartialPath: string, mapResponseConfiguration, params?: any ) : Promise<[any] | string> => { + const prefixEndpoint = type === 'agent' + ? `/agents/${params.id}/config` + : (type === 'cluster_node' + ? `/cluster/${params.name}/configuration` + : `/manager/configuration` + ); + const response = await WzRequest.apiReq('GET', `${prefixEndpoint}${configurationAPIPartialPath}`, {}); + return mapResponseConfiguration(response, type, params); +}; + +const renderEntityTitle = (entity: string, name: string) => { + return `${entity}${name ? ` - ${name}` : ''}`; +}; + +export const PanelModuleConfiguration : FunctionalComponent<{h: string}> = connect(mapStateToProps)( +({ agent, configurationAPIPartialPath, mapResponseConfiguration, moduleTitle, moduleIconType = '', settings }) => { + + const asyncAction = useAsyncAction((async () => { + try{ + if(agent.id){ + // Get the module configuration for the pinned agent + const configuration = await getMapConfigurationToState('agent', configurationAPIPartialPath, mapResponseConfiguration, agent); + return configuration ? [configuration] : null; + }else{ + const custerStatusResponse = await WzRequest.apiReq('GET', '/cluster/status', {}); + if (custerStatusResponse.data.data.enabled === 'yes' && custerStatusResponse.data.data.running === 'yes') { + // Cluster mode + // Get the cluster nodes + const nodesResponse = await WzRequest.apiReq('GET', `/cluster/nodes`, {}); + const nodesConfigurationResponses = await Promise.all( + nodesResponse.data.data.affected_items + .map(async node => await getMapConfigurationToState('cluster_node', configurationAPIPartialPath, mapResponseConfiguration, node) + ) + ); + const nodeConfigurations = nodesConfigurationResponses.filter(nodeConfiguration => nodeConfiguration); + return nodeConfigurations.length ? nodeConfigurations : null; + } else { + // Manager mode + const configuration = await getMapConfigurationToState('manager', configurationAPIPartialPath, mapResponseConfiguration); + return configuration ? [configuration] : null; + }; + } + }catch(error){ + const options: UIErrorLog = { + context: `${PanelModuleConfiguration.name}.getModuleConfig`, + level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + error: { + error: error, + message: error.message || error, + title: 'Module Unavailable', + }, + }; + getErrorOrchestrator().handleError(options); + throw error; // This lets to populate the 'asyncAction.error' property + } + }), [agent]); + + useEffect(() => { + asyncAction.run(); + },[asyncAction.run]); + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem className='wz-justify-center' grow={false}> + <EuiIcon type={moduleIconType} size='xl'/> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size='s'> + <h4 className='module-panel-configuration-title'>{moduleTitle}</h4> + </EuiTitle> + <EuiTitle size='xs'> + <h5 className='module-panel-configuration-subtitle'>Module configuration</h5> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <ConfigurationWrapper configurations={asyncAction.data} settings={settings} loading={asyncAction.running} error={asyncAction.error}/> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}); + +const ConfigurationWrapper = compose( + withGuard(({loading}) => loading, () => <EuiProgress size='xs' color='primary' />), + withGuard(({error}) => error, ({error}) => ( + <EuiCallOut className='office-stats-callout-warning' + title="Error fetching the module configuration" + color="danger" + iconType="alert" + > + {error && error.message || String(error)} + </EuiCallOut> + )), + withGuard(({configurations}) => !configurations, () => ( + <EuiCallOut className='office-stats-callout-warning' + title='Module configuration unavailable' + color='warning' + iconType='alert' + /> + )) +)(({configurations, settings}) => { + return ( + <div style={{height: 'calc(100vh - 175px)', overflow: 'scroll'}}> + {configurations.length === 1 + ? <> + <EuiText>{renderEntityTitle(configurations[0].entity, configurations[0].name)}</EuiText> + <EuiSpacer size='s'/> + <Configuration configuration={configurations[0].configuration} settings={settings}/> + </> + : configurations.map((configuration, index) => ( + <EuiAccordion id={`module_configuration_${configuration.name}_${index}`} key={`module_configuration_${configuration.name}_${index}`} buttonContent={renderEntityTitle(configuration.entity, configuration.name)}> + <Configuration {...configuration} settings={settings}/> + </EuiAccordion> + )).reduce((prev, cur) => [prev, <div style={{marginTop: '8px'}} /> , cur], []) + } + </div> + ) +}); + +const Configuration = ({ configuration, settings }) => { + const listItems = useMemo(() => { + return settings + .filter(({field}) => typeof configuration[field] !== 'undefined') + .map(({field, label, render = null}) => ({title: label, description: render ? render(configuration[field]) : configuration[field]})); + }, [configuration]); + return ( + <div> + <EuiDescriptionList + className='module-panel-configuration-list' + listItems={listItems} + compressed + /> + </div> + ) +}; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index 1d7fdd8f20..cabdb8c3cf 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -11,70 +11,20 @@ * Find more information about this on the LICENSE file. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { CustomSearchBar } from '../../common/custom-search-bar'; -import { OfficeStats } from './views'; -import { queryConfig } from '../../../react-services/query-config'; +import { ModuleConfiguration } from './views'; import { ModuleConfig, filtersValues } from './config'; -export const OfficePanel = withErrorBoundary(() => { - const [moduleStatsList, setModuleStatsList] = useState([]); - - /** Get Office 365 Side Panel Module info **/ - const getModuleConfig = async () => { - const modulesConfig = await queryConfig('000', [ - { component: 'wmodules', configuration: 'wmodules' }, - ]); - const config = Object.entries( - modulesConfig['wmodules-wmodules'].affected_items[0].wmodules.filter((module) => { - return Object.keys(module)[0] == 'office365'; - })[0]['office365'] - ).map((configProp) => { - const description = Array.isArray(configProp[1]) ? configProp[1].join(', ') : configProp[1]; - return { title: configProp[0], description }; - }); - setModuleStatsList(config); - }; - - useEffect(() => { - (async () => { - try { - await getModuleConfig(); - } catch (error) { - const options: UIErrorLog = { - context: `${OfficePanel.name}.getModuleConfig`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: 'Module Unavailable', - }, - }; - getErrorOrchestrator().handleError(options); - setModuleStatsList([]); - } - })(); - }, []); - - return ( - <> - <CustomSearchBar filtersValues={filtersValues} /> - <MainPanel - moduleConfig={ModuleConfig} - tab={'office'} - sidePanelChildren={<OfficeStats listItems={moduleStatsList} />} - /> - </> - ); -}); +export const OfficePanel = withErrorBoundary(() => ( + <> + <CustomSearchBar filtersValues={filtersValues} /> + <MainPanel + moduleConfig={ModuleConfig} + tab={'office'} + sidePanelChildren={<ModuleConfiguration />} + /> + </> +)); diff --git a/public/components/overview/office-panel/views/index.ts b/public/components/overview/office-panel/views/index.ts index 13a12dbded..6ded17322f 100644 --- a/public/components/overview/office-panel/views/index.ts +++ b/public/components/overview/office-panel/views/index.ts @@ -1,3 +1,3 @@ -export { OfficeStats } from './office-stats'; -export { OfficeBody } from './office-body'; -export { OfficeDrilldown } from './office-drilldown'; +export * from './office-stats'; +export * from './office-body'; +export * from './office-drilldown'; diff --git a/public/components/overview/office-panel/views/office-stats.scss b/public/components/overview/office-panel/views/office-stats.scss deleted file mode 100644 index 572935358a..0000000000 --- a/public/components/overview/office-panel/views/office-stats.scss +++ /dev/null @@ -1,21 +0,0 @@ -.euiTitle.office-stats-title{ - white-space: nowrap; -} -h5.euiTitle.office-stats-subtitle{ - white-space: nowrap; - margin: 0; - font-weight: normal; -} -.office-stats-callout-warning{ - margin: 0 -16px; - padding: 16px 38px; -} - -.wz-justify-center { - justify-content: center; -} - -.office-description-list dt.euiDescriptionList__title, .office-description-list dd.euiDescriptionList__description { - border-left: 2px solid rgba(0, 0, 0, 0.3); - padding-left: 10px; -} \ No newline at end of file diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index 1eef592a33..a439ec78ae 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -11,40 +11,49 @@ * Find more information about this on the LICENSE file. */ -import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiCallOut } from '@elastic/eui'; -import moduleLogo from '../../../../assets/office365.svg'; import React from 'react'; -import './office-stats.scss'; +import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; +import { PanelModuleConfiguration } from '../../../common/modules/panel'; +import moduleLogo from '../../../../assets/office365.svg'; -export const OfficeStats = ({ listItems = [] }) => { - const logoStyle = { width: 30 }; - return ( - <EuiFlexGroup direction={'column'} alignItems={'flexStart'}> - <EuiFlexItem> - <EuiFlexGroup> - <EuiFlexItem className={'wz-justify-center'}> - <img alt="moduleLogo" src={moduleLogo} style={logoStyle} /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size={'s'}> - <h4 className={'office-stats-title'}>Office 365</h4> - </EuiTitle> - <EuiTitle size={'xs'}> - <h5 className={'office-stats-subtitle'}>Module configuration</h5> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - { - listItems.length ? ( - <EuiDescriptionList className={'office-description-list'} listItems={listItems} compressed />) : ( - <EuiCallOut className={'office-stats-callout-warning'} - title="Module configuration unavailable" - color="warning" - iconType="warning" />) - } - </EuiFlexItem> - </EuiFlexGroup> - ); +const settings = [ + { field: 'enabled', label: 'Enabled'}, + { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started'}, + { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response'}, + { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds'}, + { field: 'api_auth', label: 'Credentials', render: (value) => value.map(v => + <EuiPanel paddingSize='s' key={`module_configuration_api_auth_${v.tenant_id}_${v.client_id}`}> + <EuiDescriptionList listItems={[ + {title: 'Tenant ID', description: v.tenant_id}, + {title: 'Client ID', description: v.client_id}, + {title: 'Client secret', description: v.client_secret}, + {title: 'Path file of client secret', description: v.client_secret_path}, + ].filter(item => typeof item.description !== 'undefined')}/> + </EuiPanel> + )}, + { field: 'subscriptions', label: 'Subscriptions', render: (value) => value + .map(v => <EuiDescriptionList key={`module_configuration_subscriptions_${v}`}>{v}</EuiDescriptionList>) + } +]; + +const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any}[], wmoduleID: string, entity: string, name: string = '') => { + const configuration = wmodules.find(wmodule => Object.keys(wmodule)[0] === wmoduleID); + return configuration + ? {entity, name, configuration: configuration[Object.keys(configuration)[0]]} + : null; }; + +export const ModuleConfiguration = props => <PanelModuleConfiguration + moduleTitle='Office 365' + moduleIconType={moduleLogo} + settings={settings} + configurationAPIPartialPath='/wmodules/wmodules' + mapResponseConfiguration={(response, type, params) => { + return type === 'agent' + ? mapWModuleConfigurationToRenderProperties(response.data.data.wmodules, 'office365', 'Agent', params.name) + : (type === 'cluster_node' + ? mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'office365', 'Manager', params.name) + : mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'office365', 'Manager') + ); + }} +/> From 40ad2734ca9ce38166c30fdafb93441a35c5636f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 09:08:16 +0200 Subject: [PATCH 243/493] Added time subscription in discover component --- public/components/common/modules/discover/discover.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 75101899eb..7b68fede6c 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -119,6 +119,7 @@ export const Discover = compose( super(props); this.KibanaServices = getDataPlugin(); this.timefilter = this.KibanaServices.query.timefilter.timefilter; + this.timeSubscription = null; this.state = { sort: {}, selectedTechnique: '', @@ -161,6 +162,14 @@ export const Discover = compose( async componentDidMount() { this._isMount = true; try { + this.timeSubscription = this.timefilter + .getTimeUpdate$() + .subscribe(() => { + this.setState({ + dateRange: this.timefilter.getTime(), + dateRangeHistory: this.timefilter._history, + }); + }); this.setState({ columns: this.getColumns() }); //initial columns await this.getIndexPattern(); await this.getAlerts(); @@ -182,6 +191,7 @@ export const Discover = compose( componentWillUnmount() { this._isMount = false; + this.timeSubscription && this.timeSubscription.unsubscribe(); } async componentDidUpdate(prevProps, prevState) { From c5dd90bc2613017af361252cb350ec4d58e7556c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 09:16:12 +0200 Subject: [PATCH 244/493] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ea29decc..31a2637a6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Moved the filterManager subscription to the hook useFilterManager [#3517](https://github.com/wazuh/wazuh-kibana-app/pull/3517) - Change filter from is to is one of in custom searchbar [#3529](https://github.com/wazuh/wazuh-kibana-app/pull/3529) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) +- Added time subscription to Discover component [#3549](https://github.com/wazuh/wazuh-kibana-app/pull/3549) ### Fixed From 87c3723a94b2ad9b892780669eb991367253e9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 09:16:46 +0200 Subject: [PATCH 245/493] Applied Prettier --- .../components/common/modules/discover/discover.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 7b68fede6c..d51eb38e9c 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -162,14 +162,12 @@ export const Discover = compose( async componentDidMount() { this._isMount = true; try { - this.timeSubscription = this.timefilter - .getTimeUpdate$() - .subscribe(() => { - this.setState({ - dateRange: this.timefilter.getTime(), - dateRangeHistory: this.timefilter._history, - }); + this.timeSubscription = this.timefilter.getTimeUpdate$().subscribe(() => { + this.setState({ + dateRange: this.timefilter.getTime(), + dateRangeHistory: this.timefilter._history, }); + }); this.setState({ columns: this.getColumns() }); //initial columns await this.getIndexPattern(); await this.getAlerts(); From 598c93b56cf7b071f0f5e5c1fbe2a3e168ee8f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 09:43:08 +0200 Subject: [PATCH 246/493] Removed office365 section from agent sections --- .../components/common/welcome/components/agent-sections.ts | 5 ----- public/components/common/welcome/components/menu-agent.js | 1 - 2 files changed, 6 deletions(-) diff --git a/public/components/common/welcome/components/agent-sections.ts b/public/components/common/welcome/components/agent-sections.ts index d41a6e9e62..3d90232fdb 100644 --- a/public/components/common/welcome/components/agent-sections.ts +++ b/public/components/common/welcome/components/agent-sections.ts @@ -44,11 +44,6 @@ export const getAgentSections = (menuAgent) => { text: 'Integrity monitoring', isPin: menuAgent.fim ? menuAgent.fim : false, }, - office: { - id: WAZUH_MODULES_ID.OFFICE_365, - text: 'Office 365', - isPin: menuAgent.office ? menuAgent.office : false - }, aws: { id: WAZUH_MODULES_ID.AMAZON_WEB_SERVICES, text: 'Amazon AWS', diff --git a/public/components/common/welcome/components/menu-agent.js b/public/components/common/welcome/components/menu-agent.js index fe3184893d..ed06fe96c6 100644 --- a/public/components/common/welcome/components/menu-agent.js +++ b/public/components/common/welcome/components/menu-agent.js @@ -36,7 +36,6 @@ class WzMenuAgent extends Component { this.securityInformationItems = [ this.agentSections.general, this.agentSections.fim, - this.agentSections.office, this.agentSections.aws, this.agentSections.gcp, ]; From 59b6800476e6ce984663dcb7e569234eaf5ffe88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 10:42:09 +0200 Subject: [PATCH 247/493] Changed wodle time execution label to seconds --- .../__snapshots__/SubscriptionTab.test.tsx.snap | 6 +++--- .../__snapshots__/general-tab.test.tsx.snap | 16 ++++++++-------- .../components/general-tab/general-tab.tsx | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap index 12699b396b..22d25c3d74 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap @@ -65,7 +65,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -81,7 +81,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -243,7 +243,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index b0791b6c63..f85a1190c6 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -66,7 +66,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -83,7 +83,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -263,7 +263,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "href": "https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/office-365.html", - "text": "Configuration options for the Office 365 module", + "text": "Configuration options for the module", }, ] } @@ -436,7 +436,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "field": "interval", - "label": "Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)", + "label": "Interval between Office 365 wodle executions in seconds", }, Object { "field": "curl_max_size", @@ -664,9 +664,9 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` </EuiSpacer> </WzConfigurationSetting> <WzConfigurationSetting - key="-Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)-undefined-2" - keyItem="-Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)-undefined-2" - label="Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)" + key="-Interval between Office 365 wodle executions in seconds-undefined-2" + keyItem="-Interval between Office 365 wodle executions in seconds-undefined-2" + label="Interval between Office 365 wodle executions in seconds" value={600} > <div @@ -693,7 +693,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` <div className="euiTextAlign euiTextAlign--right" > - Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days) + Interval between Office 365 wodle executions in seconds </div> </EuiTextAlign> </div> diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index 6c028f2dad..759d688929 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -31,7 +31,7 @@ export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => { field: 'interval', label: - 'Interval between Office 365 wodle executions: s(seconds), m(minutes), h(hours), d(days)', + 'Interval between Office 365 wodle executions in seconds', }, { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, ]; From bff96eaed338c2b2a3cdbd73906343893cb15afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Tue, 10 Aug 2021 10:47:03 +0200 Subject: [PATCH 248/493] Fixing height of Office configuration (#3552) --- .../office365/components/SubscriptionTab/SubscriptionTab.tsx | 2 +- .../office365/components/api-auth-tab/api-auth-tab.tsx | 2 +- .../office365/components/general-tab/general-tab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx index 0fdca9965e..472b0191c9 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/SubscriptionTab.tsx @@ -27,7 +27,7 @@ export const SubscriptionTab = ({ agent, wodleConfiguration }: SubscriptionTabPr <WzConfigurationSettingsTabSelector title="Subscriptions list" currentConfig={wodleConfiguration} - minusHeight={agent.id === '000' ? 260 : 320} + minusHeight={agent.id === '000' ? 370 : 320} helpLinks={HELP_LINKS} > <EuiBasicTable diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx index 04b6b5123e..5b9ddd62d6 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/api-auth-tab.tsx @@ -39,7 +39,7 @@ export const ApiAuthTab = ({ agent, wodleConfiguration }: ApiAuthProps) => { <WzConfigurationSettingsTabSelector title="Credentials for the authentication with the API" currentConfig={wodleConfiguration} - minusHeight={agent.id === '000' ? 260 : 320} + minusHeight={agent.id === '000' ? 370 : 320} helpLinks={HELP_LINKS} > <WzConfigurationSettingsListSelector diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index 759d688929..c5df2f7aa8 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -41,7 +41,7 @@ export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => title="Main settings" description="Configuration for the Office 365 module" currentConfig={wodleConfiguration} - minusHeight={agent.id === '000' ? 260 : 320} + minusHeight={agent.id === '000' ? 370 : 320} helpLinks={HELP_LINKS} > <WzConfigurationSettingsGroup config={wodleConfiguration[OFFICE_365]} items={mainSettings} /> From 3771a2cabf1d675e28b21794f035de6ebd8a27aa Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 10 Aug 2021 11:03:48 +0200 Subject: [PATCH 249/493] Error in management configuration --- .../components/management/configuration/configuration-switch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/controllers/management/components/management/configuration/configuration-switch.js b/public/controllers/management/components/management/configuration/configuration-switch.js index 8540fd0073..ea1f4e3551 100644 --- a/public/controllers/management/components/management/configuration/configuration-switch.js +++ b/public/controllers/management/components/management/configuration/configuration-switch.js @@ -156,7 +156,7 @@ class WzConfigurationSwitch extends Component { this.setState({ loadingOverview: true }); const masterNodeInfo = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=000'}}); this.setState({ - masterNodeInfo: masterNodeInfo.data.affected_items[0] + masterNodeInfo: masterNodeInfo.data.data.affected_items[0] }); this.setState({ loadingOverview: false }); }catch(error){ From 3f23b6fa31f6485ed684086a8fdbdf15a24e26cb Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 10 Aug 2021 11:17:22 +0200 Subject: [PATCH 250/493] Adding Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb4d60ec1..8ee6d34262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) +- Fixed error in /Management/Configuration [#3553](https://github.com/wazuh/wazuh-kibana-app/pull/3553) + ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 1eab42b9a306cdb1ec07b3f51e479c5d6293056c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Tue, 10 Aug 2021 11:19:47 +0200 Subject: [PATCH 251/493] Update CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee6d34262..9beae6c75c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,8 +59,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) -- Fixed error in /Management/Configuration [#3553](https://github.com/wazuh/wazuh-kibana-app/pull/3553) - +- Fixed error in /Management/Configuration when cluster is disabled[#3553](https://github.com/wazuh/wazuh-kibana-app/pull/3553) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 283eb78700261118e2f5fde1357b8cf62d60f722 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 10 Aug 2021 11:21:49 +0200 Subject: [PATCH 252/493] fix(changelog): Added space separator between change description and PR link --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9beae6c75c..121be7e1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) -- Fixed error in /Management/Configuration when cluster is disabled[#3553](https://github.com/wazuh/wazuh-kibana-app/pull/3553) +- Fixed error in /Management/Configuration when cluster is disabled [#3553](https://github.com/wazuh/wazuh-kibana-app/pull/3553) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 93bbccec85639c45a78ad7d75950078a4dce01d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 10 Aug 2021 11:39:14 +0200 Subject: [PATCH 253/493] fix(module_github): Add module views and configuration viewers - Add the module views for the module - Add module configuration viewer for the `Panel` tab - Replaced as the credentials are redenred in (Management/Agent)/Configuration - Replaced the description for the `enabled` setting --- .../common/modules/modules-defaults.js | 14 +- .../overview/github-panel/github-panel.tsx | 11 +- .../overview/github-panel/views/stats.tsx | 210 ++++-------------- .../configuration/github/github.tsx | 48 ++-- 4 files changed, 78 insertions(+), 205 deletions(-) diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 3286afe27b..f7887567ad 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -19,7 +19,8 @@ import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intel import { ComplianceTable } from '../../overview/compliance-table'; import ButtonModuleExploreAgent from '../../../controllers/overview/components/overview-actions/overview-actions'; import { ButtonModuleGenerateReport } from '../modules/buttons'; -import { OfficePanel } from '../../overview/office-panel' +import { OfficePanel } from '../../overview/office-panel'; +import { GitHubPanel } from '../../overview/github-panel'; const DashboardTab = { id: 'dashboard', name: 'Dashboard', buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], component: Dashboard}; const EventsTab = { id: 'events', name: 'Events', buttons: [ButtonModuleExploreAgent], component: Events }; @@ -62,12 +63,16 @@ export const ModulesDefaults = { buttons: ['settings'], availableFor: ['manager','agent'] }, - office: { init: 'dashboard', tabs: [{ id: 'inventory', name: 'Panel', buttons: [ButtonModuleExploreAgent], component: OfficePanel }, DashboardTab, EventsTab], availableFor: ['manager'] }, + github: { + init: 'dashboard', + tabs: [{ id: 'inventory', name: 'Panel', buttons: [ButtonModuleExploreAgent], component: GitHubPanel }, DashboardTab, EventsTab], + availableFor: ['manager', 'agent'] + }, ciscat: { init: 'dashboard', tabs: [DashboardTab, EventsTab], @@ -134,11 +139,6 @@ export const ModulesDefaults = { tabs: RegulatoryComplianceTabs, availableFor: ['manager','agent'] }, - github: { - init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Panel' }, { id: 'dashboard', name: 'Dashboard' }, { id: 'events', name: 'Events' }], - buttons: ['reporting'] - }, syscollector: { notModule: true }, diff --git a/public/components/overview/github-panel/github-panel.tsx b/public/components/overview/github-panel/github-panel.tsx index 45a76c9303..3c46dc1335 100644 --- a/public/components/overview/github-panel/github-panel.tsx +++ b/public/components/overview/github-panel/github-panel.tsx @@ -11,24 +11,19 @@ * Find more information about this on the LICENSE file. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { CustomSearchBar } from '../../common/custom-search-bar'; -import { Stats } from './views'; -import { queryConfig } from '../../../react-services/query-config'; +import { ModuleConfiguration } from './views'; import { ModuleConfig, filtersValues } from './config'; export const GitHubPanel = withErrorBoundary(() => { - - const [moduleStatsList, setModuleStatsList] = useState([]); - - return ( <> <CustomSearchBar filtersValues={filtersValues} /> <MainPanel moduleConfig={ModuleConfig} tab={'github'} - sidePanelChildren={<Stats listItems={moduleStatsList} />} /> + sidePanelChildren={<ModuleConfiguration />} /> </> ) }); diff --git a/public/components/overview/github-panel/views/stats.tsx b/public/components/overview/github-panel/views/stats.tsx index 956512b22b..e321fd3011 100644 --- a/public/components/overview/github-panel/views/stats.tsx +++ b/public/components/overview/github-panel/views/stats.tsx @@ -10,180 +10,48 @@ * Find more information about this on the LICENSE file. */ -import React, { useEffect, useMemo } from 'react'; -import { EuiDescriptionList, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiCallOut, EuiIcon, EuiSpacer, EuiProgress, EuiAccordion, EuiText } from '@elastic/eui'; -import { WzRequest } from '../../../../react-services'; -import { connect } from 'react-redux'; -import { useAsyncAction } from '../../../common/hooks'; -import { withGuard } from '../../../common/hocs'; -import { compose } from 'redux'; -import { getErrorOrchestrator } from '../../../../react-services/common-services'; -import { - UI_ERROR_SEVERITIES, - UIErrorLog, - UIErrorSeverity, - UILogLevel, -} from '../../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; -const mapStateToProps = state => ({ - agent: state.appStateReducers.currentAgentData -}); - -export const Stats = connect(mapStateToProps)( -({ agent }) => { - const asyncAction = useAsyncAction((async () => { - try{ - if(agent.id){ - // Get module configuration for the pinned agent - const agentConfigurationResponse = await WzRequest.apiReq('GET', `/agents/${agent.id}/config/wmodules/wmodules`, {}); - const configuration = mapWModuleConfigurationToRenderProperties(agentConfigurationResponse.data.data.wmodules, 'github', 'Agent', agent.name); - return configuration ? [configuration] : null; - }else{ - const custerStatusResponse = await WzRequest.apiReq('GET', '/cluster/status', {}); - if (custerStatusResponse.data.data.enabled === 'yes' && custerStatusResponse.data.data.running === 'yes') { - // Cluster mode - // Get cluster nodes - const nodesResponse = await WzRequest.apiReq('GET', `/cluster/nodes`, {}); - // Get module configuration for each node - const nodesConfigurationResponses = await Promise.all(nodesResponse.data.data.affected_items.map(async node => await WzRequest.apiReq('GET', `/cluster/${node.name}/configuration/wmodules/wmodules`, {}))) - const nodeConfigurations = nodesConfigurationResponses.map((response, index) => { - const configuration = mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'github', 'Manager', nodesResponse.data.data.affected_items[index].name); - return configuration || null; - }).filter(nodeConfiguration => nodeConfiguration); - return nodeConfigurations.length ? nodeConfigurations : null; - } else { - // Manager mode - const managerConfigurationResponse = await WzRequest.apiReq('GET', `/manager/configuration/wmodules/wmodules`, {}); - const configuration = mapWModuleConfigurationToRenderProperties(managerConfigurationResponse.data.data.affected_items[0].wmodules, 'github', 'Manager'); - return configuration ? [configuration] : null; - }; - } - }catch(error){ - const options: UIErrorLog = { - context: `${Stats.name}.getModuleConfig`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: 'Module Unavailable', - }, - }; - getErrorOrchestrator().handleError(options); - throw error; // This lets to populate the 'asyncAction.error' property - } - }), [agent]); - - useEffect(() => { - asyncAction.run(); - },[asyncAction.run]); - - return ( - <> - <EuiFlexGroup> - <EuiFlexItem className='wz-justify-center' grow={false}> - <EuiIcon type='logoGithub' /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size='s'> - <h4 className='office-stats-title'>GitHub</h4> - </EuiTitle> - <EuiTitle size='xs'> - <h5 className='office-stats-subtitle'>Module configuration</h5> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - <ConfigurationWrapper configurations={asyncAction.data} mappers={configurationMapToDescriptionList} loading={asyncAction.running} error={asyncAction.error}/> - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -}); - -const ConfigurationWrapper = compose( - withGuard(({loading}) => loading, () => <EuiProgress size='xs' color='primary' />), - withGuard(({error}) => error, ({error}) => ( - <EuiCallOut className='office-stats-callout-warning' - title="Error fetching the module configuration" - color="danger" - iconType="alert" - > - {error && error.message || String(error)} - </EuiCallOut> - )), - withGuard(({configurations}) => !configurations, () => ( - <EuiCallOut className='office-stats-callout-warning' - title='Module configuration unavailable' - color='warning' - iconType='alert' - /> - )) -)(({configurations, mappers}) => { - return ( - <div style={{height: 'calc(100vh - 175px)', overflow: 'scroll'}}> - {configurations.length === 1 - ? <> - <EuiText>{configurations[0].entity}{configurations[0].name ? ` - ${configurations[0].name}` : ''}</EuiText> - <EuiSpacer size='s'/> - <Configuration configuration={configurations[0].configuration} mappers={mappers}/> - </> - : configurations.map((configuration) => ( - <EuiAccordion id={`module_configuration_${configuration.name}`} key={`module_configuration_${configuration.name}`} buttonContent={`${configuration.entity}${configuration.name ? ` - ${configuration.name}` : ''}`}> - <Configuration {...configuration} mappers={mappers}/> - </EuiAccordion> - )).reduce((prev, cur) => [prev, <div style={{marginTop: '8px'}} /> , cur]) - } - </div> - ) -}); - -const Configuration = ({ configuration, mappers }) => { - const listItems = useMemo(() => { - return Object.entries(configuration) - .map(([key, value]) => ({title: mappers?.[key]?.title || key, description: mappers?.[key]?.description ? mappers[key].description(value) : value})); - }, [configuration]); - return ( - <> - <EuiDescriptionList className='office-description-list' listItems={listItems} compressed /> - </> - ) -} - -const configurationMapToDescriptionList: {[key: string]: {title?: string, description?: (value) => any}} = { - enabled: { - title: 'Enabled' - }, - only_future_events: { - title: 'Collect events generated since Wazuh agent was started', - }, - time_delay: { - title: 'Time in seconds that each scan will monitor until that delay backwards' - }, - curl_max_size: { - title: 'Maximum size allowed for the GitHub API response' - }, - interval: { - title: 'Interval between GitHub wodle executions in seconds' - }, - event_type: { - title: 'Event type' - }, - api_auth: { - title: 'Organizations', - description: (value) => ( - <> - {value.map(v => <EuiDescriptionList key={`api_auth_${v.org_name}`}>{v.org_name}</EuiDescriptionList>)} - </> - ) - } -}; - -const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any}[], wmoduleID: string, entity: string, name = '') => { +import React from 'react'; +import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; +import { PanelModuleConfiguration } from '../../../common/modules/panel'; +import { renderValueNoThenEnabled } from '../../../../controllers/management/components/management/configuration/utils/utils'; + + +const settings = [ + { field: 'enabled', label: 'Service status', render: renderValueNoThenEnabled }, + { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started'}, + { field: 'time_delay', label: 'Time in seconds that each scan will monitor until that delay backwards'}, + { field: 'curl_max_size', label: 'Maximum size allowed for the GitHub API response'}, + { field: 'interval', label: 'Interval between GitHub wodle executions in seconds'}, + { field: 'event_type', label: 'Event type'}, + { field: 'api_auth', label: 'Credentials', render: (value) => value.map(v => + <EuiPanel paddingSize='s' key={`module_configuration_api_auth_${v.org_name}_${v.client_id}`}> + <EuiDescriptionList listItems={[ + {title: 'Organization', description: v.org_name}, + {title: 'Token', description: v.api_token} + ].filter(item => typeof item.description !== 'undefined')}/> + </EuiPanel> + )} +]; + +const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any}[], wmoduleID: string, entity: string, name: string = '') => { const configuration = wmodules.find(wmodule => Object.keys(wmodule)[0] === wmoduleID); return configuration ? {entity, name, configuration: configuration[Object.keys(configuration)[0]]} : null; }; + +export const ModuleConfiguration = props => <PanelModuleConfiguration + moduleTitle='GitHub' + moduleIconType='logoGithub' + settings={settings} + configurationAPIPartialPath='/wmodules/wmodules' + mapResponseConfiguration={(response, type, params) => { + return type === 'agent' + ? mapWModuleConfigurationToRenderProperties(response.data.data.wmodules, 'github', 'Agent', params.name) + : (type === 'cluster_node' + ? mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'github', 'Manager', params.name) + : mapWModuleConfigurationToRenderProperties(response.data.data.affected_items[0].wmodules, 'github', 'Manager') + ); + }} +/> diff --git a/public/controllers/management/components/management/configuration/github/github.tsx b/public/controllers/management/components/management/configuration/github/github.tsx index 3e41b215a6..0aca9c3981 100644 --- a/public/controllers/management/components/management/configuration/github/github.tsx +++ b/public/controllers/management/components/management/configuration/github/github.tsx @@ -15,19 +15,20 @@ import { EuiBasicTable } from '@elastic/eui'; import { compose } from 'redux'; import WzConfigurationSettingsTabSelector from '../util-components/configuration-settings-tab-selector'; import WzConfigurationSettingsGroup from '../util-components/configuration-settings-group'; +import WzConfigurationSettingsListSelector from '../util-components/configuration-settings-list-selector'; import WzTabSelector, { WzTabSelectorTab } from '../util-components/tab-selector'; import WzNoConfig from '../util-components/no-config'; import { isString, renderValueYesThenEnabled } from '../utils/utils'; -import { wodleBuilder } from '../utils/builders'; +import { wodleBuilder, settingsListBuilder } from '../utils/builders'; import { withGuard } from '../../../../../../components/common/hocs'; import withWzConfig from '../util-hocs/wz-config'; const sections = [{ component: 'wmodules', configuration: 'wmodules' }]; const mainSettings = [ - { field: 'enabled', label: 'Enabled', render: renderValueYesThenEnabled }, + { field: 'enabled', label: 'Service status', render: renderValueYesThenEnabled }, { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started' }, { field: 'time_delay', label: 'Time in seconds that each scan will monitor until that delay backwards' }, { field: 'curl_max_size', label: 'Maximum size allowed for the GitHub API response' }, @@ -36,7 +37,8 @@ const mainSettings = [ ]; const columns = [ - { field: 'org_name', name: 'Name' } + { field: 'org_name', label: 'Organization' }, + { field: 'api_token', label: 'Token' } ]; const helpLinks = [ @@ -65,8 +67,8 @@ export const WzConfigurationGitHub = withWzConfig(sections)(({currentConfig, upd <WzTabSelectorTab label="General"> <GeneralTab wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} {...rest}/> </WzTabSelectorTab> - <WzTabSelectorTab label="Organizations"> - <OrganizationsTab wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} {...rest}/> + <WzTabSelectorTab label="Credentials"> + <CredentialsTab wodleConfiguration={wodleConfiguration} currentConfig={currentConfig} {...rest}/> </WzTabSelectorTab> </WzTabSelector> ) @@ -83,7 +85,7 @@ const GeneralTab = tabWrapper(({agent, wodleConfiguration}) => ( title="Main settings" description="Configuration for the GitHub module" currentConfig={wodleConfiguration} - minusHeight={agent.id === '000' ? 260 : 320} + minusHeight={agent.id === '000' ? 370 : 420} //TODO: Review the minusHeight for the agent case helpLinks={helpLinks} > <WzConfigurationSettingsGroup @@ -93,16 +95,24 @@ const GeneralTab = tabWrapper(({agent, wodleConfiguration}) => ( </WzConfigurationSettingsTabSelector> )); -const OrganizationsTab = tabWrapper(({agent, wodleConfiguration}) => ( - <WzConfigurationSettingsTabSelector - title="List of organizations to auditing" - currentConfig={wodleConfiguration} - minusHeight={agent.id === '000' ? 260 : 320} - helpLinks={helpLinks} - > - <EuiBasicTable - columns={columns} - items={wodleConfiguration['github'].api_auth} - /> - </WzConfigurationSettingsTabSelector> -)); + + +const CredentialsTab = tabWrapper(({agent, wodleConfiguration}) => { + const credentials = useMemo(() => settingsListBuilder( + wodleConfiguration['github'].api_auth, + 'org_name' + ), [wodleConfiguration]); + return ( + <WzConfigurationSettingsTabSelector + title="List of organizations to auditing" + currentConfig={wodleConfiguration} + minusHeight={agent.id === '000' ? 370 : 420} //TODO: Review the minusHeight for the agent case + helpLinks={helpLinks} + > + <WzConfigurationSettingsListSelector + items={credentials} + settings={columns} + /> + </WzConfigurationSettingsTabSelector> + ) +}); From 5f0fe3fadef2a7c66abfff8269ae27934501b463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 10 Aug 2021 11:52:01 +0200 Subject: [PATCH 254/493] fix(office365_module): Replaced description for the `enabled` setting on the module - Set `Service status` and use the renderer `renderValueYesThenEnabled` --- .../office-panel/views/office-stats.tsx | 3 ++- .../components/general-tab/general-tab.tsx | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index a439ec78ae..ab33e28f2b 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -15,9 +15,10 @@ import React from 'react'; import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; import { PanelModuleConfiguration } from '../../../common/modules/panel'; import moduleLogo from '../../../../assets/office365.svg'; +import { renderValueYesThenEnabled } from '../../../../controllers/management/components/management/configuration/utils/utils'; const settings = [ - { field: 'enabled', label: 'Enabled'}, + { field: 'enabled', label: 'Service status', render: renderValueYesThenEnabled }, { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started'}, { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response'}, { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds'}, diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index c5df2f7aa8..82e7c241ec 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -21,20 +21,21 @@ export type GeneralTableProps = { wodleConfiguration: any; }; +const mainSettings = [ + { field: 'enabled', label: 'Service status', render: renderValueYesThenEnabled }, + { + field: 'only_future_events', + label: 'Collect events generated since Wazuh is initialized', + }, + { + field: 'interval', + label: + 'Interval between Office 365 wodle executions in seconds', + }, + { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, +]; + export const GeneralTab = ({ agent, wodleConfiguration }: GeneralTableProps) => { - const mainSettings = [ - { field: 'enabled', label: 'Enabled', render: renderValueYesThenEnabled }, - { - field: 'only_future_events', - label: 'Collect events generated since Wazuh is initialized', - }, - { - field: 'interval', - label: - 'Interval between Office 365 wodle executions in seconds', - }, - { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response' }, - ]; return ( <WzConfigurationSettingsTabSelector From a66209dfd9d1a145487911aa12a829a2957c8997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 10 Aug 2021 11:54:11 +0200 Subject: [PATCH 255/493] fix(module_office365): Add spacer between credentials configurations of the `Panel` tab --- public/components/overview/office-panel/views/office-stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index ab33e28f2b..511760295d 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -31,7 +31,7 @@ const settings = [ {title: 'Path file of client secret', description: v.client_secret_path}, ].filter(item => typeof item.description !== 'undefined')}/> </EuiPanel> - )}, + ).reduce((prev, cur) => [prev, <div style={{marginTop: '8px'}} /> , cur], [])}, { field: 'subscriptions', label: 'Subscriptions', render: (value) => value .map(v => <EuiDescriptionList key={`module_configuration_subscriptions_${v}`}>{v}</EuiDescriptionList>) } From 39a4bd068e04dd05afac0b4b8ffad5640b594fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Tue, 10 Aug 2021 12:40:50 +0200 Subject: [PATCH 256/493] Added a check for unsetted tabs array --- public/components/common/modules/main-agent.tsx | 2 +- public/components/common/modules/main-overview.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/common/modules/main-agent.tsx b/public/components/common/modules/main-agent.tsx index 5a02871986..d5edc31a3c 100644 --- a/public/components/common/modules/main-agent.tsx +++ b/public/components/common/modules/main-agent.tsx @@ -242,7 +242,7 @@ export class MainModuleAgent extends Component { render() { const { agent, section, selectView } = this.props; const title = this.renderTitle(); - const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); + const ModuleTabView = (this.props.tabs || []).find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> <div className='wz-module-header-agent-wrapper'> diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index a50955fb5b..8bd863539a 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -131,7 +131,7 @@ export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverv render() { const { section, selectView } = this.props; - const ModuleTabView = this.props.tabs.find(tab => tab.id === selectView); + const ModuleTabView = (this.props.tabs ||[]).find(tab => tab.id === selectView); return ( <div className={this.state.showAgentInfo ? 'wz-module wz-module-showing-agent' : 'wz-module'}> <div className={this.props.tabs && this.props.tabs.length && 'wz-module-header-nav'}> From 17b710556f99cdf93b08c67f475858d8b9ebd7c1 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Tue, 10 Aug 2021 21:08:17 +0200 Subject: [PATCH 257/493] Specify filters for Office365 search bar (#3533) * feat(filter-office365): Added specified filters to searchbar on Office365 * feat(configuration-view): Separated responsibilities of use-value-suggestion. Added new helper. * feat(configuration-view): PR comments * feat(configuration-view): Improvements TS on helper * feat(filter-office365): Replaced switch by dictionary, added another example of custom field * doc(changelog): Update * feat(filter-office365): Added options and filterByKey property for custom filters. * feat: add multi-select filter * remove DEVELOPER GUIDE * fix: no filters found incorrectly displayed * feat(filter-office365): Fixed onChange from searchBar and added onRemove handler. * feat(filter-office365): Fixed onSearch on multi-select component. added support to use query on se-value-suggestion for predefined options. * feat(filter-office365): Implement multiSelect for all filters. * feat(filter-office365): PR comments * delete combobox component and fix is one of filters Co-authored-by: Franco Charriol <francocharriol@gmail.com> Co-authored-by: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> --- CHANGELOG.md | 1 + common/constants.ts | 1 + .../custom-search-bar/components/combobox.tsx | 26 --- .../custom-search-bar/components/index.ts | 2 +- .../components/multi-select.tsx | 160 ++++++++++++++++++ .../custom-search-bar/custom-search-bar.tsx | 36 ++-- public/components/common/hooks/index.ts | 2 +- ...suggestions.ts => use-value-suggestion.ts} | 38 +++-- .../config/helpers/helper-value-suggestion.ts | 40 +++++ .../office-panel/config/search-bar-config.ts | 32 +++- 10 files changed, 276 insertions(+), 62 deletions(-) delete mode 100644 public/components/common/custom-search-bar/components/combobox.tsx create mode 100644 public/components/common/custom-search-bar/components/multi-select.tsx rename public/components/common/hooks/{use-value-suggestions.ts => use-value-suggestion.ts} (71%) create mode 100644 public/components/overview/office-panel/config/helpers/helper-value-suggestion.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f3dda99b2..74ba19ba68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) - Added configuration viewer for Module Office365 on Management > Configuration [3524](https://github.com/wazuh/wazuh-kibana-app/pull/3524) - Added base Module Panel view with Office365 setup [#3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) +- Added specifics and custom filters for Office365 search bar [#3533](https://github.com/wazuh/wazuh-kibana-app/pull/3533) - Adding Pagination and filter to drilldown tables at Office pannel [#3544](https://github.com/wazuh/wazuh-kibana-app/issues/3544). ### Changed diff --git a/common/constants.ts b/common/constants.ts index 2a01f583b4..39051f6cb4 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -346,3 +346,4 @@ export const UI_TOAST_COLOR = { WARNING: 'warning', DANGER: 'danger', }; + diff --git a/public/components/common/custom-search-bar/components/combobox.tsx b/public/components/common/custom-search-bar/components/combobox.tsx deleted file mode 100644 index c268dae6ad..0000000000 --- a/public/components/common/custom-search-bar/components/combobox.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useState, useEffect, useLayoutEffect } from 'react'; - -import { - EuiComboBox, -} from '@elastic/eui'; - -import { useValueSuggestions } from '../../hooks/use-value-suggestions' - -export const Combobox = ({ item, ...props }) => { - - const { suggestedValues, isLoading, setQuery } = useValueSuggestions(item.key) - - const comboOptions = suggestedValues.map((value,key) => ({ key:key, label:value, value:item.key})).sort((a, b) => a.label - b.label) - - return ( - <EuiComboBox - data-test-subj={`combobox-${item.key}`} - placeholder={item.key} - className={'filters-custom-combobox'} - options={comboOptions} - isLoading={isLoading} - onSearchChange={(searchValue) => { setQuery(searchValue) }} - {...props} - /> - ) -}; \ No newline at end of file diff --git a/public/components/common/custom-search-bar/components/index.ts b/public/components/common/custom-search-bar/components/index.ts index bcbbe26655..a89d0b2a31 100644 --- a/public/components/common/custom-search-bar/components/index.ts +++ b/public/components/common/custom-search-bar/components/index.ts @@ -10,4 +10,4 @@ * Find more information about this on the LICENSE file. */ -export { Combobox } from './combobox'; \ No newline at end of file +export { MultiSelect } from './multi-select'; \ No newline at end of file diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx new file mode 100644 index 0000000000..eae7923452 --- /dev/null +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -0,0 +1,160 @@ +/* + * Wazuh app - React component Multi-select + * Copyright (C) 2015-2021 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, { useEffect, useState } from 'react'; +import { + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFilterSelectItem, + EuiIcon, + EuiLoadingChart, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + FilterChecked, +} from '@elastic/eui'; +import { IValueSuggestion, useValueSuggestion } from '../../hooks'; + +const ON = 'on'; +const OFF = 'off'; + +export const MultiSelect = ({ item, onChange, selectedOptions, onRemove }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); + const { suggestedValues, isLoading, setQuery }: IValueSuggestion = useValueSuggestion( + item.key, + item?.options + ); + const [items, setItems] = useState< + { key: any; label: any; value: any; checked: FilterChecked }[] + >([]); + const [activeFilters, setActiveFilters] = useState<number>(0); + + useEffect(() => { + if (!isLoading) { + setItems( + suggestedValues + .map((value, key) => ({ + key: key, + label: value, + value: item.key, + filterByKey: item.filterByKey, + checked: OFF as FilterChecked, + })) + .sort((a, b) => a.label - b.label) + ); + } + }, [suggestedValues, isLoading]); + + useEffect(() => { + setItems( + items.map((item) => ({ + ...item, + checked: selectedOptions.find((element) => element.label === filterBy(item)) ? ON : OFF, + })) + ); + setActiveFilters(selectedOptions.length); + }, [selectedOptions]); + + const filterBy = (item) => { + return item.filterByKey ? item.key.toString() : item.label; + }; + + const toggleFilter = (item) => { + item.checked = item.checked === ON ? OFF : ON; + updateFilters(item.value); + }; + + const updateFilters = (id) => { + const selectedItems = items.filter((item) => item.checked === ON); + setActiveFilters(selectedItems.length); + if (selectedItems.length) { + onChange(selectedItems,id); + } else { + onRemove(item.key); + } + }; + + const onSearch = (selectedOptions) => { + setQuery(selectedOptions.target.value); + }; + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + setQuery(''); + }; + + const button = ( + <EuiFilterButton + iconType="arrowDown" + onClick={onButtonClick} + isSelected={isPopoverOpen} + numFilters={selectedOptions.length} + hasActiveFilters={activeFilters > 0} + numActiveFilters={activeFilters} + > + {item.placeholder} + </EuiFilterButton> + ); + + return ( + <EuiFilterGroup data-test-subj={`multiSelect-${item.key}`}> + <EuiPopover + id={`popoverMultiSelect-${item.key}`} + ownFocus + button={button} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + withTitle + > + <EuiPopoverTitle> + <EuiFieldSearch onChange={onSearch} /> + </EuiPopoverTitle> + <div className="euiFilterSelect__items"> + {items.map((item) => ( + <EuiFilterSelectItem + checked={item.checked} + key={item.key} + onClick={() => toggleFilter(item)} + showIcons={item.checked === ON} + > + {item.label} + </EuiFilterSelectItem> + ))} + {isLoading && ( + <div className="euiFilterSelect__note"> + <div className="euiFilterSelect__noteContent"> + <EuiLoadingChart size="m" /> + <EuiSpacer size="xs" /> + <p>Loading filters</p> + </div> + </div> + )} + {suggestedValues.length === 0 && ( + <div className="euiFilterSelect__note"> + <div className="euiFilterSelect__noteContent"> + <EuiIcon type="minusInCircle" /> + <EuiSpacer size="xs" /> + <p>No filters found</p> + </div> + </div> + )} + </div> + </EuiPopover> + </EuiFilterGroup> + ); +}; diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index ea752de6af..1816b43c6c 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -1,22 +1,23 @@ import React, { useState, useEffect } from 'react'; - import { Filter } from '../../../../../../src/plugins/data/public/'; import { FilterMeta, FilterState, FilterStateStore, } from '../../../../../../src/plugins/data/common'; + import { AppState } from '../../../react-services/app-state'; import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore import { KbnSearchBar } from '../../kbn-search-bar'; -import { Combobox } from './components'; -import { useFilterManager } from '../hooks'; +import { MultiSelect } from './components'; +import { useFilterManager, useIndexPattern } from '../hooks'; export const CustomSearchBar = ({ filtersValues, ...props }) => { const { filterManager, filters } = useFilterManager(); + const indexPattern = useIndexPattern(); const defaultSelectedOptions = () => { const array = []; filtersValues.forEach((item) => { @@ -27,7 +28,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }; const [avancedFiltersState, setAvancedFiltersState] = useState(false); const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptions); - const [values, setValues] = useState([]); + const [values, setValues] = useState(Array); const [selectReference, setSelectReference] = useState(''); useEffect(() => { @@ -51,11 +52,11 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { const newFilters = values.map((element) => ({ match_phrase: { [element.value]: { - query: element.label, + query: element.filterByKey ? element.key : element.label, }, }, })); - const params = values.map((item) => item.label); + const params = values.map((item) => item.filterByKey ? item.key : item.label); const meta: FilterMeta = { disabled: false, negate: false, @@ -118,24 +119,31 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { } }; - const onChange = (values: any[]) => { + const onChange = (values: any[], id: string) => { + setSelectReference(id) setValues(values); }; + const onRemove = (filter) => { + const currentFilters = filterManager.getFilters().filter((item) => item.meta.key != filter); + filterManager.removeAll(); + filterManager.addFilters(currentFilters); + refreshCustomSelectedFilter(); + }; + const getComponent = (item: any) => { - const types = { + const types: { [key: string]: object } = { default: <></>, - combobox: ( - <Combobox - id={item.key} + multiSelect: ( + <MultiSelect item={item} selectedOptions={selectedOptions[item.key] || []} onChange={onChange} - onClick={() => setSelectReference(item.key)} + onRemove={onRemove} /> ), }; - return types[item.type] || types['default']; + return types[item.type] || types.default; }; return ( @@ -145,7 +153,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { alignItems="center" style={{ margin: '0 8px' }} > - {avancedFiltersState === false + {!avancedFiltersState ? filtersValues.map((item, key) => ( <EuiFlexItem key={key}>{getComponent(item)}</EuiFlexItem> )) diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 64591f2071..e527e76a1b 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -25,4 +25,4 @@ export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; export { useEsSearch } from './use-es-search'; -export { useValueSuggestions, IValueSuggestiions } from './use-value-suggestions'; +export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; diff --git a/public/components/common/hooks/use-value-suggestions.ts b/public/components/common/hooks/use-value-suggestion.ts similarity index 71% rename from public/components/common/hooks/use-value-suggestions.ts rename to public/components/common/hooks/use-value-suggestion.ts index 3f0d2b9a60..b1c1884837 100644 --- a/public/components/common/hooks/use-value-suggestions.ts +++ b/public/components/common/hooks/use-value-suggestion.ts @@ -9,11 +9,11 @@ * * Find more information about this on the LICENSE file. */ -import { useState, useEffect } from 'react'; + +import React, { useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useIndexPattern } from '.'; +import { useIndexPattern } from '../hooks'; import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; -import React from 'react'; import { UI_ERROR_SEVERITIES, UIErrorLog, @@ -23,19 +23,37 @@ import { import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -export interface IValueSuggestiions { +export interface IValueSuggestion { suggestedValues: string[] | boolean[]; isLoading: boolean; setQuery: React.Dispatch<React.SetStateAction<string>>; } -export const useValueSuggestions = (filterField: string, type: 'string' | 'boolean' = 'string') : IValueSuggestiions => { +export const useValueSuggestion = ( + filterField: string, + options?: string[], + type: 'string' | 'boolean' = 'string' +): IValueSuggestion => { const [suggestedValues, setSuggestedValues] = useState<string[] | boolean[]>([]); const [query, setQuery] = useState<string>(''); const [isLoading, setIsLoading] = useState(true); const data = getDataPlugin(); const indexPattern = useIndexPattern(); + const getOptions = (): string[] => { + return options?.filter((element) => element.toLowerCase().includes(query.toLowerCase())) || []; + }; + + const getValueSuggestions = async (field) => { + return options + ? getOptions() + : await data.autocomplete.getValueSuggestions({ + query, + indexPattern: indexPattern as IIndexPattern, + field, + }); + }; + useEffect(() => { if (indexPattern) { setIsLoading(true); @@ -46,16 +64,10 @@ export const useValueSuggestions = (filterField: string, type: 'string' | 'boole aggregatable: true, } as IFieldType; try { - setSuggestedValues( - await data.autocomplete.getValueSuggestions({ - query, - indexPattern: indexPattern as IIndexPattern, - field, - }) - ); + setSuggestedValues(await getValueSuggestions(field)); } catch (error) { const options: UIErrorLog = { - context: `${useValueSuggestions.name}.valueSuggestions`, + context: `${useValueSuggestion.name}.getValueSuggestions`, level: UI_LOGGER_LEVELS.ERROR as UILogLevel, severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, error: { diff --git a/public/components/overview/office-panel/config/helpers/helper-value-suggestion.ts b/public/components/overview/office-panel/config/helpers/helper-value-suggestion.ts new file mode 100644 index 0000000000..b93423ecd5 --- /dev/null +++ b/public/components/overview/office-panel/config/helpers/helper-value-suggestion.ts @@ -0,0 +1,40 @@ +/* + * Wazuh app - React helper of 0hook for getting value suggestions + * Copyright (C) 2015-2021 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. + */ + +/** UserTypes Office365 module filter + * https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#user-type + */ +const OFFICE_365_USER_TYPE: string[] = [ + '[0] Regular', + '[1] Reserved', + '[2] Admin', + '[3] DcAdmin', + '[4] System', + '[5] Application', + '[6] ServicePrincipal', + '[7] CustomPolicy', + '[8] SystemPolicy', +]; + +/** UserTypes Office365 module filter + * https://docs.microsoft.com/en-us/office/office-365-management-api/office-365-management-activity-api-schema#auditlogscope + */ +const OFFICE_365_AUDIT_LOG_SCOPE: string[] = ['[0] Online', '[1] Onprem']; + +const dataFields = { + 'data.office365.UserType': OFFICE_365_USER_TYPE, + 'data.office365.AuditLogScope': OFFICE_365_AUDIT_LOG_SCOPE, +}; + +export const getCustomValueSuggestion = (fieldName: string): string[] => { + return dataFields[fieldName] || []; +}; diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 0d3641be40..4829f60dc0 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -11,17 +11,35 @@ * Find more information about this on the LICENSE file. */ -export const filtersValues: { type: string; key: string }[] = [ +import { getCustomValueSuggestion } from './helpers/helper-value-suggestion'; + +export const filtersValues: { + type: string; + key: string; + placeholder: string; + filterByKey?: boolean; + options?: string[]; +}[] = [ + { + type: 'multiSelect', + key: 'data.office365.Subscription', + placeholder: 'Subscription', + }, { - type: 'combobox', - key: 'agent.id', + type: 'multiSelect', + key: 'data.office365.UserType', + placeholder: 'User Type', + filterByKey: true, + options: getCustomValueSuggestion('data.office365.UserType'), }, { - type: 'combobox', - key: 'agent.name', + type: 'multiSelect', + key: 'data.office365.ResultStatus', + placeholder: 'Result Status', }, { - type: 'combobox', - key: 'agent.ip', + type: 'multiSelect', + key: 'data.office365.ClientIP', + placeholder: 'Client IP', }, ]; From 3af3b95c5dc9c5d1f2c935182ff56194107a60a3 Mon Sep 17 00:00:00 2001 From: Franco Charriol <francocharriol@gmail.com> Date: Tue, 10 Aug 2021 16:33:54 -0300 Subject: [PATCH 258/493] feat: replace combobox with multi select --- .../github-panel/config/search-bar-config.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/public/components/overview/github-panel/config/search-bar-config.ts b/public/components/overview/github-panel/config/search-bar-config.ts index 5aaf19f202..1dc9290d9d 100644 --- a/public/components/overview/github-panel/config/search-bar-config.ts +++ b/public/components/overview/github-panel/config/search-bar-config.ts @@ -15,19 +15,23 @@ export const filtersValues = [ { - type: 'combobox', + type: 'multiSelect', key: 'data.github.actor', + placeholder: 'Actor', }, { - type: 'combobox', + type: 'multiSelect', key: 'data.github.org', + placeholder: 'Organization', }, { - type: 'combobox', + type: 'multiSelect', key: 'data.github.repo', + placeholder: 'Repository', }, { - type: 'combobox', + type: 'multiSelect', key: 'data.github.action', + placeholder: 'Action', }, ]; From 1f4afe03b6ddb00a3c6ebd1883db429443b4440b Mon Sep 17 00:00:00 2001 From: Franco Charriol <francocharriol@gmail.com> Date: Tue, 10 Aug 2021 17:10:18 -0300 Subject: [PATCH 259/493] fix: multi select filters dont works properly --- .../common/hooks/use-value-suggestion.ts | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/public/components/common/hooks/use-value-suggestion.ts b/public/components/common/hooks/use-value-suggestion.ts index 1d01369a97..42d8d57f55 100644 --- a/public/components/common/hooks/use-value-suggestion.ts +++ b/public/components/common/hooks/use-value-suggestion.ts @@ -22,7 +22,6 @@ import { } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { useFilterManager } from '.'; export interface IValueSuggestion { suggestedValues: string[] | boolean[]; @@ -40,7 +39,7 @@ export const useValueSuggestion = ( const [isLoading, setIsLoading] = useState(true); const data = getDataPlugin(); const indexPattern = useIndexPattern(); - const { filters } = useFilterManager(); + //const { filters } = useFilterManager(); const getOptions = (): string[] => { return options?.filter((element) => element.toLowerCase().includes(query.toLowerCase())) || []; @@ -57,23 +56,6 @@ export const useValueSuggestion = ( }; useEffect(() => { - const boolFilter = filters - .filter( - (managedFilter) => - managedFilter && - managedFilter.query && - managedFilter.query.match && - Object.keys(managedFilter.query.match)[0] !== filterField - ) - .map((managedFilter) => { - return { - term: { - [Object.keys(managedFilter.query.match)[0]]: - managedFilter.query.match[Object.keys(managedFilter.query.match)[0]].query, - }, - }; - }); - if (indexPattern) { setIsLoading(true); (async () => { @@ -101,7 +83,7 @@ export const useValueSuggestion = ( } })(); } - }, [indexPattern, query, filterField, type, filters]); + }, [indexPattern, query, filterField, type]); return { suggestedValues, isLoading, setQuery }; }; From 27bc4ce0f5b4d42da8597c41ed9d650ad927073e Mon Sep 17 00:00:00 2001 From: Franco Charriol <francocharriol@gmail.com> Date: Tue, 10 Aug 2021 17:18:53 -0300 Subject: [PATCH 260/493] fix: drilldown tables wrong displayed --- .../github-panel/config/drilldown-action.tsx | 44 +++++++++++++------ .../github-panel/config/drilldown-actor.tsx | 44 +++++++++++++------ .../config/drilldown-organization.tsx | 44 +++++++++++++------ .../config/drilldown-repository.tsx | 44 +++++++++++++------ 4 files changed, 124 insertions(+), 52 deletions(-) diff --git a/public/components/overview/github-panel/config/drilldown-action.tsx b/public/components/overview/github-panel/config/drilldown-action.tsx index be52e018aa..d0ebba6dd7 100644 --- a/public/components/overview/github-panel/config/drilldown-action.tsx +++ b/public/components/overview/github-panel/config/drilldown-action.tsx @@ -15,7 +15,6 @@ import { VisCard } from '../../../common/modules/panel/'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; - export const DrilldownConfigAction = { rows: [ { @@ -23,30 +22,40 @@ export const DrilldownConfigAction = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actors" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Repositories" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Organizations" tab="github" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Countries" tab="github" {...props} /> + ), }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Alert-Level-Evolution" tab="github" {...props} /> + ), }, - ] + ], }, { columns: [ @@ -55,14 +64,23 @@ export const DrilldownConfigAction = { component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts - initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.repo', 'data.github.actor', 'rule.level', 'rule.id']} + <SecurityAlerts + initialColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.description' }, + { field: 'data.github.org', label: 'Organization' }, + { field: 'data.github.repo', label: 'Repository' }, + { field: 'data.github.actor', label: 'Actor' }, + { field: 'rule.level' }, + { field: 'rule.id' }, + ]} /> </EuiPanel> </EuiFlexItem> - ) + ), }, - ] + ], }, - ] + ], }; diff --git a/public/components/overview/github-panel/config/drilldown-actor.tsx b/public/components/overview/github-panel/config/drilldown-actor.tsx index 4505d20b29..539b0c3736 100644 --- a/public/components/overview/github-panel/config/drilldown-actor.tsx +++ b/public/components/overview/github-panel/config/drilldown-actor.tsx @@ -15,7 +15,6 @@ import { VisCard } from '../../../common/modules/panel/'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; - export const DrilldownConfigActor = { rows: [ { @@ -23,30 +22,40 @@ export const DrilldownConfigActor = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actions" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Repositories" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Organizations" tab="github" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Countries" tab="github" {...props} /> + ), }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Alert-Level-Evolution" tab="github" {...props} /> + ), }, - ] + ], }, { columns: [ @@ -55,14 +64,23 @@ export const DrilldownConfigActor = { component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts - initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.repo', 'data.github.action', 'rule.level', 'rule.id']} + <SecurityAlerts + initialColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.description' }, + { field: 'data.github.org', label: 'Organization' }, + { field: 'data.github.repo', label: 'Repository' }, + { field: 'data.github.action', label: 'Action' }, + { field: 'rule.level' }, + { field: 'rule.id' }, + ]} /> </EuiPanel> </EuiFlexItem> - ) + ), }, - ] + ], }, - ] + ], }; diff --git a/public/components/overview/github-panel/config/drilldown-organization.tsx b/public/components/overview/github-panel/config/drilldown-organization.tsx index 0557675d03..870f34c343 100644 --- a/public/components/overview/github-panel/config/drilldown-organization.tsx +++ b/public/components/overview/github-panel/config/drilldown-organization.tsx @@ -15,7 +15,6 @@ import { VisCard } from '../../../common/modules/panel/'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; - export const DrilldownConfigOrganization = { rows: [ { @@ -23,30 +22,40 @@ export const DrilldownConfigOrganization = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actions" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Repositories' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Repositories" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actors" tab="github" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Countries" tab="github" {...props} /> + ), }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Alert-Level-Evolution" tab="github" {...props} /> + ), }, - ] + ], }, { columns: [ @@ -55,14 +64,23 @@ export const DrilldownConfigOrganization = { component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts - initialColumns={["icon", "timestamp", 'rule.description', 'data.github.repo', 'data.github.actor', 'data.github.action', 'rule.level', 'rule.id']} + <SecurityAlerts + initialColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.description' }, + { field: 'data.github.repo', label: 'Repository' }, + { field: 'data.github.actor', label: 'Actor' }, + { field: 'data.github.action', label: 'Action' }, + { field: 'rule.level' }, + { field: 'rule.id' }, + ]} /> </EuiPanel> </EuiFlexItem> - ) + ), }, - ] + ], }, - ] + ], }; diff --git a/public/components/overview/github-panel/config/drilldown-repository.tsx b/public/components/overview/github-panel/config/drilldown-repository.tsx index bccf57e449..d82f62e3d5 100644 --- a/public/components/overview/github-panel/config/drilldown-repository.tsx +++ b/public/components/overview/github-panel/config/drilldown-repository.tsx @@ -15,7 +15,6 @@ import { VisCard } from '../../../common/modules/panel/'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { SecurityAlerts } from '../../../visualize/components'; - export const DrilldownConfigRepository = { rows: [ { @@ -23,30 +22,40 @@ export const DrilldownConfigRepository = { columns: [ { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actions' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actions" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Actors' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Actors" tab="github" {...props} /> + ), }, { width: 30, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Top-Ten-Organizations' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Top-Ten-Organizations" tab="github" {...props} /> + ), }, - ] + ], }, { height: 300, columns: [ { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Countries' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Countries" tab="github" {...props} /> + ), }, { width: 50, - component: (props) => <VisCard id='Wazuh-App-Overview-GitHub-Alert-Level-Evolution' tab='github' {...props} /> + component: (props) => ( + <VisCard id="Wazuh-App-Overview-GitHub-Alert-Level-Evolution" tab="github" {...props} /> + ), }, - ] + ], }, { columns: [ @@ -55,14 +64,23 @@ export const DrilldownConfigRepository = { component: () => ( <EuiFlexItem> <EuiPanel paddingSize={'s'}> - <SecurityAlerts - initialColumns={["icon", "timestamp", 'rule.description', 'data.github.org', 'data.github.actor', 'data.github.action', 'rule.level', 'rule.id']} + <SecurityAlerts + initialColumns={[ + { field: 'icon' }, + { field: 'timestamp' }, + { field: 'rule.description' }, + { field: 'data.github.org', label: 'Organization' }, + { field: 'data.github.actor', label: 'Actor' }, + { field: 'data.github.action', label: 'Action' }, + { field: 'rule.level' }, + { field: 'rule.id' }, + ]} /> </EuiPanel> </EuiFlexItem> - ) + ), }, - ] + ], }, - ] + ], }; From f4fbfdd197b9cbdd6e9728ac9ac5e534e3e93eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 11 Aug 2021 09:09:51 +0200 Subject: [PATCH 261/493] fix(github_module): Add reference to the module in the `README.md` file - Fix the module description in the `Modules directoy` --- README.md | 1 + common/constants.ts | 2 +- common/wazuh-modules.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da284633ba..53b468b971 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This plugin for Kibana allows you to visualize and analyze Wazuh alerts stored i - Integrity monitoring: Alerts related to file changes, including permissions, content, ownership and attributes. - Amazon AWS: Security events related to your Amazon AWS services, collected directly via AWS API. - Office 365: Security events related to your Office 365 services. + - GitHub: Security events related to your GitHub organizations, collected via GitHub audit logs API. - Google Cloud Platform: Security events related to your Google Cloud Platform services, collected directly via GCP API. - Auditing and Policy Monitoring - Policy monitoring: Verify that your systems are configured according to your security policies baseline. diff --git a/common/constants.ts b/common/constants.ts index d6fb36513d..c87493b31a 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -71,7 +71,7 @@ export const WAZUH_SAMPLE_ALERTS_CATEGORIES_TYPE_ALERTS = { { apache: true, alerts: 2000 }, { web: true }, { windows: { service_control_manager: true }, alerts: 1000 }, - { github: true} + { github: true } ], [WAZUH_SAMPLE_ALERTS_CATEGORY_AUDITING_POLICY_MONITORING]: [ { rootcheck: true }, diff --git a/common/wazuh-modules.ts b/common/wazuh-modules.ts index 590e547ca1..b1dd44453d 100644 --- a/common/wazuh-modules.ts +++ b/common/wazuh-modules.ts @@ -126,7 +126,7 @@ export const WAZUH_MODULES = { github: { title: 'GitHub', description: - 'Monitoring events from audit logs that helps detect threats targeting your GitHub organizations.' + 'Monitoring events from audit logs of your GitHub organizations.' }, devTools: { title: 'API console', From 6e16da68f46223aca90ad2b928e8f121514a233d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 11 Aug 2021 09:50:03 +0200 Subject: [PATCH 262/493] Removed duplicate entries in changelog --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ba19ba68..eacb00b025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,12 +36,6 @@ All notable changes to the Wazuh app project will be documented in this file. [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) -- Added try catch strategy with ErrorOrchestrator service on User section [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) -- Added try catch strategy with ErrorOrchestrator service on WzLogs and documentation for ErrorOrchestrator [#3373](https://github.com/wazuh/wazuh-kibana-app/pull/3373) -- Added try catch strategy with ErrorOrchestrator service on Management > Ruleset [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) -- Added try catch strategy with ErrorOrchestrator service on Overview [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) -- Added try catch strategy with ErrorOrchestrator service on ManagementController [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) -- Added try catch strategy with ErrorOrchestrator service on FIM & SCA sections [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) From b9fd01d25431094ec593c9f2591faa75cc268341 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 11 Aug 2021 09:56:38 +0200 Subject: [PATCH 263/493] fix selector usertype --- .../common/custom-search-bar/components/multi-select.tsx | 6 +----- .../common/custom-search-bar/custom-search-bar.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index eae7923452..f35d92d5da 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -59,16 +59,12 @@ export const MultiSelect = ({ item, onChange, selectedOptions, onRemove }) => { setItems( items.map((item) => ({ ...item, - checked: selectedOptions.find((element) => element.label === filterBy(item)) ? ON : OFF, + checked: selectedOptions.find((element) => element.label === item.label) ? ON : OFF, })) ); setActiveFilters(selectedOptions.length); }, [selectedOptions]); - const filterBy = (item) => { - return item.filterByKey ? item.key.toString() : item.label; - }; - const toggleFilter = (item) => { item.checked = item.checked === ON ? OFF : ON; updateFilters(item.value); diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 1816b43c6c..158cc0f051 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { KbnSearchBar } from '../../kbn-search-bar'; import { MultiSelect } from './components'; import { useFilterManager, useIndexPattern } from '../hooks'; +import { getCustomValueSuggestion } from '../../../components/overview/office-panel/config/helpers/helper-value-suggestion'; export const CustomSearchBar = ({ filtersValues, ...props }) => { const { filterManager, filters } = useFilterManager(); @@ -56,7 +57,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { }, }, })); - const params = values.map((item) => item.filterByKey ? item.key : item.label); + const params = values.map((item) => item.filterByKey ? item.key.toString() : item.label); const meta: FilterMeta = { disabled: false, negate: false, @@ -104,8 +105,12 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { .map((element) => ({ params: element.meta.params, key: element.meta.key })) || []; const getFilterCustom = (item) => { - return item.params.map((element) => ({ label: element, value: item.key })); + return item.params.map((element) => ({ checked: 'on', label: item.key === 'data.office365.UserType' ? getLabelUserType(element) : element, value: item.key, key: element, filterByKey: item.key === 'data.office365.UserType' ? true : false})); }; + const getLabelUserType = (element) => { + const userTypeOptions = getCustomValueSuggestion('data.office365.UserType') + return userTypeOptions.find((item,index) => index.toString() === element) + } const filterCustom = filters.map((item) => getFilterCustom(item)) || []; if (filterCustom.length != 0) { filterCustom.forEach((item) => { From 0affc6e5f750b92c74a5f661872838766393e8a7 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 11 Aug 2021 10:14:08 +0200 Subject: [PATCH 264/493] [Module] [Office365] Add `rule.level` and `rule.id` fields to alerts table of each drill down view (#3560) * fix(module_office365): Add `rule.level` and `rule.id` field to drilldown tables in the `Panel` tab * fix(module_office365): Move the column of `rule.description` in the alerts table of the Panel drill down views --- .../overview/office-panel/config/drilldown-ip-config.tsx | 4 +++- .../office-panel/config/drilldown-operations-config.tsx | 4 +++- .../overview/office-panel/config/drilldown-rules-config.tsx | 3 +++ .../overview/office-panel/config/drilldown-user-config.tsx | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/public/components/overview/office-panel/config/drilldown-ip-config.tsx b/public/components/overview/office-panel/config/drilldown-ip-config.tsx index 0ba03931f5..357e69f7eb 100644 --- a/public/components/overview/office-panel/config/drilldown-ip-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-ip-config.tsx @@ -72,9 +72,11 @@ export const drilldownIPConfig = { initialColumns={[ { field: 'icon' }, { field: 'timestamp' }, - { field: 'data.office365.UserId', label: 'User ID' }, { field: 'rule.description', label: 'Description' }, + { field: 'data.office365.UserId', label: 'User ID' }, { field: 'data.office365.Operation', label: 'Operation' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} useAgentColumns={false} /> diff --git a/public/components/overview/office-panel/config/drilldown-operations-config.tsx b/public/components/overview/office-panel/config/drilldown-operations-config.tsx index 70a50efdb4..49ead49eb3 100644 --- a/public/components/overview/office-panel/config/drilldown-operations-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-operations-config.tsx @@ -61,9 +61,11 @@ export const drilldownOperationsConfig = { initialColumns={[ { field: 'icon' }, { field: 'timestamp' }, + { field: 'rule.description', label: 'Description' }, { field: 'data.office365.UserId', label: 'User ID' }, { field: 'data.office365.ClientIP', label: 'Client IP' }, - { field: 'rule.description', label: 'Description' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} useAgentColumns={false} /> diff --git a/public/components/overview/office-panel/config/drilldown-rules-config.tsx b/public/components/overview/office-panel/config/drilldown-rules-config.tsx index 0cc4aac3c9..5c5a648127 100644 --- a/public/components/overview/office-panel/config/drilldown-rules-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-rules-config.tsx @@ -67,9 +67,12 @@ export const drilldownRulesConfig = { initialColumns={[ { field: 'icon' }, { field: 'timestamp' }, + { field: 'rule.description', label: 'Description' }, { field: 'data.office365.UserId', label: 'User ID' }, { field: 'data.office365.ClientIP', label: 'Client IP' }, { field: 'data.office365.Operation', label: 'Operation' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} useAgentColumns={false} /> diff --git a/public/components/overview/office-panel/config/drilldown-user-config.tsx b/public/components/overview/office-panel/config/drilldown-user-config.tsx index cc0ec9fd5a..db19f1dd04 100644 --- a/public/components/overview/office-panel/config/drilldown-user-config.tsx +++ b/public/components/overview/office-panel/config/drilldown-user-config.tsx @@ -72,9 +72,11 @@ export const drilldownUserConfig = { initialColumns={[ { field: 'icon' }, { field: 'timestamp' }, - { field: 'data.office365.ClientIP', label: 'Client IP' }, { field: 'rule.description', label: 'Description' }, + { field: 'data.office365.ClientIP', label: 'Client IP' }, { field: 'data.office365.Operation', label: 'Operation' }, + { field: 'rule.level', label: 'Level' }, + { field: 'rule.id', label: 'Rule ID' }, ]} useAgentColumns={false} /> From 2c8874432c17c43b73421739cde9baad754980a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 11 Aug 2021 10:31:16 +0200 Subject: [PATCH 265/493] Removed Client Ip filter from the Office search bar --- .../overview/office-panel/config/search-bar-config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/public/components/overview/office-panel/config/search-bar-config.ts b/public/components/overview/office-panel/config/search-bar-config.ts index 4829f60dc0..c1a3c68d99 100644 --- a/public/components/overview/office-panel/config/search-bar-config.ts +++ b/public/components/overview/office-panel/config/search-bar-config.ts @@ -37,9 +37,4 @@ export const filtersValues: { key: 'data.office365.ResultStatus', placeholder: 'Result Status', }, - { - type: 'multiSelect', - key: 'data.office365.ClientIP', - placeholder: 'Client IP', - }, ]; From 4da3847b6f2bb51423fa5583884b87f1ed136d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 11 Aug 2021 12:36:25 +0200 Subject: [PATCH 266/493] Updated snapshots --- .../__snapshots__/SubscriptionTab.test.tsx.snap | 2 +- .../__snapshots__/api-auth-tab.test.tsx.snap | 2 +- .../__snapshots__/general-tab.test.tsx.snap | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap index 22d25c3d74..56526b6435 100644 --- a/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/SubscriptionTab/__snapshots__/SubscriptionTab.test.tsx.snap @@ -69,7 +69,7 @@ exports[`SubscriptionTab component renders correctly to match the snapshot 1`] = }, ] } - minusHeight={260} + minusHeight={370} title="Subscriptions list" > <WzConfigurationSettingsHeader diff --git a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap index ef4df7e55d..6224f0217b 100644 --- a/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/api-auth-tab/__snapshots__/api-auth-tab.test.tsx.snap @@ -79,7 +79,7 @@ exports[`ApiAuthTab component renders correctly to match the snapshot 1`] = ` }, ] } - minusHeight={260} + minusHeight={370} title="Credentials for the authentication with the API" > <WzConfigurationSettingsHeader diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index f85a1190c6..d201ad64fa 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -70,7 +70,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, ] } - minusHeight={260} + minusHeight={370} title="Main settings" > <WzConfigurationSettingsHeader @@ -427,7 +427,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` Array [ Object { "field": "enabled", - "label": "Enabled", + "label": "Service status", "render": [Function], }, Object { @@ -508,9 +508,9 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` className="euiFlexItem" > <WzConfigurationSetting - key="-Enabled-undefined-0" - keyItem="-Enabled-undefined-0" - label="Enabled" + key="-Service status-undefined-0" + keyItem="-Service status-undefined-0" + label="Service status" value="enabled" > <div @@ -537,7 +537,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` <div className="euiTextAlign euiTextAlign--right" > - Enabled + Service status </div> </EuiTextAlign> </div> From 7a9cbfbc641210f4e4d218442a07444c35155d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Wed, 11 Aug 2021 12:40:17 +0200 Subject: [PATCH 267/493] Removed more duplicates from changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eacb00b025..1a2dde73e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,10 +53,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Change filter from is to is one of in custom searchbar [#3529](https://github.com/wazuh/wazuh-kibana-app/pull/3529) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) - Added time subscription to Discover component [#3549](https://github.com/wazuh/wazuh-kibana-app/pull/3549) -- Refactored all try catch strategy on Settings section [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) -- Refactored all try catch strategy on Controller/Agent section [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) -- Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) -- Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) ### Fixed From 3ef979925b682cabf4b6ce8aa8d82fd029907b58 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:54:28 +0200 Subject: [PATCH 268/493] [OFFICE MODULE] Dashboard errors with pinned agent (#3556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Looking for errors * check for agentList length * check for agentList length * check for agentList length * fix: remove tooltip for agent pinned and add hoc for events in office * fix: unppin agent from botton doesn't work * fix(module_office): Avoid building the agents visualizations for the Office 365 module - Moved the handle error to upper function instead of the service `VisFactoryHandler` * Resolving unpin agent in office panel Co-authored-by: Franco Charriol <francocharriol@gmail.com> Co-authored-by: Antonio David Gutiérrez <antonio.gutierrez@wazuh.com> Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> --- .../prompts/prompt_module_not_for_agent.tsx | 12 +- public/components/common/hocs/index.ts | 1 + .../common/hocs/with_module_not_for_agent.tsx | 21 ++- .../common/modules/modules-defaults.js | 139 +++++++++++++----- .../common/welcome/agents-welcome.js | 6 +- .../overview-actions/overview-actions.js | 127 ++++++++-------- public/controllers/overview/overview.js | 72 +++++---- public/react-services/vis-factory-handler.js | 24 +-- public/services/vis-factory-handler.js | 2 +- server/controllers/wazuh-elastic.ts | 3 + 10 files changed, 242 insertions(+), 165 deletions(-) diff --git a/public/components/agents/prompts/prompt_module_not_for_agent.tsx b/public/components/agents/prompts/prompt_module_not_for_agent.tsx index 500b230c3d..3801c6bda0 100644 --- a/public/components/agents/prompts/prompt_module_not_for_agent.tsx +++ b/public/components/agents/prompts/prompt_module_not_for_agent.tsx @@ -19,22 +19,22 @@ import { useFilterManager } from '../../common/hooks'; type PromptSelectAgentProps = { body?: string; title: string; - agentSelectionProps: { + agentsSelectionProps: { setAgent: (agent: boolean) => void } }; -export const PromptModuleNotForAgent = ({ body, title, ...agentSelectionProps }: PromptSelectAgentProps) => { +export const PromptModuleNotForAgent = ({ body, title, agentsSelectionProps }: PromptSelectAgentProps) => { const dispatch = useDispatch(); - const filterManager = useFilterManager(); + const { filterManager, filters } = useFilterManager(); const unpinAgent = async () => { dispatch(updateCurrentAgentData({})); - await agentSelectionProps.setAgent(false); - const filters = filterManager.filters.filter(x => { + await agentsSelectionProps.setAgent(false); + const moduleFilters = filters.filter(x => { return x.meta.key !== 'agent.id'; }); - filterManager.setFilters(filters); + filterManager.setFilters(moduleFilters); }; return ( diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index 4c84b317e5..a6ba03580b 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -22,3 +22,4 @@ export * from './withAgentSupportModule'; export * from './withUserLogged'; export * from './error-boundary/with-error-boundary'; export * from './with_module_tab_loader'; +export * from './with_module_not_for_agent'; diff --git a/public/components/common/hocs/with_module_not_for_agent.tsx b/public/components/common/hocs/with_module_not_for_agent.tsx index ca362f85a1..0a58dafb9c 100644 --- a/public/components/common/hocs/with_module_not_for_agent.tsx +++ b/public/components/common/hocs/with_module_not_for_agent.tsx @@ -20,10 +20,17 @@ const mapStateToProps = (state) => ({ agent: state.appStateReducers.currentAgentData, }); -export const withModuleNotForAgent = WrappedComponent => compose( - connect(mapStateToProps), - withGuard( - ({agent}) => agent?.id, - (props) => <PromptModuleNotForAgent title='Module not avaliable for agents' body='Remove the pinned agent.' {...props}/> - ) -)(WrappedComponent); +export const withModuleNotForAgent = (WrappedComponent) => + compose( + connect(mapStateToProps), + withGuard( + ({ agent }) => agent?.id, + (props) => ( + <PromptModuleNotForAgent + title="Module not available for agents" + body="Remove the pinned agent." + {...props} + /> + ) + ) + )(WrappedComponent); diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 1b7de16433..ea70a733ba 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -19,128 +19,197 @@ import { ModuleMitreAttackIntelligence } from '../../overview/mitre_attack_intel import { ComplianceTable } from '../../overview/compliance-table'; import ButtonModuleExploreAgent from '../../../controllers/overview/components/overview-actions/overview-actions'; import { ButtonModuleGenerateReport } from '../modules/buttons'; -import { OfficePanel } from '../../overview/office-panel' +import { OfficePanel } from '../../overview/office-panel'; +import { withModuleNotForAgent } from '../hocs'; -const DashboardTab = { id: 'dashboard', name: 'Dashboard', buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], component: Dashboard}; -const EventsTab = { id: 'events', name: 'Events', buttons: [ButtonModuleExploreAgent], component: Events }; -const RegulatoryComplianceTabs = [{ id: 'inventory', name: 'Controls', buttons: [ButtonModuleExploreAgent], component: ComplianceTable }, DashboardTab, EventsTab]; +const DashboardTab = { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], + component: Dashboard, +}; +const EventsTab = { + id: 'events', + name: 'Events', + buttons: [ButtonModuleExploreAgent], + component: Events, +}; +const RegulatoryComplianceTabs = [ + { + id: 'inventory', + name: 'Controls', + buttons: [ButtonModuleExploreAgent], + component: ComplianceTable, + }, + DashboardTab, + EventsTab, +]; export const ModulesDefaults = { general: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, fim: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainFim }, DashboardTab, EventsTab], - availableFor: ['manager','agent'] + tabs: [ + { + id: 'inventory', + name: 'Inventory', + buttons: [ButtonModuleExploreAgent], + component: MainFim, + }, + DashboardTab, + EventsTab, + ], + availableFor: ['manager', 'agent'], }, aws: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'], + availableFor: ['manager', 'agent'], }, gcp: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, pm: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, audit: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, sca: { init: 'inventory', - tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainSca }, EventsTab], + tabs: [ + { + id: 'inventory', + name: 'Inventory', + buttons: [ButtonModuleExploreAgent], + component: MainSca, + }, + EventsTab, + ], buttons: ['settings'], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, - office: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Panel', buttons: [ButtonModuleExploreAgent], component: OfficePanel }, DashboardTab, EventsTab], - availableFor: ['manager'] + tabs: [ + { + id: 'inventory', + name: 'Panel', + buttons: [ButtonModuleExploreAgent], + component: withModuleNotForAgent(OfficePanel), + }, + { + id: 'dashboard', + name: 'Dashboard', + buttons: [ButtonModuleExploreAgent, ButtonModuleGenerateReport], + component: withModuleNotForAgent(Dashboard), + }, + { ...EventsTab, component: withModuleNotForAgent(Events) }, + ], + availableFor: ['manager'], }, ciscat: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, vuls: { init: 'dashboard', - tabs: [{ id: 'inventory', name: 'Inventory', buttons: [ButtonModuleExploreAgent], component: MainVuls }, DashboardTab, EventsTab], + tabs: [ + { + id: 'inventory', + name: 'Inventory', + buttons: [ButtonModuleExploreAgent], + component: MainVuls, + }, + DashboardTab, + EventsTab, + ], buttons: ['settings'], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, mitre: { init: 'dashboard', - tabs: [{ id: 'intelligence', name: 'Intelligence', component: ModuleMitreAttackIntelligence }, { id: 'inventory', name: 'Framework', buttons: [ButtonModuleExploreAgent], component: MainMitre }, DashboardTab, EventsTab], - availableFor: ['manager','agent'] + tabs: [ + { id: 'intelligence', name: 'Intelligence', component: ModuleMitreAttackIntelligence }, + { + id: 'inventory', + name: 'Framework', + buttons: [ButtonModuleExploreAgent], + component: MainMitre, + }, + DashboardTab, + EventsTab, + ], + availableFor: ['manager', 'agent'], }, virustotal: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, docker: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, pci: { init: 'dashboard', tabs: RegulatoryComplianceTabs, - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, osquery: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, oscap: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, pci: { init: 'dashboard', tabs: [DashboardTab, EventsTab], - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, hipaa: { init: 'dashboard', tabs: RegulatoryComplianceTabs, - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, nist: { init: 'dashboard', tabs: RegulatoryComplianceTabs, - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, gdpr: { init: 'dashboard', tabs: RegulatoryComplianceTabs, - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, tsc: { init: 'dashboard', tabs: RegulatoryComplianceTabs, - availableFor: ['manager','agent'] + availableFor: ['manager', 'agent'], }, syscollector: { - notModule: true + notModule: true, }, configuration: { - notModule: true + notModule: true, }, stats: { - notModule: true - } + notModule: true, + }, }; diff --git a/public/components/common/welcome/agents-welcome.js b/public/components/common/welcome/agents-welcome.js index 044d83aa45..7014ce1174 100644 --- a/public/components/common/welcome/agents-welcome.js +++ b/public/components/common/welcome/agents-welcome.js @@ -128,15 +128,15 @@ export const AgentsWelcome = withErrorBoundary (class AgentsWelcome extends Comp welcome: 8 }); const filterHandler = new FilterHandler(AppState.getCurrentPattern()); + const $injector = getAngularModule().$injector; + this.router = $injector.get('$route'); + window.addEventListener('resize', this.updateWidth); //eslint-disable-line await VisFactoryHandler.buildAgentsVisualizations( filterHandler, 'welcome', null, this.props.agent.id ); - const $injector = getAngularModule().$injector; - this.router = $injector.get('$route'); - window.addEventListener('resize', this.updateWidth); //eslint-disable-line } updateMenuAgents() { diff --git a/public/controllers/overview/components/overview-actions/overview-actions.js b/public/controllers/overview/components/overview-actions/overview-actions.js index c92715e2b8..ac8ded3403 100644 --- a/public/controllers/overview/components/overview-actions/overview-actions.js +++ b/public/controllers/overview/components/overview-actions/overview-actions.js @@ -11,7 +11,10 @@ */ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { showExploreAgentModal, updateCurrentAgentData } from '../../../../redux/actions/appStateActions'; +import { + showExploreAgentModal, + updateCurrentAgentData, +} from '../../../../redux/actions/appStateActions'; import { EuiOverlayMask, EuiOutsideClickDetector, @@ -39,30 +42,28 @@ class OverviewActions extends Component { } async removeAgentsFilter(shouldUpdate = true) { - await this.props.setAgent(false); const currentAppliedFilters = this.state.filterManager.filters; - const agentFilters = currentAppliedFilters.filter(x => { + const agentFilters = currentAppliedFilters.filter((x) => { return x.meta.key !== 'agent.id'; }); this.state.filterManager.setFilters(agentFilters); } componentDidMount() { - const { filterManager } = getDataPlugin().query; this.setState({ filterManager: filterManager }, () => { - if (this.props.initialFilter) this.agentTableSearch([this.props.initialFilter]) - if (this.props.agent.id) this.agentTableSearch([this.props.agent.id]) + if (this.props.initialFilter) this.agentTableSearch([this.props.initialFilter]); + if (this.props.agent.id) this.agentTableSearch([this.props.agent.id]); }); } - componentDidUpdate(){ - if(this.state.isAgent && !this.props.agent.id){ - this.setState({isAgent: false}) - }else if(this.props.agent.id && this.state.isAgent !== this.props.agent.id){ - this.setState({isAgent: this.props.agent.id}) + componentDidUpdate() { + if (this.state.isAgent && !this.props.agent.id) { + this.setState({ isAgent: false }); + } else if (this.props.agent.id && this.state.isAgent !== this.props.agent.id) { + this.setState({ isAgent: this.props.agent.id }); } } @@ -98,28 +99,28 @@ class OverviewActions extends Component { if (agentIdList && agentIdList.length) { if (agentIdList.length === 1) { const currentAppliedFilters = this.state.filterManager.filters; - const agentFilters = currentAppliedFilters.filter(x => { + const agentFilters = currentAppliedFilters.filter((x) => { return x.meta.key !== 'agent.id'; }); const filter = { - "meta": { - "alias": null, - "disabled": false, - "key": "agent.id", - "negate": false, - "params": { "query": agentIdList[0] }, - "type": "phrase", - "index": AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN + meta: { + alias: null, + disabled: false, + key: 'agent.id', + negate: false, + params: { query: agentIdList[0] }, + type: 'phrase', + index: AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN, }, - "query": { - "match": { + query: { + match: { 'agent.id': { query: agentIdList[0], - type: 'phrase' - } - } + type: 'phrase', + }, + }, }, - "$state": { "store": "appState", "isImplicit": true}, + $state: { store: 'appState', isImplicit: true }, }; agentFilters.push(filter); this.state.filterManager.setFilters(agentFilters); @@ -132,7 +133,12 @@ class OverviewActions extends Component { getSelectedAgents() { let selectedAgentsObject = {}; - for (var i = 0; this.state.isAgent && this.state.isAgent.length && i < this.state.isAgent.length; ++i) selectedAgentsObject[this.state.isAgent[i]] = true; + for ( + var i = 0; + this.state.isAgent && this.state.isAgent.length && i < this.state.isAgent.length; + ++i + ) + selectedAgentsObject[this.state.isAgent[i]] = true; return selectedAgentsObject; } @@ -154,7 +160,7 @@ class OverviewActions extends Component { <EuiModalBody> <AgentSelectionTable - updateAgentSearch={agentsIdList => this.agentTableSearch(agentsIdList)} + updateAgentSearch={(agentsIdList) => this.agentTableSearch(agentsIdList)} removeAgentsFilter={(shouldUpdate) => this.removeAgentsFilter(shouldUpdate)} selectedAgents={this.getSelectedAgents()} ></AgentSelectionTable> @@ -165,71 +171,70 @@ class OverviewActions extends Component { ); } - const thereAgentSelected = (this.props.agent || {}).id + const thereAgentSelected = (this.props.agent || {}).id; - const avaliableForAgent = this.props.module.availableFor && this.props.module.availableFor.includes('agent'); + const avaliableForAgent = + this.props.module.availableFor && this.props.module.availableFor.includes('agent'); let buttonUnpinAgent, buttonExploreAgent; - if(thereAgentSelected){ + if (thereAgentSelected) { buttonUnpinAgent = ( <WzButton - buttonType='icon' + buttonType="icon" className="wz-unpin-agent" - iconType='pinFilled' + iconType="pinFilled" onClick={() => { this.props.updateCurrentAgentData({}); this.removeAgentsFilter(); }} - tooltip={{position: 'bottom', content: 'Unpin agent'}} - aria-label='Unpin agent' + tooltip={{ position: 'bottom', content: 'Unpin agent' }} + aria-label="Unpin agent" /> ); - }; + } buttonExploreAgent = ( <WzButton - buttonType='empty' + buttonType="empty" isLoading={this.state.loadingReport} - color='primary' + color="primary" isDisabled={!avaliableForAgent} - tooltip={{position: 'bottom', content: !avaliableForAgent ? 'This module is not supported for agents.' : (thereAgentSelected ? 'Change agent selected' : 'Select an agent to explore its modules') }} - style={thereAgentSelected ? {background: 'rgba(0, 107, 180, 0.1)'} : undefined} - iconType='watchesApp' - onClick={() => this.showAgentModal()}> - {thereAgentSelected ? `${this.props.agent.name} (${this.props.agent.id})` : 'Explore agent'} + tooltip={{ + position: 'bottom', + content: !avaliableForAgent + ? 'This module is not supported for agents.' + : thereAgentSelected + ? 'Change agent selected' + : 'Select an agent to explore its modules', + }} + style={thereAgentSelected ? { background: 'rgba(0, 107, 180, 0.1)' } : undefined} + iconType="watchesApp" + onClick={() => this.showAgentModal()} + > + {thereAgentSelected ? `${this.props.agent.name} (${this.props.agent.id})` : 'Explore agent'} </WzButton> - ) - + ); + return ( - <div style={{ display: "inline-flex" }}> + <div style={{ display: 'inline-flex' }}> {buttonExploreAgent} - {thereAgentSelected && ( - !avaliableForAgent && ( - <EuiPopover - button={buttonUnpinAgent} - isOpen={thereAgentSelected} - closePopover={()=> {}}> - This module is not supported for agents. Remove the pinned agent. - </EuiPopover> - - ) || buttonUnpinAgent - )} + {thereAgentSelected && buttonUnpinAgent} {modal} </div> ); } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { state: state.appStateReducers, - agent: state.appStateReducers.currentAgentData + agent: state.appStateReducers.currentAgentData, }; }; -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch) => ({ updateCurrentAgentData: (agent) => dispatch(updateCurrentAgentData(agent)), - showExploreAgentModal: (data) => dispatch(showExploreAgentModal(data)) + showExploreAgentModal: (data) => dispatch(showExploreAgentModal(data)), }); export default connect(mapStateToProps, mapDispatchToProps)(OverviewActions); diff --git a/public/controllers/overview/overview.js b/public/controllers/overview/overview.js index 4dc42cc56e..08eddb382a 100644 --- a/public/controllers/overview/overview.js +++ b/public/controllers/overview/overview.js @@ -168,37 +168,51 @@ export class OverviewController { } async updateSelectedAgents(agentList) { - if (this.initialFilter) { - this.initialFilter = false; - this.agentsSelectionProps.initialFilter = false; - } - this.isAgent = agentList ? agentList[0] : false; - this.$scope.isAgentText = this.isAgent && agentList.length === 1 ? ` of agent ${agentList.toString()}` : this.isAgent && agentList.length > 1 ? ` of ${agentList.length.toString()} agents` : false; - if (agentList && agentList.length) { - await this.visFactoryService.buildAgentsVisualizations( - this.filterHandler, - this.tab, - this.tabView, - agentList[0], - (this.tabView === 'discover' || this.oldFilteredTab === this.tab) - ); - this.oldFilteredTab = this.tab; - } else if (!agentList && this.tab !== 'welcome') { - if (!store.getState().appStateReducers.currentAgentData.id) { - await this.visFactoryService.buildOverviewVisualizations( - this.filterHandler, - this.tab, - this.tabView, - (this.tabView === 'discover' || this.oldFilteredTab === this.tab) - ); - this.oldFilteredTab = this.tab; + try{ + if (this.initialFilter) { + this.initialFilter = false; + this.agentsSelectionProps.initialFilter = false; } + this.isAgent = agentList && agentList.length ? agentList[0] : false; + this.$scope.isAgentText = this.isAgent && agentList.length === 1 ? ` of agent ${agentList.toString()}` : this.isAgent && agentList.length > 1 ? ` of ${agentList.length.toString()} agents` : false; + if (agentList && agentList.length) { + await this.visFactoryService.buildAgentsVisualizations( + this.filterHandler, + this.tab, + this.tabView, + agentList[0], + (this.tabView === 'discover' || this.oldFilteredTab === this.tab) + ); + this.oldFilteredTab = this.tab; + } else if (!agentList && this.tab !== 'welcome') { + if (!store.getState().appStateReducers.currentAgentData.id) { + await this.visFactoryService.buildOverviewVisualizations( + this.filterHandler, + this.tab, + this.tabView, + (this.tabView === 'discover' || this.oldFilteredTab === this.tab) + ); + this.oldFilteredTab = this.tab; + } + }; + setTimeout(() => { this.$location.search('agentId', store.getState().appStateReducers.currentAgentData.id ? String(store.getState().appStateReducers.currentAgentData.id) : null) }, 1); + + this.visualizeProps["isAgent"] = agentList ? agentList[0] : false; + this.$rootScope.$applyAsync(); + }catch(error){ + const options = { + context: `${OverviewController.name}.updateSelectedAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; } - setTimeout(() => { this.$location.search('agentId', store.getState().appStateReducers.currentAgentData.id ? String(store.getState().appStateReducers.currentAgentData.id) : null) }, 1); - - this.visualizeProps["isAgent"] = agentList ? agentList[0] : false; - this.$rootScope.$applyAsync(); - } // Switch subtab diff --git a/public/react-services/vis-factory-handler.js b/public/react-services/vis-factory-handler.js index 3f4a190ea1..ea33c53d22 100644 --- a/public/react-services/vis-factory-handler.js +++ b/public/react-services/vis-factory-handler.js @@ -80,17 +80,6 @@ export class VisFactoryHandler { } store.dispatch(updateVis({ update: true, raw: rawVisualizations.getList() })); } catch (error) { - const options = { - context: `${VisFactoryHandler.name}.buildOverviewVisualizations`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); throw error; } } @@ -112,7 +101,7 @@ export class VisFactoryHandler { try { const data = - tab !== 'sca' + (!['sca', 'office'].some(moduleID => tab !== moduleID)) ? await GenericRequest.request( 'GET', `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` @@ -124,17 +113,6 @@ export class VisFactoryHandler { } store.dispatch(updateVis({ update: true })); } catch (error) { - const options = { - context: `${VisFactoryHandler.name}.buildAgentsVisualizations`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); throw error; } } diff --git a/public/services/vis-factory-handler.js b/public/services/vis-factory-handler.js index 0f3714d7fa..d7432eb6d8 100644 --- a/public/services/vis-factory-handler.js +++ b/public/services/vis-factory-handler.js @@ -102,7 +102,7 @@ export class VisFactoryService { async buildAgentsVisualizations(filterHandler, tab, subtab, id) { try { const data = - tab !== 'sca' + (!['sca', 'office'].some(moduleID => tab !== moduleID)) ? await this.genericReq.request( 'GET', `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` diff --git a/server/controllers/wazuh-elastic.ts b/server/controllers/wazuh-elastic.ts index 73ef36fd7d..b89121ad3c 100644 --- a/server/controllers/wazuh-elastic.ts +++ b/server/controllers/wazuh-elastic.ts @@ -534,6 +534,9 @@ export class WazuhElasticCtrl { tabPrefix === 'overview' ? OverviewVisualizations[tabSufix] : AgentsVisualizations[tabSufix]; + if (!file) { + return response.notFound({body:{message: `Visualizations not found for ${request.params.tab}`}}); + } log('wazuh-elastic:createVis', `${tabPrefix}[${tabSufix}] with index pattern ${request.params.pattern}`, 'debug'); const namespace = context.wazuh.plugins.spaces && context.wazuh.plugins.spaces.spacesService && context.wazuh.plugins.spaces.spacesService.getSpaceId(request); const raw = await this.buildVisualizationsRaw( From f4551320604f76e74db69e48d15059e67b67aecc Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Wed, 11 Aug 2021 19:48:07 +0200 Subject: [PATCH 269/493] block filter when drilldown is open (#3562) * block filter when drilldown is open * fix PR comments * feat: disable entire botton of multiselect on is disabled Co-authored-by: Franco Charriol <francocharriol@gmail.com> --- .../components/multi-select.tsx | 3 ++- .../custom-search-bar/custom-search-bar.tsx | 10 ++++--- .../common/modules/panel/main-panel.tsx | 24 ++++++++++------- .../overview/office-panel/office-panel.tsx | 27 +++++++++++-------- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index f35d92d5da..7beb9c3f81 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -28,7 +28,7 @@ import { IValueSuggestion, useValueSuggestion } from '../../hooks'; const ON = 'on'; const OFF = 'off'; -export const MultiSelect = ({ item, onChange, selectedOptions, onRemove }) => { +export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisabled }) => { const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); const { suggestedValues, isLoading, setQuery }: IValueSuggestion = useValueSuggestion( item.key, @@ -101,6 +101,7 @@ export const MultiSelect = ({ item, onChange, selectedOptions, onRemove }) => { numFilters={selectedOptions.length} hasActiveFilters={activeFilters > 0} numActiveFilters={activeFilters} + isDisabled={isDisabled} > {item.placeholder} </EuiFilterButton> diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index 158cc0f051..ea6f716081 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -13,12 +13,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; //@ts-ignore import { KbnSearchBar } from '../../kbn-search-bar'; import { MultiSelect } from './components'; -import { useFilterManager, useIndexPattern } from '../hooks'; +import { useFilterManager } from '../hooks'; import { getCustomValueSuggestion } from '../../../components/overview/office-panel/config/helpers/helper-value-suggestion'; -export const CustomSearchBar = ({ filtersValues, ...props }) => { +export const CustomSearchBar = ({ filtersValues, filterDrillDownValue = { field: '', value: '' }, ...props }) => { const { filterManager, filters } = useFilterManager(); - const indexPattern = useIndexPattern(); const defaultSelectedOptions = () => { const array = []; filtersValues.forEach((item) => { @@ -41,6 +40,10 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { onFiltersUpdated(); }, [filters]); + + const checkSelectDrillDownValue = (key) => { + return filterDrillDownValue.field === key && filterDrillDownValue.value != '' ? true : false + } const onFiltersUpdated = () => { refreshCustomSelectedFilter(); }; @@ -145,6 +148,7 @@ export const CustomSearchBar = ({ filtersValues, ...props }) => { selectedOptions={selectedOptions[item.key] || []} onChange={onChange} onRemove={onRemove} + isDisabled={checkSelectDrillDownValue(item.key)} /> ), }; diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 8eeb884de0..17d63f9d30 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -35,7 +35,7 @@ import { import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; -export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, ...props }) => { +export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, filterDrillDownValue = (value) => {}, ...props }) => { const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); const {filterManager} = useFilterManager(); @@ -88,7 +88,7 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { useEffect(() => { applyFilter(); - + filterDrillDownValue(selectedFilter) return () => applyFilter(true); }, [selectedFilter]); @@ -102,9 +102,10 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { disabled: false, negate: false, key: field, - params: { query: value }, + params: [value], alias: null, - type: 'phrase', + type: 'phrases', + value: value, index: AppState.getCurrentPattern(), }; const $state: FilterState = { @@ -112,11 +113,16 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { isImplicit: true, }; const query = { - match: { - [field]: { - query: value, - type: 'phrase', - }, + bool: { + minimum_should_match: 1, + should: [{ + match_phrase: { + [field]: { + query: value, + }, + }, + } + ] }, }; diff --git a/public/components/overview/office-panel/office-panel.tsx b/public/components/overview/office-panel/office-panel.tsx index cabdb8c3cf..16550cfd07 100644 --- a/public/components/overview/office-panel/office-panel.tsx +++ b/public/components/overview/office-panel/office-panel.tsx @@ -11,20 +11,25 @@ * Find more information about this on the LICENSE file. */ -import React from 'react'; +import React, {useState} from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { CustomSearchBar } from '../../common/custom-search-bar'; import { ModuleConfiguration } from './views'; import { ModuleConfig, filtersValues } from './config'; -export const OfficePanel = withErrorBoundary(() => ( - <> - <CustomSearchBar filtersValues={filtersValues} /> - <MainPanel - moduleConfig={ModuleConfig} - tab={'office'} - sidePanelChildren={<ModuleConfiguration />} - /> - </> -)); +export const OfficePanel = withErrorBoundary(() => { + const [drillDownValue, setDrillDownValue] = useState({ field: '', value: '' }); + const filterDrillDownValue = (value) => { + setDrillDownValue(value) + } + return <> + <CustomSearchBar filtersValues={filtersValues} filterDrillDownValue={drillDownValue} /> + <MainPanel + moduleConfig={ModuleConfig} + tab={'office'} + filterDrillDownValue={filterDrillDownValue} + sidePanelChildren={<ModuleConfiguration />} + /> +</> +}); From a8af1e82e893a0538fd6801f63c5ca966757df5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20G=C3=B3mez=20Castro?= <manuel.gomez@wazuh.com> Date: Thu, 12 Aug 2021 11:39:13 +0200 Subject: [PATCH 270/493] Added unique keys to padding elements in configuration menu --- public/components/overview/office-panel/views/office-stats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index 511760295d..3c2f3dc8c0 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -31,7 +31,7 @@ const settings = [ {title: 'Path file of client secret', description: v.client_secret_path}, ].filter(item => typeof item.description !== 'undefined')}/> </EuiPanel> - ).reduce((prev, cur) => [prev, <div style={{marginTop: '8px'}} /> , cur], [])}, + ).reduce((prev, cur) => [prev, <div key={`padding-len-${prev.length}`} style={{marginTop: '8px'}} /> , cur], [])}, { field: 'subscriptions', label: 'Subscriptions', render: (value) => value .map(v => <EuiDescriptionList key={`module_configuration_subscriptions_${v}`}>{v}</EuiDescriptionList>) } From 10929b35189b632e431d7fac8e0be37804695a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 12 Aug 2021 14:39:26 +0200 Subject: [PATCH 271/493] fix(module_github): Add spacer to the Panel configuration viewer --- public/components/overview/github-panel/views/stats.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/components/overview/github-panel/views/stats.tsx b/public/components/overview/github-panel/views/stats.tsx index e321fd3011..30e93ae4bc 100644 --- a/public/components/overview/github-panel/views/stats.tsx +++ b/public/components/overview/github-panel/views/stats.tsx @@ -16,7 +16,6 @@ import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; import { PanelModuleConfiguration } from '../../../common/modules/panel'; import { renderValueNoThenEnabled } from '../../../../controllers/management/components/management/configuration/utils/utils'; - const settings = [ { field: 'enabled', label: 'Service status', render: renderValueNoThenEnabled }, { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started'}, @@ -31,7 +30,7 @@ const settings = [ {title: 'Token', description: v.api_token} ].filter(item => typeof item.description !== 'undefined')}/> </EuiPanel> - )} + ).reduce((prev, cur) => [prev, <div key={`padding-len-${prev.length}`} style={{marginTop: '8px'}} /> , cur], [])} ]; const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any}[], wmoduleID: string, entity: string, name: string = '') => { From 759c5f310985667369ab1ec6ee5440f0f2262720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Mon, 6 Sep 2021 21:38:33 +0200 Subject: [PATCH 272/493] Simple filters change between panel and drilldown panel (#3568) * Simple filters change between panel and drilldown panel * Adding Changelog * Adding type to boolFilter * Rename filterDrillDownValue to boolFilterValue * Fixing simple filters * Adding default value to boolFilter * Sort suggested values alphabetically and by selected Co-authored-by: Federico Rodriguez <federico.rodriguez@wazuh.com> --- CHANGELOG.md | 1 + .../components/multi-select.tsx | 14 +++++------ .../custom-search-bar/custom-search-bar.tsx | 1 + .../common/hooks/use-value-suggestion.ts | 23 +++++++++++++++++-- .../overview/github-panel/github-panel.tsx | 9 ++++++-- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1dfb88b6..84f9040ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added base Module Panel view with Office365 setup [#3518](https://github.com/wazuh/wazuh-kibana-app/pull/3518) - Added specifics and custom filters for Office365 search bar [#3533](https://github.com/wazuh/wazuh-kibana-app/pull/3533) - Adding Pagination and filter to drilldown tables at Office pannel [#3544](https://github.com/wazuh/wazuh-kibana-app/issues/3544). +- Simple filters change between panel and drilldown panel [#3568](https://github.com/wazuh/wazuh-kibana-app/issues/3568). ### Changed diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index 7beb9c3f81..e4b309ee5d 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -28,11 +28,12 @@ import { IValueSuggestion, useValueSuggestion } from '../../hooks'; const ON = 'on'; const OFF = 'off'; -export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisabled }) => { +export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisabled, filterDrillDownValue }) => { const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); const { suggestedValues, isLoading, setQuery }: IValueSuggestion = useValueSuggestion( item.key, - item?.options + filterDrillDownValue, + item?.options, ); const [items, setItems] = useState< { key: any; label: any; value: any; checked: FilterChecked }[] @@ -48,9 +49,8 @@ export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisab label: value, value: item.key, filterByKey: item.filterByKey, - checked: OFF as FilterChecked, - })) - .sort((a, b) => a.label - b.label) + checked: selectedOptions.find((element) => element.label === value) ? ON as FilterChecked: OFF as FilterChecked, + })).sort((a, b) => (a.label < b.label ? 1 : -1)).sort((a, b) => (a.checked < b.checked ? 1 : -1)) ); } }, [suggestedValues, isLoading]); @@ -59,8 +59,8 @@ export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisab setItems( items.map((item) => ({ ...item, - checked: selectedOptions.find((element) => element.label === item.label) ? ON : OFF, - })) + checked: selectedOptions.find((element) => element.label === item.label) ? ON as FilterChecked: OFF as FilterChecked, + })).sort((a, b) => (a.label < b.label ? 1 : -1)).sort((a, b) => (a.checked < b.checked ? 1 : -1)) ); setActiveFilters(selectedOptions.length); }, [selectedOptions]); diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index ea6f716081..ee34349e3d 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -149,6 +149,7 @@ export const CustomSearchBar = ({ filtersValues, filterDrillDownValue = { field: onChange={onChange} onRemove={onRemove} isDisabled={checkSelectDrillDownValue(item.key)} + filterDrillDownValue={filterDrillDownValue} /> ), }; diff --git a/public/components/common/hooks/use-value-suggestion.ts b/public/components/common/hooks/use-value-suggestion.ts index 42d8d57f55..86e2e92a34 100644 --- a/public/components/common/hooks/use-value-suggestion.ts +++ b/public/components/common/hooks/use-value-suggestion.ts @@ -29,8 +29,17 @@ export interface IValueSuggestion { setQuery: React.Dispatch<React.SetStateAction<string>>; } +interface BoolFilter { + field: string; + value: string; +} + export const useValueSuggestion = ( filterField: string, + boolFilterValue: BoolFilter = { + field: '', + value: '', + }, options?: string[], type: 'string' | 'boolean' = 'string' ): IValueSuggestion => { @@ -39,19 +48,29 @@ export const useValueSuggestion = ( const [isLoading, setIsLoading] = useState(true); const data = getDataPlugin(); const indexPattern = useIndexPattern(); - //const { filters } = useFilterManager(); const getOptions = (): string[] => { return options?.filter((element) => element.toLowerCase().includes(query.toLowerCase())) || []; }; const getValueSuggestions = async (field) => { + const boolFilter = + boolFilterValue.value !== '' + ? [ + { + term: { + [boolFilterValue.field]: `${boolFilterValue.value}`, + }, + }, + ] + : []; return options ? getOptions() : await data.autocomplete.getValueSuggestions({ query, indexPattern: indexPattern as IIndexPattern, field, + boolFilter: boolFilter, }); }; @@ -83,7 +102,7 @@ export const useValueSuggestion = ( } })(); } - }, [indexPattern, query, filterField, type]); + }, [indexPattern, query, filterField, type, boolFilterValue]); return { suggestedValues, isLoading, setQuery }; }; diff --git a/public/components/overview/github-panel/github-panel.tsx b/public/components/overview/github-panel/github-panel.tsx index 3c46dc1335..c0700d8214 100644 --- a/public/components/overview/github-panel/github-panel.tsx +++ b/public/components/overview/github-panel/github-panel.tsx @@ -11,7 +11,7 @@ * Find more information about this on the LICENSE file. */ -import React from 'react'; +import React, {useState} from 'react'; import { MainPanel } from '../../common/modules/panel'; import { withErrorBoundary } from '../../common/hocs'; import { CustomSearchBar } from '../../common/custom-search-bar'; @@ -19,10 +19,15 @@ import { ModuleConfiguration } from './views'; import { ModuleConfig, filtersValues } from './config'; export const GitHubPanel = withErrorBoundary(() => { + const [drillDownValue, setDrillDownValue] = useState({ field: '', value: '' }); + const filterDrillDownValue = (value) => { + setDrillDownValue(value) + } return ( <> - <CustomSearchBar filtersValues={filtersValues} /> + <CustomSearchBar filtersValues={filtersValues} filterDrillDownValue={drillDownValue}/> <MainPanel moduleConfig={ModuleConfig} tab={'github'} + filterDrillDownValue={filterDrillDownValue} sidePanelChildren={<ModuleConfiguration />} /> </> ) From 72b05b3229d937ee5087272e14278d323f742630 Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Wed, 6 Oct 2021 16:10:07 -0300 Subject: [PATCH 273/493] fix(multi-select): Fixed searcher of component multi-select. --- .../components/multi-select.tsx | 94 ++++++++++++++----- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index e4b309ee5d..08ea2d953a 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -28,45 +28,93 @@ import { IValueSuggestion, useValueSuggestion } from '../../hooks'; const ON = 'on'; const OFF = 'off'; -export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisabled, filterDrillDownValue }) => { +interface Item { + key: any; + label: string; + value: any; + checked: FilterChecked; +} + +export const MultiSelect = ({ + item, + onChange, + selectedOptions, + onRemove, + isDisabled, + filterDrillDownValue, +}) => { const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); const { suggestedValues, isLoading, setQuery }: IValueSuggestion = useValueSuggestion( item.key, filterDrillDownValue, - item?.options, + item?.options ); - const [items, setItems] = useState< - { key: any; label: any; value: any; checked: FilterChecked }[] - >([]); + const [items, setItems] = useState<Item[]>([]); const [activeFilters, setActiveFilters] = useState<number>(0); + const addCheckedOptions = () => { + selectedOptions.map((value) => { + if (ON === value.checked && suggestedValues.indexOf(value.label) === -1) { + suggestedValues.push(value.label); + } + }); + }; + + const sortByLabel = (items) => { + return items.sort((a, b) => (a.label < b.label ? 1 : -1)); + }; + + const sortByChecked = (items) => { + return items.sort((a, b) => (a.checked < b.checked ? 1 : -1)); + }; + + const buildSuggestedValues = () => { + const result = suggestedValues.map((value, key) => ({ + key: key, + label: value, + value: item.key, + filterByKey: item.filterByKey, + checked: selectedOptions.find((element) => element.label === value) + ? (ON as FilterChecked) + : (OFF as FilterChecked), + })); + + return sortByChecked(sortByLabel(result)); + }; + useEffect(() => { if (!isLoading) { - setItems( - suggestedValues - .map((value, key) => ({ - key: key, - label: value, - value: item.key, - filterByKey: item.filterByKey, - checked: selectedOptions.find((element) => element.label === value) ? ON as FilterChecked: OFF as FilterChecked, - })).sort((a, b) => (a.label < b.label ? 1 : -1)).sort((a, b) => (a.checked < b.checked ? 1 : -1)) - ); + addCheckedOptions(); + setItems(buildSuggestedValues()); } }, [suggestedValues, isLoading]); + const setSelectedKey = (item: Item) => { + return parseInt(item.label.match(/(?<=\[).+?(?=\])/g) || item.key); + }; + + const buildSelectedOptions = () => { + const result = items.map((item) => ({ + ...item, + checked: selectedOptions.find((element) => element.label === item.label) + ? (ON as FilterChecked) + : (OFF as FilterChecked), + key: setSelectedKey(item), + })); + + return sortByChecked(sortByLabel(result)); + }; + useEffect(() => { - setItems( - items.map((item) => ({ - ...item, - checked: selectedOptions.find((element) => element.label === item.label) ? ON as FilterChecked: OFF as FilterChecked, - })).sort((a, b) => (a.label < b.label ? 1 : -1)).sort((a, b) => (a.checked < b.checked ? 1 : -1)) - ); + addCheckedOptions(); + setItems(buildSelectedOptions()); setActiveFilters(selectedOptions.length); }, [selectedOptions]); - const toggleFilter = (item) => { + const toggleFilter = (item: Item) => { + items.map((item) => (item.key = setSelectedKey(item))); item.checked = item.checked === ON ? OFF : ON; + item.key = setSelectedKey(item); updateFilters(item.value); }; @@ -74,7 +122,7 @@ export const MultiSelect = ({ item, onChange, selectedOptions, onRemove, isDisab const selectedItems = items.filter((item) => item.checked === ON); setActiveFilters(selectedItems.length); if (selectedItems.length) { - onChange(selectedItems,id); + onChange(selectedItems, id); } else { onRemove(item.key); } From 6bf42ec61fba7366d49508fc72f5ca719eadb29e Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Tue, 12 Oct 2021 12:50:48 -0300 Subject: [PATCH 274/493] fix(office365): Fixed orders, replaced by lodash methods. --- .../custom-search-bar/components/multi-select.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index 08ea2d953a..7eabcb3ce1 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -24,6 +24,7 @@ import { FilterChecked, } from '@elastic/eui'; import { IValueSuggestion, useValueSuggestion } from '../../hooks'; +import _ from 'lodash'; const ON = 'on'; const OFF = 'off'; @@ -60,12 +61,12 @@ export const MultiSelect = ({ }); }; - const sortByLabel = (items) => { - return items.sort((a, b) => (a.label < b.label ? 1 : -1)); + const orderByLabel = (items) => { + return _.orderBy(items, 'label', 'asc'); }; - const sortByChecked = (items) => { - return items.sort((a, b) => (a.checked < b.checked ? 1 : -1)); + const orderByChecked = (items) => { + return _.orderBy(items, 'checked', 'desc'); }; const buildSuggestedValues = () => { @@ -79,7 +80,7 @@ export const MultiSelect = ({ : (OFF as FilterChecked), })); - return sortByChecked(sortByLabel(result)); + return orderByChecked(orderByLabel(result)); }; useEffect(() => { @@ -102,7 +103,7 @@ export const MultiSelect = ({ key: setSelectedKey(item), })); - return sortByChecked(sortByLabel(result)); + return orderByChecked(orderByLabel(result)); }; useEffect(() => { From ca9f06eeedbd69324582d429e94041a805c049fd Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Wed, 13 Oct 2021 19:33:22 +0200 Subject: [PATCH 275/493] change format permissions in FIM inventory --- .../agents/fim/inventory/fileDetail.tsx | 61 +++++-------------- .../components/agents/fim/inventory/table.tsx | 7 --- 2 files changed, 16 insertions(+), 52 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 6d846532fb..7dbfaa51ed 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -23,6 +23,7 @@ import { EuiStat, EuiToolTip, EuiBadge, + EuiCodeBlock, } from '@elastic/eui'; import { Discover } from '../../../common/modules/discover'; import { ModulesHelper } from '../../../common/modules/modules-helper'; @@ -131,13 +132,6 @@ export class FileDetails extends Component { icon: 'usersRolesApp', link: true, }, - { - field: 'perm', - name: 'Permissions', - icon: 'lock', - link: false, - transformValue: (value) => this.renderFileDetailsPermissions(value), - }, { field: 'size', name: 'Size', @@ -173,6 +167,13 @@ export class FileDetails extends Component { icon: 'check', link: true, }, + { + field: 'perm', + name: 'Permissions', + icon: 'lock', + link: false, + transformValue: (value) => this.renderFileDetailsPermissions(value), + }, ]; } @@ -329,11 +330,10 @@ export class FileDetails extends Component { ); } }); - return ( <div> <EuiFlexGrid columns={3}> {generalDetails} </EuiFlexGrid> - </div> + </div> ); } @@ -344,43 +344,14 @@ export class FileDetails extends Component { renderFileDetailsPermissions(value) { if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { const components = value - .split(', ') - .map((userNameAndPermissionsFullString) => { - const [_, username, userPermissionsString] = userNameAndPermissionsFullString.match( - /(\S+) \(allowed\): (\S+)/ - ); - const permissions = userPermissionsString.split('|').sort(); - return { username, permissions }; - }) - .sort((a, b) => { - if (a.username > b.username) { - return 1; - } else if (a.username < b.username) { - return -1; - } else { - return 0; - } - }) - .map(({ username, permissions }) => { - return ( - <EuiToolTip - key={`permissions-windows-user-${username}`} - content={permissions.join(', ')} - title={`${username} permissions`} - > - <EuiBadge color="hollow" title={null} style={{ margin: '2px 2px' }}> - {username} - </EuiBadge> - </EuiToolTip> - ); - }); return ( - <TruncateHorizontalComponents - components={components} - labelButtonHideComponents={(count) => `+${count} users`} - buttonProps={{ size: 'xs' }} - componentsWidthPercentage={0.85} - /> + <EuiAccordion + id={Math.random().toString() } + paddingSize="none" + initialIsOpen={false} + > + <EuiCodeBlock language="json" paddingSize="l">{JSON.stringify(components, null, 2)}</EuiCodeBlock> + </EuiAccordion> ); } return value; diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index 5c4b24b9aa..bf5609fb8b 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -233,13 +233,6 @@ export class InventoryTable extends Component { truncateText: true, width: `${width}` }, - { - field: 'perm', - name: 'Permissions', - sortable: true, - truncateText: true, - width: `${width}` - }, { field: 'size', name: 'Size', From 8d8c82e76ae9b25bd71f800aa3769c1a319efa57 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Thu, 14 Oct 2021 22:20:01 +0200 Subject: [PATCH 276/493] fix(test) --- .../__snapshots__/health-check.container.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap index 128d5ec6e4..24eca50be6 100644 --- a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap +++ b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` <img alt="" className="health-check-logo" - src="/plugins/wazuh/assets/icon_blue.svg" + src="/plugins/wazuh/assets/undefined" /> <div className="margin-top-30" From 37272a3f7f38a9fedfef39df910d472d9334344f Mon Sep 17 00:00:00 2001 From: gabiwassan <gabriel.wassan@wazuh.com> Date: Fri, 15 Oct 2021 05:32:48 -0300 Subject: [PATCH 277/493] doc(changelog): Added fix to changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f9040ccd..f259a62b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) +- Fixed multi-select component searcher handler [#3645](https://github.com/wazuh/wazuh-kibana-app/pull/3645) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From 559d697e406d3dd4833982b64811502ad12676d6 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Fri, 15 Oct 2021 16:55:09 +0200 Subject: [PATCH 278/493] fix(fileDetail): Refactor return of renderFileDetailsPermissions --- .../agents/fim/inventory/fileDetail.tsx | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 7dbfaa51ed..0d079f3379 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -96,7 +96,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'mtime', @@ -104,7 +104,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'uname', @@ -184,15 +184,15 @@ export class FileDetails extends Component { name: 'Last analysis', grow: 2, icon: 'clock', - transformValue: formatUIDate + transformValue: formatUIDate, }, { field: 'mtime', name: 'Last modified', grow: 2, icon: 'clock', - transformValue: formatUIDate - } + transformValue: formatUIDate, + }, ]; } @@ -265,7 +265,10 @@ export class FileDetails extends Component { getDetails() { const { view } = this.props; - const columns = this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' ? this.registryDetails() : this.details(); + const columns = + this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' + ? this.registryDetails() + : this.details(); const generalDetails = columns.map((item, idx) => { var value = this.props.currentFile[item.field] || '-'; if (item.transformValue) { @@ -333,7 +336,7 @@ export class FileDetails extends Component { return ( <div> <EuiFlexGrid columns={3}> {generalDetails} </EuiFlexGrid> - </div> + </div> ); } @@ -343,14 +346,11 @@ export class FileDetails extends Component { renderFileDetailsPermissions(value) { if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { - const components = value return ( - <EuiAccordion - id={Math.random().toString() } - paddingSize="none" - initialIsOpen={false} - > - <EuiCodeBlock language="json" paddingSize="l">{JSON.stringify(components, null, 2)}</EuiCodeBlock> + <EuiAccordion id={Math.random().toString()} paddingSize="none" initialIsOpen={false}> + <EuiCodeBlock language="json" paddingSize="l"> + {JSON.stringify(value, null, 2)} + </EuiCodeBlock> </EuiAccordion> ); } @@ -389,29 +389,27 @@ export class FileDetails extends Component { > <div className="flyout-row details-row">{this.getDetails()}</div> </EuiAccordion> - { (type === 'registry_key' || currentFile.type === 'registry_key') && <> - <EuiSpacer size="s" /> - <EuiAccordion - id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} - buttonContent={ - <EuiTitle size="s"> - <h3> - Registry values - </h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true} - > - <EuiFlexGroup className="flyout-row"> - <EuiFlexItem> - <RegistryValues - currentFile={currentFile} - agent={agent} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiAccordion> </>} + {(type === 'registry_key' || currentFile.type === 'registry_key') && ( + <> + <EuiSpacer size="s" /> + <EuiAccordion + id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} + buttonContent={ + <EuiTitle size="s"> + <h3>Registry values</h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > + <EuiFlexGroup className="flyout-row"> + <EuiFlexItem> + <RegistryValues currentFile={currentFile} agent={agent} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiAccordion>{' '} + </> + )} <EuiSpacer /> <EuiAccordion id={fileName === undefined ? Math.random().toString() : `${fileName}_events`} From 0c5ecc19a73fb63afa010b64647ec3c61628b972 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Fri, 15 Oct 2021 16:57:23 +0200 Subject: [PATCH 279/493] fix(fileDetail): Refactor return of renderFileDetailsPermissions --- CHANGELOG.md | 112 ++++++++++++++++++++------------------------------- 1 file changed, 44 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a34a7534..9bc42c7adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,28 +7,28 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) -- Improved the frontend handle errors strategy: UI, Toasts, console log and log in file - [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) +- Improved the frontend handle errors strategy: UI, Toasts, console log and log in file + [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) + [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) + [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) + [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) - [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) - [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) - [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) + [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) + [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) + [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) + [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) + [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) + [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) + [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) - [#3480](https://github.com/wazuh/wazuh-kibana-app/pull/3480) - [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) - [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) + [#3480](https://github.com/wazuh/wazuh-kibana-app/pull/3480) + [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) + [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) - [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) - [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) + [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) + [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) + [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) + [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) @@ -49,7 +49,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed -- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) - Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) - Notify when you are registering an agent without permissions [#3430](https://github.com/wazuh/wazuh-kibana-app/pull/3430) - Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) @@ -59,6 +59,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) +- change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ## Wazuh v4.2.3 - Kibana 7.10.2 , 7.12.1 - Revision 4204 @@ -112,10 +113,10 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) - Fixed Alerts Summary of modules for reports [#3303](https://github.com/wazuh/wazuh-kibana-app/pull/3303) - Fixed dark mode visualization background in pdf reports [#3315](https://github.com/wazuh/wazuh-kibana-app/pull/3315) -- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) -- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) -- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) -- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) +- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) +- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) +- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) +- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) - Fixed error that caused the labels in certain visualizations to overlap [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) - Fixed export to csv button in dashboards tables [#3358](https://github.com/wazuh/wazuh-kibana-app/pull/3358) - Fixed Elastic UI breaking changes in 7.12 [#3345](https://github.com/wazuh/wazuh-kibana-app/pull/3345) @@ -147,7 +148,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Changed -- Moved Dev Tools inside of Tools menu as Api Console. [#1434](https://github.com/wazuh/wazuh-kibana-app/pull/1434) +- Moved Dev Tools inside of Tools menu as Api Console. [#1434](https://github.com/wazuh/wazuh-kibana-app/pull/1434) - Changed position of Top users on Integrity Monitoring Top 5 user. [#2892](https://github.com/wazuh/wazuh-kibana-app/pull/2892) - Changed user allow_run_as way of editing. [#3080](https://github.com/wazuh/wazuh-kibana-app/pull/3080) - Rename some ossec references to Wazuh [#3046](https://github.com/wazuh/wazuh-kibana-app/pull/3046) @@ -191,6 +192,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Conflict with the creation of the index pattern when performing the Health Check [#3223](https://github.com/wazuh/wazuh-kibana-app/pull/3223) - Fixing mac os agents add command [#3207](https://github.com/wazuh/wazuh-kibana-app/pull/3207) + ## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2 - Revision 4106 - Adapt for Wazuh 4.1.5 @@ -208,7 +210,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added loading view while the user is logging to prevent permissions prompts [#3041](https://github.com/wazuh/wazuh-kibana-app/pull/3041) - Added custom message for each possible run_as setup [#3048](https://github.com/wazuh/wazuh-kibana-app/pull/3048) -### Changed +### Changed - Change all dates labels to Kibana formatting time zone [#3047](https://github.com/wazuh/wazuh-kibana-app/pull/3047) - Improve toast message when selecting a default API [#3049](https://github.com/wazuh/wazuh-kibana-app/pull/3049) @@ -315,6 +317,7 @@ All notable changes to the Wazuh app project will be documented in this file. ## Wazuh v4.0.4 - Kibana 7.10.0 , 7.10.2 - Revision 4017 ### Added + - Adapt the app to the new Kibana platform [#2475](https://github.com/wazuh/wazuh-kibana-app/issues/2475) - Wazuh data directory moved from `optimize` to `data` Kibana directory [#2591](https://github.com/wazuh/wazuh-kibana-app/issues/2591) - Show the wui_rules belong to wazuh-wui API user [#2702](https://github.com/wazuh/wazuh-kibana-app/issues/2702) @@ -325,7 +328,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed wrong shards and replicas for statistics indices and also fixed wrong prefix for monitoring indices [#2732](https://github.com/wazuh/wazuh-kibana-app/issues/2732) - Report's creation dates set to 1970-01-01T00:00:00.000Z [#2772](https://github.com/wazuh/wazuh-kibana-app/issues/2772) - Fixed bug for missing commands in ubuntu/debian and centos [#2786](https://github.com/wazuh/wazuh-kibana-app/issues/2786) -- Fixed bug that show an hour before in /security-events/dashboard [#2785](https://github.com/wazuh/wazuh-kibana-app/issues/2785) +- Fixed bug that show an hour before in /security-events/dashboard [#2785](https://github.com/wazuh/wazuh-kibana-app/issues/2785) - Fixed permissions to access agents [#2838](https://github.com/wazuh/wazuh-kibana-app/issues/2838) - Fix searching in groups [#2825](https://github.com/wazuh/wazuh-kibana-app/issues/2825) - Fix the pagination in SCA ckecks table [#2815](https://github.com/wazuh/wazuh-kibana-app/issues/2815) @@ -380,7 +383,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Wui rules label should have only one tooltip [#2723](https://github.com/wazuh/wazuh-kibana-app/issues/2723) - Move upper the Wazuh item in the Kibana menu and default index pattern [#2867](https://github.com/wazuh/wazuh-kibana-app/pull/2867) - ## Wazuh v4.0.4 - Kibana v7.9.1, v7.9.3 - Revision 4015 ### Added @@ -411,7 +413,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added -- Sample data indices name should take index pattern in use [#2593](https://github.com/wazuh/wazuh-kibana-app/issues/2593) +- Sample data indices name should take index pattern in use [#2593](https://github.com/wazuh/wazuh-kibana-app/issues/2593) - Added start option to macos Agents [#2653](https://github.com/wazuh/wazuh-kibana-app/pull/2653) ### Changed @@ -483,8 +485,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Undefined field in event view [#2588](https://github.com/wazuh/wazuh-kibana-app/issues/2588) - Several calls to the same stats request (esAlerts) [#2586](https://github.com/wazuh/wazuh-kibana-app/issues/2586) - The filter options popup doesn't open on click once the filter is pinned [#2581](https://github.com/wazuh/wazuh-kibana-app/issues/2581) -- The formatedFields are missing from the index-pattern of wazuh-alerts-* [#2574](https://github.com/wazuh/wazuh-kibana-app/issues/2574) - +- The formatedFields are missing from the index-pattern of wazuh-alerts-\* [#2574](https://github.com/wazuh/wazuh-kibana-app/issues/2574) ## Wazuh v4.0.0 - Kibana v7.9.3 - Revision 4005 @@ -530,6 +531,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh v3.13.2 ## Wazuh v3.13.2 - Kibana v7.8.0 - Revision 887 + ### Added - Support for Wazuh v3.13.2 @@ -546,28 +548,24 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.9.0 - ## Wazuh v3.13.1 - Kibana v7.8.1 - Revision 884 ### Added - Support for Kibana v7.8.1 - ## Wazuh v3.13.1 - Kibana v7.8.0 - Revision 883 ### Added - Support for Wazuh v3.13.1 - ## Wazuh v3.13.0 - Kibana v7.8.0 - Revision 881 ### Added - Support for Kibana v7.8.0 - ## Wazuh v3.13.0 - Kibana v7.7.0, v7.7.1 - Revision 880 ### Added @@ -600,35 +598,30 @@ All notable changes to the Wazuh app project will be documented in this file. - Avoid creating the wazuh-monitoring index pattern if it is disabled [#2100](https://github.com/wazuh/wazuh-kibana-app/issues/2100) - SCA checks without compliance field can't be expanded [#2264](https://github.com/wazuh/wazuh-kibana-app/issues/2264) - ## Wazuh v3.12.3 - Kibana v7.7.1 - Revision 876 ### Added - Support for Kibana v7.7.1 - ## Wazuh v3.12.3 - Kibana v7.7.0 - Revision 875 ### Added - Support for Kibana v7.7.0 - ## Wazuh v3.12.3 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 874 ### Added - Support for Wazuh v3.12.3 - ## Wazuh v3.12.2 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 873 ### Added - Support for Wazuh v3.12.2 - ## Wazuh v3.12.1 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 872 ### Added @@ -644,7 +637,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Pagination is now shown in table-type visualizations. [#2180](https://github.com/wazuh/wazuh-kibana-app/issues/2180) - ## Wazuh v3.12.0 - Kibana v6.8.8, v7.6.2 - Revision 871 ### Added @@ -672,28 +664,24 @@ All notable changes to the Wazuh app project will be documented in this file. - Added the win_auth_failure rule group to Authentication failure metrics. [#2099](https://github.com/wazuh/wazuh-kibana-app/pull/2099) - Negative values in Syscheck attributes now have their correct value in reports. [7c3e84e](https://github.com/wazuh/wazuh-kibana-app/commit/7c3e84ec8f00760b4f650cfc00a885d868123f99) - ## Wazuh v3.11.4 - Kibana v7.6.1 - Revision 858 ### Added - Support for Kibana v7.6.1 - ## Wazuh v3.11.4 - Kibana v6.8.6, v7.4.2, v7.6.0 - Revision 857 ### Added - Support for Wazuh v3.11.4 - ## Wazuh v3.11.3 - Kibana v7.6.0 - Revision 856 ### Added - Support for Kibana v7.6.0 - ## Wazuh v3.11.3 - Kibana v7.4.2 - Revision 855 ### Added @@ -710,14 +698,12 @@ All notable changes to the Wazuh app project will be documented in this file. - Windows Updates table is now displayed in the Inventory Data report [#2028](https://github.com/wazuh/wazuh-kibana-app/pull/2028) - ## Wazuh v3.11.2 - Kibana v7.5.2 - Revision 853 ### Added - Support for Kibana v7.5.2 - ## Wazuh v3.11.2 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 852 ### Added @@ -733,12 +719,11 @@ All notable changes to the Wazuh app project will be documented in this file. - The xml validator now correctly handles the `--` string within comments [#1980](https://github.com/wazuh/wazuh-kibana-app/pull/1980) - The AWS map visualization wasn't been loaded until the user interacts with it [dd31bd7](https://github.com/wazuh/wazuh-kibana-app/commit/dd31bd7a155354bc50fe0af22fca878607c8936a) - ## Wazuh v3.11.1 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 581 ### Added -- Support for Wazuh v3.11.1. +- Support for Wazuh v3.11.1. ## Wazuh v3.11.0 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 580 @@ -784,28 +769,24 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.5.1 - ## Wazuh v3.10.2 - Kibana v7.5.0 - Revision 555 ### Added - Support for Kibana v7.5.0 - ## Wazuh v3.10.2 - Kibana v7.4.2 - Revision 549 ### Added - Support for Kibana v7.4.2 - ## Wazuh v3.10.2 - Kibana v7.4.1 - Revision 548 ### Added - Support for Kibana v7.4.1 - ## Wazuh v3.10.2 - Kibana v7.4.0 - Revision 547 ### Added @@ -813,28 +794,25 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.4.0 - Support for Wazuh v3.10.2. - ## Wazuh v3.10.2 - Kibana v7.3.2 - Revision 546 ### Added - Support for Wazuh v3.10.2. - ## Wazuh v3.10.1 - Kibana v7.3.2 - Revision 545 ### Added - Support for Wazuh v3.10.1. - ## Wazuh v3.10.0 - Kibana v7.3.2 - Revision 543 ### Added - Support for Wazuh v3.10.0. - Added an interactive guide for registering agents, things are now easier for the user, guiding it through the steps needed ending in a _copy & paste_ snippet for deploying his agent [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1468). -- Added new dashboards for the recently added regulatory compliance groups into the Wazuh core. They are HIPAA and NIST-800-53 [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1448), [#1638]( https://github.com/wazuh/wazuh-kibana-app/issues/1638). +- Added new dashboards for the recently added regulatory compliance groups into the Wazuh core. They are HIPAA and NIST-800-53 [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1448), [#1638](https://github.com/wazuh/wazuh-kibana-app/issues/1638). - Make the app work under a custom Kibana space [#1234](https://github.com/wazuh/wazuh-kibana-app/issues/1234), [#1450](https://github.com/wazuh/wazuh-kibana-app/issues/1450). - Added the ability to manage the app as a native plugin when using Kibana spaces, now you can safely hide/show the app depending on the selected space [#1601](https://github.com/wazuh/wazuh-kibana-app/issues/1601). - Adapt the app the for Kibana dark mode [#1562](https://github.com/wazuh/wazuh-kibana-app/issues/1562). @@ -842,13 +820,12 @@ All notable changes to the Wazuh app project will be documented in this file. - Export all the information of a Wazuh group and its related agents in a PDF document [#1341](https://github.com/wazuh/wazuh-kibana-app/issues/1341). - Export the configuration of a certain agent as a PDF document. Supports granularity for exporting just certain sections of the configuration [#1340](https://github.com/wazuh/wazuh-kibana-app/issues/1340). - ### Changed - Reduced _Agents preview_ load time using the new API endpoint `/summary/agents` [#1687](https://github.com/wazuh/wazuh-kibana-app/pull/1687). - Replaced most of the _md-nav-bar_ Angular.js components with React components using EUI [#1705](https://github.com/wazuh/wazuh-kibana-app/pull/1705). - Replaced the requirements slider component with a new styled component [#1708](https://github.com/wazuh/wazuh-kibana-app/pull/1708). -- Soft deprecated the _.wazuh-version_ internal index, now the app dumps its content if applicable to a registry file, then the app removes that index. Further versions will hard deprecate this index [#1467](https://github.com/wazuh/wazuh-kibana-app/issues/1467). +- Soft deprecated the _.wazuh-version_ internal index, now the app dumps its content if applicable to a registry file, then the app removes that index. Further versions will hard deprecate this index [#1467](https://github.com/wazuh/wazuh-kibana-app/issues/1467). - Visualizations now don't fetch the documents _source_, also, they now use _size: 0_ for fetching [#1663](https://github.com/wazuh/wazuh-kibana-app/issues/1663). - The app menu is now fixed on top of the view, it's not being hidden on every state change. Also, the Wazuh logo was placed in the top bar of Kibana UI [#1502](https://github.com/wazuh/wazuh-kibana-app/issues/1502). - Improved _getTimestamp_ method not returning a promise object because it's no longer needed [014bc3a](https://github.com/wazuh/wazuh-kibana-app/commit/014b3aba0d2e9cda0c4d521f5f16faddc434a21e). Also improved main Discover listener for Wazuh not returning a promise object [bd82823](https://github.com/wazuh/wazuh-kibana-app/commit/bd8282391a402b8c567b32739cf914a0135d74bc). @@ -870,7 +847,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed _Network interfaces_ table in Inventory section, the table was not paginating [#1474](https://github.com/wazuh/wazuh-kibana-app/issues/1474). - Fixed APIs passwords are now obfuscated in server responses [adc3152](https://github.com/wazuh/wazuh-kibana-app/pull/1782/commits/adc31525e26b25e4cb62d81cbae70a8430728af5). - ## Wazuh v3.9.5 - Kibana v6.8.2 / Kibana v7.2.1 / Kibana v7.3.0 - Revision 531 ### Added @@ -910,13 +886,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix not properly updated breadcrumb in ruleset section [9645903](https://github.com/wazuh/wazuh-kibana-app/commit/96459031cd4edbe047970bf0d22d0c099771879f) - Fix badly dimensioned table in Integrity Monitoring section [9645903](https://github.com/wazuh/wazuh-kibana-app/commit/96459031cd4edbe047970bf0d22d0c099771879f) - Fix implicit filters can be destroyed [9cf8578](https://github.com/wazuh/wazuh-kibana-app/commit/9cf85786f504f5d67edddeea6cfbf2ab577e799b) -- Windows agent dashboard doesn't show failure logon access. [d38d088](https://github.com/wazuh/wazuh-kibana-app/commit/d38d0881ac8e4294accde83d63108337b74cdd91) -- Number of agents is not properly updated. [f7cbbe5](https://github.com/wazuh/wazuh-kibana-app/commit/f7cbbe54394db825827715c3ad4370ac74317108) -- Missing scrollbar on Firefox file viewer. [df4e8f9](https://github.com/wazuh/wazuh-kibana-app/commit/df4e8f9305b35e9ee1473bed5f5d452dd3420567) -- Agent search filter by name, lost when refreshing. [71b5274](https://github.com/wazuh/wazuh-kibana-app/commit/71b5274ccc332d8961a158587152f7badab28a95) -- Alerts of level 12 cannot be displayed in the Summary table. [ec0e888](https://github.com/wazuh/wazuh-kibana-app/commit/ec0e8885d9f1306523afbc87de01a31f24e36309) -- Restored query from search bar in visualizations. [439128f](https://github.com/wazuh/wazuh-kibana-app/commit/439128f0a1f65b649a9dcb81ab5804ca20f65763) -- Fix Kibana filters loop in Firefox. [82f0f32](https://github.com/wazuh/wazuh-kibana-app/commit/82f0f32946d844ce96a28f0185f903e8e05c5589) +- Windows agent dashboard doesn't show failure logon access. [d38d088](https://github.com/wazuh/wazuh-kibana-app/commit/d38d0881ac8e4294accde83d63108337b74cdd91) +- Number of agents is not properly updated. [f7cbbe5](https://github.com/wazuh/wazuh-kibana-app/commit/f7cbbe54394db825827715c3ad4370ac74317108) +- Missing scrollbar on Firefox file viewer. [df4e8f9](https://github.com/wazuh/wazuh-kibana-app/commit/df4e8f9305b35e9ee1473bed5f5d452dd3420567) +- Agent search filter by name, lost when refreshing. [71b5274](https://github.com/wazuh/wazuh-kibana-app/commit/71b5274ccc332d8961a158587152f7badab28a95) +- Alerts of level 12 cannot be displayed in the Summary table. [ec0e888](https://github.com/wazuh/wazuh-kibana-app/commit/ec0e8885d9f1306523afbc87de01a31f24e36309) +- Restored query from search bar in visualizations. [439128f](https://github.com/wazuh/wazuh-kibana-app/commit/439128f0a1f65b649a9dcb81ab5804ca20f65763) +- Fix Kibana filters loop in Firefox. [82f0f32](https://github.com/wazuh/wazuh-kibana-app/commit/82f0f32946d844ce96a28f0185f903e8e05c5589) ## Wazuh v3.9.3 - Kibana v6.8.1 / v7.1.1 / v7.2.0 - Revision 523 @@ -975,7 +951,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed missing dependency for Discover [43f5dd5](https://github.com/wazuh/wazuh-kibana-app/commit/43f5dd5f64065c618ba930b2a4087f0a9e706c0e). -- Fixed visualization for Agents > Overview [#1477](https://github.com/wazuh/wazuh-kibana-app/pull/1477). +- Fixed visualization for Agents > Overview [#1477](https://github.com/wazuh/wazuh-kibana-app/pull/1477). - Fixed SCA policy checks table [#1478](https://github.com/wazuh/wazuh-kibana-app/pull/1478). ## Wazuh v3.9.1 - Kibana v7.1.0 - Revision 508 From a13a29af63482f1510d249258d4c3b45a3d8440f Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 18 Oct 2021 17:44:40 +0200 Subject: [PATCH 280/493] [FIX] [Module] Pinned filters were removed when accessing to Panel tab (#3565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(panel_module): Fix the pinned filters were removed when accessing to the Modules' Panel tab * changelog: Add PR to the changelog * Added a conditional check for pinned filters This prevents the aggregation tables from removing a pinned filter when leaving the section * fix(frontend_module): Replace method of `filterManager` by `filters` offered by the hook Co-authored-by: Manuel Gómez Castro <manuel.gomez@wazuh.com> --- CHANGELOG.md | 1 + .../common/modules/panel/main-panel.tsx | 16 ++++++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2dde73e4..89d78b60d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) +- Fix the pinned filters were removed when accessing to the `Panel` tab of a module [#3565](https://github.com/wazuh/wazuh-kibana-app/pull/3565) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/public/components/common/modules/panel/main-panel.tsx b/public/components/common/modules/panel/main-panel.tsx index 17d63f9d30..9013d195a7 100644 --- a/public/components/common/modules/panel/main-panel.tsx +++ b/public/components/common/modules/panel/main-panel.tsx @@ -38,7 +38,7 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = {}, filterDrillDownValue = (value) => {}, ...props }) => { const [viewId, setViewId] = useState('main'); const [selectedFilter, setSelectedFilter] = useState({ field: '', value: '' }); - const {filterManager} = useFilterManager(); + const { filterManager, filters } = useFilterManager(); const buildOverviewVisualization = async () => { const tabVisualizations = new TabVisualizations(); @@ -75,15 +75,11 @@ export const MainPanel = ({ sidePanelChildren, tab = 'general', moduleConfig = { * When a filter is toggled applies de selection */ const applyFilter = (clearOnly = false) => { - const appliedFilters = filterManager.getAppFilters(); - const filters = appliedFilters.filter((filter) => { - return filter.meta.key != selectedFilter.field; - }); - if (!clearOnly && selectedFilter.value) { - const customFilter = buildCustomFilter(selectedFilter); - filters.push(customFilter); - } - filterManager.setFilters(filters); + const newFilters = [ + ...filters.filter((filter) => filter.meta.key !== selectedFilter.field || filter.$state?.store == "globalState"), + ...(!clearOnly && selectedFilter.value ? [buildCustomFilter(selectedFilter)] : []) + ]; + filterManager.setFilters(newFilters); }; useEffect(() => { From b889bff6efd91f6c3e9cff463222790892ddd727 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 17:48:11 +0200 Subject: [PATCH 281/493] fix(DetailFile):Refactor --- .../agents/fim/inventory/fileDetail.tsx | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 0d079f3379..7dbfaa51ed 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -96,7 +96,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate, + transformValue: formatUIDate }, { field: 'mtime', @@ -104,7 +104,7 @@ export class FileDetails extends Component { grow: 2, icon: 'clock', link: true, - transformValue: formatUIDate, + transformValue: formatUIDate }, { field: 'uname', @@ -184,15 +184,15 @@ export class FileDetails extends Component { name: 'Last analysis', grow: 2, icon: 'clock', - transformValue: formatUIDate, + transformValue: formatUIDate }, { field: 'mtime', name: 'Last modified', grow: 2, icon: 'clock', - transformValue: formatUIDate, - }, + transformValue: formatUIDate + } ]; } @@ -265,10 +265,7 @@ export class FileDetails extends Component { getDetails() { const { view } = this.props; - const columns = - this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' - ? this.registryDetails() - : this.details(); + const columns = this.props.type === 'registry_key' || this.props.currentFile.type === 'registry_key' ? this.registryDetails() : this.details(); const generalDetails = columns.map((item, idx) => { var value = this.props.currentFile[item.field] || '-'; if (item.transformValue) { @@ -336,7 +333,7 @@ export class FileDetails extends Component { return ( <div> <EuiFlexGrid columns={3}> {generalDetails} </EuiFlexGrid> - </div> + </div> ); } @@ -346,11 +343,14 @@ export class FileDetails extends Component { renderFileDetailsPermissions(value) { if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { + const components = value return ( - <EuiAccordion id={Math.random().toString()} paddingSize="none" initialIsOpen={false}> - <EuiCodeBlock language="json" paddingSize="l"> - {JSON.stringify(value, null, 2)} - </EuiCodeBlock> + <EuiAccordion + id={Math.random().toString() } + paddingSize="none" + initialIsOpen={false} + > + <EuiCodeBlock language="json" paddingSize="l">{JSON.stringify(components, null, 2)}</EuiCodeBlock> </EuiAccordion> ); } @@ -389,27 +389,29 @@ export class FileDetails extends Component { > <div className="flyout-row details-row">{this.getDetails()}</div> </EuiAccordion> - {(type === 'registry_key' || currentFile.type === 'registry_key') && ( - <> - <EuiSpacer size="s" /> - <EuiAccordion - id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} - buttonContent={ - <EuiTitle size="s"> - <h3>Registry values</h3> - </EuiTitle> - } - paddingSize="none" - initialIsOpen={true} - > - <EuiFlexGroup className="flyout-row"> - <EuiFlexItem> - <RegistryValues currentFile={currentFile} agent={agent} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiAccordion>{' '} - </> - )} + { (type === 'registry_key' || currentFile.type === 'registry_key') && <> + <EuiSpacer size="s" /> + <EuiAccordion + id={fileName === undefined ? Math.random().toString() : `${fileName}_values`} + buttonContent={ + <EuiTitle size="s"> + <h3> + Registry values + </h3> + </EuiTitle> + } + paddingSize="none" + initialIsOpen={true} + > + <EuiFlexGroup className="flyout-row"> + <EuiFlexItem> + <RegistryValues + currentFile={currentFile} + agent={agent} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiAccordion> </>} <EuiSpacer /> <EuiAccordion id={fileName === undefined ? Math.random().toString() : `${fileName}_events`} From 285a919927df365395fe9b5314ca1c5112063c7b Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 17:50:41 +0200 Subject: [PATCH 282/493] fix(DetailFile):Refactor --- .../components/agents/fim/inventory/fileDetail.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 7dbfaa51ed..0be33bf98d 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -331,7 +331,7 @@ export class FileDetails extends Component { } }); return ( - <div> + <div> <EuiFlexGrid columns={3}> {generalDetails} </EuiFlexGrid> </div> ); @@ -343,14 +343,11 @@ export class FileDetails extends Component { renderFileDetailsPermissions(value) { if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { - const components = value return ( - <EuiAccordion - id={Math.random().toString() } - paddingSize="none" - initialIsOpen={false} - > - <EuiCodeBlock language="json" paddingSize="l">{JSON.stringify(components, null, 2)}</EuiCodeBlock> + <EuiAccordion id={Math.random().toString()} paddingSize="none" initialIsOpen={false}> + <EuiCodeBlock language="json" paddingSize="l"> + {JSON.stringify(value, null, 2)} + </EuiCodeBlock> </EuiAccordion> ); } From cb0783617a112d2ee02097e22b5bb0490ba1caaf Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 18 Oct 2021 17:53:23 +0200 Subject: [PATCH 283/493] [FIX] [Module] [Mitre Att&ck] [Framework] Double fetching alerts count on pin/unpin agent (#3484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend_module_mitre): Fix double fetch in Modules/Mitre Att&ck/Framework due to changes in the filters, redid the request. - Added a conditional when the selected tab is inventory * changelog: Add PR to changelog Co-authored-by: Manuel Gómez Castro <manuel.gomez@wazuh.com> --- CHANGELOG.md | 1 + .../overview/mitre/components/techniques/techniques.tsx | 3 ++- public/controllers/overview/overview.js | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a34a7534..039eaaf1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed double fetching alerts count when pinnin/unpinning the agent in Mitre Att&ck/Framework [#3484](https://github.com/wazuh/wazuh-kibana-app/pull/3484) - Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) - Notify when you are registering an agent without permissions [#3430](https://github.com/wazuh/wazuh-kibana-app/pull/3430) - Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 406b88b3b1..8f72e4b83e 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -103,7 +103,8 @@ export const Techniques = withWindowSize( const { isLoading, tacticsObject, filters } = this.props; if ( JSON.stringify(prevProps.tacticsObject) !== JSON.stringify(tacticsObject) || - isLoading !== prevProps.isLoading + isLoading !== prevProps.isLoading || + JSON.stringify(prevProps.filterParams) !== JSON.stringify(this.props.filterParams) ) this.getTechniquesCount(); } diff --git a/public/controllers/overview/overview.js b/public/controllers/overview/overview.js index 4dc42cc56e..d299018f09 100644 --- a/public/controllers/overview/overview.js +++ b/public/controllers/overview/overview.js @@ -180,7 +180,7 @@ export class OverviewController { this.tab, this.tabView, agentList[0], - (this.tabView === 'discover' || this.oldFilteredTab === this.tab) + (this.tabView === 'discover' || this.oldFilteredTab === this.tab || this.tabView === 'inventory') ); this.oldFilteredTab = this.tab; } else if (!agentList && this.tab !== 'welcome') { @@ -189,7 +189,7 @@ export class OverviewController { this.filterHandler, this.tab, this.tabView, - (this.tabView === 'discover' || this.oldFilteredTab === this.tab) + (this.tabView === 'discover' || this.oldFilteredTab === this.tab || this.tabView === 'inventory') ); this.oldFilteredTab = this.tab; } From 155d965b1b64033b1c6dbe647bc173db73b43b33 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 17:56:27 +0200 Subject: [PATCH 284/493] fix(Changelog): add PR --- CHANGELOG.md | 113 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc42c7adc..d83d962155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,28 +7,28 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) -- Improved the frontend handle errors strategy: UI, Toasts, console log and log in file - [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) - [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) - [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) - [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) +- Improved the frontend handle errors strategy: UI, Toasts, console log and log in file + [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) + [#3321](https://github.com/wazuh/wazuh-kibana-app/pull/3321) + [#3367](https://github.com/wazuh/wazuh-kibana-app/pull/3367) + [#3374](https://github.com/wazuh/wazuh-kibana-app/pull/3374) [#3390](https://github.com/wazuh/wazuh-kibana-app/pull/3390) - [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) - [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) - [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) - [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) - [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) - [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) - [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) + [#3410](https://github.com/wazuh/wazuh-kibana-app/pull/3410) + [#3408](https://github.com/wazuh/wazuh-kibana-app/pull/3408) + [#3429](https://github.com/wazuh/wazuh-kibana-app/pull/3429) + [#3427](https://github.com/wazuh/wazuh-kibana-app/pull/3427) + [#3417](https://github.com/wazuh/wazuh-kibana-app/pull/3417) + [#3462](https://github.com/wazuh/wazuh-kibana-app/pull/3462) + [#3451](https://github.com/wazuh/wazuh-kibana-app/pull/3451) [#3442](https://github.com/wazuh/wazuh-kibana-app/pull/3442) - [#3480](https://github.com/wazuh/wazuh-kibana-app/pull/3480) - [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) - [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) + [#3480](https://github.com/wazuh/wazuh-kibana-app/pull/3480) + [#3472](https://github.com/wazuh/wazuh-kibana-app/pull/3472) + [#3434](https://github.com/wazuh/wazuh-kibana-app/pull/3434) [#3392](https://github.com/wazuh/wazuh-kibana-app/pull/3392) - [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) - [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) + [#3404](https://github.com/wazuh/wazuh-kibana-app/pull/3404) + [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) + [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) + [#3469](https://github.com/wazuh/wazuh-kibana-app/pull/3469) [#3448](https://github.com/wazuh/wazuh-kibana-app/pull/3448) [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) @@ -46,10 +46,11 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) +- Change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ### Fixed -- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) +- Fixed creation of log files [#3384](https://github.com/wazuh/wazuh-kibana-app/pull/3384) - Fixed rules and decoders test flyout clickout event [#3412](https://github.com/wazuh/wazuh-kibana-app/pull/3412) - Notify when you are registering an agent without permissions [#3430](https://github.com/wazuh/wazuh-kibana-app/pull/3430) - Remove not used `redirectRule` query param when clicking the row table on CDB Lists/Decoders [#3438](https://github.com/wazuh/wazuh-kibana-app/pull/3438) @@ -59,7 +60,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) -- change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ## Wazuh v4.2.3 - Kibana 7.10.2 , 7.12.1 - Revision 4204 @@ -113,10 +113,10 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed modules are missing in the agent menu [#3244](https://github.com/wazuh/wazuh-kibana-app/pull/3244) - Fixed Alerts Summary of modules for reports [#3303](https://github.com/wazuh/wazuh-kibana-app/pull/3303) - Fixed dark mode visualization background in pdf reports [#3315](https://github.com/wazuh/wazuh-kibana-app/pull/3315) -- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) -- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) -- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) -- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) +- Adapt Kibana integrations to Kibana 7.11 and 7.12 [#3309](https://github.com/wazuh/wazuh-kibana-app/pull/3309) +- Fixed error agent view does not render correctly [#3306](https://github.com/wazuh/wazuh-kibana-app/pull/3306) +- Fixed miscalculation in table column width in PDF reports [#3326](https://github.com/wazuh/wazuh-kibana-app/pull/3326) +- Normalized visData table property for 7.12 retro-compatibility [#3323](https://github.com/wazuh/wazuh-kibana-app/pull/3323) - Fixed error that caused the labels in certain visualizations to overlap [#3355](https://github.com/wazuh/wazuh-kibana-app/pull/3355) - Fixed export to csv button in dashboards tables [#3358](https://github.com/wazuh/wazuh-kibana-app/pull/3358) - Fixed Elastic UI breaking changes in 7.12 [#3345](https://github.com/wazuh/wazuh-kibana-app/pull/3345) @@ -148,7 +148,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Changed -- Moved Dev Tools inside of Tools menu as Api Console. [#1434](https://github.com/wazuh/wazuh-kibana-app/pull/1434) +- Moved Dev Tools inside of Tools menu as Api Console. [#1434](https://github.com/wazuh/wazuh-kibana-app/pull/1434) - Changed position of Top users on Integrity Monitoring Top 5 user. [#2892](https://github.com/wazuh/wazuh-kibana-app/pull/2892) - Changed user allow_run_as way of editing. [#3080](https://github.com/wazuh/wazuh-kibana-app/pull/3080) - Rename some ossec references to Wazuh [#3046](https://github.com/wazuh/wazuh-kibana-app/pull/3046) @@ -192,7 +192,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Conflict with the creation of the index pattern when performing the Health Check [#3223](https://github.com/wazuh/wazuh-kibana-app/pull/3223) - Fixing mac os agents add command [#3207](https://github.com/wazuh/wazuh-kibana-app/pull/3207) - ## Wazuh v4.1.5 - Kibana 7.10.0 , 7.10.2 - Revision 4106 - Adapt for Wazuh 4.1.5 @@ -210,7 +209,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added loading view while the user is logging to prevent permissions prompts [#3041](https://github.com/wazuh/wazuh-kibana-app/pull/3041) - Added custom message for each possible run_as setup [#3048](https://github.com/wazuh/wazuh-kibana-app/pull/3048) -### Changed +### Changed - Change all dates labels to Kibana formatting time zone [#3047](https://github.com/wazuh/wazuh-kibana-app/pull/3047) - Improve toast message when selecting a default API [#3049](https://github.com/wazuh/wazuh-kibana-app/pull/3049) @@ -317,7 +316,6 @@ All notable changes to the Wazuh app project will be documented in this file. ## Wazuh v4.0.4 - Kibana 7.10.0 , 7.10.2 - Revision 4017 ### Added - - Adapt the app to the new Kibana platform [#2475](https://github.com/wazuh/wazuh-kibana-app/issues/2475) - Wazuh data directory moved from `optimize` to `data` Kibana directory [#2591](https://github.com/wazuh/wazuh-kibana-app/issues/2591) - Show the wui_rules belong to wazuh-wui API user [#2702](https://github.com/wazuh/wazuh-kibana-app/issues/2702) @@ -328,7 +326,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed wrong shards and replicas for statistics indices and also fixed wrong prefix for monitoring indices [#2732](https://github.com/wazuh/wazuh-kibana-app/issues/2732) - Report's creation dates set to 1970-01-01T00:00:00.000Z [#2772](https://github.com/wazuh/wazuh-kibana-app/issues/2772) - Fixed bug for missing commands in ubuntu/debian and centos [#2786](https://github.com/wazuh/wazuh-kibana-app/issues/2786) -- Fixed bug that show an hour before in /security-events/dashboard [#2785](https://github.com/wazuh/wazuh-kibana-app/issues/2785) +- Fixed bug that show an hour before in /security-events/dashboard [#2785](https://github.com/wazuh/wazuh-kibana-app/issues/2785) - Fixed permissions to access agents [#2838](https://github.com/wazuh/wazuh-kibana-app/issues/2838) - Fix searching in groups [#2825](https://github.com/wazuh/wazuh-kibana-app/issues/2825) - Fix the pagination in SCA ckecks table [#2815](https://github.com/wazuh/wazuh-kibana-app/issues/2815) @@ -383,6 +381,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Wui rules label should have only one tooltip [#2723](https://github.com/wazuh/wazuh-kibana-app/issues/2723) - Move upper the Wazuh item in the Kibana menu and default index pattern [#2867](https://github.com/wazuh/wazuh-kibana-app/pull/2867) + ## Wazuh v4.0.4 - Kibana v7.9.1, v7.9.3 - Revision 4015 ### Added @@ -413,7 +412,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added -- Sample data indices name should take index pattern in use [#2593](https://github.com/wazuh/wazuh-kibana-app/issues/2593) +- Sample data indices name should take index pattern in use [#2593](https://github.com/wazuh/wazuh-kibana-app/issues/2593) - Added start option to macos Agents [#2653](https://github.com/wazuh/wazuh-kibana-app/pull/2653) ### Changed @@ -485,7 +484,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Undefined field in event view [#2588](https://github.com/wazuh/wazuh-kibana-app/issues/2588) - Several calls to the same stats request (esAlerts) [#2586](https://github.com/wazuh/wazuh-kibana-app/issues/2586) - The filter options popup doesn't open on click once the filter is pinned [#2581](https://github.com/wazuh/wazuh-kibana-app/issues/2581) -- The formatedFields are missing from the index-pattern of wazuh-alerts-\* [#2574](https://github.com/wazuh/wazuh-kibana-app/issues/2574) +- The formatedFields are missing from the index-pattern of wazuh-alerts-* [#2574](https://github.com/wazuh/wazuh-kibana-app/issues/2574) + ## Wazuh v4.0.0 - Kibana v7.9.3 - Revision 4005 @@ -531,7 +531,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh v3.13.2 ## Wazuh v3.13.2 - Kibana v7.8.0 - Revision 887 - ### Added - Support for Wazuh v3.13.2 @@ -548,24 +547,28 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.9.0 + ## Wazuh v3.13.1 - Kibana v7.8.1 - Revision 884 ### Added - Support for Kibana v7.8.1 + ## Wazuh v3.13.1 - Kibana v7.8.0 - Revision 883 ### Added - Support for Wazuh v3.13.1 + ## Wazuh v3.13.0 - Kibana v7.8.0 - Revision 881 ### Added - Support for Kibana v7.8.0 + ## Wazuh v3.13.0 - Kibana v7.7.0, v7.7.1 - Revision 880 ### Added @@ -598,30 +601,35 @@ All notable changes to the Wazuh app project will be documented in this file. - Avoid creating the wazuh-monitoring index pattern if it is disabled [#2100](https://github.com/wazuh/wazuh-kibana-app/issues/2100) - SCA checks without compliance field can't be expanded [#2264](https://github.com/wazuh/wazuh-kibana-app/issues/2264) + ## Wazuh v3.12.3 - Kibana v7.7.1 - Revision 876 ### Added - Support for Kibana v7.7.1 + ## Wazuh v3.12.3 - Kibana v7.7.0 - Revision 875 ### Added - Support for Kibana v7.7.0 + ## Wazuh v3.12.3 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 874 ### Added - Support for Wazuh v3.12.3 + ## Wazuh v3.12.2 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 873 ### Added - Support for Wazuh v3.12.2 + ## Wazuh v3.12.1 - Kibana v6.8.8, v7.6.1, v7.6.2 - Revision 872 ### Added @@ -637,6 +645,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Pagination is now shown in table-type visualizations. [#2180](https://github.com/wazuh/wazuh-kibana-app/issues/2180) + ## Wazuh v3.12.0 - Kibana v6.8.8, v7.6.2 - Revision 871 ### Added @@ -664,24 +673,28 @@ All notable changes to the Wazuh app project will be documented in this file. - Added the win_auth_failure rule group to Authentication failure metrics. [#2099](https://github.com/wazuh/wazuh-kibana-app/pull/2099) - Negative values in Syscheck attributes now have their correct value in reports. [7c3e84e](https://github.com/wazuh/wazuh-kibana-app/commit/7c3e84ec8f00760b4f650cfc00a885d868123f99) + ## Wazuh v3.11.4 - Kibana v7.6.1 - Revision 858 ### Added - Support for Kibana v7.6.1 + ## Wazuh v3.11.4 - Kibana v6.8.6, v7.4.2, v7.6.0 - Revision 857 ### Added - Support for Wazuh v3.11.4 + ## Wazuh v3.11.3 - Kibana v7.6.0 - Revision 856 ### Added - Support for Kibana v7.6.0 + ## Wazuh v3.11.3 - Kibana v7.4.2 - Revision 855 ### Added @@ -698,12 +711,14 @@ All notable changes to the Wazuh app project will be documented in this file. - Windows Updates table is now displayed in the Inventory Data report [#2028](https://github.com/wazuh/wazuh-kibana-app/pull/2028) + ## Wazuh v3.11.2 - Kibana v7.5.2 - Revision 853 ### Added - Support for Kibana v7.5.2 + ## Wazuh v3.11.2 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 852 ### Added @@ -719,12 +734,13 @@ All notable changes to the Wazuh app project will be documented in this file. - The xml validator now correctly handles the `--` string within comments [#1980](https://github.com/wazuh/wazuh-kibana-app/pull/1980) - The AWS map visualization wasn't been loaded until the user interacts with it [dd31bd7](https://github.com/wazuh/wazuh-kibana-app/commit/dd31bd7a155354bc50fe0af22fca878607c8936a) + ## Wazuh v3.11.1 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 581 ### Added - - Support for Wazuh v3.11.1. + ## Wazuh v3.11.0 - Kibana v6.8.6, v7.3.2, v7.5.1 - Revision 580 ### Added @@ -769,24 +785,28 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.5.1 + ## Wazuh v3.10.2 - Kibana v7.5.0 - Revision 555 ### Added - Support for Kibana v7.5.0 + ## Wazuh v3.10.2 - Kibana v7.4.2 - Revision 549 ### Added - Support for Kibana v7.4.2 + ## Wazuh v3.10.2 - Kibana v7.4.1 - Revision 548 ### Added - Support for Kibana v7.4.1 + ## Wazuh v3.10.2 - Kibana v7.4.0 - Revision 547 ### Added @@ -794,25 +814,28 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana v7.4.0 - Support for Wazuh v3.10.2. + ## Wazuh v3.10.2 - Kibana v7.3.2 - Revision 546 ### Added - Support for Wazuh v3.10.2. + ## Wazuh v3.10.1 - Kibana v7.3.2 - Revision 545 ### Added - Support for Wazuh v3.10.1. + ## Wazuh v3.10.0 - Kibana v7.3.2 - Revision 543 ### Added - Support for Wazuh v3.10.0. - Added an interactive guide for registering agents, things are now easier for the user, guiding it through the steps needed ending in a _copy & paste_ snippet for deploying his agent [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1468). -- Added new dashboards for the recently added regulatory compliance groups into the Wazuh core. They are HIPAA and NIST-800-53 [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1448), [#1638](https://github.com/wazuh/wazuh-kibana-app/issues/1638). +- Added new dashboards for the recently added regulatory compliance groups into the Wazuh core. They are HIPAA and NIST-800-53 [#1468](https://github.com/wazuh/wazuh-kibana-app/issues/1448), [#1638]( https://github.com/wazuh/wazuh-kibana-app/issues/1638). - Make the app work under a custom Kibana space [#1234](https://github.com/wazuh/wazuh-kibana-app/issues/1234), [#1450](https://github.com/wazuh/wazuh-kibana-app/issues/1450). - Added the ability to manage the app as a native plugin when using Kibana spaces, now you can safely hide/show the app depending on the selected space [#1601](https://github.com/wazuh/wazuh-kibana-app/issues/1601). - Adapt the app the for Kibana dark mode [#1562](https://github.com/wazuh/wazuh-kibana-app/issues/1562). @@ -820,12 +843,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Export all the information of a Wazuh group and its related agents in a PDF document [#1341](https://github.com/wazuh/wazuh-kibana-app/issues/1341). - Export the configuration of a certain agent as a PDF document. Supports granularity for exporting just certain sections of the configuration [#1340](https://github.com/wazuh/wazuh-kibana-app/issues/1340). + ### Changed - Reduced _Agents preview_ load time using the new API endpoint `/summary/agents` [#1687](https://github.com/wazuh/wazuh-kibana-app/pull/1687). - Replaced most of the _md-nav-bar_ Angular.js components with React components using EUI [#1705](https://github.com/wazuh/wazuh-kibana-app/pull/1705). - Replaced the requirements slider component with a new styled component [#1708](https://github.com/wazuh/wazuh-kibana-app/pull/1708). -- Soft deprecated the _.wazuh-version_ internal index, now the app dumps its content if applicable to a registry file, then the app removes that index. Further versions will hard deprecate this index [#1467](https://github.com/wazuh/wazuh-kibana-app/issues/1467). +- Soft deprecated the _.wazuh-version_ internal index, now the app dumps its content if applicable to a registry file, then the app removes that index. Further versions will hard deprecate this index [#1467](https://github.com/wazuh/wazuh-kibana-app/issues/1467). - Visualizations now don't fetch the documents _source_, also, they now use _size: 0_ for fetching [#1663](https://github.com/wazuh/wazuh-kibana-app/issues/1663). - The app menu is now fixed on top of the view, it's not being hidden on every state change. Also, the Wazuh logo was placed in the top bar of Kibana UI [#1502](https://github.com/wazuh/wazuh-kibana-app/issues/1502). - Improved _getTimestamp_ method not returning a promise object because it's no longer needed [014bc3a](https://github.com/wazuh/wazuh-kibana-app/commit/014b3aba0d2e9cda0c4d521f5f16faddc434a21e). Also improved main Discover listener for Wazuh not returning a promise object [bd82823](https://github.com/wazuh/wazuh-kibana-app/commit/bd8282391a402b8c567b32739cf914a0135d74bc). @@ -847,6 +871,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed _Network interfaces_ table in Inventory section, the table was not paginating [#1474](https://github.com/wazuh/wazuh-kibana-app/issues/1474). - Fixed APIs passwords are now obfuscated in server responses [adc3152](https://github.com/wazuh/wazuh-kibana-app/pull/1782/commits/adc31525e26b25e4cb62d81cbae70a8430728af5). + ## Wazuh v3.9.5 - Kibana v6.8.2 / Kibana v7.2.1 / Kibana v7.3.0 - Revision 531 ### Added @@ -886,13 +911,13 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix not properly updated breadcrumb in ruleset section [9645903](https://github.com/wazuh/wazuh-kibana-app/commit/96459031cd4edbe047970bf0d22d0c099771879f) - Fix badly dimensioned table in Integrity Monitoring section [9645903](https://github.com/wazuh/wazuh-kibana-app/commit/96459031cd4edbe047970bf0d22d0c099771879f) - Fix implicit filters can be destroyed [9cf8578](https://github.com/wazuh/wazuh-kibana-app/commit/9cf85786f504f5d67edddeea6cfbf2ab577e799b) -- Windows agent dashboard doesn't show failure logon access. [d38d088](https://github.com/wazuh/wazuh-kibana-app/commit/d38d0881ac8e4294accde83d63108337b74cdd91) -- Number of agents is not properly updated. [f7cbbe5](https://github.com/wazuh/wazuh-kibana-app/commit/f7cbbe54394db825827715c3ad4370ac74317108) -- Missing scrollbar on Firefox file viewer. [df4e8f9](https://github.com/wazuh/wazuh-kibana-app/commit/df4e8f9305b35e9ee1473bed5f5d452dd3420567) -- Agent search filter by name, lost when refreshing. [71b5274](https://github.com/wazuh/wazuh-kibana-app/commit/71b5274ccc332d8961a158587152f7badab28a95) -- Alerts of level 12 cannot be displayed in the Summary table. [ec0e888](https://github.com/wazuh/wazuh-kibana-app/commit/ec0e8885d9f1306523afbc87de01a31f24e36309) -- Restored query from search bar in visualizations. [439128f](https://github.com/wazuh/wazuh-kibana-app/commit/439128f0a1f65b649a9dcb81ab5804ca20f65763) -- Fix Kibana filters loop in Firefox. [82f0f32](https://github.com/wazuh/wazuh-kibana-app/commit/82f0f32946d844ce96a28f0185f903e8e05c5589) +- Windows agent dashboard doesn't show failure logon access. [d38d088](https://github.com/wazuh/wazuh-kibana-app/commit/d38d0881ac8e4294accde83d63108337b74cdd91) +- Number of agents is not properly updated. [f7cbbe5](https://github.com/wazuh/wazuh-kibana-app/commit/f7cbbe54394db825827715c3ad4370ac74317108) +- Missing scrollbar on Firefox file viewer. [df4e8f9](https://github.com/wazuh/wazuh-kibana-app/commit/df4e8f9305b35e9ee1473bed5f5d452dd3420567) +- Agent search filter by name, lost when refreshing. [71b5274](https://github.com/wazuh/wazuh-kibana-app/commit/71b5274ccc332d8961a158587152f7badab28a95) +- Alerts of level 12 cannot be displayed in the Summary table. [ec0e888](https://github.com/wazuh/wazuh-kibana-app/commit/ec0e8885d9f1306523afbc87de01a31f24e36309) +- Restored query from search bar in visualizations. [439128f](https://github.com/wazuh/wazuh-kibana-app/commit/439128f0a1f65b649a9dcb81ab5804ca20f65763) +- Fix Kibana filters loop in Firefox. [82f0f32](https://github.com/wazuh/wazuh-kibana-app/commit/82f0f32946d844ce96a28f0185f903e8e05c5589) ## Wazuh v3.9.3 - Kibana v6.8.1 / v7.1.1 / v7.2.0 - Revision 523 @@ -951,7 +976,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed missing dependency for Discover [43f5dd5](https://github.com/wazuh/wazuh-kibana-app/commit/43f5dd5f64065c618ba930b2a4087f0a9e706c0e). -- Fixed visualization for Agents > Overview [#1477](https://github.com/wazuh/wazuh-kibana-app/pull/1477). +- Fixed visualization for Agents > Overview [#1477](https://github.com/wazuh/wazuh-kibana-app/pull/1477). - Fixed SCA policy checks table [#1478](https://github.com/wazuh/wazuh-kibana-app/pull/1478). ## Wazuh v3.9.1 - Kibana v7.1.0 - Revision 508 From 6f42bc5613c289c60f5ee97334c4bb54d8fb6fd9 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 17:56:51 +0200 Subject: [PATCH 285/493] fix(Changelog): add PR --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d83d962155..62a34a7534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) -- Change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ### Fixed From 13f117019089c3e257da0650ab8a92c7f1c429e7 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 17:59:46 +0200 Subject: [PATCH 286/493] fix(Changelog): add PR --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a34a7534..46d341ca74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) +- change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ### Fixed From ce180d009f264be4688ed42ac43f1e0014431512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Mon, 18 Oct 2021 18:08:39 +0200 Subject: [PATCH 287/493] Ordering logs properly (#3609) --- CHANGELOG.md | 1 + .../management/components/management/mg-logs/logs.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 039eaaf1fa..6d9c593e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix size api selector when name is too long [#3445](https://github.com/wazuh/wazuh-kibana-app/pull/3445) - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) +- Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) ## Wazuh v4.2.3 - Kibana 7.10.2 , 7.12.1 - Revision 4204 diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index 9ff6aed466..0afe4df99e 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -177,8 +177,7 @@ export default compose( if (this.state.logLevelSelect !== 'all') result['level'] = this.state.logLevelSelect; if (this.state.selectedDaemon !== 'all') result['tag'] = this.state.selectedDaemon; if (this.state.appliedSearch) result['search'] = this.state.appliedSearch; - if (this.state.descendingSort) result['sort'] = '+timestamp'; - else result['sort'] = '-timestamp'; + result['sort'] = `${this.state.descendingSort ? '-' : '+'}timestamp`; return result; } From 5b2905c59b2a9a2aeb10e57fc1a1ec6b4a4e0a88 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Mon, 18 Oct 2021 22:07:20 +0200 Subject: [PATCH 288/493] fix(Changelog): add PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d341ca74..530c37905c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) -- change format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) +- Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) ### Fixed From c9fa0a14d97fca0f1f276821f287e88c40bc8262 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Tue, 19 Oct 2021 14:31:15 +0200 Subject: [PATCH 289/493] fix(DetailFile):Refactor --- public/components/agents/fim/inventory/fileDetail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 0be33bf98d..dedf0d42ae 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -331,9 +331,9 @@ export class FileDetails extends Component { } }); return ( - <div> + <div> <EuiFlexGrid columns={3}> {generalDetails} </EuiFlexGrid> - </div> + </div> ); } From c21b3bf30ab7ac21fe8130ce3f0e85ee7fd54066 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 20 Oct 2021 09:16:09 +0200 Subject: [PATCH 290/493] Fixing visualization when pinned agent is activated --- public/react-services/vis-factory-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/react-services/vis-factory-handler.js b/public/react-services/vis-factory-handler.js index ea33c53d22..698490b0e4 100644 --- a/public/react-services/vis-factory-handler.js +++ b/public/react-services/vis-factory-handler.js @@ -101,7 +101,7 @@ export class VisFactoryHandler { try { const data = - (!['sca', 'office'].some(moduleID => tab !== moduleID)) + (!['sca', 'office'].includes(tab)) ? await GenericRequest.request( 'GET', `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` From 75f2d6e1e1d0f8d0774b67e5bbdb8c1eed7a5a30 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Wed, 20 Oct 2021 18:15:36 +0200 Subject: [PATCH 291/493] feat(overview-vuls): columns are added to the report --- .../visualizations/overview/overview-vuls.ts | 94 +++++++++++++++++-- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/server/integration-files/visualizations/overview/overview-vuls.ts b/server/integration-files/visualizations/overview/overview-vuls.ts index 023792da10..cee2e2ab5d 100644 --- a/server/integration-files/visualizations/overview/overview-vuls.ts +++ b/server/integration-files/visualizations/overview/overview-vuls.ts @@ -139,7 +139,7 @@ export default [ schema: 'bucket', params: { field: 'data.vulnerability.severity', - size: 5, + size: 10, order: 'asc', orderBy: '_key', otherBucket: false, @@ -155,15 +155,15 @@ export default [ type: 'terms', schema: 'bucket', params: { - field: 'data.vulnerability.title', - size: 1000, + field: 'agent.id', + size: 1, order: 'desc', orderBy: '1', otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - customLabel: 'Title', + customLabel: 'ID', }, }, { @@ -172,15 +172,15 @@ export default [ type: 'terms', schema: 'bucket', params: { - field: 'data.vulnerability.published', - size: 2, + field: 'data.vulnerability.rationale', + size: 1, order: 'desc', orderBy: '1', otherBucket: false, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', - customLabel: 'Published', + customLabel: 'Rationale', }, }, { @@ -188,6 +188,22 @@ export default [ enabled: true, type: 'terms', schema: 'bucket', + params: { + field: 'data.vulnerability.title', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Title', + }, + },{ + id: '7', + enabled: true, + type: 'terms', + schema: 'bucket', params: { field: 'data.vulnerability.cve', size: 1, @@ -199,6 +215,70 @@ export default [ missingBucketLabel: 'Missing', customLabel: 'CVE', }, + },{ + id: '8', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.package.name', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Name', + }, + },{ + id: '9', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.package.condition', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Condition', + }, + },{ + id: '10', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.references', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'References', + }, + },{ + id: '11', + enabled: true, + type: 'terms', + schema: 'bucket', + params: { + field: 'data.vulnerability.package.version', + size: 1, + order: 'desc', + orderBy: '1', + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + customLabel: 'Version', + }, }, ], }), From 9c6d23954625dd5e93ba4766c05118ac7977015b Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Fri, 22 Oct 2021 11:39:14 +0200 Subject: [PATCH 292/493] feat(GET //): GET changes // to / --- public/components/common/welcome/agents-info.js | 3 +-- public/components/health-check/services/check-setup.service.ts | 2 +- public/components/wz-menu/components/wz-agent-info.js | 2 +- public/controllers/agent/agents-preview.js | 2 +- public/controllers/agent/components/agents-table.js | 2 +- public/controllers/management/monitoring.js | 2 +- server/controllers/wazuh-utils/wazuh-utils.ts | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/public/components/common/welcome/agents-info.js b/public/components/common/welcome/agents-info.js index 5d539088c3..61e507876f 100644 --- a/public/components/common/welcome/agents-info.js +++ b/public/components/common/welcome/agents-info.js @@ -34,8 +34,7 @@ export class AgentInfo extends Component { async componentDidMount() { - const managerVersion = await WzRequest.apiReq('GET', '//', {}); - + const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); this.setState({ managerVersion: (((managerVersion || {}).data || {}).data || {}).api_version || {} }); diff --git a/public/components/health-check/services/check-setup.service.ts b/public/components/health-check/services/check-setup.service.ts index b224fa52bb..e06d8f3791 100644 --- a/public/components/health-check/services/check-setup.service.ts +++ b/public/components/health-check/services/check-setup.service.ts @@ -20,7 +20,7 @@ export const checkSetupService = appInfo => async (checkLogger: CheckLogger) => if (currentApi && currentApi.id) { checkLogger.info(`Current API in cookie: [${currentApi.id}]`); checkLogger.info(`Getting API version data...`); - const versionData = await WzRequest.apiReq('GET', '//', {}); + const versionData = await WzRequest.apiReq('GET', '/ ', {}); const apiVersion = versionData.data.data['api_version']; checkLogger.info(`API version: [${apiVersion}]`); checkLogger.info(`Getting the app version...`); diff --git a/public/components/wz-menu/components/wz-agent-info.js b/public/components/wz-menu/components/wz-agent-info.js index 23c2c49d44..a29542736d 100644 --- a/public/components/wz-menu/components/wz-agent-info.js +++ b/public/components/wz-menu/components/wz-agent-info.js @@ -39,7 +39,7 @@ export class AgentInfo extends Component { } async componentDidMount() { - const managerVersion = await WzRequest.apiReq('GET', '//', {}); + const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); this.setState({ managerVersion: (((managerVersion || {}).data || {}).data || {}).api_version || {} diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index 5732c5e58b..bed39301d4 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -290,7 +290,7 @@ export class AgentsPreviewController { */ async getWazuhVersion() { try { - const data = await WzRequest.apiReq('GET', '//', {}); + const data = await WzRequest.apiReq('GET', '/ ', {}); const result = ((data || {}).data || {}).data || {}; return result.api_version; } catch (error) { diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index f4c8b852e6..5ba9ff44bf 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -162,7 +162,7 @@ export const AgentsTable = withErrorBoundary( } async UNSAFE_componentWillMount() { - const managerVersion = await WzRequest.apiReq('GET', '//', {}); + const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); const agentActive = await WzRequest.apiReq('GET', '/agents', { params: { diff --git a/public/controllers/management/monitoring.js b/public/controllers/management/monitoring.js index 12d3e9fded..33eb8bc8b7 100644 --- a/public/controllers/management/monitoring.js +++ b/public/controllers/management/monitoring.js @@ -266,7 +266,7 @@ export function ClusterController( const data = await Promise.all([ WzRequest.apiReq('GET', '/cluster/nodes', {}), WzRequest.apiReq('GET', '/cluster/local/config', {}), - WzRequest.apiReq('GET', '//', {}), + WzRequest.apiReq('GET', '/ ', {}), WzRequest.apiReq('GET', '/agents', { limit: 1 }), WzRequest.apiReq('GET', '/cluster/healthcheck', {}) ]); diff --git a/server/controllers/wazuh-utils/wazuh-utils.ts b/server/controllers/wazuh-utils/wazuh-utils.ts index d1fecda4c3..6f10e9a9e7 100644 --- a/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/server/controllers/wazuh-utils/wazuh-utils.ts @@ -81,7 +81,7 @@ export class WazuhUtilsCtrl { if( !apiHostID ){ return ErrorResponse('No API id provided', 401, 401, response); }; - const responseTokenIsWorking = await context.wazuh.api.client.asCurrentUser.request('GET', '//', {}, {apiHostID}); + const responseTokenIsWorking = await context.wazuh.api.client.asCurrentUser.request('GET', '/ ', {}, {apiHostID}); if(responseTokenIsWorking.status !== 200){ return ErrorResponse('Token is not valid', 401, 401, response); }; From e13fd54fe3ee17808d4c9c58315d6d2b0d9e1d8c Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Fri, 22 Oct 2021 11:51:39 -0300 Subject: [PATCH 293/493] Updated api endpoints --- common/api-info/endpoints.json | 444 +++++----- common/api-info/security-actions.json | 35 +- server/lib/api-request-list.json | 1121 +++++++++++++++++++------ 3 files changed, 1112 insertions(+), 488 deletions(-) diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index 089b5ce3a8..9e2136a0bd 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -62,13 +62,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -162,7 +162,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -462,13 +462,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -498,7 +498,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -544,13 +544,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -580,7 +580,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -626,13 +626,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -662,23 +662,12 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, - { - "name": "select", - "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "names" - } - } - }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -841,13 +830,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -912,7 +901,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1220,7 +1209,7 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of lines to return.", "schema": { "type": "integer", "format": "int32", @@ -1256,7 +1245,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1769,13 +1758,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -1815,7 +1804,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1921,13 +1910,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -1965,7 +1954,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2035,13 +2024,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2072,7 +2061,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2166,13 +2155,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2195,7 +2184,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2280,13 +2269,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2344,7 +2333,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2444,13 +2433,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2491,7 +2480,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2557,13 +2546,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2586,7 +2575,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2661,13 +2650,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2706,7 +2695,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2773,13 +2762,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2855,7 +2844,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2996,13 +2985,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3025,7 +3014,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3100,13 +3089,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3153,7 +3142,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3236,13 +3225,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3273,7 +3262,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3307,7 +3296,7 @@ }, { "name": "version", - "description": "Filter by version name", + "description": "Filter by package version", "schema": { "type": "string" } @@ -3346,13 +3335,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3423,7 +3412,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3522,13 +3511,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3615,7 +3604,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3718,13 +3707,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3747,7 +3736,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3794,13 +3783,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3830,7 +3819,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3905,13 +3894,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3986,13 +3975,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4015,7 +4004,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4042,7 +4031,7 @@ { "name": "/groups/:group_id/files/:file_name/json", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_json", - "description": "Return the contents of the specified group file parsed to JSON", + "description": "Return the content of the specified group file parsed to JSON", "summary": "Get a file in group", "tags": [ "Groups" @@ -4189,13 +4178,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4226,7 +4215,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4283,13 +4272,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4320,7 +4309,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4667,7 +4656,7 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of lines to return.", "schema": { "type": "integer", "format": "int32", @@ -4703,7 +4692,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4985,13 +4974,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5021,7 +5010,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5094,13 +5083,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5141,7 +5130,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5187,13 +5176,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5234,7 +5223,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5280,13 +5269,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5316,7 +5305,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5373,13 +5362,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5409,7 +5398,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5466,13 +5455,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5502,7 +5491,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5615,13 +5604,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5659,7 +5648,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5804,13 +5793,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5884,7 +5873,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5962,13 +5951,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5999,7 +5988,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6093,13 +6082,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6122,7 +6111,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6175,13 +6164,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6204,7 +6193,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6260,13 +6249,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6310,7 +6299,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6405,13 +6394,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6496,7 +6485,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6600,13 +6589,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6641,7 +6630,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6726,13 +6715,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6767,7 +6756,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6813,13 +6802,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6854,7 +6843,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6919,13 +6908,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6948,7 +6937,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7100,13 +7089,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7144,7 +7133,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7353,13 +7342,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7389,7 +7378,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7472,13 +7461,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7524,7 +7513,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7591,13 +7580,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7680,7 +7669,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7751,7 +7740,7 @@ }, { "name": "type", - "description": "Type of file", + "description": "Type of interface", "schema": { "type": "string" } @@ -7820,13 +7809,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7856,7 +7845,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7990,13 +7979,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8034,7 +8023,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8068,7 +8057,7 @@ }, { "name": "version", - "description": "Filter by version name", + "description": "Filter by package version", "schema": { "type": "string" } @@ -8107,13 +8096,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8191,7 +8180,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8290,13 +8279,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8390,7 +8379,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8481,13 +8470,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8533,7 +8522,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8636,13 +8625,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8680,7 +8669,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8837,7 +8826,7 @@ "query": [ { "name": "force_single_group", - "description": "Whether to append the new group to current agent's multigroup or replace it", + "description": "Removes the agent from all groups to which it belongs and assigns it to the specified group", "schema": { "type": "boolean" } @@ -8924,7 +8913,7 @@ }, { "name": "force_single_group", - "description": "Whether to append the new group to current agent's multigroup or replace it", + "description": "Removes the agent from all groups to which it belongs and assigns it to the specified group", "schema": { "type": "boolean" } @@ -10063,7 +10052,7 @@ "body": [ { "name": "force_time", - "description": "Remove the old agent with the same IP if disconnected since <force_time> seconds", + "description": "Remove the old agent with the same name or IP if disconnected for <force_time> seconds", "type": "integer", "format": "int32", "minimum": 0 @@ -10085,7 +10074,7 @@ { "name": "/agents/insert", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.insert_agent", - "description": "Add an agent specifying its name, ID and IP. If an agent with the same ID already exists, replace it using `force` parameter", + "description": "Add an agent specifying its name, ID and IP. If an agent with the same name, the same ID or the same IP already exists, replace it using `force_time` parameter", "summary": "Add agent full", "tags": [ "Agents" @@ -10111,7 +10100,7 @@ "body": [ { "name": "force_time", - "description": "Remove the old agent with the same IP if disconnected for <force_time> seconds", + "description": "Remove the old agent with the same name, ID or IP if disconnected for <force_time> seconds", "type": "integer", "format": "int32", "minimum": 0 @@ -10156,7 +10145,7 @@ "query": [ { "name": "agent_name", - "description": "Agent name", + "description": "Agent name. The special characters allowed are: '-','_','.' ", "required": true, "schema": { "type": "string", @@ -10607,7 +10596,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10645,7 +10634,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10846,7 +10835,7 @@ { "name": "/agents/:agent_id/group/:group_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_single_group", - "description": "Remove an agent from an specified group. If the agent has multigroups, it will preserve all previous groups except the last one", + "description": "Remove an agent from a specified group. If the agent belongs to several groups, only the specified group will be deleted.", "summary": "Remove agent from group", "tags": [ "Agents" @@ -10912,7 +10901,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10982,6 +10971,47 @@ } ] }, + { + "name": "/experimental/rootcheck", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_rootcheck_database", + "description": "Clear rootcheck database for all agents or a list of them", + "summary": "Clear rootcheck results", + "tags": [ + "Experimental" + ], + "query": [ + { + "name": "agents_list", + "description": "List of agent IDs (separated by comma), use the keyword `all` to select all agents", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "minLength": 3, + "description": "Agent ID|all", + "format": "numbers_or_all" + } + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, { "name": "/experimental/syscheck", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_syscheck_database", @@ -11001,7 +11031,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11042,7 +11072,7 @@ "type": "string", "minLength": 1, "description": "Group name|all", - "format": "group_names_delete" + "format": "group_names_or_all" } } }, @@ -11141,27 +11171,27 @@ ] }, { - "name": "/rootcheck", + "name": "/rootcheck/:agent_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.delete_rootcheck", - "description": "Clear rootcheck database for all agents or a list of them", + "description": "Clear an agent's rootcheck database", "summary": "Clear results", "tags": [ "Rootcheck" ], - "query": [ + "args": [ { - "name": "agents_list", - "description": "List of agent IDs (separated by comma), all agents selected by default if not specified", + "name": ":agent_id", + "description": "Agent ID. All possible values from 000 onwards", + "required": true, "schema": { - "type": "array", - "items": { - "type": "string", - "minLength": 3, - "description": "Agent ID", - "format": "numbers" - } + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" } - }, + } + ], + "query": [ { "name": "pretty", "description": "Show results in human-readable format", @@ -11263,7 +11293,7 @@ "items": { "type": "string", "description": "Policy ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11311,7 +11341,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11355,7 +11385,7 @@ "items": { "type": "string", "description": "Policy ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11414,7 +11444,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "Security rule ID|all" } } @@ -11454,7 +11484,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "Security rule ID|all" } } @@ -11503,7 +11533,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "User ID|all" } } @@ -11556,7 +11586,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11573,7 +11603,7 @@ { "name": "/syscheck/:agent_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.delete_syscheck_agent", - "description": "Clear file integrity monitoring scan results for a specified agent", + "description": "Clear file integrity monitoring scan results for a specified agent. Only available for agents < 3.12.0, it doesn't apply for more recent ones", "summary": "Clear results", "tags": [ "Syscheck" diff --git a/common/api-info/security-actions.json b/common/api-info/security-actions.json index 87f3a4ef57..2907b397d4 100644 --- a/common/api-info/security-actions.json +++ b/common/api-info/security-actions.json @@ -595,62 +595,63 @@ "GET /mitre/techniques" ] }, - "rootcheck:clear": { - "description": "Clear the agents rootcheck database", + "rootcheck:run": { + "description": "Run agents rootcheck scan", "resources": [ "agent:id", "agent:group" ], "example": { "actions": [ - "rootcheck:clear" + "rootcheck:run" ], "resources": [ "agent:id:*" ], - "effect": "deny" + "effect": "allow" }, "related_endpoints": [ - "DELETE /rootcheck" + "PUT /rootcheck" ] }, - "rootcheck:run": { - "description": "Run agents rootcheck scan", + "rootcheck:read": { + "description": "Access information from agents rootcheck database", "resources": [ "agent:id", "agent:group" ], "example": { "actions": [ - "rootcheck:run" + "rootcheck:read" ], "resources": [ - "agent:id:*" + "agent:id:011" ], "effect": "allow" }, "related_endpoints": [ - "PUT /rootcheck" + "GET /rootcheck/{agent_id}", + "GET /rootcheck/{agent_id}/last_scan" ] }, - "rootcheck:read": { - "description": "Access information from agents rootcheck database", + "rootcheck:clear": { + "description": "Clear the agents rootcheck database", "resources": [ "agent:id", "agent:group" ], "example": { "actions": [ - "rootcheck:read" + "rootcheck:clear" ], "resources": [ - "agent:id:011" + "agent:id:*" ], - "effect": "allow" + "effect": "deny" }, "related_endpoints": [ - "GET /rootcheck/{agent_id}", - "GET /rootcheck/{agent_id}/last_scan" + "DELETE /rootcheck/{agent_id}", + "DELETE /experimental/rootcheck" ] }, "rules:read": { diff --git a/server/lib/api-request-list.json b/server/lib/api-request-list.json index dfae980a92..9e2136a0bd 100644 --- a/server/lib/api-request-list.json +++ b/server/lib/api-request-list.json @@ -62,13 +62,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -162,7 +162,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -462,13 +462,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -498,7 +498,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -544,13 +544,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -580,7 +580,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -626,13 +626,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -662,23 +662,12 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, - { - "name": "select", - "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "names" - } - } - }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -841,13 +830,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -912,7 +901,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1220,7 +1209,7 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of lines to return.", "schema": { "type": "integer", "format": "int32", @@ -1256,7 +1245,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1769,13 +1758,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -1815,7 +1804,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -1921,13 +1910,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -1965,7 +1954,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2035,13 +2024,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2072,7 +2061,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2166,13 +2155,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2195,7 +2184,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2280,13 +2269,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2344,7 +2333,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2444,13 +2433,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2491,7 +2480,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2557,13 +2546,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2586,7 +2575,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2661,13 +2650,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2706,7 +2695,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2773,13 +2762,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -2855,7 +2844,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -2996,13 +2985,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3025,7 +3014,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3100,13 +3089,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3153,7 +3142,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3236,13 +3225,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3273,7 +3262,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3307,7 +3296,7 @@ }, { "name": "version", - "description": "Filter by version name", + "description": "Filter by package version", "schema": { "type": "string" } @@ -3346,13 +3335,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3423,7 +3412,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3522,13 +3511,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3615,7 +3604,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3718,13 +3707,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3747,7 +3736,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3794,13 +3783,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3830,7 +3819,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -3905,13 +3894,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -3986,13 +3975,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4015,7 +4004,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4042,7 +4031,7 @@ { "name": "/groups/:group_id/files/:file_name/json", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_json", - "description": "Return the contents of the specified group file parsed to JSON", + "description": "Return the content of the specified group file parsed to JSON", "summary": "Get a file in group", "tags": [ "Groups" @@ -4189,13 +4178,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4226,7 +4215,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4283,13 +4272,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -4320,7 +4309,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4667,7 +4656,7 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of lines to return.", "schema": { "type": "integer", "format": "int32", @@ -4703,7 +4692,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -4900,57 +4889,549 @@ } }, { - "name": "wait_for_complete", - "description": "Disable timeout response", + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/manager/stats/weekly", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_weekly", + "description": "Return Wazuh statistical information per week. Each number in the averages field represents the average of alerts per hour for that specific day", + "summary": "Get stats week", + "tags": [ + "Manager" + ], + "query": [ + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/manager/status", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_status", + "description": "Return the status of all Wazuh daemons", + "summary": "Get status", + "tags": [ + "Manager" + ], + "query": [ + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/groups", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_groups", + "description": "Return the groups from MITRE database", + "summary": "Get MITRE groups", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "group_ids", + "description": "List of MITRE's group IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE group ID" + } + } + }, + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/metadata", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_metadata", + "description": "Return the metadata from MITRE database", + "summary": "Get MITRE metadata", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/mitigations", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_mitigations", + "description": "Return the mitigations from MITRE database", + "summary": "Get MITRE mitigations", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "mitigation_ids", + "description": "List of MITRE's mitigations IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE mitigation ID" + } + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/references", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_references", + "description": "Return the references from MITRE database", + "summary": "Get MITRE references", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "reference_ids", + "description": "List of MITRE's references IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE Reference ID" + } + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/software", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_software", + "description": "Return the software from MITRE database", + "summary": "Get MITRE software", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", + "schema": { + "type": "string", + "format": "search" + } + }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, + { + "name": "software_ids", + "description": "List of MITRE's software IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE software ID" + } + } + }, + { + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "string", + "format": "sort" + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, + { + "name": "/mitre/tactics", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_tactics", + "description": "Return the tactics from MITRE database", + "summary": "Get MITRE tactics", + "tags": [ + "MITRE" + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "offset", + "description": "First element to return in the collection", + "schema": { + "type": "integer", + "format": "int32", + "default": 0, + "minimum": 0 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { - "type": "boolean", - "default": false + "type": "string", + "format": "search" } - } - ] - }, - { - "name": "/manager/stats/weekly", - "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_weekly", - "description": "Return Wazuh statistical information per week. Each number in the averages field represents the average of alerts per hour for that specific day", - "summary": "Get stats week", - "tags": [ - "Manager" - ], - "query": [ + }, { - "name": "pretty", - "description": "Show results in human-readable format", + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", "schema": { - "type": "boolean", - "default": false + "type": "array", + "items": { + "type": "string", + "format": "names" + } } }, { - "name": "wait_for_complete", - "description": "Disable timeout response", + "name": "sort", + "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", "schema": { - "type": "boolean", - "default": false + "type": "string", + "format": "sort" } - } - ] - }, - { - "name": "/manager/status", - "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_status", - "description": "Return the status of all Wazuh daemons", - "summary": "Get status", - "tags": [ - "Manager" - ], - "query": [ + }, { - "name": "pretty", - "description": "Show results in human-readable format", + "name": "tactic_ids", + "description": "List of MITRE's tactics IDs (separated by comma)", "schema": { - "type": "boolean", - "default": false + "type": "array", + "items": { + "type": "string", + "description": "MITRE tactic ID" + } } }, { @@ -4964,31 +5445,23 @@ ] }, { - "name": "/mitre", - "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_attack", - "description": "Return the requested attacks from MITRE database", - "summary": "Get MITRE attacks", + "name": "/mitre/techniques", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_techniques", + "description": "Return the techniques from MITRE database", + "summary": "Get MITRE techniques", "tags": [ - "Mitre" + "MITRE" ], "query": [ - { - "name": "id", - "description": "MITRE attack ID", - "schema": { - "type": "string", - "format": "alphanumeric" - } - }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5001,22 +5474,6 @@ "minimum": 0 } }, - { - "name": "phase_name", - "description": "Show results filtered by phase", - "schema": { - "type": "string", - "format": "alphanumeric" - } - }, - { - "name": "platform_name", - "description": "Show results filtered by platform", - "schema": { - "type": "string", - "format": "alphanumeric" - } - }, { "name": "pretty", "description": "Show results in human-readable format", @@ -5034,7 +5491,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5059,6 +5516,17 @@ "format": "sort" } }, + { + "name": "technique_ids", + "description": "List of MITRE's techniques IDs (separated by comma)", + "schema": { + "type": "array", + "items": { + "type": "string", + "description": "MITRE technique ID" + } + } + }, { "name": "wait_for_complete", "description": "Disable timeout response", @@ -5136,13 +5604,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5180,7 +5648,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5325,18 +5793,18 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { "name": "mitre", - "description": "Filters by MITRE attack ID", + "description": "Filters by MITRE technique ID", "schema": { "type": "string", "format": "alphanumeric" @@ -5405,7 +5873,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5483,13 +5951,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5520,7 +5988,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5614,13 +6082,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5643,7 +6111,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5696,13 +6164,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5725,7 +6193,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5781,13 +6249,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -5831,7 +6299,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -5891,7 +6359,7 @@ "description": "Filter by command", "schema": { "type": "string", - "format": "alphanumeric" + "format": "symbols_alphanumeric_param" } }, { @@ -5926,13 +6394,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6017,7 +6485,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6121,13 +6589,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6162,12 +6630,23 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6236,13 +6715,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6277,12 +6756,23 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6312,13 +6802,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6353,12 +6843,23 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6407,13 +6908,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6436,12 +6937,23 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" } }, + { + "name": "select", + "description": "Select which fields to return (separated by comma). Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "names" + } + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -6577,13 +7089,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6621,7 +7133,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6830,13 +7342,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -6866,7 +7378,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -6949,13 +7461,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7001,7 +7513,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7068,13 +7580,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7157,7 +7669,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7228,7 +7740,7 @@ }, { "name": "type", - "description": "Type of file", + "description": "Type of interface", "schema": { "type": "string" } @@ -7297,13 +7809,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7333,7 +7845,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7467,13 +7979,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7511,7 +8023,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7545,7 +8057,7 @@ }, { "name": "version", - "description": "Filter by version name", + "description": "Filter by package version", "schema": { "type": "string" } @@ -7584,13 +8096,13 @@ "query": [ { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7668,7 +8180,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7767,13 +8279,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -7867,7 +8379,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -7953,18 +8465,18 @@ "description": "Filter by command", "schema": { "type": "string", - "format": "alphanumeric" + "format": "symbols_alphanumeric_param" } }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8010,7 +8522,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8113,13 +8625,13 @@ }, { "name": "limit", - "description": "Maximum number of elements to return", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", "schema": { "type": "integer", "format": "int32", "default": 500, "minimum": 1, - "maximum": 500 + "maximum": 100000 } }, { @@ -8157,7 +8669,7 @@ }, { "name": "search", - "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beggining", + "description": "Look for elements containing the specified string. To obtain a complementary search, use '-' at the beginning", "schema": { "type": "string", "format": "search" @@ -8314,7 +8826,7 @@ "query": [ { "name": "force_single_group", - "description": "Whether to append the new group to current agent's multigroup or replace it", + "description": "Removes the agent from all groups to which it belongs and assigns it to the specified group", "schema": { "type": "boolean" } @@ -8401,7 +8913,7 @@ }, { "name": "force_single_group", - "description": "Whether to append the new group to current agent's multigroup or replace it", + "description": "Removes the agent from all groups to which it belongs and assigns it to the specified group", "schema": { "type": "boolean" } @@ -8511,6 +9023,46 @@ } ] }, + { + "name": "/agents/reconnect", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.reconnect_agents", + "description": "Force reconnect all agents or a list of them", + "summary": "Force reconnect agents", + "tags": [ + "Agents" + ], + "query": [ + { + "name": "agents_list", + "description": "List of agent IDs (separated by comma), all agents selected by default if not specified", + "schema": { + "type": "array", + "items": { + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" + } + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, { "name": "/agents/restart", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents", @@ -9500,7 +10052,7 @@ "body": [ { "name": "force_time", - "description": "Remove the old agent with the same IP if disconnected since <force_time> seconds", + "description": "Remove the old agent with the same name or IP if disconnected for <force_time> seconds", "type": "integer", "format": "int32", "minimum": 0 @@ -9522,7 +10074,7 @@ { "name": "/agents/insert", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.insert_agent", - "description": "Add an agent specifying its name, ID and IP. If an agent with the same ID already exists, replace it using `force` parameter", + "description": "Add an agent specifying its name, ID and IP. If an agent with the same name, the same ID or the same IP already exists, replace it using `force_time` parameter", "summary": "Add agent full", "tags": [ "Agents" @@ -9548,7 +10100,7 @@ "body": [ { "name": "force_time", - "description": "Remove the old agent with the same IP if disconnected for <force_time> seconds", + "description": "Remove the old agent with the same name, ID or IP if disconnected for <force_time> seconds", "type": "integer", "format": "int32", "minimum": 0 @@ -9593,7 +10145,7 @@ "query": [ { "name": "agent_name", - "description": "Agent name", + "description": "Agent name. The special characters allowed are: '-','_','.' ", "required": true, "schema": { "type": "string", @@ -10044,7 +10596,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10082,7 +10634,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10283,7 +10835,7 @@ { "name": "/agents/:agent_id/group/:group_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_single_group", - "description": "Remove an agent from an specified group. If the agent has multigroups, it will preserve all previous groups except the last one", + "description": "Remove an agent from a specified group. If the agent belongs to several groups, only the specified group will be deleted.", "summary": "Remove agent from group", "tags": [ "Agents" @@ -10349,7 +10901,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10419,6 +10971,47 @@ } ] }, + { + "name": "/experimental/rootcheck", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_rootcheck_database", + "description": "Clear rootcheck database for all agents or a list of them", + "summary": "Clear rootcheck results", + "tags": [ + "Experimental" + ], + "query": [ + { + "name": "agents_list", + "description": "List of agent IDs (separated by comma), use the keyword `all` to select all agents", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "minLength": 3, + "description": "Agent ID|all", + "format": "numbers_or_all" + } + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] + }, { "name": "/experimental/syscheck", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_syscheck_database", @@ -10438,7 +11031,7 @@ "type": "string", "minLength": 3, "description": "Agent ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10479,7 +11072,7 @@ "type": "string", "minLength": 1, "description": "Group name|all", - "format": "group_names_delete" + "format": "group_names_or_all" } } }, @@ -10578,27 +11171,27 @@ ] }, { - "name": "/rootcheck", + "name": "/rootcheck/:agent_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.delete_rootcheck", - "description": "Clear rootcheck database for all agents or a list of them", + "description": "Clear an agent's rootcheck database", "summary": "Clear results", "tags": [ "Rootcheck" ], - "query": [ + "args": [ { - "name": "agents_list", - "description": "List of agent IDs (separated by comma), all agents selected by default if not specified", + "name": ":agent_id", + "description": "Agent ID. All possible values from 000 onwards", + "required": true, "schema": { - "type": "array", - "items": { - "type": "string", - "minLength": 3, - "description": "Agent ID", - "format": "numbers" - } + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" } - }, + } + ], + "query": [ { "name": "pretty", "description": "Show results in human-readable format", @@ -10700,7 +11293,7 @@ "items": { "type": "string", "description": "Policy ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10748,7 +11341,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10792,7 +11385,7 @@ "items": { "type": "string", "description": "Policy ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -10851,7 +11444,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "Security rule ID|all" } } @@ -10891,7 +11484,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "Security rule ID|all" } } @@ -10940,7 +11533,7 @@ "type": "array", "items": { "type": "string", - "format": "numbers_delete", + "format": "numbers_or_all", "description": "User ID|all" } } @@ -10993,7 +11586,7 @@ "items": { "type": "string", "description": "Role ID|all", - "format": "numbers_delete" + "format": "numbers_or_all" } } }, @@ -11010,7 +11603,7 @@ { "name": "/syscheck/:agent_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.delete_syscheck_agent", - "description": "Clear file integrity monitoring scan results for a specified agent", + "description": "Clear file integrity monitoring scan results for a specified agent. Only available for agents < 3.12.0, it doesn't apply for more recent ones", "summary": "Clear results", "tags": [ "Syscheck" From 28d5196d6604641f9b5b5c7ac07875f4cd58e3dc Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Mon, 25 Oct 2021 09:05:00 -0300 Subject: [PATCH 294/493] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56877021a..faf4203cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) +- Updated depracated and new references authd [#3663](https://github.com/wazuh/wazuh-kibana-app/pull/3663) ### Fixed From d0c4eb5bb5a5eb344799f3551ba5131f9da293d4 Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Mon, 25 Oct 2021 12:43:18 -0300 Subject: [PATCH 295/493] Updated registration service configuration ui --- .../registration-service/registration-service.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/controllers/management/components/management/configuration/registration-service/registration-service.js b/public/controllers/management/components/management/configuration/registration-service/registration-service.js index b9c4822b38..e10013c768 100644 --- a/public/controllers/management/components/management/configuration/registration-service/registration-service.js +++ b/public/controllers/management/components/management/configuration/registration-service/registration-service.js @@ -46,8 +46,9 @@ const mainSettings = [ label: 'Limit registration to maximum number of agents' }, { - field: 'force_insert', - label: 'Force registration when using an existing IP address' + field: 'force', + label: 'Force registration when using an existing IP address', + render: (value) => value.enabled } ]; const sslSettings = [ From cb164e7432467f9ade11c883d4cc1d3b44b39ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Nov 2021 10:38:31 +0100 Subject: [PATCH 296/493] fix: Fixes in the requests to Wazuh API endpoint `//` - Replaced the check for the endpoint path to accept the root Wazuh API endpoint `/` - Removed the space in the requests for the root Wazuh API endpoint --- public/components/common/welcome/agents-info.js | 2 +- public/components/health-check/services/check-setup.service.ts | 2 +- public/components/wz-menu/components/wz-agent-info.js | 2 +- public/controllers/agent/agents-preview.js | 2 +- public/controllers/agent/components/agents-table.js | 2 +- public/controllers/management/monitoring.js | 2 +- server/controllers/wazuh-api.ts | 2 +- server/controllers/wazuh-utils/wazuh-utils.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/components/common/welcome/agents-info.js b/public/components/common/welcome/agents-info.js index 61e507876f..7b4dc22470 100644 --- a/public/components/common/welcome/agents-info.js +++ b/public/components/common/welcome/agents-info.js @@ -34,7 +34,7 @@ export class AgentInfo extends Component { async componentDidMount() { - const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); + const managerVersion = await WzRequest.apiReq('GET', '/', {}); this.setState({ managerVersion: (((managerVersion || {}).data || {}).data || {}).api_version || {} }); diff --git a/public/components/health-check/services/check-setup.service.ts b/public/components/health-check/services/check-setup.service.ts index e06d8f3791..5425515a76 100644 --- a/public/components/health-check/services/check-setup.service.ts +++ b/public/components/health-check/services/check-setup.service.ts @@ -20,7 +20,7 @@ export const checkSetupService = appInfo => async (checkLogger: CheckLogger) => if (currentApi && currentApi.id) { checkLogger.info(`Current API in cookie: [${currentApi.id}]`); checkLogger.info(`Getting API version data...`); - const versionData = await WzRequest.apiReq('GET', '/ ', {}); + const versionData = await WzRequest.apiReq('GET', '/', {}); const apiVersion = versionData.data.data['api_version']; checkLogger.info(`API version: [${apiVersion}]`); checkLogger.info(`Getting the app version...`); diff --git a/public/components/wz-menu/components/wz-agent-info.js b/public/components/wz-menu/components/wz-agent-info.js index a29542736d..d853c56401 100644 --- a/public/components/wz-menu/components/wz-agent-info.js +++ b/public/components/wz-menu/components/wz-agent-info.js @@ -39,7 +39,7 @@ export class AgentInfo extends Component { } async componentDidMount() { - const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); + const managerVersion = await WzRequest.apiReq('GET', '/', {}); this.setState({ managerVersion: (((managerVersion || {}).data || {}).data || {}).api_version || {} diff --git a/public/controllers/agent/agents-preview.js b/public/controllers/agent/agents-preview.js index bed39301d4..9f51b4ec0a 100644 --- a/public/controllers/agent/agents-preview.js +++ b/public/controllers/agent/agents-preview.js @@ -290,7 +290,7 @@ export class AgentsPreviewController { */ async getWazuhVersion() { try { - const data = await WzRequest.apiReq('GET', '/ ', {}); + const data = await WzRequest.apiReq('GET', '/', {}); const result = ((data || {}).data || {}).data || {}; return result.api_version; } catch (error) { diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 5ba9ff44bf..95d8499858 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -162,7 +162,7 @@ export const AgentsTable = withErrorBoundary( } async UNSAFE_componentWillMount() { - const managerVersion = await WzRequest.apiReq('GET', '/ ', {}); + const managerVersion = await WzRequest.apiReq('GET', '/', {}); const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); const agentActive = await WzRequest.apiReq('GET', '/agents', { params: { diff --git a/public/controllers/management/monitoring.js b/public/controllers/management/monitoring.js index 33eb8bc8b7..1785a417e0 100644 --- a/public/controllers/management/monitoring.js +++ b/public/controllers/management/monitoring.js @@ -266,7 +266,7 @@ export function ClusterController( const data = await Promise.all([ WzRequest.apiReq('GET', '/cluster/nodes', {}), WzRequest.apiReq('GET', '/cluster/local/config', {}), - WzRequest.apiReq('GET', '/ ', {}), + WzRequest.apiReq('GET', '/', {}), WzRequest.apiReq('GET', '/agents', { limit: 1 }), WzRequest.apiReq('GET', '/cluster/healthcheck', {}) ]); diff --git a/server/controllers/wazuh-api.ts b/server/controllers/wazuh-api.ts index 7cf14f4c80..5c14b71fd8 100644 --- a/server/controllers/wazuh-api.ts +++ b/server/controllers/wazuh-api.ts @@ -718,7 +718,7 @@ export class WazuhApiCtrl { return ErrorResponse('Request method is not valid.', 3015, 400, response); } else if (!request.body.path) { return ErrorResponse('Missing param: path', 3016, 400, response); - } else if (!request.body.path.match(/^\/.+/)) { + } else if (!request.body.path.startsWith('/')) { log('wazuh-api:makeRequest', 'Request path is not valid.'); //Path doesn't start with '/' return ErrorResponse('Request path is not valid.', 3015, 400, response); diff --git a/server/controllers/wazuh-utils/wazuh-utils.ts b/server/controllers/wazuh-utils/wazuh-utils.ts index 6f10e9a9e7..c505b4fe9f 100644 --- a/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/server/controllers/wazuh-utils/wazuh-utils.ts @@ -81,7 +81,7 @@ export class WazuhUtilsCtrl { if( !apiHostID ){ return ErrorResponse('No API id provided', 401, 401, response); }; - const responseTokenIsWorking = await context.wazuh.api.client.asCurrentUser.request('GET', '/ ', {}, {apiHostID}); + const responseTokenIsWorking = await context.wazuh.api.client.asCurrentUser.request('GET', '/', {}, {apiHostID}); if(responseTokenIsWorking.status !== 200){ return ErrorResponse('Token is not valid', 401, 401, response); }; From d122555d6dad2afcc776af455177dc1f6eb45b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Nov 2021 10:59:26 +0100 Subject: [PATCH 297/493] changelog: Add PR to chengelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56877021a..ffb6ac86ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) - Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) +- Fixed the Wazuh API requests to `GET //` [#3661](https://github.com/wazuh/wazuh-kibana-app/pull/3661) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 From ecb077dc9998fb569b4fa6eab8e2bde4806de63d Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Tue, 2 Nov 2021 11:28:18 -0300 Subject: [PATCH 298/493] fix(tactics): fixed search and details --- .../components/flyout-technique/flyout-technique.tsx | 4 ++-- .../overview/mitre/components/techniques/techniques.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 6fa1b41b1b..391ec6ba75 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -124,7 +124,7 @@ export class FlyoutTechnique extends Component { const { currentTechnique } = this.props; const result = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `references.external_id=${currentTechnique}` + q: `external_id=${currentTechnique}` } }); const rawData = (((result || {}).data || {}).data || {}).affected_items @@ -151,7 +151,7 @@ export class FlyoutTechnique extends Component { const { tacticsObject } = this.props; return tactics.map((element) => { const tactic = Object.values(tacticsObject).find(obj => obj.id === element); - return { id:tactic.references[0].external_id, name: tactic.name}; + return { id:tactic.external_id, name: tactic.name}; }); } diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 8f72e4b83e..98127358b9 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -466,7 +466,7 @@ export const Techniques = withWindowSize( }); const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( (item) => - item.references.filter((reference) => reference.source === MITRE_ATTACK)[0] + [item].filter((reference) => reference.source === MITRE_ATTACK)[0] .external_id ); this._isMount && this.setState({ filteredTechniques, isSearching: false }); From e431a9cd11b61209c78bbc6a31b77dc2bdb0a89e Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Tue, 2 Nov 2021 11:42:25 -0300 Subject: [PATCH 299/493] fix(test):fix test --- .../__snapshots__/health-check.container.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap index 128d5ec6e4..24eca50be6 100644 --- a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap +++ b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` <img alt="" className="health-check-logo" - src="/plugins/wazuh/assets/icon_blue.svg" + src="/plugins/wazuh/assets/undefined" /> <div className="margin-top-30" From f4cd6d5626ea7a6fe4d37bb79466392a26265271 Mon Sep 17 00:00:00 2001 From: yenienserrano <yenienserrano75@gmail.com> Date: Tue, 2 Nov 2021 11:59:31 -0300 Subject: [PATCH 300/493] fix(changelog): add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56877021a..531266fbf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) - Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) +- Fixed missing mitre tactics [#3675](https://github.com/wazuh/wazuh-kibana-app/pull/3675) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 From 8bd2a0c99deac305be706f5ef5193881db1a4b6c Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Tue, 2 Nov 2021 15:01:53 -0300 Subject: [PATCH 301/493] Update api requests and points json --- common/api-info/endpoints.json | 44 +++++++++++++++++++++++--------- server/lib/api-request-list.json | 44 +++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index 9e2136a0bd..44df2521e1 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -10050,13 +10050,6 @@ } ], "body": [ - { - "name": "force_time", - "description": "Remove the old agent with the same name or IP if disconnected for <force_time> seconds", - "type": "integer", - "format": "int32", - "minimum": 0 - }, { "name": "ip", "description": "If this is not included, the API will get the IP automatically. Allowed values: IP, IP/NET, ANY", @@ -10099,11 +10092,38 @@ ], "body": [ { - "name": "force_time", - "description": "Remove the old agent with the same name, ID or IP if disconnected for <force_time> seconds", - "type": "integer", - "format": "int32", - "minimum": 0 + "name": "force", + "type": "object", + "description": "Remove the old agent with the same name, ID or IP if the configuration is matched", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable force option" + }, + "disconnected_time": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable force disconnected_time option" + }, + "value": { + "type": "string", + "default": "1h", + "description": "Time the agent must has been disconnected to force the insertion. Time in seconds, ‘[n_days]d’, ‘[n_hours]h’, ‘[n_minutes]m’ or ‘[n_seconds]s’. For example, `7d`, `10s` and `10` are valid values. If no time unit is specified, seconds are used", + "format": "timeframe" + } + } + }, + "after_registration_time": { + "type": "string", + "default": "1h", + "description": "Time the agent must has been registered to force the insertion. Time in seconds, ‘[n_days]d’, ‘[n_hours]h’, ‘[n_minutes]m’ or ‘[n_seconds]s’. For example, `7d`, `10s` and `10` are valid values. If no time unit is specified, seconds are used", + "format": "timeframe" + } + } }, { "name": "id", diff --git a/server/lib/api-request-list.json b/server/lib/api-request-list.json index 9e2136a0bd..44df2521e1 100644 --- a/server/lib/api-request-list.json +++ b/server/lib/api-request-list.json @@ -10050,13 +10050,6 @@ } ], "body": [ - { - "name": "force_time", - "description": "Remove the old agent with the same name or IP if disconnected for <force_time> seconds", - "type": "integer", - "format": "int32", - "minimum": 0 - }, { "name": "ip", "description": "If this is not included, the API will get the IP automatically. Allowed values: IP, IP/NET, ANY", @@ -10099,11 +10092,38 @@ ], "body": [ { - "name": "force_time", - "description": "Remove the old agent with the same name, ID or IP if disconnected for <force_time> seconds", - "type": "integer", - "format": "int32", - "minimum": 0 + "name": "force", + "type": "object", + "description": "Remove the old agent with the same name, ID or IP if the configuration is matched", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable force option" + }, + "disconnected_time": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable force disconnected_time option" + }, + "value": { + "type": "string", + "default": "1h", + "description": "Time the agent must has been disconnected to force the insertion. Time in seconds, ‘[n_days]d’, ‘[n_hours]h’, ‘[n_minutes]m’ or ‘[n_seconds]s’. For example, `7d`, `10s` and `10` are valid values. If no time unit is specified, seconds are used", + "format": "timeframe" + } + } + }, + "after_registration_time": { + "type": "string", + "default": "1h", + "description": "Time the agent must has been registered to force the insertion. Time in seconds, ‘[n_days]d’, ‘[n_hours]h’, ‘[n_minutes]m’ or ‘[n_seconds]s’. For example, `7d`, `10s` and `10` are valid values. If no time unit is specified, seconds are used", + "format": "timeframe" + } + } }, { "name": "id", From 085f00abcda9c104e840ae4b41571b779fc12397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Thu, 4 Nov 2021 19:10:39 +0100 Subject: [PATCH 302/493] Bug/3296 cdb list view not working with ipv6 (#3488) * CDB list view not working with IPv6 * Adding Changelog --- CHANGELOG.md | 2 ++ .../management/components/management/ruleset/list-editor.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56877021a..091b83214e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error when edit a rule or decoder [#3456](https://github.com/wazuh/wazuh-kibana-app/pull/3456) - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) - Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) +- Fix CDB list view not working with IPv6 [#3488](https://github.com/wazuh/wazuh-kibana-app/pull/3488) + ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/public/controllers/management/components/management/ruleset/list-editor.js b/public/controllers/management/components/management/ruleset/list-editor.js index 044c05bc6e..d8e576d429 100644 --- a/public/controllers/management/components/management/ruleset/list-editor.js +++ b/public/controllers/management/components/management/ruleset/list-editor.js @@ -93,7 +93,7 @@ class WzListEditor extends Component { const items = {}; const lines = content.split('\n'); lines.forEach((line) => { - const split = line.split(':'); + const split = line.startsWith('"') ? line.split('":') : line.split(':'); const key = split[0]; const value = split[1] || ''; if (key) items[key] = value; // Prevent add empty keys From 239e4fb45bbdd0abb852e26fddaa4d2d36fdaf35 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Thu, 4 Nov 2021 19:12:28 +0100 Subject: [PATCH 303/493] Test decoders before matching rules (#3446) --- CHANGELOG.md | 1 + .../wz-logtest/components/logtest.tsx | 73 ++++++++++--------- public/react-services/wz-request.ts | 3 +- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091b83214e..191728b070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored all try catch value of context for ErrorOrchestrator service. [#3432](https://github.com/wazuh/wazuh-kibana-app/pull/3432) - Refactored all try catch strategy on Controller/Groups section [#3415](https://github.com/wazuh/wazuh-kibana-app/pull/3415) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) +- Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) ### Fixed diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index ee3ee9b95b..8893e75219 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -65,34 +65,37 @@ export const Logtest = compose( }; const formatResult = (result, alert) => { + let returnedDataFormatted =`**Phase 1: Completed pre-decoding. \n ` + + `full event: ${result.full_log || '-'} \n ` + + `timestamp: ${(result.predecoder || '').timestamp || '-'} \n ` + + `hostname: ${(result.predecoder || '').hostname || '-'} \n ` + + `program_name: ${(result.predecoder || '').program_name || '-'} \n\n` + + `**Phase 2: Completed decoding. \n ` + + `name: ${(result.decoder || '').name || '-'} \n ` + + `${(result.decoder || '').parent ? `parent: ${(result.decoder || '').parent} \n ` : ''}` + + `data: ${JSON.stringify(result.data || '-', null, 6).replace('}', ' }')} \n\n` ; + + result.rule && ( + returnedDataFormatted += `**Phase 3: Completed filtering (rules). \n ` + + `id: ${(result.rule || '').id || '-'} \n ` + + `level: ${(result.rule || '').level || '-'} \n ` + + `description: ${(result.rule || '').description || '-'} \n ` + + `groups: ${JSON.stringify((result.rule || '').groups || '-')} \n ` + + `firedtimes: ${(result.rule || '').firedtimes || '-'} \n ` + + `gdpr: ${JSON.stringify((result.rule || '').gdpr || '-')} \n ` + + `gpg13: ${JSON.stringify((result.rule || '').gpg13 || '-')} \n ` + + `hipaa: ${JSON.stringify((result.rule || '').hipaa || '-')} \n ` + + `mail: ${JSON.stringify((result.rule || '').mail || '-')} \n ` + + `mitre.id: ${JSON.stringify((result.rule || '').mitre || ''.id || '-')} \n ` + + `mitre.technique: ${JSON.stringify((result.rule || '').mitre || ''.technique || '-')} \n ` + + `nist_800_53: ${JSON.stringify((result.rule || '').nist_800_53 || '-')} \n ` + + `pci_dss: ${JSON.stringify((result.rule || '').pci_dss || '-')} \n ` + + `tsc: ${JSON.stringify((result.rule || '').tsc || '-')} \n` + ); + + returnedDataFormatted += `${alert ? `**Alert to be generated. \n\n\n` : '\n\n'}` return ( - `**Phase 1: Completed pre-decoding. \n ` + - `full event: ${result.full_log || '-'} \n ` + - `timestamp: ${(result.predecoder || '').timestamp || '-'} \n ` + - `hostname: ${(result.predecoder || '').hostname || '-'} \n ` + - `program_name: ${(result.predecoder || '').program_name || '-'} \n\n` + - `**Phase 2: Completed decoding. \n ` + - `name: ${(result.decoder || '').name || '-'} \n ` + - `${(result.decoder || '').parent ? `parent: ${(result.decoder || '').parent} \n ` : ''}` + - `data: ${JSON.stringify(result.data || '-', null, 6).replace('}', ' }')} \n\n` + - `**Phase 3: Completed filtering (rules). \n ` + - `id: ${(result.rule || '').id || '-'} \n ` + - `level: ${(result.rule || '').level || '-'} \n ` + - `description: ${(result.rule || '').description || '-'} \n ` + - `groups: ${JSON.stringify((result.rule || '').groups || '-')} \n ` + - `firedtimes: ${(result.rule || '').firedtimes || '-'} \n ` + - `gdpr: ${JSON.stringify((result.rule || '').gdpr || '-')} \n ` + - `gpg13: ${JSON.stringify((result.rule || '').gpg13 || '-')} \n ` + - `hipaa: ${JSON.stringify((result.rule || '').hipaa || '-')} \n ` + - `mail: ${JSON.stringify((result.rule || '').mail || '-')} \n ` + - `mitre.id: ${JSON.stringify((result.rule || '').mitre || ''.id || '-')} \n ` + - `mitre.technique: ${JSON.stringify( - (result.rule || '').mitre || ''.technique || '-' - )} \n ` + - `nist_800_53: ${JSON.stringify((result.rule || '').nist_800_53 || '-')} \n ` + - `pci_dss: ${JSON.stringify((result.rule || '').pci_dss || '-')} \n ` + - `tsc: ${JSON.stringify((result.rule || '').tsc || '-')} \n` + - `${alert ? `**Alert to be generated. \n\n\n` : '\n\n'}` + returnedDataFormatted ); }; @@ -104,24 +107,24 @@ export const Logtest = compose( let gotToken = Boolean(token); try { - for (let event of events) { + for (let event of events) { const response = await WzRequest.apiReq('PUT', '/logtest', { log_format: 'syslog', location: 'logtest', event, ...(token ? { token } : {}), }); + token = response.data.data.token; !sessionToken && !gotToken && token && dispatch(updateLogtestToken(token)); token && (gotToken = true); responses.push(response); - } - - const testResults = responses.map((response) => - response.data.data.output.rule || '' - ? formatResult(response.data.data.output, response.data.data.alert) - : `No result found for: ${response.data.data.output.full_log} \n\n\n` - ); + }; + const testResults = responses.map((response) => { + return response.data.data.output || '' + ? formatResult(response.data.data.output, response.data.data.alert) + : `No result found for: ${response.data.data.output.full_log} \n\n\n` + }); setTestResult(testResults); } finally { setTesting(false); diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 5dcb361d96..86c6abdeab 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -104,11 +104,12 @@ export class WzRequest { try { if (!method || !path || !body) { throw new Error('Missing parameters'); - } + } const id = JSON.parse(AppState.getCurrentAPI()).id; const requestData = { method, path, body, id }; const response = await this.genericReq('POST', '/api/request', requestData); const hasFailed = (((response || {}).data || {}).data || {}).total_failed_items || 0; + if (hasFailed) { const error = ((((response.data || {}).data || {}).failed_items || [])[0] || {}).error || {}; From 2aec1dc580ce1659baea11632f734f6aba17150b Mon Sep 17 00:00:00 2001 From: mpRegalado <80431234+mpRegalado@users.noreply.github.com> Date: Thu, 4 Nov 2021 19:16:01 +0100 Subject: [PATCH 304/493] Removed api selector toggle from the settings menu (#3604) * Removed api selector toggle from the settings menu * Removed api.selector entries from config-equivalences * Updated changelog * Removed comment referring to api selector toggle --- CHANGELOG.md | 1 + common/constants.ts | 1 - .../configuration/utils/configuration-handler.js | 3 --- public/react-services/app-state.js | 11 ----------- public/react-services/wz-api-check.js | 1 - public/services/resolves/get-config.js | 1 - public/utils/config-equivalences.js | 5 ----- 7 files changed, 1 insertion(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 191728b070..62300a09e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) - Fixed generation of huge logs from backend errors [#3397](https://github.com/wazuh/wazuh-kibana-app/pull/3397) - Fixed vulnerabilities flyout not showing alerts if the vulnerability had a field missing [#3593](https://github.com/wazuh/wazuh-kibana-app/pull/3593) +- Removed api selector toggle from settings menu since it performed no useful function [#3604](https://github.com/wazuh/wazuh-kibana-app/pull/3604) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/common/constants.ts b/common/constants.ts index 8b9fcb7d49..a65af0ee0c 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -198,7 +198,6 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'extensions.osquery': false, 'extensions.docker': false, timeout: 20000, - 'api.selector': true, 'ip.selector': true, 'ip.ignore': [], 'xpack.rbac.enabled': true, diff --git a/public/components/settings/configuration/utils/configuration-handler.js b/public/components/settings/configuration/utils/configuration-handler.js index dca9bbfdfc..a7a337e928 100644 --- a/public/components/settings/configuration/utils/configuration-handler.js +++ b/public/components/settings/configuration/utils/configuration-handler.js @@ -19,9 +19,6 @@ export default class ConfigurationHandler { */ static async editKey(key, value) { try { - if (key === 'api.selector') { - AppState.setAPISelector(value); - } if (key === 'ip.selector') { AppState.setPatternSelector(value); } diff --git a/public/react-services/app-state.js b/public/react-services/app-state.js index a4a5fc9b00..f583e1a3d6 100644 --- a/public/react-services/app-state.js +++ b/public/react-services/app-state.js @@ -299,17 +299,6 @@ export class AppState { : false; } - /** - * Set a new value to the 'patternSelector' cookie - * @param {*} value - */ - static setAPISelector(value) { - const encodedPattern = encodeURI(value); - getCookies().set('APISelector', encodedPattern, { - path: window.location.pathname - }); - } - /** * Get 'patternSelector' value */ diff --git a/public/react-services/wz-api-check.js b/public/react-services/wz-api-check.js index 8454cdb894..ad19b028de 100644 --- a/public/react-services/wz-api-check.js +++ b/public/react-services/wz-api-check.js @@ -37,7 +37,6 @@ export class ApiCheck { if (Object.keys(configuration).length) { AppState.setPatternSelector(configuration['ip.selector']); - AppState.setAPISelector(configuration['api.selector']); } const response = await axios(options); diff --git a/public/services/resolves/get-config.js b/public/services/resolves/get-config.js index 944c51bdfc..ba1ae8220f 100644 --- a/public/services/resolves/get-config.js +++ b/public/services/resolves/get-config.js @@ -45,7 +45,6 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { 'extensions.osquery': false, 'extensions.docker': false, timeout: 20000, - 'api.selector': true, 'ip.selector': true, 'ip.ignore': [], 'xpack.rbac.enabled': true, diff --git a/public/utils/config-equivalences.js b/public/utils/config-equivalences.js index e0a438887e..ce3682054c 100644 --- a/public/utils/config-equivalences.js +++ b/public/utils/config-equivalences.js @@ -37,8 +37,6 @@ export const configEquivalences = { 'Enable or disable the Docker listener tab on Overview and Agents.', timeout: 'Defines the maximum time the app will wait for an API response when making requests to it.', - 'api.selector': - 'Defines if the user is allowed to change the selected API directly from the top menu bar.', 'ip.selector': 'Defines if the user is allowed to change the selected index pattern directly from the top menu bar.', 'ip.ignore': @@ -90,7 +88,6 @@ export const nameEquivalence = { 'checks.timeFilter': 'Set time filter to 24h', 'checks.maxBuckets': 'Set max buckets to 200000', timeout: 'Request timeout', - 'api.selector': 'API selector', 'ip.selector': 'IP selector', 'ip.ignore': 'IP ignore', 'xpack.rbac.enabled': 'X-Pack RBAC', @@ -137,7 +134,6 @@ export const categoriesEquivalence = { 'checks.timeFilter': HEALTH_CHECK, 'checks.maxBuckets': HEALTH_CHECK, timeout: GENERAL, - 'api.selector': GENERAL, 'ip.selector': GENERAL, 'ip.ignore': GENERAL, 'wazuh.monitoring.enabled': MONITORING, @@ -182,7 +178,6 @@ export const formEquivalence = { 'checks.timeFilter': { type: BOOLEAN }, 'checks.maxBuckets': { type: BOOLEAN }, timeout: { type: NUMBER }, - 'api.selector': { type: BOOLEAN }, 'ip.selector': { type: BOOLEAN }, 'ip.ignore': { type: ARRAY }, 'xpack.rbac.enabled': { type: BOOLEAN }, From f83bceff803b60bccdf9b8f95f10eb7f4ff8fb74 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Fri, 5 Nov 2021 09:29:59 -0300 Subject: [PATCH 305/493] feat(detail): display form changes --- .../agents/fim/inventory/fileDetail.tsx | 25 +++++++++++++++++-- public/styles/inventory.scss | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index dedf0d42ae..6ab84a4e24 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -276,6 +276,7 @@ export class FileDetails extends Component { if (!item.onlyLinux || (item.onlyLinux && this.props.agent && agentPlatform !== 'windows')) { let className = item.checksum ? 'detail-value detail-value-checksum' : 'detail-value'; className += item.field === 'perm' ? ' detail-value-perm' : ''; + className += ' wz-width-100'; return ( <EuiFlexItem key={idx}> <EuiStat @@ -320,7 +321,7 @@ export class FileDetails extends Component { ) : ( this.userSvg )} - <span className="detail-title">{item.name}</span> + {item.name === 'Permissions' && agentPlatform === 'windows' ? '' : <span className="detail-title">{item.name}</span> } </span> } textAlign="left" @@ -344,7 +345,27 @@ export class FileDetails extends Component { renderFileDetailsPermissions(value) { if (((this.props.agent || {}).os || {}).platform === 'windows' && value && value !== '-') { return ( - <EuiAccordion id={Math.random().toString()} paddingSize="none" initialIsOpen={false}> + <EuiAccordion + id={Math.random().toString()} + paddingSize="none" + initialIsOpen={false} + arrowDisplay="none" + buttonContent={ + <EuiTitle size="s"> + <h3> + Permissions + <span style={{ marginLeft: 16 }}> + <EuiToolTip position="top" content="Show"> + <EuiIcon + className="euiButtonIcon euiButtonIcon--primary" + type="inspect" + aria-label="show" + /> + </EuiToolTip> + </span> + </h3> + </EuiTitle> + }> <EuiCodeBlock language="json" paddingSize="l"> {JSON.stringify(value, null, 2)} </EuiCodeBlock> diff --git a/public/styles/inventory.scss b/public/styles/inventory.scss index 481606565e..9e9ae54239 100644 --- a/public/styles/inventory.scss +++ b/public/styles/inventory.scss @@ -78,7 +78,8 @@ padding: 0px 0px 8px 0; } - .events-accordion { + .events-accordion, + .euiStat .euiTitle .detail-value .euiAccordion { border-bottom: none !important; } From 89d1ef41a3af51e1e3764c167a4526d1475aa2f1 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Fri, 5 Nov 2021 13:34:02 +0100 Subject: [PATCH 306/493] Wrong active-response endpoint parameter from dev tools (#3466) --- CHANGELOG.md | 2 +- .../overview/mitre/components/techniques/techniques.tsx | 3 ++- public/controllers/dev-tools/dev-tools.ts | 5 +++-- public/react-services/wz-request.ts | 2 ++ server/controllers/wazuh-api.ts | 5 ++++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62300a09e4..74c3f1ad15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed index pattern selector doesn't display the ignored index patterns [#3458](https://github.com/wazuh/wazuh-kibana-app/pull/3458) - Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) - Fix CDB list view not working with IPv6 [#3488](https://github.com/wazuh/wazuh-kibana-app/pull/3488) - +- Fixed the bad requests using Console tool to `PUT /active-response` API endpoint [#3466](https://github.com/wazuh/wazuh-kibana-app/pull/3466) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 8f72e4b83e..fc2f02ddfb 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -553,6 +553,7 @@ export const Techniques = withWindowSize( <EuiSpacer size="s" /> <div>{this.renderFacet()}</div> + { isFlyoutVisible && <EuiOverlayMask headerZindexLocation="below"> <EuiOutsideClickDetector onOutsideClick={() => this.onChangeFlyout(false)}> @@ -573,4 +574,4 @@ export const Techniques = withWindowSize( ); } } -); +); \ No newline at end of file diff --git a/public/controllers/dev-tools/dev-tools.ts b/public/controllers/dev-tools/dev-tools.ts index 0a37edeeab..67c25de2d8 100644 --- a/public/controllers/dev-tools/dev-tools.ts +++ b/public/controllers/dev-tools/dev-tools.ts @@ -499,7 +499,7 @@ export class DevToolsController { const spaceLineStart = (line.match(reLineStart) || [])[1] || ''; const inputKeyBodyParam = (line.match(reLineStart) || [])[2] || ''; - const renderBodyParam = (parameter, spaceLineStart) => { + const renderBodyParam = (parameter, spaceLineStart) => { let valueBodyParam = ''; if (parameter.type === 'string') { valueBodyParam = '""' @@ -793,7 +793,7 @@ export class DevToolsController { : '/'; let JSONraw = {}; - try { + try { JSONraw = JSON.parse(paramsInline || desiredGroup.requestTextJson); } catch (error) { JSONraw = {}; @@ -820,6 +820,7 @@ export class DevToolsController { if (typeof JSONraw === 'object') JSONraw.devTools = true; if (!firstTime) { const output = await this.wzRequest.apiReq(method, path, JSONraw); + if (typeof output === 'string' && output.includes('3029')) { this.apiOutputBox.setValue('This method is not allowed without admin mode'); } diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 86c6abdeab..8ac97409b8 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -50,10 +50,12 @@ export class WzRequest { data: payload, timeout: customTimeout || timeout, }; + const data = await axios(options); if (data['error']) { throw new Error(data['error']); } + return Promise.resolve(data); } catch (error) { OdfeUtils.checkOdfeSessionExpired(error); diff --git a/server/controllers/wazuh-api.ts b/server/controllers/wazuh-api.ts index 7cf14f4c80..275ffefaa3 100644 --- a/server/controllers/wazuh-api.ts +++ b/server/controllers/wazuh-api.ts @@ -522,7 +522,7 @@ export class WazuhApiCtrl { shouldKeepArrayAsIt(method, path) { // Methods that we must respect a do not transform them const isAgentsRestart = method === 'POST' && path === '/agents/restart'; - const isActiveResponse = method === 'PUT' && path.startsWith('/active-response/'); + const isActiveResponse = method === 'PUT' && path.startsWith('/active-response'); const isAddingAgentsToGroup = method === 'POST' && path.startsWith('/agents/group/'); // Returns true only if one of the above conditions is true @@ -539,6 +539,7 @@ export class WazuhApiCtrl { * @returns {Object} API response or ErrorResponse */ async makeRequest(context, method, path, data, id, response) { + const devTools = !!(data || {}).devTools; try { const api = await this.manageHosts.getHostById(id); @@ -629,6 +630,7 @@ export class WazuhApiCtrl { } } } + const responseToken = await context.wazuh.api.client.asCurrentUser.request(method, path, data, options); const responseIsDown = this.checkResponseIsDown(responseToken); if (responseIsDown) { @@ -701,6 +703,7 @@ export class WazuhApiCtrl { * @returns {Object} api response or ErrorResponse */ requestApi(context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) { + const idApi = getCookieValueByName(request.headers.cookie, 'wz-api'); if (idApi !== request.body.id) { // if the current token belongs to a different API id, we relogin to obtain a new token return ErrorResponse( From e91a42cca138e2a389cdf5c9b9ac15e2b14facb8 Mon Sep 17 00:00:00 2001 From: sortizowlh <47242022+sortizowlh@users.noreply.github.com> Date: Fri, 5 Nov 2021 13:57:54 +0100 Subject: [PATCH 307/493] Disable apply button at initial state and move agents to their original position (#3605) --- CHANGELOG.md | 1 + common/api-info/endpoints.json | 838 +++------- .../add-modules-data/sample-data.tsx | 12 +- .../common/hooks/use-filter-manager.ts | 9 +- .../groups/multiple-agent-selector.tsx | 1388 +++++++++-------- .../security/roles-mapping/roles-mapping.tsx | 66 +- .../roles/services/get-roles.service.ts | 6 +- .../users/services/get-users.service.ts | 7 +- .../configuration/configuration-switch.js | 118 +- .../management/groups/groups-main.js | 46 +- .../components/management/management-main.js | 7 +- .../management/management-provider.js | 5 +- .../management/status/status-overview.js | 90 +- public/controllers/management/management.js | 16 +- public/kibana-services.ts | 25 +- public/react-services/toast-notifications.tsx | 28 +- public/react-services/wz-request.ts | 3 + 17 files changed, 1170 insertions(+), 1495 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74c3f1ad15..4eb06dfc7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed order logs properly in Management/Logs [#3609](https://github.com/wazuh/wazuh-kibana-app/pull/3609) - Fix CDB list view not working with IPv6 [#3488](https://github.com/wazuh/wazuh-kibana-app/pull/3488) - Fixed the bad requests using Console tool to `PUT /active-response` API endpoint [#3466](https://github.com/wazuh/wazuh-kibana-app/pull/3466) +- Fixed group agent management table does not update on error [#3605](https://github.com/wazuh/wazuh-kibana-app/pull/3605) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index 089b5ce3a8..b4f1a88fc3 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -7,9 +7,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.default_controller.default_info", "description": "Return basic information about the API", "summary": "Get API info", - "tags": [ - "API Info" - ], + "tags": ["API Info"], "query": [ { "name": "pretty", @@ -26,9 +24,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agents", "description": "Return information about all available agents or a list of them", "summary": "List agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -194,12 +190,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "active", - "pending", - "never_connected", - "disconnected" - ] + "enum": ["active", "pending", "never_connected", "disconnected"] }, "minItems": 1 } @@ -227,9 +218,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_config", "description": "Return the active configuration the agent is currently using. This can be different from the configuration present in the configuration file, if it has been modified and the agent has not been restarted yet", "summary": "Get active configuration", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -324,9 +313,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_sync_agent", "description": "Return whether the agent configuration has been synchronized with the agent or not. This can be useful to check after updating a group configuration", "summary": "Get configuration sync status", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -364,9 +351,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_key", "description": "Return the key of an agent", "summary": "Get key", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -404,9 +389,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_component_stats", "description": "Return Wazuh's {component} statistical information from agent {agent_id}", "summary": "Get agent's component stats", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -425,10 +408,7 @@ "required": true, "schema": { "type": "string", - "enum": [ - "logcollector", - "agent" - ] + "enum": ["logcollector", "agent"] } } ], @@ -456,9 +436,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_no_group", "description": "Return a list with all the available agents without an assigned group", "summary": "List agents without group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "limit", @@ -538,9 +516,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_outdated", "description": "Return the list of outdated agents", "summary": "List outdated agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "limit", @@ -609,9 +585,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_fields", "description": "Return all the different combinations that agents have for the selected fields. It also indicates the total number of agents that have each combination", "summary": "List agents distinct", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "fields", @@ -702,9 +676,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_summary_os", "description": "Return a summary of the OS of available agents", "summary": "Summarize agents OS", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "pretty", @@ -729,9 +701,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_summary_status", "description": "Return a summary of the status of available agents", "summary": "Summarize agents status", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "pretty", @@ -756,9 +726,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_upgrade", "description": "Return the agents upgrade results", "summary": "Get upgrade results", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -796,9 +764,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.ciscat_controller.get_agents_ciscat_results", "description": "Return the agent's ciscat results info", "summary": "Get results", - "tags": [ - "Ciscat" - ], + "tags": ["Ciscat"], "args": [ { "name": ":agent_id", @@ -960,9 +926,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_configuration_node", "description": "Return wazuh configuration used in node {node_id}. The 'section' and 'field' parameters will be ignored if 'raw' parameter is provided.", "summary": "Get node config", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1055,9 +1019,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_node_config", "description": "Return the requested configuration in JSON format for the specified node", "summary": "Get node active configuration", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":component", @@ -1150,9 +1112,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_info_node", "description": "Return basic information about a specified node such as version, compilation date, installation path", "summary": "Get node info", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1188,9 +1148,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_log_node", "description": "Return the last 2000 wazuh log entries in the specified node", "summary": "Get node logs", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1208,14 +1166,7 @@ "description": "Filter by log level", "schema": { "type": "string", - "enum": [ - "critical", - "debug", - "debug2", - "error", - "info", - "warning" - ] + "enum": ["critical", "debug", "debug2", "error", "info", "warning"] } }, { @@ -1324,9 +1275,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_log_summary_node", "description": "Return a summary of the last 2000 wazuh log entries in the specified node", "summary": "Get node logs summary", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1362,9 +1311,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_node", "description": "Return Wazuh statistical information in node {node_id} for the current or specified date", "summary": "Get node stats", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1408,9 +1355,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_analysisd_node", "description": "Return Wazuh analysisd statistical information in node {node_id}", "summary": "Get node stats analysisd", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1446,9 +1391,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_hourly_node", "description": "Return Wazuh statistical information in node {node_id} per hour. Each number in the averages field represents the average of alerts per hour", "summary": "Get node stats hour", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1484,9 +1427,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_remoted_node", "description": "Return Wazuh remoted statistical information in node {node_id}", "summary": "Get node stats remoted", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1522,9 +1463,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_weekly_node", "description": "Return Wazuh statistical information in node {node_id} per week. Each number in the averages field represents the average of alerts per hour for that specific day", "summary": "Get node stats week", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1560,9 +1499,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_status_node", "description": "Return the status of all Wazuh daemons in node node_id", "summary": "Get node status", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -1598,9 +1535,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_api_config", "description": "Return the API configuration of all nodes (or a list of them) in JSON format", "summary": "Get nodes API config", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "nodes_list", @@ -1635,9 +1570,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_conf_validation", "description": "Return whether the Wazuh configuration is correct or not in all cluster nodes or a list of them", "summary": "Check nodes config", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "nodes_list", @@ -1672,9 +1605,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_healthcheck", "description": "Return cluster healthcheck information for all nodes or a list of them. Such information includes last keep alive, last synchronization time and number of agents reporting on each node", "summary": "Get nodes healthcheck", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "nodes_list", @@ -1709,9 +1640,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_config", "description": "Return the current node cluster configuration", "summary": "Get local node config", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "pretty", @@ -1736,9 +1665,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_cluster_node", "description": "Return basic information about the cluster node receiving the request", "summary": "Get local node info", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "pretty", @@ -1763,9 +1690,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_cluster_nodes", "description": "Get information about all nodes in the cluster or a list of them", "summary": "Get nodes info", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "limit", @@ -1845,10 +1770,7 @@ "description": "Filter by node type", "schema": { "type": "string", - "enum": [ - "worker", - "master" - ] + "enum": ["worker", "master"] } }, { @@ -1866,9 +1788,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_status", "description": "Return information about the cluster status", "summary": "Get cluster status", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "pretty", @@ -1893,9 +1813,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders", "description": "Return information about all decoders included in ossec.conf. This information include decoder's route, decoder's name, decoder's file among others", "summary": "List decoders", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "query": [ { "name": "decoder_names", @@ -1995,11 +1913,7 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": [ - "enabled", - "disabled", - "all" - ], + "enum": ["enabled", "disabled", "all"], "minItems": 1 } }, @@ -2018,9 +1932,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders_files", "description": "Return information about all decoders files used in Wazuh. This information include decoder's file, decoder's route and decoder's status among others", "summary": "Get files", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "query": [ { "name": "filename", @@ -2091,11 +2003,7 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": [ - "enabled", - "disabled", - "all" - ], + "enum": ["enabled", "disabled", "all"], "minItems": 1 } }, @@ -2114,9 +2022,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_file", "description": "Get the content of a specified decoder file", "summary": "Get decoders file content", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "args": [ { "name": ":filename", @@ -2160,9 +2066,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders_parents", "description": "Return information about all parent decoders. A parent decoder is a decoder used as base of other decoders", "summary": "Get parent decoders", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "query": [ { "name": "limit", @@ -2235,9 +2139,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_cis_cat_results", "description": "Return CIS-CAT results for all agents or a list of them", "summary": "Get agents CIS-CAT results", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -2392,9 +2294,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_hardware_info", "description": "Return all agents (or a list of them) hardware info. This information include cpu, ram, scan info among others of all agents", "summary": "Get agents hardware", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -2531,9 +2431,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_hotfixes_info", "description": "Return all agents (or a list of them) hotfixes info", "summary": "Get agents hotfixes", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -2626,9 +2524,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_address_info", "description": "Return all agents (or a list of them) IPv4 and IPv6 addresses associated to their network interfaces. This information include used IP protocol, interface, and IP address among others", "summary": "Get agents netaddr", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "address", @@ -2746,9 +2642,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_interface_info", "description": "Return all agents (or a list of them) network interfaces. This information includes rx, scan, tx info and some network information among other", "summary": "Get agents netiface", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "adapter", @@ -2947,9 +2841,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_protocol_info", "description": "Return all agents (or a list of them) routing configuration for each network interface. This information includes interface, type protocol information among other", "summary": "Get agents netproto", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -2970,12 +2862,7 @@ "schema": { "type": "string", "description": "DHCP status", - "enum": [ - "enabled", - "disabled", - "unknown", - "BOOTP" - ] + "enum": ["enabled", "disabled", "unknown", "BOOTP"] } }, { @@ -3073,9 +2960,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_os_info", "description": "Return all agents (or a list of them) OS info. This information includes os information, architecture information among other", "summary": "Get agents OS", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -3201,9 +3086,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_packages_info", "description": "Return all agents (or a list of them) packages info. This information includes name, section, size, and priority information of all packages among other", "summary": "Get agents packages", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -3327,9 +3210,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_ports_info", "description": "Return all agents (or a list of them) ports info. This information includes local IP, Remote IP, protocol information among other", "summary": "Get agents ports", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -3479,9 +3360,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_processes_info", "description": "Return all agents (or a list of them) processes info", "summary": "Get agents processes", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -3679,9 +3558,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_list_group", "description": "Get information about all groups or a list of them. Returns a list containing basic information about each group such as number of agents belonging to the group and the checksums of the configuration and shared files", "summary": "Get groups", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "query": [ { "name": "groups_list", @@ -3776,9 +3653,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agents_in_group", "description": "Return the list of agents that belong to the specified group", "summary": "Get agents in a group", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":group_id", @@ -3862,12 +3737,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "active", - "pending", - "never_connected", - "disconnected" - ] + "enum": ["active", "pending", "never_connected", "disconnected"] }, "minItems": 1 } @@ -3887,9 +3757,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_config", "description": "Return the group configuration defined in the `agent.conf` file", "summary": "Get group configuration", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":group_id", @@ -3947,9 +3815,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_files", "description": "Return the files placed under the group directory", "summary": "Get group files", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":group_id", @@ -4044,9 +3910,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_json", "description": "Return the contents of the specified group file parsed to JSON", "summary": "Get a file in group", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":file_name", @@ -4084,12 +3948,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "conf", - "rootkit_files", - "rootkit_trojans", - "rcl" - ] + "enum": ["conf", "rootkit_files", "rootkit_trojans", "rcl"] } } }, @@ -4108,9 +3967,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_xml", "description": "Return the contents of the specified group file parsed to XML", "summary": "Get a file in group", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":file_name", @@ -4148,12 +4005,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "conf", - "rootkit_files", - "rootkit_trojans", - "rcl" - ] + "enum": ["conf", "rootkit_files", "rootkit_trojans", "rcl"] } } }, @@ -4172,9 +4024,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_lists", "description": "Return the contents of all CDB lists. Optionally, the result can be filtered by several criteria. See available parameters for more details", "summary": "Get CDB lists info", - "tags": [ - "Lists" - ], + "tags": ["Lists"], "query": [ { "name": "filename", @@ -4266,9 +4116,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_lists_files", "description": "Return the path from all CDB lists. Use this method to know all the CDB lists and their location in the filesystem relative to Wazuh installation folder", "summary": "Get CDB lists files", - "tags": [ - "Lists" - ], + "tags": ["Lists"], "query": [ { "name": "filename", @@ -4349,9 +4197,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_file", "description": "Return the content of a CDB list file. Only the filename can be specified. It will be searched recursively if not found", "summary": "Get CDB list file content", - "tags": [ - "Lists" - ], + "tags": ["Lists"], "args": [ { "name": ":filename", @@ -4395,9 +4241,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_api_config", "description": "Return the local API configuration in JSON format", "summary": "Get API config", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4422,9 +4266,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_configuration", "description": "Return wazuh configuration used. The 'section' and 'field' parameters will be ignored if 'raw' parameter is provided.", "summary": "Get configuration", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "field", @@ -4506,9 +4348,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_manager_config_ondemand", "description": "Return the requested active configuration in JSON format", "summary": "Get active configuration", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "args": [ { "name": ":component", @@ -4592,9 +4432,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_conf_validation", "description": "Return whether the Wazuh configuration is correct", "summary": "Check config", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4619,9 +4457,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_info", "description": "Return basic information such as version, compilation date, installation path", "summary": "Get information", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4646,23 +4482,14 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_log", "description": "Return the last 2000 wazuh log entries", "summary": "Get logs", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "level", "description": "Filter by log level", "schema": { "type": "string", - "enum": [ - "critical", - "debug", - "debug2", - "error", - "info", - "warning" - ] + "enum": ["critical", "debug", "debug2", "error", "info", "warning"] } }, { @@ -4771,9 +4598,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_log_summary", "description": "Return a summary of the last 2000 wazuh log entries", "summary": "Get logs summary", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4798,9 +4623,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats", "description": "Return Wazuh statistical information for the current or specified date", "summary": "Get stats", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "date", @@ -4833,9 +4656,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_analysisd", "description": "Return Wazuh analysisd statistical information", "summary": "Get stats analysisd", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4860,9 +4681,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_hourly", "description": "Return Wazuh statistical information per hour. Each number in the averages field represents the average of alerts per hour", "summary": "Get stats hour", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4887,9 +4706,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_remoted", "description": "Return Wazuh remoted statistical information", "summary": "Get stats remoted", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4914,9 +4731,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_weekly", "description": "Return Wazuh statistical information per week. Each number in the averages field represents the average of alerts per hour for that specific day", "summary": "Get stats week", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4941,9 +4756,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_status", "description": "Return the status of all Wazuh daemons", "summary": "Get status", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -4968,9 +4781,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_groups", "description": "Return the groups from MITRE database", "summary": "Get MITRE groups", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "group_ids", @@ -5061,9 +4872,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_metadata", "description": "Return the metadata from MITRE database", "summary": "Get MITRE metadata", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "pretty", @@ -5088,9 +4897,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_mitigations", "description": "Return the mitigations from MITRE database", "summary": "Get MITRE mitigations", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "limit", @@ -5181,9 +4988,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_references", "description": "Return the references from MITRE database", "summary": "Get MITRE references", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "limit", @@ -5274,9 +5079,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_software", "description": "Return the software from MITRE database", "summary": "Get MITRE software", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "limit", @@ -5367,9 +5170,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_tactics", "description": "Return the tactics from MITRE database", "summary": "Get MITRE tactics", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "limit", @@ -5460,9 +5261,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_techniques", "description": "Return the techniques from MITRE database", "summary": "Get MITRE techniques", - "tags": [ - "MITRE" - ], + "tags": ["MITRE"], "query": [ { "name": "limit", @@ -5553,9 +5352,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.overview_controller.get_overview_agents", "description": "Return a dictionary with a full agents overview", "summary": "Get agents overview", - "tags": [ - "Overview" - ], + "tags": ["Overview"], "query": [ { "name": "pretty", @@ -5580,9 +5377,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.get_rootcheck_agent", "description": "Return the rootcheck database of an agent", "summary": "Get results", - "tags": [ - "Rootcheck" - ], + "tags": ["Rootcheck"], "args": [ { "name": ":agent_id", @@ -5707,9 +5502,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.get_last_scan_agent", "description": "Return the timestamp of the last rootcheck scan of an agent", "summary": "Get last scan datetime", - "tags": [ - "Rootcheck" - ], + "tags": ["Rootcheck"], "args": [ { "name": ":agent_id", @@ -5747,9 +5540,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules", "description": "Return a list containing information about each rule such as file where it's defined, description, rule group, status, etc", "summary": "List rules", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "query": [ { "name": "filename", @@ -5914,11 +5705,7 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": [ - "enabled", - "disabled", - "all" - ], + "enum": ["enabled", "disabled", "all"], "minItems": 1 } }, @@ -5945,9 +5732,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_files", "description": "Return a list containing all files used to define rules and their status", "summary": "Get files", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "query": [ { "name": "filename", @@ -6018,11 +5803,7 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": [ - "enabled", - "disabled", - "all" - ], + "enum": ["enabled", "disabled", "all"], "minItems": 1 } }, @@ -6041,9 +5822,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_file", "description": "Get the content of a specified rule in the ruleset", "summary": "Get rules file content", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "args": [ { "name": ":filename", @@ -6087,9 +5866,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_groups", "description": "Return a list containing all rule groups names", "summary": "Get groups", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "query": [ { "name": "limit", @@ -6151,24 +5928,14 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_requirement", "description": "Return all specified requirement names defined in the Wazuh ruleset", "summary": "Get requirements", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "args": [ { "name": ":requirement", "required": true, "schema": { "type": "string", - "enum": [ - "pci_dss", - "gdpr", - "hipaa", - "nist-800-53", - "gpg13", - "tsc", - "mitre" - ] + "enum": ["pci_dss", "gdpr", "hipaa", "nist-800-53", "gpg13", "tsc", "mitre"] } } ], @@ -6233,9 +6000,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.sca_controller.get_sca_agent", "description": "Return the security SCA database of an agent", "summary": "Get results", - "tags": [ - "SCA" - ], + "tags": ["SCA"], "args": [ { "name": ":agent_id", @@ -6339,9 +6104,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.sca_controller.get_sca_checks", "description": "Return the policy monitoring alerts for a given policy", "summary": "Get policy checks", - "tags": [ - "SCA" - ], + "tags": ["SCA"], "args": [ { "name": ":agent_id", @@ -6541,9 +6304,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rbac_actions", "description": "Get all RBAC actions, including the potential related resources and endpoints.", "summary": "List RBAC actions", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "endpoint", @@ -6567,9 +6328,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_security_config", "description": "Return the security configuration in JSON format", "summary": "Get security config", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -6594,9 +6353,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_policies", "description": "Get all policies in the system, including the administrator policy", "summary": "List policies", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "limit", @@ -6681,9 +6438,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rbac_resources", "description": "This method should be called to get all current defined RBAC resources.", "summary": "List RBAC resources", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -6720,9 +6475,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_roles", "description": "For a specific list, indicate the ids separated by commas. Example: ?role_ids=1,2,3", "summary": "List roles", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "limit", @@ -6807,9 +6560,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rules", "description": "Get a list of security rules from the system or all of them. These rules must be mapped with roles to obtain certain access privileges. For a specific list, indicate the ids separated by commas. Example: ?rule_ids=1,2,3", "summary": "List security rules", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "limit", @@ -6894,9 +6645,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.login_user", "description": "This method should be called to get an API token. This token will expire after auth_token_exp_timeout seconds (default: 900). This value can be changed using PUT /security/config", "summary": "Login", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "raw", @@ -6913,9 +6662,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_users", "description": "Get the information of a specified user", "summary": "List users", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "limit", @@ -7000,9 +6747,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_user_me", "description": "Get the information of the current user", "summary": "Get current user info", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -7027,9 +6772,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_user_me_policies", "description": "Get the processed policies information for the current user", "summary": "Get current user processed policies", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -7046,9 +6789,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.get_syscheck_agent", "description": "Return FIM findings in the specified agent", "summary": "Get results", - "tags": [ - "Syscheck" - ], + "tags": ["Syscheck"], "args": [ { "name": ":agent_id", @@ -7068,10 +6809,7 @@ "description": "Filter by architecture", "schema": { "type": "string", - "enum": [ - "[x32]", - "[x64]" - ] + "enum": ["[x32]", "[x64]"] } }, { @@ -7198,11 +6936,7 @@ "description": "Filter by file type. Registry_key and registry_value types are only available in Windows agents", "schema": { "type": "string", - "enum": [ - "file", - "registry_key", - "registry_value" - ] + "enum": ["file", "registry_key", "registry_value"] } }, { @@ -7236,9 +6970,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.get_last_scan_agent", "description": "Return when the last syscheck scan started and ended. If the scan is still in progress the end date will be unknown", "summary": "Get last scan datetime", - "tags": [ - "Syscheck" - ], + "tags": ["Syscheck"], "args": [ { "name": ":agent_id", @@ -7276,9 +7008,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_hardware_info", "description": "Return the agent's hardware info. This information include cpu, ram, scan info among others", "summary": "Get agent hardware", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7327,9 +7057,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_hotfix_info", "description": "Return all hotfixes installed by Microsoft(R) in Windows(R) systems (KB... fixes)", "summary": "Get agent hotfixes", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7429,9 +7157,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_address_info", "description": "Return the agent's network address info. This information include used IP protocol, interface, IP address among others", "summary": "Get agent netaddr", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7564,9 +7290,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_interface_info", "description": "Return the agent's network interface info. This information include rx, scan, tx info and some network information among others", "summary": "Get agent netiface", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7771,9 +7495,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_protocol_info", "description": "Return the agent's routing configuration for each network interface", "summary": "Get agent netproto", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7794,12 +7516,7 @@ "schema": { "type": "string", "description": "DHCP status", - "enum": [ - "enabled", - "disabled", - "unknown", - "BOOTP" - ] + "enum": ["enabled", "disabled", "unknown", "BOOTP"] } }, { @@ -7904,9 +7621,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_os_info", "description": "Return the agent's OS info. This information include os information, architecture information among others of all agents", "summary": "Get agent OS", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -7955,9 +7670,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_packages_info", "description": "Return the agent's packages info. This information include name, section, size, priority information of all packages among others", "summary": "Get agent packages", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -8088,9 +7801,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_ports_info", "description": "Return the agent's ports info. This information include local IP, Remote IP, protocol information among others", "summary": "Get agent ports", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -8247,9 +7958,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_processes_info", "description": "Return the agent's processes info", "summary": "Get agent processes", - "tags": [ - "Syscollector" - ], + "tags": ["Syscollector"], "args": [ { "name": ":agent_id", @@ -8454,9 +8163,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.task_controller.get_tasks_status", "description": "Returns all available information about the specified tasks", "summary": "List tasks", - "tags": [ - "Tasks" - ], + "tags": ["Tasks"], "query": [ { "name": "agents_list", @@ -8593,9 +8300,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.vulnerability_controller.get_vulnerability_agent", "description": "Return the vulnerabilities of an agent", "summary": "Get vulnerabilities", - "tags": [ - "Vulnerability" - ], + "tags": ["Vulnerability"], "args": [ { "name": ":agent_id", @@ -8733,9 +8438,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.active_response_controller.run_command", "description": "Run an Active Response command on all agents or a list of them", "summary": "Run command", - "tags": [ - "Active-response" - ], + "tags": ["Active-response"], "query": [ { "name": "agents_list", @@ -8797,9 +8500,7 @@ } } }, - "required": [ - "command" - ] + "required": ["command"] } ] }, @@ -8808,9 +8509,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_agent_single_group", "description": "Assign an agent to a specified group", "summary": "Assign agent to group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -8865,9 +8564,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agent", "description": "Restart the specified agent", "summary": "Restart agent", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -8905,9 +8602,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_multiple_agent_single_group", "description": "Assign all agents or a list of them to the specified group", "summary": "Assign agents to group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -8962,9 +8657,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents_by_group", "description": "Restart all agents which belong to a given group", "summary": "Restart agents in group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":group_id", @@ -9001,9 +8694,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents_by_node", "description": "Restart all agents which belong to a specific given node", "summary": "Restart agents in node", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":node_id", @@ -9039,9 +8730,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.reconnect_agents", "description": "Force reconnect all agents or a list of them", "summary": "Force reconnect agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -9079,9 +8768,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents", "description": "Restart all agents or a list of them", "summary": "Restart agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -9119,9 +8806,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_upgrade_agents", "description": "Upgrade agents using a WPK file from online repository", "summary": "Upgrade agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -9192,9 +8877,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_upgrade_custom_agents", "description": "Upgrade the agents using a local WPK file", "summary": "Upgrade agents custom", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -9251,9 +8934,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.update_configuration", "description": "Replace wazuh configuration for the given node with the data contained in the API request", "summary": "Update node configuration", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "args": [ { "name": ":node_id", @@ -9289,9 +8970,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.put_restart", "description": "Restart all nodes in the cluster or a list of them", "summary": "Restart nodes", - "tags": [ - "Cluster" - ], + "tags": ["Cluster"], "query": [ { "name": "nodes_list", @@ -9326,9 +9005,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.put_file", "description": "Upload or replace a user decoder file content", "summary": "Update decoders file", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "args": [ { "name": ":filename", @@ -9372,9 +9049,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_group_config", "description": "Update an specified group's configuration. This API call expects a full valid XML file with the shared configuration tags/syntax", "summary": "Update group configuration", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "args": [ { "name": ":group_id", @@ -9411,9 +9086,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.put_file", "description": "Replace or upload a CDB list file with the data contained in the API request", "summary": "Update CDB list file", - "tags": [ - "Lists" - ], + "tags": ["Lists"], "args": [ { "name": ":filename", @@ -9457,9 +9130,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.logtest_controller.run_logtest_tool", "description": "Run logtest tool to check if a specified log raises any alert among other information", "summary": "Run logtest", - "tags": [ - "Logtest" - ], + "tags": ["Logtest"], "query": [ { "name": "pretty", @@ -9481,11 +9152,7 @@ "body": [ { "type": "object", - "required": [ - "event", - "log_format", - "location" - ], + "required": ["event", "log_format", "location"], "properties": { "token": { "type": "string", @@ -9512,9 +9179,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.update_configuration", "description": "Replace Wazuh configuration with the data contained in the API request", "summary": "Update Wazuh configuration", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -9539,9 +9204,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.put_restart", "description": "Restart the wazuh manager", "summary": "Restart manager", - "tags": [ - "Manager" - ], + "tags": ["Manager"], "query": [ { "name": "pretty", @@ -9566,9 +9229,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.put_rootcheck", "description": "Run rootcheck scan in all agents or a list of them", "summary": "Run scan", - "tags": [ - "Rootcheck" - ], + "tags": ["Rootcheck"], "query": [ { "name": "agents_list", @@ -9606,9 +9267,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.put_file", "description": "Upload or replace a user ruleset file content", "summary": "Update rules file", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "args": [ { "name": ":filename", @@ -9652,9 +9311,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.put_security_config", "description": "Update the security configuration with the data contained in the API request", "summary": "Update security config", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -9688,10 +9345,7 @@ "rbac_mode": { "description": "RBAC mode (white/black)", "type": "string", - "enum": [ - "white", - "black" - ], + "enum": ["white", "black"], "example": "white" } } @@ -9703,9 +9357,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_policy", "description": "Modify a policy, at least one property must be indicated", "summary": "Update policy", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":policy_id", @@ -9769,11 +9421,7 @@ "description": "Effect of the policy" } }, - "required": [ - "actions", - "resources", - "effect" - ] + "required": ["actions", "resources", "effect"] } } } @@ -9784,9 +9432,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_role", "description": "Modify a role, cannot modify associated policies in this endpoint, at least one property must be indicated", "summary": "Update role", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":role_id", @@ -9836,9 +9482,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_rule", "description": "Modify a security rule by specifying its ID", "summary": "Update security rule", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":rule_id", @@ -9892,18 +9536,14 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.revoke_all_tokens", "description": "This method should be called to revoke all active JWT tokens", "summary": "Revoke JWT tokens", - "tags": [ - "Security" - ] + "tags": ["Security"] }, { "name": "/security/users/:user_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_user", "description": "Modify a user's password by specifying their ID", "summary": "Update users", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":user_id", @@ -9947,9 +9587,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.edit_run_as", "description": "Modify a user's allow_run_as flag by specifying their ID", "summary": "Enable/Disable run_as", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":user_id", @@ -9994,9 +9632,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.put_syscheck", "description": "Run FIM scan in all agents", "summary": "Run scan", - "tags": [ - "Syscheck" - ], + "tags": ["Syscheck"], "query": [ { "name": "agents_list", @@ -10039,9 +9675,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.add_agent", "description": "Add a new agent", "summary": "Add agent", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "pretty", @@ -10087,9 +9721,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.insert_agent", "description": "Add an agent specifying its name, ID and IP. If an agent with the same ID already exists, replace it using `force` parameter", "summary": "Add agent full", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "pretty", @@ -10150,9 +9782,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.post_new_agent", "description": "Add a new agent with name `agent_name`. This agent will use `any` as IP", "summary": "Add agent quick", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agent_name", @@ -10187,9 +9817,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.post_group", "description": "Create a new group", "summary": "Create a group", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "query": [ { "name": "pretty", @@ -10222,9 +9850,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_policy", "description": "Add a new policy, all fields need to be specified", "summary": "Add policy", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -10246,10 +9872,7 @@ "body": [ { "type": "object", - "required": [ - "name", - "policy" - ], + "required": ["name", "policy"], "properties": { "name": { "description": "Policy name", @@ -10280,11 +9903,7 @@ "description": "Effect of the policy" } }, - "required": [ - "actions", - "resources", - "effect" - ] + "required": ["actions", "resources", "effect"] } } } @@ -10295,9 +9914,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_role", "description": "Add a new role, all fields need to be specified", "summary": "Add role", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -10319,9 +9936,7 @@ "body": [ { "type": "object", - "required": [ - "name" - ], + "required": ["name"], "properties": { "name": { "type": "string", @@ -10338,9 +9953,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_role_policy", "description": "Create a specified relation role-policy, one role may have multiples policies", "summary": "Add policies to role", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":role_id", @@ -10399,9 +10012,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_role_rule", "description": "Create a specific role-rule relation. One role may have multiple security rules", "summary": "Add security rules to role", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":role_id", @@ -10451,9 +10062,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_rule", "description": "Add a new security rule", "summary": "Add security rule", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -10475,10 +10084,7 @@ "body": [ { "type": "object", - "required": [ - "name", - "rule" - ], + "required": ["name", "rule"], "properties": { "name": { "type": "string", @@ -10499,9 +10105,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.run_as_login", "description": "This method should be called to get an API token using an authorization context body. This token will expire after auth_token_exp_timeout seconds (default: 900). This value can be changed using PUT /security/config", "summary": "Login auth_context", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "raw", @@ -10524,9 +10128,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.create_user", "description": "Add a new API user to the system", "summary": "Add user", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -10565,9 +10167,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_user_role", "description": "Create a specified relation role-policy, one user may have multiples roles", "summary": "Add roles to user", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":user_id", @@ -10631,9 +10231,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_agents", "description": "Delete all agents or a list of them based on optional criteria", "summary": "Delete agents", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -10762,13 +10360,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "all", - "active", - "pending", - "never_connected", - "disconnected" - ] + "enum": ["all", "active", "pending", "never_connected", "disconnected"] }, "minItems": 1 } @@ -10796,9 +10388,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_multiple_groups", "description": "Remove the agent from all groups or a list of them. The agent will automatically revert to the default group if it is removed from all its assigned groups", "summary": "Remove agent from groups", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -10848,9 +10438,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_single_group", "description": "Remove an agent from an specified group. If the agent has multigroups, it will preserve all previous groups except the last one", "summary": "Remove agent from group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "args": [ { "name": ":agent_id", @@ -10898,9 +10486,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_multiple_agent_single_group", "description": "Remove all agents assignment or a list of them from the specified group", "summary": "Remove agents from group", - "tags": [ - "Agents" - ], + "tags": ["Agents"], "query": [ { "name": "agents_list", @@ -10949,9 +10535,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.delete_file", "description": "Delete a specified decoder file", "summary": "Delete decoders file", - "tags": [ - "Decoders" - ], + "tags": ["Decoders"], "args": [ { "name": ":filename", @@ -10987,9 +10571,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_syscheck_database", "description": "Clear the syscheck database for all agents or a list of them", "summary": "Clear agents FIM results", - "tags": [ - "Experimental" - ], + "tags": ["Experimental"], "query": [ { "name": "agents_list", @@ -11028,9 +10610,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_groups", "description": "Delete all groups or a list of them", "summary": "Delete groups", - "tags": [ - "Groups" - ], + "tags": ["Groups"], "query": [ { "name": "groups_list", @@ -11069,9 +10649,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.delete_file", "description": "Delete a specified CDB list file. Only the filename can be specified. It will be searched recursively if not found", "summary": "Delete CDB list file", - "tags": [ - "Lists" - ], + "tags": ["Lists"], "args": [ { "name": ":filename", @@ -11107,9 +10685,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.logtest_controller.end_logtest_session", "description": "Delete the saved logtest session corresponding to {token}", "summary": "End session", - "tags": [ - "Logtest" - ], + "tags": ["Logtest"], "args": [ { "name": ":token", @@ -11145,9 +10721,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.delete_rootcheck", "description": "Clear rootcheck database for all agents or a list of them", "summary": "Clear results", - "tags": [ - "Rootcheck" - ], + "tags": ["Rootcheck"], "query": [ { "name": "agents_list", @@ -11185,9 +10759,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.delete_file", "description": "Delete a specified rule file", "summary": "Delete rules file", - "tags": [ - "Rules" - ], + "tags": ["Rules"], "args": [ { "name": ":filename", @@ -11223,9 +10795,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.delete_security_config", "description": "Replaces the security configuration with the original one", "summary": "Restore default security config", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -11250,9 +10820,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_policies", "description": "Delete a list of policies or all policies in the system, roles linked to policies are not going to be removed", "summary": "Delete policies", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "policy_ids", @@ -11290,9 +10858,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_roles", "description": "Policies linked to roles are not going to be removed", "summary": "Delete roles", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -11330,9 +10896,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_role_policy", "description": "Delete a specified relation role-policy", "summary": "Remove policies from role", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":role_id", @@ -11382,9 +10946,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_role_rule", "description": "Delete a specific role-rule relation", "summary": "Remove security rules from role", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":role_id", @@ -11434,9 +10996,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_rules", "description": "Delete a list of security rules or all security rules in the system, roles linked to rules are not going to be deleted", "summary": "Delete security rules", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -11474,18 +11034,14 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.logout_user", "description": "This method should be called to invalidate all the current user's tokens", "summary": "Logout current user", - "tags": [ - "Security" - ] + "tags": ["Security"] }, { "name": "/security/users", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.delete_users", "description": "Delete a list of users by specifying their IDs", "summary": "Delete users", - "tags": [ - "Security" - ], + "tags": ["Security"], "query": [ { "name": "pretty", @@ -11523,9 +11079,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_user_role", "description": "Delete a specified relation user-roles", "summary": "Remove roles from user", - "tags": [ - "Security" - ], + "tags": ["Security"], "args": [ { "name": ":user_id", @@ -11575,9 +11129,7 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.delete_syscheck_agent", "description": "Clear file integrity monitoring scan results for a specified agent", "summary": "Clear results", - "tags": [ - "Syscheck" - ], + "tags": ["Syscheck"], "args": [ { "name": ":agent_id", @@ -11612,4 +11164,4 @@ } ] } -] \ No newline at end of file +] diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index dd7a85c108..b28b9eb91f 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -13,12 +13,7 @@ import React, { Component, Fragment } from 'react'; import { WzButtonPermissions } from '../../components/common/permissions/button'; -import { - EuiFlexItem, - EuiCard, - EuiFlexGrid, - EuiFlexGroup, -} from '@elastic/eui'; +import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup } from '@elastic/eui'; import { getToasts } from '../../kibana-services'; import { WzRequest } from '../../react-services/wz-request'; @@ -234,9 +229,8 @@ export default class WzSampleData extends Component { } } renderCard(category) { - const { addDataLoading, exists, removeDataLoading } = this.state[ - category.categorySampleAlertsIndex - ]; + const { addDataLoading, exists, removeDataLoading } = + this.state[category.categorySampleAlertsIndex]; return ( <EuiFlexItem key={`sample-data-${category.title}`}> <EuiCard diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index ba06229b37..a461692223 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -10,10 +10,9 @@ * Find more information about this on the LICENSE file. */ import { getDataPlugin } from '../../../kibana-services'; -import { useState, useEffect} from 'react'; - +import { useState, useEffect } from 'react'; export const useFilterManager = () => { - const [filterManager, setFilterManager] = useState(getDataPlugin().query.filterManager); - return filterManager; -} \ No newline at end of file + const [filterManager, setFilterManager] = useState(getDataPlugin().query.filterManager); + return filterManager; +}; diff --git a/public/components/management/groups/multiple-agent-selector.tsx b/public/components/management/groups/multiple-agent-selector.tsx index 1e0274d0dc..4282b46d1f 100644 --- a/public/components/management/groups/multiple-agent-selector.tsx +++ b/public/components/management/groups/multiple-agent-selector.tsx @@ -9,707 +9,849 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component } from 'react'; +import React, { Component, useEffect, useState } from 'react'; import { - EuiPage, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiButton, - EuiButtonIcon, EuiBadge, EuiTitle, EuiLoadingSpinner, EuiFieldSearch, EuiKeyPadMenu, EuiKeyPadMenuItem, EuiIcon + EuiPage, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiSpacer, + EuiButton, + EuiButtonIcon, + EuiBadge, + EuiTitle, + EuiLoadingSpinner, + EuiFieldSearch, + EuiKeyPadMenu, + EuiKeyPadMenuItem, + EuiIcon, } from '@elastic/eui'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzRequest } from '../../../react-services/wz-request'; -import './multiple-agent-selector.scss' +import './multiple-agent-selector.scss'; import $ from 'jquery'; import { WzFieldSearchDelay } from '../../common/search'; import { withErrorBoundary } from '../../common/hocs'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { Logtest } from '../../../directives/wz-logtest/components/logtest'; -export const MultipleAgentSelector = withErrorBoundary (class MultipleAgentSelector extends Component { - constructor(props) { - super(props); - this.state = { - availableAgents: { - loaded: false, - data: [], - offset: 0, - loadedAll: false - }, - selectedAgents: { - loaded: false, - data: [], - offset: 0, - loadedAll: false - }, - availableItem: [], - selectedElement: [], - selectedFilter: '', - currentAdding: 0, - currentDeleting: 0, - moreThan500: false, - load: false, - savingChanges: false - }; - } +export const MultipleAgentSelector = withErrorBoundary( + class MultipleAgentSelector extends Component { + constructor(props) { + super(props); + this.state = { + previousAvailableAgents: {}, + previousSelectedAgents: {}, + availableAgents: { + loaded: false, + data: [], + offset: 0, + loadedAll: false, + }, + selectedAgents: { + loaded: false, + data: [], + offset: 0, + loadedAll: false, + }, + availableItem: [], + selectedElement: [], + firstSelectedList: [], + selectedFilter: '', + availableFilter: '', + currentAdding: 0, + currentDeleting: 0, + moreThan500: false, + load: false, + savingChanges: false, + initState: true, + typedClasses: { + a: 'wzMultipleSelectorAdding', + r: 'wzMultipleSelectorRemoving', + }, + }; + } + + clearAgents() { + this.setState({ + currentAdding: 0, + currentDeleting: 0, + availableAgents: { + loaded: false, + data: [], + offset: 0, + loadedAll: false, + }, + selectedAgents: { + loaded: false, + data: [], + offset: 0, + loadedAll: false, + }, + }); + } - async componentDidMount() { - this.setState({ load: true }); - try { - try{ + async componentDidMount() { + this.setState({ load: true }); + + try { while (!this.state.selectedAgents.loadedAll) { await this.loadSelectedAgents(); this.setState({ selectedAgents: { ...this.state.selectedAgents, offset: this.state.selectedAgents.offset + 499, - } + }, }); } - }catch(error){} - this.firstSelectedList = [...this.state.selectedAgents.data]; - await this.loadAllAgents("", true); - this.setState({ - load: false - }); - } catch (error) { - const options = { - context: `${MultipleAgentSelector.name}.componentDidMount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: 'Error loading agents', - }, - }; - getErrorOrchestrator().handleError(options); - this.setState({ load: false}); - - } - } - - async loadAllAgents(searchTerm, start) { - try { - const params = { - limit: 500, - offset: !start ? this.state.availableAgents.offset : 0, - select: ['id', 'name'].toString() - }; + this.firstSelectedList = [...this.state.selectedAgents.data]; + await this.loadAllAgents('', true); - if (searchTerm) { - params.search = searchTerm; + this.setState({ load: false }); + } catch (error) { + const options = { + context: `${MultipleAgentSelector.name}.componentDidMount`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error loading agents', + }, + }; + getErrorOrchestrator().handleError(options); + this.setState({ load: false }); } + } - const req = await WzRequest.apiReq('GET', '/agents', { - params: params - }); + async loadAllAgents(searchTerm, start) { + try { + const params = { + limit: 500, + offset: !start ? this.state.availableAgents.offset : 0, + select: ['id', 'name'].toString(), + }; - const totalAgents = req.data.data.total_affected_items; - - const mapped = req.data.data.affected_items - .filter(item => { - return ( - this.state.selectedAgents.data.filter(selected => { - return selected.key == item.id; - }).length == 0 && item.id !== '000' - ); - }) - .map(item => { - return { key: item.id, value: item.name }; + if (searchTerm) { + params.search = searchTerm; + } + + const req = await WzRequest.apiReq('GET', '/agents', { + params: params, }); - if (start) { - this.setState({ - availableAgents: { - ...this.state.availableAgents, - data: mapped, - } - }) - } else { - this.setState({ - availableAgents: { - ...this.state.availableAgents, - data: (this.state.availableAgents.data || []).concat(mapped) - } - }) - } - if (this.state.availableAgents.data.length < 10 && !searchTerm) { - if (this.state.availableAgents.offset >= totalAgents) { + const totalAgents = req.data.data.total_affected_items; + + const mapped = req.data.data.affected_items + .filter((item) => { + return ( + this.state.selectedAgents.data.filter((selected) => { + return selected.key == item.id; + }).length == 0 && item.id !== '000' + ); + }) + .map((item) => { + return { key: item.id, value: item.name }; + }); + if (start) { this.setState({ availableAgents: { ...this.state.availableAgents, - loadedAll: true, - } - }) - } - if (!this.state.availableAgents.loadedAll) { + data: mapped, + }, + }); + } else { this.setState({ availableAgents: { ...this.state.availableAgents, - offset: this.state.availableAgents.offset + 499, - } - }) - await this.loadAllAgents(searchTerm); + data: (this.state.availableAgents.data || []).concat(mapped), + }, + }); } - } - } catch (error) { - throw new Error('Error fetching all available agents'); - } - } - async loadSelectedAgents(searchTerm) { - try { - let params = { - offset: !searchTerm ? this.state.selectedAgents.offset : 0, - select: ['id', 'name'].toString() - }; - if (searchTerm) { - params.search = searchTerm; - } - const result = await WzRequest.apiReq( - 'GET', - `/groups/${this.props.currentGroup.name}/agents`, { - params - }, - ); - this.setState({ totalSelectedAgents: result.data.data.total_affected_items }) - const mapped = result.data.data.affected_items.map(item => { - return { key: item.id, value: item.name }; - }); - this.firstSelectedList = mapped; - if (searchTerm) { - this.setState({ - selectedAgents: { - ...this.state.selectedAgents, - data: mapped, - loadedAll: true + if (this.state.availableAgents.data.length < 10 && !searchTerm) { + if (this.state.availableAgents.offset >= totalAgents) { + this.setState({ + availableAgents: { + ...this.state.availableAgents, + loadedAll: true, + }, + }); } - }); - } else { - this.setState({ - selectedAgents: { - ...this.state.selectedAgents, - data: (this.state.selectedAgents.data || []).concat(mapped) + if (!this.state.availableAgents.loadedAll) { + this.setState({ + availableAgents: { + ...this.state.availableAgents, + offset: this.state.availableAgents.offset + 499, + }, + }); + await this.loadAllAgents(searchTerm); } - }) + } + } catch (error) { + throw new Error('Error fetching all available agents'); } - if ( - this.state.selectedAgents.data.length === 0 || - this.state.selectedAgents.data.length < 500 || - this.state.selectedAgents.offset >= this.state.totalSelectedAgents - ) { - this.setState({ - selectedAgents: { - ...this.state.selectedAgents, - loadedAll: true + } + + async loadSelectedAgents(searchTerm) { + try { + let params = { + offset: !searchTerm ? this.state.selectedAgents.offset : 0, + select: ['id', 'name'].toString(), + }; + if (searchTerm) { + params.search = searchTerm; + } + const result = await WzRequest.apiReq( + 'GET', + `/groups/${this.props.currentGroup.name}/agents`, + { + params, } - }) + ); + this.setState({ totalSelectedAgents: result.data.data.total_affected_items }); + const mapped = result.data.data.affected_items.map((item) => { + return { key: item.id, value: item.name }; + }); + this.firstSelectedList = mapped; + if (searchTerm) { + this.setState({ + selectedAgents: { + ...this.state.selectedAgents, + data: mapped, + loadedAll: true, + }, + }); + } else { + this.setState({ + selectedAgents: { + ...this.state.selectedAgents, + data: (this.state.selectedAgents.data || []).concat(mapped), + }, + }); + } + if ( + this.state.selectedAgents.data.length === 0 || + this.state.selectedAgents.data.length < 500 || + this.state.selectedAgents.offset >= this.state.totalSelectedAgents + ) { + this.setState({ + selectedAgents: { + ...this.state.selectedAgents, + loadedAll: true, + }, + }); + } + } catch (error) { + throw new Error('Error fetching group agents'); } - } catch (error) { - throw new Error('Error fetching group agents'); + this.setState({ + selectedAgents: { + ...this.state.selectedAgents, + loaded: true, + }, + }); } - this.setState({ - selectedAgents: { - ...this.state.selectedAgents, - loaded: true - } - }) - } - - getItemsToSave() { - const original = this.firstSelectedList; - const modified = this.state.selectedAgents.data; - const deletedAgents = []; - const addedAgents = []; - modified.forEach(mod => { - if (original.filter(e => e.key === mod.key).length === 0) { - addedAgents.push(mod); - } - }); - original.forEach(orig => { - if (modified.filter(e => e.key === orig.key).length === 0) { - deletedAgents.push(orig); - } - }); + getItemsToSave() { + const original = this.firstSelectedList; + const modified = this.state.selectedAgents.data; + const deletedAgents = []; + const addedAgents = []; - const addedIds = [...new Set(addedAgents.map(x => x.key))]; - const deletedIds = [...new Set(deletedAgents.map(x => x.key))]; + modified.forEach((mod) => { + if (original.filter((e) => e.key === mod.key).length === 0) { + addedAgents.push(mod); + } + }); + original.forEach((orig) => { + if (modified.filter((e) => e.key === orig.key).length === 0) { + deletedAgents.push(orig); + } + }); - return { addedIds, deletedIds }; - } + const addedIds = [...new Set(addedAgents.map((x) => x.key))]; + const deletedIds = [...new Set(deletedAgents.map((x) => x.key))]; - groupBy(collection, property) { - try { - const values = []; - const result = []; - - for (const item of collection) { - const index = values.indexOf(item[property]); - if (index > -1) result[index].push(item); - else { - values.push(item[property]); - result.push([item]); - } - } - return result.length ? result : false; - } catch (error) { - return false; + return { addedIds, deletedIds }; } - } - async saveAddAgents() { - const itemsToSave = this.getItemsToSave(); - const failedIds = []; - try { - this.setState({ savingChanges: true }); - if (itemsToSave.addedIds.length) { - const addResponse = await WzRequest.apiReq( - 'PUT', - `/agents/group`, { - params: { - group_id: this.props.currentGroup.name, - agents_list: itemsToSave.addedIds.toString() + groupBy(collection, property) { + try { + const values = []; + const result = []; + + for (const item of collection) { + const index = values.indexOf(item[property]); + if (index > -1) result[index].push(item); + else { + values.push(item[property]); + result.push([item]); } } - ); - if (addResponse.data.data.failed_ids) { - failedIds.push(...addResponse.data.data.failed_ids); - } + return result.length ? result : false; + } catch (error) { + return false; } - if (itemsToSave.deletedIds.length) { - const deleteResponse = await WzRequest.apiReq( - 'DELETE', - `/agents/group`, { - params: { - group_id: this.props.currentGroup.name, - agents_list: itemsToSave.deletedIds.toString() - } + } + + async saveAddAgents() { + const itemsToSave = this.getItemsToSave(); + try { + this.setState({ savingChanges: true, initState: false }); + if (itemsToSave.addedIds.length) { + await WzRequest.apiReq('PUT', `/agents/group`, { + params: { + group_id: this.props.currentGroup.name, + agents_list: itemsToSave.addedIds.toString(), + }, + }); } - ); - if (deleteResponse.data.data.total_failed_items) { - failedIds.push(...deleteResponse.data.data.failed_items); + + if (itemsToSave.deletedIds.length) { + await WzRequest.apiReq('DELETE', `/agents/group`, { + params: { + group_id: this.props.currentGroup.name, + agents_list: itemsToSave.deletedIds.toString(), + }, + }); } - } - if (failedIds.length) { - const failedErrors = failedIds.map(item => ({ - id: ((item || {}).error || {}).code, - message: ((item || {}).error || {}).message, - })); - - this.failedErrors = this.groupBy(failedErrors, 'message') || false; - ErrorHandler.info( - `Group has been updated but an error has occurred with ${failedIds.length} agents`, - '', - { warning: true } - ); - } else { + this.setState({ savingChanges: false, initState: true }); ErrorHandler.info('Group has been updated'); + this.clearAgents(); + await this.componentDidMount(); + } catch (error) { + this.setState({ savingChanges: false, initState: true }); + + this.clearAgents(); + await this.componentDidMount(); + + const options = { + context: `${MultipleAgentSelector.name}.saveAddAgents`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: { + message: error, + stack: error, + }, + message: error.message || error, + title: error.name, + }, + }; + + getErrorOrchestrator().handleError(options); } - this.setState({ savingChanges: false }); - this.props.cancelButton(); - } catch (error) { - this.setState({ savingChanges: false }); - const options = { - context: `${MultipleAgentSelector.name}.saveAddAgents`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error applying changes`, - }, - }; - getErrorOrchestrator().handleError(options); + return; } - return; - } - clearFailedErrors() { - this.failedErrors = false; - } + clearFailedErrors() { + this.failedErrors = false; + } - checkLimit() { - if (this.firstSelectedList) { - const itemsToSave = this.getItemsToSave(); - const currentAdding = itemsToSave.addedIds.length; - const currentDeleting = itemsToSave.deletedIds.length; - this.setState({ - currentAdding, - currentDeleting, - moreThan500: currentAdding > 500 || currentDeleting > 500 - }) + checkLimit() { + if (this.firstSelectedList) { + const itemsToSave = this.getItemsToSave(); + const currentAdding = itemsToSave.addedIds.length; + const currentDeleting = itemsToSave.deletedIds.length; + this.setState({ + currentAdding, + currentDeleting, + moreThan500: currentAdding > 500 || currentDeleting > 500, + }); + } } - } - moveItem = (item, from, to, type) => { - if (Array.isArray(item)) { - item.forEach(elem => this.moveItem(elem, from, to, type)); - this.checkLimit(); - } else { - item = JSON.parse(item); - const idx = from.findIndex(x => x.key === item.key); - if (idx !== -1) { - from.splice(idx, 1); - item.type = !item.type ? type : ''; - to.push(item) + moveItem = (item, from, to, type) => { + if (Array.isArray(item)) { + item.forEach((elem) => this.moveItem(elem, from, to, type)); + this.checkLimit(); + } else { + item = JSON.parse(item); + const idx = from.findIndex((x) => x.key === item.key); + if (idx !== -1) { + from.splice(idx, 1); + item.type = !item.type ? type : ''; + to.push(item); + } } + $('#wzMultipleSelectorLeft').val(''); + $('#wzMultipleSelectorRight').val(''); + }; + + moveAll = (from, to, type) => { + from.forEach((item) => { + item.type = !item.type ? type : ''; + to.push(item); + }); + from.length = 0; + this.checkLimit(); + }; + + sort = (a) => { + return parseInt(a.key); + }; + + unselectElementsOfSelectByID(containerID) { + document.getElementById(containerID).options.forEach((option) => { + option.selected = false; + }); } - $('#wzMultipleSelectorLeft').val(''); - $('#wzMultipleSelectorRight').val(''); - }; - - moveAll = (from, to, type) => { - from.forEach(item => { - item.type = !item.type ? type : ''; - to.push(item); - }); - from.length = 0; - this.checkLimit(); - }; - - sort = a => { - return parseInt(a.key); - }; - - unselectElementsOfSelectByID(containerID) { - document.getElementById(containerID).options.forEach(option => { - option.selected = false - }); - } - async reload(element, searchTerm, start = false, addOffset = 0) { - if (element === 'left') { - const callbackLoadAgents = async () => { - try { - await this.loadAllAgents(searchTerm, start); - } catch (error) { - const options = { - context: `${MultipleAgentSelector.name}.reload`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: 'Error fetching all available agents', - }, - }; - getErrorOrchestrator().handleError(options); + async reload(element, searchTerm, start = false, addOffset = 0) { + if (element === 'left') { + const callbackLoadAgents = async () => { + try { + await this.loadAllAgents(searchTerm, start); + } catch (error) { + const options = { + context: `${MultipleAgentSelector.name}.reload`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error fetching all available agents', + }, + }; + getErrorOrchestrator().handleError(options); + } }; - }; - if (!this.state.availableAgents.loadedAll) { - if (start) { - this.setState({ - availableAgents: { - ...this.state.availableAgents, - offset: 0, - }, - selectedAgents: { - ...this.state.selectedAgents, - offset: 0, - } - }, callbackLoadAgents) + if (!this.state.availableAgents.loadedAll) { + if (start) { + this.setState( + { + availableAgents: { + ...this.state.availableAgents, + offset: 0, + }, + selectedAgents: { + ...this.state.selectedAgents, + offset: 0, + }, + }, + callbackLoadAgents + ); + } else { + this.setState( + { + availableAgents: { + ...this.state.availableAgents, + offset: this.state.availableAgents.offset + 500, + }, + }, + callbackLoadAgents + ); + } } else { - this.setState({ - availableAgents: { - ...this.state.availableAgents, - offset: this.state.availableAgents.offset + 500, - } - }, callbackLoadAgents) + callbackLoadAgents(); } } else { - callbackLoadAgents(); - } - } else { - if (!this.state.selectedAgents.loadedAll) { - this.setState( - { + if (!this.state.selectedAgents.loadedAll) { + this.setState({ selectedAgents: { ...this.state.selectedAgents, offset: this.state.selectedAgents.offset + addOffset + 1, - } - }); - try { - await this.loadSelectedAgents(searchTerm); - } catch (error) { - const options = { - context: `${MultipleAgentSelector.name}.reload`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: 'Error fetching group agents', }, - }; - getErrorOrchestrator().handleError(options); + }); + try { + await this.loadSelectedAgents(searchTerm); + } catch (error) { + const options = { + context: `${MultipleAgentSelector.name}.reload`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: 'Error fetching group agents', + }, + }; + getErrorOrchestrator().handleError(options); + } } } } - } - scrollList = async (target) => { - if (target === 'left') { - await this.reload('left', this.state.availableFilter, false); - } else { - await this.reload('right', this.state.selectedFilter, false); - } - }; - - render() { - return ( - <EuiPage style={{ background: 'transparent' }}> - <EuiFlexGroup> - <EuiFlexItem> - {this.state.load && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiProgress size='xs' color='primary'></EuiProgress> - <EuiSpacer size='l'></EuiSpacer> - </EuiFlexItem> - </EuiFlexGroup> - )} - {!this.state.load && ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiPanel> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFlexGroup> - <EuiFlexItem grow={false} style={{ marginRight: 0 }}> - <EuiButtonIcon - aria-label="Back" - style={{ paddingTop: 8 }} - color="primary" - iconSize="l" - iconType="arrowLeft" - onClick={() => this.props.cancelButton()} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiTitle size="m"> - <h1>Manage agents of group {this.props.currentGroup.name}</h1> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - {!this.state.moreThan500 && ( - <EuiButton fill onClick={() => this.saveAddAgents()} - isLoading={this.state.savingChanges} - isDisabled={this.state.currentDeleting === 0 && this.state.currentAdding === 0}> - Apply changes - </EuiButton> - )} - {this.state.moreThan500 && ( - <span className='error-msg'><i className="fa fa-exclamation-triangle"></i> -  Changes cannot be applied with more than 500 additions or removals - </span> - )} - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem style={{ marginTop: 30 }}> - <div id='wzMultipleSelector'> - <div className='wzMultipleSelectorLeft'> - <EuiPanel paddingSize="m"> - <EuiFlexGroup> - <EuiFlexItem> - <EuiTitle size="s"> - <h4>Available agents</h4> - </EuiTitle> - </EuiFlexItem> - {/*{!this.state.selectedAgents.loadedAll && ( */} - <EuiFlexItem grow={false}> - <EuiButtonIcon - aria-label="Back" - color="primary" - iconType="refresh" - onClick={() => this.reload("left", this.state.availableFilter, true)} - /> - </EuiFlexItem> - {/*)} */} - </EuiFlexGroup> - <EuiSpacer size={"s"}></EuiSpacer> - <WzFieldSearchDelay - placeholder="Filter..." - onChange={(searchValue) => { - this.setState({ availableFilter: searchValue, availableItem: [] }); - }} - onSearch={async searchValue => { - await this.reload("left", searchValue, true); - }} - isClearable={true} - fullWidth={true} - aria-label="Filter" + scrollList = async (target) => { + if (target === 'left') { + await this.reload('left', this.state.availableFilter, false); + } else { + await this.reload('right', this.state.selectedFilter, false); + } + }; + + render() { + return ( + <EuiPage style={{ background: 'transparent' }}> + <EuiFlexGroup> + <EuiFlexItem> + {this.state.load && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiProgress size="xs" color="primary"></EuiProgress> + <EuiSpacer size="l"></EuiSpacer> + </EuiFlexItem> + </EuiFlexGroup> + )} + {!this.state.load && ( + <EuiFlexGroup> + <EuiFlexItem> + <EuiPanel> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem grow={false} style={{ marginRight: 0 }}> + <EuiButtonIcon + aria-label="Back" + style={{ paddingTop: 8 }} + color="primary" + iconSize="l" + iconType="arrowLeft" + onClick={() => this.props.cancelButton()} /> - <EuiSpacer size={"m"}></EuiSpacer> - <select - id="wzMultipleSelectorLeft" - size='15' - multiple - onChange={(e) => { - this.unselectElementsOfSelectByID('wzMultipleSelectorRight') + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="m"> + <h1>Manage agents of group {this.props.currentGroup.name}</h1> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + {!this.state.moreThan500 && ( + <EuiButton + fill + onClick={() => this.saveAddAgents()} + isLoading={this.state.savingChanges} + isDisabled={ + this.state.initState || + (this.state.currentDeleting === 0 && this.state.currentAdding === 0) + } + > + Apply changes + </EuiButton> + )} + {this.state.moreThan500 && ( + <span className="error-msg"> + <i className="fa fa-exclamation-triangle"></i> +  Changes cannot be applied with more than 500 additions or + removals + </span> + )} + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem style={{ marginTop: 30 }}> + <div id="wzMultipleSelector"> + <div className="wzMultipleSelectorLeft"> + <EuiPanel paddingSize="m"> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="s"> + <h4>Available agents</h4> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label="Back" + color="primary" + iconType="refresh" + onClick={() => + this.reload('left', this.state.availableFilter, true) + } + /> + </EuiFlexItem> + {/*)} */} + </EuiFlexGroup> + <EuiSpacer size={'s'}></EuiSpacer> + <WzFieldSearchDelay + placeholder="Filter..." + onChange={(searchValue) => { + this.setState({ + availableFilter: searchValue, + availableItem: [], + }); + }} + onSearch={async (searchValue) => { + await this.reload('left', searchValue, true); + }} + isClearable={true} + fullWidth={true} + aria-label="Filter" + /> + <EuiSpacer size={'m'}></EuiSpacer> + <select + id="wzMultipleSelectorLeft" + size="15" + multiple + onChange={(e) => { + this.unselectElementsOfSelectByID('wzMultipleSelectorRight'); + this.setState( + { + availableItem: Array.from( + e.target.selectedOptions, + (option) => option.value + ), + selectedElement: [], + }, + () => { + this.checkLimit(); + } + ); + }} + className="wzMultipleSelectorSelect" + onDoubleClick={() => { + this.moveItem( + this.state.availableItem, + this.state.availableAgents.data, + this.state.selectedAgents.data, + 'a' + ); + this.setState({ availableItem: [], initState: false }); + }} + > + {this.state.availableAgents.data + .sort(this.sort) + .map((item, index) => ( + <option + key={index} + className={this.state.typedClasses[item.type]} + value={JSON.stringify(item)} + > + {`${item.key} - ${item.value}`} + </option> + ))} + </select> + {(!this.state.availableAgents.loadedAll && + !this.state.loadingAvailableAgents && ( + <> + <EuiSpacer size="m" /> + <p + className="wz-load-extra" + onClick={() => { + this.setState( + { loadingAvailableAgents: true }, + async () => { + await this.scrollList('left'); + this.setState({ loadingAvailableAgents: false }); + } + ); + }} + > + {' '} + <EuiIcon type="refresh" />   Click here to load more + agents + </p> + </> + )) || + (this.state.loadingAvailableAgents && ( + <> + <EuiSpacer size="m" /> + <p className="wz-load-extra"> + {' '} + <EuiLoadingSpinner size="m" />   Loading... + </p> + </> + ))} + </EuiPanel> + </div> + <EuiKeyPadMenu className="wzMultipleSelectorButtons"> + <EuiKeyPadMenuItem + label="Add all items" + onClick={() => { + this.moveAll( + this.state.availableAgents.data, + this.state.selectedAgents.data, + 'a' + ); + this.setState( + { availableItem: [], availableFilter: '', initState: false }, + () => { + this.reload('left', this.state.availableFilter, true); + } + ); + }} + isDisabled={ + this.state.availableAgents.data.length === 0 || + this.state.availableAgents.data.length > 500 + } + > + <EuiIcon type="editorRedo" color={'primary'} size="l" /> + </EuiKeyPadMenuItem> + <EuiKeyPadMenuItem + label="Add selected items" + onClick={() => { + this.moveItem( + this.state.availableItem, + this.state.availableAgents.data, + this.state.selectedAgents.data, + 'a' + ); this.setState({ - availableItem: Array.from(e.target.selectedOptions, option => option.value), - selectedElement: [] - }, () => { this.checkLimit() }); + availableItem: [], + availableFilter: '', + initState: false, + }); }} - className='wzMultipleSelectorSelect' - onDoubleClick={() => { - this.moveItem(this.state.availableItem, this.state.availableAgents.data, this.state.selectedAgents.data, "a"); - this.setState({ availableItem: [] }); - }}> - {this.state.availableAgents.data.sort(this.sort).map((item, index) => ( - <option - key={index} - className={ - item.type === 'a' ? 'wzMultipleSelectorAdding' : item.type === 'r' ? 'wzMultipleSelectorRemoving' : ''} - value={JSON.stringify(item)}>{`${item.key} - ${item.value}`} - </option> - ))} - </select> - {(!this.state.availableAgents.loadedAll && - !this.state.loadingAvailableAgents && ( - <> - <EuiSpacer size="m" /> - <p - className="wz-load-extra" - onClick={() => { - this.setState({ loadingAvailableAgents: true }, - async () => { - await this.scrollList('left'); - this.setState({ loadingAvailableAgents: false }) - }); - }} - > - {' '} - <EuiIcon type="refresh" />   Click here to load more agents - </p> - </> - )) || - (this.state.loadingAvailableAgents && ( - <> - <EuiSpacer size="m" /> - <p className="wz-load-extra"> - {' '} - <EuiLoadingSpinner size="m" />   Loading... - </p> - </> - ))} - </EuiPanel> - </div> - <EuiKeyPadMenu className="wzMultipleSelectorButtons"> - <EuiKeyPadMenuItem - label="Add all items" - onClick={() => { - this.moveAll(this.state.availableAgents.data, this.state.selectedAgents.data, "a"); - this.setState({ availableItem: [], availableFilter: '' }, - () => { this.reload("left", this.state.availableFilter, true) }); - }} - isDisabled={this.state.availableAgents.data.length === 0 || this.state.availableAgents.data.length > 500} - > - <EuiIcon type="editorRedo" color={'primary'} size="l" /> - </EuiKeyPadMenuItem> - <EuiKeyPadMenuItem - label="Add selected items" - onClick={() => { - this.moveItem(this.state.availableItem, this.state.availableAgents.data, this.state.selectedAgents.data, "a"); - this.setState({ availableItem: [], availableFilter: '' }); - }} - isDisabled={!this.state.availableItem.length || this.state.availableItem.length > 500} - > - <EuiIcon type="arrowRight" color={'primary'} size="l" /> - </EuiKeyPadMenuItem> - <EuiKeyPadMenuItem - label="Remove selected items" - onClick={() => { - this.moveItem(this.state.selectedElement, this.state.selectedAgents.data, this.state.availableAgents.data, "r"); - this.setState({ selectedFilter: "", selectedElement: [] }); - }} - isDisabled={!this.state.selectedElement.length || this.state.selectedElement.length > 500} - > - <EuiIcon type="arrowLeft" color={'primary'} size="l" /> - </EuiKeyPadMenuItem> - <EuiKeyPadMenuItem - label="Remove all items" - onClick={() => { - this.moveAll(this.state.selectedAgents.data, this.state.availableAgents.data, "r"); - this.setState({ selectedElement: [], selectedFilter: "" }, - () => { this.reload("right") }); - }} - isDisabled={this.state.selectedAgents.data.length === 0 || this.state.selectedAgents.data.length > 500} - > - <EuiIcon type="editorUndo" color={'primary'} size="l" /> - </EuiKeyPadMenuItem> - </EuiKeyPadMenu> - <div className='wzMultipleSelectorRight'> - <EuiPanel paddingSize="m"> - <EuiFlexGroup> - <EuiFlexItem> - <EuiTitle size="s"> - <h4>Current agents in the group ({this.state.totalSelectedAgents})</h4> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false} style={{ marginRight: 0 }}> - <EuiBadge color={'#017D73'}>Added: {this.state.currentAdding}</EuiBadge> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiBadge color={'#BD271E'}>Removed: {this.state.currentDeleting}</EuiBadge> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size={"s"}></EuiSpacer> - <EuiFieldSearch - placeholder="Filter..." - onChange={(ev) => this.setState({ selectedFilter: ev.target.value, selectedElement: [] })} - onSearch={value => { this.setState({ selectedFilter: value }) }} - isClearable={true} - fullWidth={true} - aria-label="Filter" - /> - <EuiSpacer size={"m"}></EuiSpacer> - <select - id="wzMultipleSelectorRight" - size='15' - multiple - onChange={(e) => { - this.unselectElementsOfSelectByID('wzMultipleSelectorLeft') + isDisabled={ + !this.state.availableItem.length || + this.state.availableItem.length > 500 + } + > + <EuiIcon type="arrowRight" color={'primary'} size="l" /> + </EuiKeyPadMenuItem> + <EuiKeyPadMenuItem + label="Remove selected items" + onClick={() => { + this.moveItem( + this.state.selectedElement, + this.state.selectedAgents.data, + this.state.availableAgents.data, + 'r' + ); this.setState({ - selectedElement: Array.from(e.target.selectedOptions, option => option.value), - availableItem: [] - }, () => { this.checkLimit() }); + selectedFilter: '', + selectedElement: [], + initState: false, + }); }} - className='wzMultipleSelectorSelect' - onDoubleClick={(e) => { - this.moveItem(this.state.selectedElement, this.state.selectedAgents.data, this.state.availableAgents.data, "r"); - this.setState({ selectedElement: [] }); - }}> - {this.state.selectedAgents.data - .filter(x => !this.state.selectedFilter || x.key.includes(this.state.selectedFilter) || x.value.includes(this.state.selectedFilter)) - .sort(this.sort) - .map((item, index) => ( - <option - key={index} - className={ - item.type === 'a' ? 'wzMultipleSelectorAdding' : item.type === 'r' ? 'wzMultipleSelectorRemoving' : ''} - value={JSON.stringify(item)}>{`${item.key} - ${item.value}`} - </option> - ))} - </select> - </EuiPanel> + isDisabled={ + !this.state.selectedElement.length || + this.state.selectedElement.length > 500 + } + > + <EuiIcon type="arrowLeft" color={'primary'} size="l" /> + </EuiKeyPadMenuItem> + <EuiKeyPadMenuItem + label="Remove all items" + onClick={() => { + this.moveAll( + this.state.selectedAgents.data, + this.state.availableAgents.data, + 'r' + ); + this.setState( + { selectedElement: [], selectedFilter: '', initState: false }, + () => { + this.reload('right'); + } + ); + }} + isDisabled={ + this.state.selectedAgents.data.length === 0 || + this.state.selectedAgents.data.length > 500 + } + > + <EuiIcon type="editorUndo" color={'primary'} size="l" /> + </EuiKeyPadMenuItem> + </EuiKeyPadMenu> + <div className="wzMultipleSelectorRight"> + <EuiPanel paddingSize="m"> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="s"> + <h4> + Current agents in the group ( + {this.state.totalSelectedAgents}) + </h4> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false} style={{ marginRight: 0 }}> + <EuiBadge color={'#017D73'}> + Added: {this.state.currentAdding} + </EuiBadge> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiBadge color={'#BD271E'}> + Removed: {this.state.currentDeleting} + </EuiBadge> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size={'s'}></EuiSpacer> + <EuiFieldSearch + placeholder="Filter..." + onChange={(ev) => + this.setState({ + selectedFilter: ev.target.value, + selectedElement: [], + }) + } + onSearch={(value) => { + this.setState({ selectedFilter: value }); + }} + isClearable={true} + fullWidth={true} + aria-label="Filter" + /> + <EuiSpacer size={'m'}></EuiSpacer> + <select + id="wzMultipleSelectorRight" + size="15" + multiple + onChange={(e) => { + this.unselectElementsOfSelectByID('wzMultipleSelectorLeft'); + this.setState( + { + selectedElement: Array.from( + e.target.selectedOptions, + (option) => option.value + ), + availableItem: [], + }, + () => { + this.checkLimit(); + } + ); + }} + className="wzMultipleSelectorSelect" + onDoubleClick={(e) => { + this.moveItem( + this.state.selectedElement, + this.state.selectedAgents.data, + this.state.availableAgents.data, + 'r' + ); + this.setState({ selectedElement: [], initState: false }); + }} + > + {this.state.selectedAgents.data + .filter( + (x) => + !this.state.selectedFilter || + x.key.includes(this.state.selectedFilter) || + x.value.includes(this.state.selectedFilter) + ) + .sort(this.sort) + .map((item, index) => ( + <option + key={index} + className={this.state.typedClasses[item.type]} + value={JSON.stringify(item)} + > + {`${item.key} - ${item.value}`} + </option> + ))} + </select> + </EuiPanel> + </div> </div> - </div> - <EuiSpacer size={"l"}></EuiSpacer> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiPage > - ); + <EuiSpacer size={'l'}></EuiSpacer> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiFlexItem> + </EuiFlexGroup> + </EuiPage> + ); + } } -}); +); diff --git a/public/components/security/roles-mapping/roles-mapping.tsx b/public/components/security/roles-mapping/roles-mapping.tsx index 70a5937fd3..bf20790523 100644 --- a/public/components/security/roles-mapping/roles-mapping.tsx +++ b/public/components/security/roles-mapping/roles-mapping.tsx @@ -113,9 +113,9 @@ export const RolesMapping = () => { const initData = async () => { setLoadingTable(true); await getRules(); - if(currentPlatform){ + if (currentPlatform) { await getInternalUsers(); - }; + } setLoadingTable(false); }; @@ -126,34 +126,34 @@ export const RolesMapping = () => { let editFlyout; if (isEditingRule) { editFlyout = ( - <RolesMappingEdit - rule={selectedRule} - closeFlyout={(isVisible) => { - setIsEditingRule(isVisible); - initData(); - }} - rolesEquivalences={rolesEquivalences} - roles={roles} - internalUsers={internalUsers} - onSave={async () => await updateRoles()} - currentPlatform={currentPlatform} - /> + <RolesMappingEdit + rule={selectedRule} + closeFlyout={(isVisible) => { + setIsEditingRule(isVisible); + initData(); + }} + rolesEquivalences={rolesEquivalences} + roles={roles} + internalUsers={internalUsers} + onSave={async () => await updateRoles()} + currentPlatform={currentPlatform} + /> ); } let createFlyout; if (isCreatingRule) { editFlyout = ( - <RolesMappingCreate - closeFlyout={(isVisible) => { - setIsCreatingRule(isVisible); - initData(); - }} - rolesEquivalences={rolesEquivalences} - roles={roles} - internalUsers={internalUsers} - onSave={async () => await updateRoles()} - currentPlatform={currentPlatform} - /> + <RolesMappingCreate + closeFlyout={(isVisible) => { + setIsCreatingRule(isVisible); + initData(); + }} + rolesEquivalences={rolesEquivalences} + roles={roles} + internalUsers={internalUsers} + onSave={async () => await updateRoles()} + currentPlatform={currentPlatform} + /> ); } return ( @@ -165,15 +165,19 @@ export const RolesMapping = () => { </EuiTitle> </EuiPageContentHeaderSection> <EuiPageContentHeaderSection> - { - !loadingTable - && + {!loadingTable && ( <div> - <EuiButton onClick={() => {setIsCreatingRule(true)}}>Create Role mapping</EuiButton> + <EuiButton + onClick={() => { + setIsCreatingRule(true); + }} + > + Create Role mapping + </EuiButton> {createFlyout} {editFlyout} </div> - } + )} </EuiPageContentHeaderSection> </EuiPageContentHeader> <EuiPageContentBody> @@ -181,7 +185,7 @@ export const RolesMapping = () => { rolesEquivalences={rolesEquivalences} loading={loadingTable || rolesLoading} rules={rules} - editRule={item => { + editRule={(item) => { setSelectedRule(item); setIsEditingRule(true); }} diff --git a/public/components/security/roles/services/get-roles.service.ts b/public/components/security/roles/services/get-roles.service.ts index fa9862f8de..95074ffb0e 100644 --- a/public/components/security/roles/services/get-roles.service.ts +++ b/public/components/security/roles/services/get-roles.service.ts @@ -15,7 +15,11 @@ import { WzRequest } from '../../../../react-services/wz-request'; import { Role } from '../types/role.type'; const GetRolesService = async (): Promise<Role[]> => { - const response = (await WzRequest.apiReq('GET', '/security/roles?sort=name', {})) as IApiResponse<Role>; + const response = (await WzRequest.apiReq( + 'GET', + '/security/roles?sort=name', + {} + )) as IApiResponse<Role>; const roles = ((response.data || {}).data || {}).affected_items || []; return roles; }; diff --git a/public/components/security/users/services/get-users.service.ts b/public/components/security/users/services/get-users.service.ts index 23d4e7acbf..776b8f446c 100644 --- a/public/components/security/users/services/get-users.service.ts +++ b/public/components/security/users/services/get-users.service.ts @@ -15,8 +15,13 @@ import { WzRequest } from '../../../../react-services/wz-request'; import IApiResponse from '../../../../react-services/interfaces/api-response.interface'; const GetUsersService = async (): Promise<User[]> => { - const response = (await WzRequest.apiReq('GET', '/security/users?sort=username', {})) as IApiResponse<User>; + const response = (await WzRequest.apiReq( + 'GET', + '/security/users?sort=username', + {} + )) as IApiResponse<User>; const users = ((response.data || {}).data || {}).affected_items || []; + return users; }; diff --git a/public/controllers/management/components/management/configuration/configuration-switch.js b/public/controllers/management/components/management/configuration/configuration-switch.js index 8540fd0073..4014977b95 100644 --- a/public/controllers/management/components/management/configuration/configuration-switch.js +++ b/public/controllers/management/components/management/configuration/configuration-switch.js @@ -15,7 +15,7 @@ import React, { Component, Fragment } from 'react'; import WzConfigurationOverview from './configuration-overview'; import { WzConfigurationGlobalConfigurationManager, - WzConfigurationGlobalConfigurationAgent + WzConfigurationGlobalConfigurationAgent, } from './global-configuration/global-configuration'; import WzConfigurationEditConfiguration from './edit-configuration/edit-configuration'; import WzConfigurationRegistrationService from './registration-service/registration-service'; @@ -42,9 +42,7 @@ import WzConfigurationIntegrityAgentless from './agentless/agentless'; import WzConfigurationIntegrityAmazonS3 from './aws-s3/aws-s3'; import WzConfigurationAzureLogs from './azure-logs/azure-logs'; import WzConfigurationGoogleCloudPubSub from './google-cloud-pub-sub/google-cloud-pub-sub'; -import WzViewSelector, { - WzViewSelectorSwitch -} from './util-components/view-selector'; +import WzViewSelector, { WzViewSelectorSwitch } from './util-components/view-selector'; import WzLoading from './util-components/loading'; import { withRenderIfOrWrapped } from './util-hocs/render-if'; import { WzAgentNeverConnectedPrompt } from './configuration-no-agent'; @@ -61,13 +59,7 @@ import { import { connect } from 'react-redux'; import { compose } from 'redux'; -import { - EuiPage, - EuiPanel, - EuiSpacer, - EuiButtonEmpty, - EuiFlexItem -} from '@elastic/eui'; +import { EuiPage, EuiPanel, EuiSpacer, EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; import { agentIsSynchronized } from './utils/wz-fetch'; import { WzRequest } from '../../../../../react-services/wz-request'; @@ -83,7 +75,7 @@ class WzConfigurationSwitch extends Component { viewProps: {}, agentSynchronized: undefined, masterNodeInfo: undefined, - loadingOverview: this.props.agent.id === '000' + loadingOverview: this.props.agent.id === '000', }; } componentWillUnmount() { @@ -93,10 +85,10 @@ class WzConfigurationSwitch extends Component { updateConfigurationSection = (view, title, description) => { this.setState({ view, viewProps: { title: title, description } }); }; - updateBadge = badgeStatus => { + updateBadge = (badgeStatus) => { // default value false? this.setState({ - viewProps: { ...this.state.viewProps, badge: badgeStatus } + viewProps: { ...this.state.viewProps, badge: badgeStatus }, }); }; async componentDidMount() { @@ -105,15 +97,15 @@ class WzConfigurationSwitch extends Component { try { const agentSynchronized = await agentIsSynchronized(this.props.agent); this.setState({ agentSynchronized }); - }catch(error){ + } catch (error) { const options = { context: `${WzConfigurationSwitch.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: error.message || error, - title: error.name || error + message: error.message || error, + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); @@ -122,15 +114,18 @@ class WzConfigurationSwitch extends Component { try { // try if it is a cluster const clusterStatus = await clusterReq(); - if(clusterStatus.data.data.enabled === 'yes' && clusterStatus.data.data.running === 'yes'){ + if ( + clusterStatus.data.data.enabled === 'yes' && + clusterStatus.data.data.running === 'yes' + ) { const nodes = await clusterNodes(); // set cluster nodes in Redux Store this.props.updateClusterNodes(nodes.data.data.affected_items); // set cluster node selected in Redux Store this.props.updateClusterNodeSelected( - nodes.data.data.affected_items.find(node => node.type === 'master').name + nodes.data.data.affected_items.find((node) => node.type === 'master').name ); - }else{ + } else { // do nothing if it isn't a cluster this.props.updateClusterNodes(false); this.props.updateClusterNodeSelected(false); @@ -145,21 +140,23 @@ class WzConfigurationSwitch extends Component { severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: error.message || error, - title: error.name || error + message: error.message || error, + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); } // If manager/cluster require agent platform info to filter sections in overview. It isn't coming from props for Management/Configuration - try{ + try { this.setState({ loadingOverview: true }); - const masterNodeInfo = await WzRequest.apiReq('GET', '/agents', { params: { q: 'id=000'}}); + const masterNodeInfo = await WzRequest.apiReq('GET', '/agents', { + params: { q: 'id=000' }, + }); this.setState({ - masterNodeInfo: masterNodeInfo.data.affected_items[0] + masterNodeInfo: masterNodeInfo.data.affected_items[0], }); this.setState({ loadingOverview: false }); - }catch(error){ + } catch (error) { this.setState({ loadingOverview: false }); const options = { context: `${WzConfigurationSwitch.name}.componentDidMount`, @@ -167,8 +164,8 @@ class WzConfigurationSwitch extends Component { severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: error.message || error, - title: error.name || error + message: error.message || error, + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); @@ -180,7 +177,7 @@ class WzConfigurationSwitch extends Component { view, viewProps: { title, description, badge }, agentSynchronized, - masterNodeInfo + masterNodeInfo, } = this.state; const { agent, goGroups } = this.props; // TODO: goGroups and exportConfiguration is used for Manager and depends of AngularJS return ( @@ -190,10 +187,7 @@ class WzConfigurationSwitch extends Component { <Fragment> <span>Groups:</span> {agent.group.map((group, key) => ( - <EuiButtonEmpty - key={`agent-group-${key}`} - onClick={() => goGroups(agent, key)} - > + <EuiButtonEmpty key={`agent-group-${key}`} onClick={() => goGroups(agent, key)}> {group} </EuiButtonEmpty> ))} @@ -214,14 +208,15 @@ class WzConfigurationSwitch extends Component { )} </WzConfigurationPath> )} - {view === '' && ((!this.state.loadingOverview && ( - <WzConfigurationOverview - agent={masterNodeInfo || agent} - agentSynchronized={agentSynchronized} - exportConfiguration={this.props.exportConfiguration} - updateConfigurationSection={this.updateConfigurationSection} - /> - )) || <WzLoading />)} + {view === '' && + ((!this.state.loadingOverview && ( + <WzConfigurationOverview + agent={masterNodeInfo || agent} + agentSynchronized={agentSynchronized} + exportConfiguration={this.props.exportConfiguration} + updateConfigurationSection={this.updateConfigurationSection} + /> + )) || <WzLoading />)} {view === 'edit-configuration' && ( <WzConfigurationEditConfiguration clusterNodeSelected={this.props.clusterNodeSelected} @@ -434,28 +429,33 @@ class WzConfigurationSwitch extends Component { } } -const mapStateToProps = state => ({ +const mapStateToProps = (state) => ({ clusterNodes: state.configurationReducers.clusterNodes, clusterNodeSelected: state.configurationReducers.clusterNodeSelected, - wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet + wazuhNotReadyYet: state.appStateReducers.wazuhNotReadyYet, }); -const mapDispatchToProps = dispatch => ({ - updateClusterNodes: clusterNodes => - dispatch(updateClusterNodes(clusterNodes)), - updateClusterNodeSelected: clusterNodeSelected => - dispatch(updateClusterNodeSelected(clusterNodeSelected)) +const mapDispatchToProps = (dispatch) => ({ + updateClusterNodes: (clusterNodes) => dispatch(updateClusterNodes(clusterNodes)), + updateClusterNodeSelected: (clusterNodeSelected) => + dispatch(updateClusterNodeSelected(clusterNodeSelected)), }); export default compose( - withUserAuthorizationPrompt((props) => [props.agent.id === '000' ? - {action: 'manager:read', resource: '*:*:*'} : - [ - {action: 'agent:read', resource: `agent:id:${props.agent.id}`}, - ...(props.agent.group || []).map(group => ({ action: 'agent:read', resource: `agent:group:${group}` })) - ]]), //TODO: this need cluster:read permission but manager/cluster is managed in WzConfigurationSwitch component - withRenderIfOrWrapped((props) => props.agent.status === 'never_connected', WzAgentNeverConnectedPrompt), - connect( - mapStateToProps, - mapDispatchToProps -))(WzConfigurationSwitch); + withUserAuthorizationPrompt((props) => [ + props.agent.id === '000' + ? { action: 'manager:read', resource: '*:*:*' } + : [ + { action: 'agent:read', resource: `agent:id:${props.agent.id}` }, + ...(props.agent.group || []).map((group) => ({ + action: 'agent:read', + resource: `agent:group:${group}`, + })), + ], + ]), //TODO: this need cluster:read permission but manager/cluster is managed in WzConfigurationSwitch component + withRenderIfOrWrapped( + (props) => props.agent.status === 'never_connected', + WzAgentNeverConnectedPrompt + ), + connect(mapStateToProps, mapDispatchToProps) +)(WzConfigurationSwitch); diff --git a/public/controllers/management/components/management/groups/groups-main.js b/public/controllers/management/components/management/groups/groups-main.js index a5fdd3e1e9..f6db6d3ff1 100644 --- a/public/controllers/management/components/management/groups/groups-main.js +++ b/public/controllers/management/components/management/groups/groups-main.js @@ -18,10 +18,7 @@ import WzGroupsOverview from './groups-overview'; import WzGroupDetail from './group-detail'; import WzGroupEditor from './groups-editor'; import { updateGroupDetail } from '../../../../../redux/actions/groupsActions'; -import { - updateShowAddAgents, - resetGroup -} from '../../../../../redux/actions/groupsActions'; +import { updateShowAddAgents, resetGroup } from '../../../../../redux/actions/groupsActions'; import { connect } from 'react-redux'; import { updateGlobalBreadcrumb } from '../../../../../redux/actions/globalBreadcrumbActions'; import { WzRequest } from '../../../../../react-services/wz-request'; @@ -29,7 +26,6 @@ import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; - class WzGroups extends Component { constructor(props) { super(props); @@ -39,7 +35,7 @@ class WzGroups extends Component { const breadcrumb = [ { text: '' }, { text: 'Management', href: '#/manager' }, - { text: 'Groups' } + { text: 'Groups' }, ]; store.dispatch(updateGlobalBreadcrumb(breadcrumb)); } @@ -49,13 +45,15 @@ class WzGroups extends Component { // Check if there is a group in the URL const [_, group] = window.location.href.match(new RegExp('group=' + '([^&]*)')) || []; window.location.href = window.location.href.replace(new RegExp('group=' + '[^&]*'), ''); - if(group){ - try{ + if (group) { + try { // Try if the group can be accesed - const responseGroup = await WzRequest.apiReq('GET', '/groups', {params: {groups_list: group}}); + const responseGroup = await WzRequest.apiReq('GET', '/groups', { + params: { groups_list: group }, + }); const dataGroup = responseGroup?.data?.data?.affected_items?.[0]; this.props.updateGroupDetail(dataGroup); - }catch(error){ + } catch (error) { const options = { context: `${WzGroups.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, @@ -68,21 +66,17 @@ class WzGroups extends Component { }, }; getErrorOrchestrator().handleError(options); - }; - }; + } + } } UNSAFE_componentWillReceiveProps(nextProps) { - if ( - nextProps.groupsProps.closeAddingAgents && - this.props.state.showAddAgents - ) { + if (nextProps.groupsProps.closeAddingAgents && this.props.state.showAddAgents) { this.props.updateShowAddAgents(false); } if ( nextProps.groupsProps.selectedGroup && - nextProps.groupsProps.selectedGroup !== - this.props.groupsProps.selectedGroup + nextProps.groupsProps.selectedGroup !== this.props.groupsProps.selectedGroup ) { store.dispatch(updateGroupDetail(nextProps.groupsProps.selectedGroup)); } @@ -107,20 +101,16 @@ class WzGroups extends Component { ); } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.groupsReducers + state: state.groupsReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { resetGroup: () => dispatch(resetGroup()), - updateShowAddAgents: showAddAgents => - dispatch(updateShowAddAgents(showAddAgents)), - updateGroupDetail: groupDetail => dispatch(updateGroupDetail(groupDetail)) + updateShowAddAgents: (showAddAgents) => dispatch(updateShowAddAgents(showAddAgents)), + updateGroupDetail: (groupDetail) => dispatch(updateGroupDetail(groupDetail)), }; }; -export default connect( - mapStateToProps, - mapDispatchToProps -)(WzGroups); +export default connect(mapStateToProps, mapDispatchToProps)(WzGroups); diff --git a/public/controllers/management/components/management/management-main.js b/public/controllers/management/components/management/management-main.js index ab2c789a12..ea64a6c6ea 100644 --- a/public/controllers/management/components/management/management-main.js +++ b/public/controllers/management/components/management/management-main.js @@ -77,7 +77,12 @@ class WzManagementMain extends Component { (section === 'statistics' && <WzStatistics />) || (section === 'logs' && <WzLogs />) || (section === 'configuration' && <WzConfiguration {...this.props.configurationProps} />) || - (ruleset.includes(section) && <WzRuleset logtestProps={this.props.logtestProps} clusterStatus={this.props.clusterStatus} />)} + (ruleset.includes(section) && ( + <WzRuleset + logtestProps={this.props.logtestProps} + clusterStatus={this.props.clusterStatus} + /> + ))} </Fragment> ); } diff --git a/public/controllers/management/components/management/management-provider.js b/public/controllers/management/components/management/management-provider.js index 6d2dbcf586..96d3960a71 100644 --- a/public/controllers/management/components/management/management-provider.js +++ b/public/controllers/management/components/management/management-provider.js @@ -3,7 +3,4 @@ import { withErrorBoundary, withReduxProvider } from '../../../../components/com // Redux import WzManagementMain from '../management/management-main'; -export default compose( - withErrorBoundary, - withReduxProvider -)(WzManagementMain); \ No newline at end of file +export default compose(withErrorBoundary, withReduxProvider)(WzManagementMain); diff --git a/public/controllers/management/components/management/status/status-overview.js b/public/controllers/management/components/management/status/status-overview.js index edf551429f..305b4e53e4 100644 --- a/public/controllers/management/components/management/status/status-overview.js +++ b/public/controllers/management/components/management/status/status-overview.js @@ -20,7 +20,7 @@ import { EuiPage, EuiSpacer, EuiFlexGrid, - EuiButton + EuiButton, } from '@elastic/eui'; import { connect } from 'react-redux'; @@ -33,7 +33,7 @@ import { updateNodeInfo, updateAgentInfo, updateClusterEnabled, - cleanInfo + cleanInfo, } from '../../../../../redux/actions/statusActions'; import StatusHandler from './utils/status-handler'; @@ -44,9 +44,12 @@ import WzStatusStats from './status-stats'; import WzStatusNodeInfo from './status-node-info'; import WzStatusAgentInfo from './status-agent-info'; -import { getToasts } from '../../../../../kibana-services'; +import { getToasts } from '../../../../../kibana-services'; -import { withUserAuthorizationPrompt, withGlobalBreadcrumb } from '../../../../../components/common/hocs'; +import { + withUserAuthorizationPrompt, + withGlobalBreadcrumb, +} from '../../../../../components/common/hocs'; import { compose } from 'redux'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; @@ -61,7 +64,7 @@ export class WzStatusOverview extends Component { this.statusHandler = StatusHandler; this.state = { - isLoading: false + isLoading: false, }; } @@ -70,7 +73,7 @@ export class WzStatusOverview extends Component { this.fetchData(); } - componentDidUpdate() { } + componentDidUpdate() {} componentWillUnmount() { this._isMounted = false; @@ -101,9 +104,7 @@ export class WzStatusOverview extends Component { data.push(manInfo); data.push(agentsCountResponse); - const parsedData = data.map( - item => ((item || {}).data || {}).data || false - ); + const parsedData = data.map((item) => ((item || {}).data || {}).data || false); const [stats, clusterStatus, managerInfo, agentsCount] = parsedData; // Once Wazuh core fixes agent 000 issues, this should be adjusted @@ -116,22 +117,18 @@ export class WzStatusOverview extends Component { agentsCountDisconnected: stats.disconnected, agentsCountNeverConnected: stats.never_connected, agentsCountTotal: total, - agentsCoverity: total ? (active / total) * 100 : 0 + agentsCoverity: total ? (active / total) * 100 : 0, }); - this.props.updateClusterEnabled( - clusterStatus && clusterStatus.enabled === 'yes' - ); + this.props.updateClusterEnabled(clusterStatus && clusterStatus.enabled === 'yes'); - if ( - clusterStatus && - clusterStatus.enabled === 'yes' && - clusterStatus.running === 'yes' - ) { + if (clusterStatus && clusterStatus.enabled === 'yes' && clusterStatus.running === 'yes') { const nodes = await this.statusHandler.clusterNodes(); const listNodes = nodes.data.data.affected_items; this.props.updateListNodes(listNodes); - const masterNode = nodes.data.data.affected_items.filter(item => item.type === 'master')[0]; + const masterNode = nodes.data.data.affected_items.filter( + (item) => item.type === 'master' + )[0]; this.props.updateSelectedNode(masterNode.name); const daemons = await this.statusHandler.clusterNodeStatus(masterNode.name); const listDaemons = this.objToArr(daemons.data.data.affected_items[0]); @@ -139,11 +136,7 @@ export class WzStatusOverview extends Component { const nodeInfo = await this.statusHandler.clusterNodeInfo(masterNode.name); this.props.updateNodeInfo(nodeInfo.data.data.affected_items[0]); } else { - if ( - clusterStatus && - clusterStatus.enabled === 'yes' && - clusterStatus.running === 'no' - ) { + if (clusterStatus && clusterStatus.enabled === 'yes' && clusterStatus.running === 'no') { this.showToast( 'danger', `Cluster is enabled but it's not running, please check your cluster health.`, @@ -169,7 +162,7 @@ export class WzStatusOverview extends Component { error: { error: error, message: error.message || error, - title: `${error.name}: management:status:overview` + title: `${error.name}: management:status:overview`, }, }; getErrorOrchestrator().handleError(options); @@ -181,18 +174,12 @@ export class WzStatusOverview extends Component { getToasts().add({ color: color, title: text, - toastLifeTimeMs: time + toastLifeTimeMs: time, }); }; render() { - const { - isLoading, - listDaemons, - stats, - nodeInfo, - agentInfo - } = this.props.state; + const { isLoading, listDaemons, stats, nodeInfo, agentInfo } = this.props.state; return ( <EuiPage style={{ background: 'transparent' }}> @@ -238,25 +225,23 @@ export class WzStatusOverview extends Component { } } -const mapStateToProps = state => { +const mapStateToProps = (state) => { return { - state: state.statusReducers + state: state.statusReducers, }; }; -const mapDispatchToProps = dispatch => { +const mapDispatchToProps = (dispatch) => { return { - updateLoadingStatus: status => dispatch(updateLoadingStatus(status)), - updateListNodes: listNodes => dispatch(updateListNodes(listNodes)), - updateSelectedNode: selectedNode => - dispatch(updateSelectedNode(selectedNode)), - updateListDaemons: listDaemons => dispatch(updateListDaemons(listDaemons)), - updateStats: stats => dispatch(updateStats(stats)), - updateNodeInfo: nodeInfo => dispatch(updateNodeInfo(nodeInfo)), - updateAgentInfo: agentInfo => dispatch(updateAgentInfo(agentInfo)), - updateClusterEnabled: clusterEnabled => - dispatch(updateClusterEnabled(clusterEnabled)), - cleanInfo: () => dispatch(cleanInfo()) + updateLoadingStatus: (status) => dispatch(updateLoadingStatus(status)), + updateListNodes: (listNodes) => dispatch(updateListNodes(listNodes)), + updateSelectedNode: (selectedNode) => dispatch(updateSelectedNode(selectedNode)), + updateListDaemons: (listDaemons) => dispatch(updateListDaemons(listDaemons)), + updateStats: (stats) => dispatch(updateStats(stats)), + updateNodeInfo: (nodeInfo) => dispatch(updateNodeInfo(nodeInfo)), + updateAgentInfo: (agentInfo) => dispatch(updateAgentInfo(agentInfo)), + updateClusterEnabled: (clusterEnabled) => dispatch(updateClusterEnabled(clusterEnabled)), + cleanInfo: () => dispatch(cleanInfo()), }; }; @@ -264,18 +249,15 @@ export default compose( withGlobalBreadcrumb([ { text: '' }, { text: 'Management', href: '#/manager' }, - { text: 'Status' } + { text: 'Status' }, ]), withUserAuthorizationPrompt([ [ { action: 'agent:read', resource: 'agent:id:*' }, - { action: 'agent:read', resource: 'agent:group:*' } + { action: 'agent:read', resource: 'agent:group:*' }, ], { action: 'manager:read', resource: '*:*:*' }, - { action: 'cluster:read', resource: 'node:id:*' } + { action: 'cluster:read', resource: 'node:id:*' }, ]), - connect( - mapStateToProps, - mapDispatchToProps - ) + connect(mapStateToProps, mapDispatchToProps) )(WzStatusOverview); diff --git a/public/controllers/management/management.js b/public/controllers/management/management.js index d7963955e2..b90d51c67e 100644 --- a/public/controllers/management/management.js +++ b/public/controllers/management/management.js @@ -49,7 +49,6 @@ export class ManagementController { this.uploadOpened = false; this.rulesetTab = RulesetResources.RULES; - this.$scope.$on('setCurrentGroup', (ev, params) => { this.currentGroup = (params || {}).currentGroup || false; }); @@ -149,17 +148,15 @@ export class ManagementController { this.$rootScope.timeoutIsReady; this.$rootScope.$watch('resultState', () => { - if(this.$rootScope.timeoutIsReady){ + if (this.$rootScope.timeoutIsReady) { clearTimeout(this.$rootScope.timeoutIsReady); } - if(this.$rootScope.resultState === 'ready'){ + if (this.$rootScope.resultState === 'ready') { this.$scope.isReady = true; + } else { + this.$rootScope.timeoutIsReady = setTimeout(() => (this.$scope.isReady = false), 1000); } - else - { - this.$rootScope.timeoutIsReady = setTimeout(() => this.$scope.isReady = false, 1000); - } - }) + }); this.welcomeCardsProps = { switchTab: (tab, setNav) => this.switchTab(tab, setNav), @@ -199,7 +196,6 @@ export class ManagementController { }, logtestProps: this.logtestProps, }; - } /** @@ -449,7 +445,7 @@ export class ManagementController { error: { error: error, message: error?.message || '', - title: 'Error loading node list' + title: 'Error loading node list', }, }; diff --git a/public/kibana-services.ts b/public/kibana-services.ts index 8a2cdd2e08..899d0b9829 100644 --- a/public/kibana-services.ts +++ b/public/kibana-services.ts @@ -23,22 +23,17 @@ export const [getToasts, setToasts] = createGetterSetter<ToastsStart>('Toasts'); export const [getHttp, setHttp] = createGetterSetter<HttpStart>('Http'); export const [getUiSettings, setUiSettings] = createGetterSetter<IUiSettingsClient>('UiSettings'); export const [getChrome, setChrome] = createGetterSetter<ChromeStart>('Chrome'); -export const [getScopedHistory, setScopedHistory] = createGetterSetter<ScopedHistory>( - 'ScopedHistory' -); +export const [getScopedHistory, setScopedHistory] = + createGetterSetter<ScopedHistory>('ScopedHistory'); export const [getOverlays, setOverlays] = createGetterSetter<OverlayStart>('Overlays'); -export const [getSavedObjects, setSavedObjects] = createGetterSetter<SavedObjectsStart>( - 'SavedObjects' -); -export const [getDataPlugin, setDataPlugin] = createGetterSetter<DataPublicPluginStart>( - 'DataPlugin' -); -export const [getVisualizationsPlugin, setVisualizationsPlugin] = createGetterSetter< - VisualizationsStart ->('VisualizationsPlugin'); -export const [getNavigationPlugin, setNavigationPlugin] = createGetterSetter< - NavigationPublicPluginStart ->('NavigationPlugin'); +export const [getSavedObjects, setSavedObjects] = + createGetterSetter<SavedObjectsStart>('SavedObjects'); +export const [getDataPlugin, setDataPlugin] = + createGetterSetter<DataPublicPluginStart>('DataPlugin'); +export const [getVisualizationsPlugin, setVisualizationsPlugin] = + createGetterSetter<VisualizationsStart>('VisualizationsPlugin'); +export const [getNavigationPlugin, setNavigationPlugin] = + createGetterSetter<NavigationPublicPluginStart>('NavigationPlugin'); /** * set bootstrapped inner angular module diff --git a/public/react-services/toast-notifications.tsx b/public/react-services/toast-notifications.tsx index 42955f5c6a..1ca85e5ed3 100644 --- a/public/react-services/toast-notifications.tsx +++ b/public/react-services/toast-notifications.tsx @@ -10,34 +10,34 @@ * Find more information about this on the LICENSE file. */ import React, { Fragment } from 'react'; -import store from '../redux/store'; +import store from '../redux/store'; import { updateToastNotificationsModal } from '../redux/actions/appStateActions'; -import { getToasts } from '../kibana-services'; +import { getToasts } from '../kibana-services'; import { EuiButton } from '@elastic/eui'; -export class ToastNotifications{ - static add(toast){ +export class ToastNotifications { + static add(toast) { getToasts().add(toast); } - static success(toast){ + static success(toast) { getToasts().add({ ...toast, color: 'success', }); } - static warning(toast){ + static warning(toast) { getToasts().add({ ...toast, color: 'warning', }); } - static danger(toast){ + static danger(toast) { getToasts().add({ ...toast, color: 'danger', }); } - static error(path, error, title = 'Error unexpected'){ + static error(path, error, title = 'Error unexpected') { getToasts().danger({ title, iconType: 'alert', @@ -45,10 +45,16 @@ export class ToastNotifications{ <Fragment> <p data-test-subj="errorToastMessage">{error.message}</p> <div className="eui-textRight"> - <EuiButton color='danger' onClick={() => store.dispatch(updateToastNotificationsModal({path, error, title}))} size="s">See the full error</EuiButton> + <EuiButton + color="danger" + onClick={() => store.dispatch(updateToastNotificationsModal({ path, error, title }))} + size="s" + > + See the full error + </EuiButton> </div> </Fragment> - ) + ), }); } -} \ No newline at end of file +} diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 8ac97409b8..6978134043 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -52,6 +52,7 @@ export class WzRequest { }; const data = await axios(options); + if (data['error']) { throw new Error(data['error']); } @@ -110,6 +111,7 @@ export class WzRequest { const id = JSON.parse(AppState.getCurrentAPI()).id; const requestData = { method, path, body, id }; const response = await this.genericReq('POST', '/api/request', requestData); + const hasFailed = (((response || {}).data || {}).data || {}).total_failed_items || 0; if (hasFailed) { @@ -118,6 +120,7 @@ export class WzRequest { const failed_ids = ((((response.data || {}).data || {}).failed_items || [])[0] || {}).id || {}; const message = (response.data || {}).message || 'Unexpected error'; + return Promise.reject( `${message} (${error.code}) - ${error.message} ${ failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : '' From 6aad89acabfd27d8204cec4e79beb49190e105cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex?= <alejandro.ruiz.becerra@wazuh.com> Date: Fri, 5 Nov 2021 14:02:46 +0100 Subject: [PATCH 308/493] Apply enhance #3622 -> Add filter bar on Network ports table (#3639) --- CHANGELOG.md | 1 + public/components/agents/syscollector/inventory.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4eb06dfc7e..d8ea724671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added +- Added ability to filter the results fo the `Network Ports` table in the `Inventory data` section [#3639](https://github.com/wazuh/wazuh-kibana-app/pull/3639) - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Improved the frontend handle errors strategy: UI, Toasts, console log and log in file [#3327](https://github.com/wazuh/wazuh-kibana-app/pull/3327) diff --git a/public/components/agents/syscollector/inventory.tsx b/public/components/agents/syscollector/inventory.tsx index 4826512a18..fd0d25f551 100644 --- a/public/components/agents/syscollector/inventory.tsx +++ b/public/components/agents/syscollector/inventory.tsx @@ -103,7 +103,7 @@ export function SyscollectorInventory({ agent }) { title: 'Network ports', columns: portsColumns[soPlatform], icon: 'inputOutput', - searchBar: false, + searchBar: true, exportFormatted: false, }} /> From 0638e0627ca684db1be38956453f1541f74f73c4 Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Fri, 5 Nov 2021 10:07:51 -0300 Subject: [PATCH 309/493] Wazuh app not showing packages details in free bsd agent (#3651) --- CHANGELOG.md | 1 + .../syscollector/columns/packages-columns.ts | 29 ++++++++++++------- .../syscollector/columns/ports-columns.ts | 27 ++++++++--------- .../syscollector/columns/process-columns.ts | 18 +++++++----- .../agents/syscollector/inventory.tsx | 2 ++ 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ea724671..ede2f13055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix CDB list view not working with IPv6 [#3488](https://github.com/wazuh/wazuh-kibana-app/pull/3488) - Fixed the bad requests using Console tool to `PUT /active-response` API endpoint [#3466](https://github.com/wazuh/wazuh-kibana-app/pull/3466) - Fixed group agent management table does not update on error [#3605](https://github.com/wazuh/wazuh-kibana-app/pull/3605) +- Fixed not showing packages details in agent inventory for a freeBSD agent SO [#3651](https://github.com/wazuh/wazuh-kibana-app/pull/3651) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/public/components/agents/syscollector/columns/packages-columns.ts b/public/components/agents/syscollector/columns/packages-columns.ts index e0c57b17f6..503c70242e 100644 --- a/public/components/agents/syscollector/columns/packages-columns.ts +++ b/public/components/agents/syscollector/columns/packages-columns.ts @@ -1,26 +1,35 @@ const windowsColumns = [ { id: 'name' }, - { id: 'architecture', width: "10%" }, + { id: 'architecture', width: '10%' }, { id: 'version' }, - { id: 'vendor', width: '30%' } + { id: 'vendor', width: '30%' }, ]; const linuxColumns = [ { id: 'name' }, { id: 'architecture', width: '10%' }, { id: 'version' }, - { id: 'vendor', width: "30%" }, - { id: 'description', width: '30%' } + { id: 'vendor', width: '30%' }, + { id: 'description', width: '30%' }, ]; const MacColumns = [ { id: 'name' }, { id: 'version' }, { id: 'format' }, - { id: 'location', width: "30%" }, - { id: 'description', width: '20%' } + { id: 'location', width: '30%' }, + { id: 'description', width: '20%' }, +]; +const FreebsdColumns = [ + { id: 'name' }, + { id: 'version' }, + { id: 'format' }, + { id: 'architecture', width: '20%' }, + { id: 'vendor', width: '20%' }, + { id: 'description', width: '30%' }, ]; export const packagesColumns = { - 'windows': windowsColumns, - 'linux': linuxColumns, - 'apple': MacColumns -} + windows: windowsColumns, + linux: linuxColumns, + apple: MacColumns, + freebsd: FreebsdColumns, +}; diff --git a/public/components/agents/syscollector/columns/ports-columns.ts b/public/components/agents/syscollector/columns/ports-columns.ts index fe60f32d0e..b2da16411b 100644 --- a/public/components/agents/syscollector/columns/ports-columns.ts +++ b/public/components/agents/syscollector/columns/ports-columns.ts @@ -1,19 +1,20 @@ const windowsColumns = [ - { id: "process" }, - { id: "local.ip", sortable: false }, - { id: "local.port", sortable: false }, - { id: "state" }, - { id: "protocol" } + { id: 'process' }, + { id: 'local.ip', sortable: false }, + { id: 'local.port', sortable: false }, + { id: 'state' }, + { id: 'protocol' }, ]; const defaultColumns = [ - { id: "local.ip", sortable: false }, - { id: "local.port", sortable: false }, - { id: "state" }, - { id: "protocol" } + { id: 'local.ip', sortable: false }, + { id: 'local.port', sortable: false }, + { id: 'state' }, + { id: 'protocol' }, ]; export const portsColumns = { - 'windows': windowsColumns, - 'linux': defaultColumns, - 'apple': defaultColumns -} + windows: windowsColumns, + linux: defaultColumns, + apple: defaultColumns, + freebsd: defaultColumns, +}; diff --git a/public/components/agents/syscollector/columns/process-columns.ts b/public/components/agents/syscollector/columns/process-columns.ts index 2b76f5f886..e7940fb7db 100644 --- a/public/components/agents/syscollector/columns/process-columns.ts +++ b/public/components/agents/syscollector/columns/process-columns.ts @@ -5,7 +5,8 @@ const windowsColumns = [ { id: 'vm_size' }, { id: 'priority' }, { id: 'nlwp' }, - { id: 'cmd', width: '30%' }]; + { id: 'cmd', width: '30%' }, +]; const linuxColumns = [ { id: 'name', width: '10%' }, { id: 'euser' }, @@ -13,12 +14,12 @@ const linuxColumns = [ { id: 'pid' }, { id: 'ppid' }, { id: 'cmd', width: '15%' }, - { id: 'argvs', width: "15%" }, + { id: 'argvs', width: '15%' }, { id: 'vm_size' }, { id: 'size' }, { id: 'session' }, { id: 'nice' }, - { id: 'state', width: "15%" } + { id: 'state', width: '15%' }, ]; const macColumns = [ { id: 'name', width: '10%' }, @@ -27,11 +28,12 @@ const macColumns = [ { id: 'ppid' }, { id: 'vm_size' }, { id: 'nice' }, - { id: 'state', width: "15%" } + { id: 'state', width: '15%' }, ]; export const processColumns = { - 'windows': windowsColumns, - 'linux': linuxColumns, - 'apple': macColumns -} + windows: windowsColumns, + linux: linuxColumns, + apple: macColumns, + freebsd: linuxColumns, +}; diff --git a/public/components/agents/syscollector/inventory.tsx b/public/components/agents/syscollector/inventory.tsx index fd0d25f551..5a097c4373 100644 --- a/public/components/agents/syscollector/inventory.tsx +++ b/public/components/agents/syscollector/inventory.tsx @@ -51,6 +51,8 @@ export function SyscollectorInventory({ agent }) { soPlatform = 'windows'; } else if ((agent.os || {}).platform === 'darwin') { soPlatform = 'apple'; + } else if (((agent.os || {}).uname.toLowerCase() || '').includes('freebsd')) { + soPlatform = 'freebsd'; } const netifaceColumns = [ From 37d649e67f04c2f8cba0d4bfa49d504d08d1f968 Mon Sep 17 00:00:00 2001 From: Franco Charriol <franco.charriol@wazuh.com> Date: Fri, 5 Nov 2021 10:09:39 -0300 Subject: [PATCH 310/493] Delete wazuh token called twice (#3652) --- CHANGELOG.md | 1 + public/app.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede2f13055..282e1a478d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the bad requests using Console tool to `PUT /active-response` API endpoint [#3466](https://github.com/wazuh/wazuh-kibana-app/pull/3466) - Fixed group agent management table does not update on error [#3605](https://github.com/wazuh/wazuh-kibana-app/pull/3605) - Fixed not showing packages details in agent inventory for a freeBSD agent SO [#3651](https://github.com/wazuh/wazuh-kibana-app/pull/3651) +- Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/public/app.js b/public/app.js index f41aa2c6cb..2ece13179a 100644 --- a/public/app.js +++ b/public/app.js @@ -116,7 +116,7 @@ app.run(function ($rootElement) { // Bind deleteExistentToken on Log out component. $('.euiHeaderSectionItem__button').on('mouseleave', function () { // opendistro - $('span:contains(Log out)').on('click', function () { + $('button:contains(Log out)').on('click', function () { WzAuthentication.deleteExistentToken(); }); // x-pack From cfe4349b589dbc726304c1a25f4735410ed166f4 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Mon, 8 Nov 2021 13:53:01 +0100 Subject: [PATCH 311/493] Renaming office sample data users --- server/lib/generate-alerts/sample-data/office.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/lib/generate-alerts/sample-data/office.js b/server/lib/generate-alerts/sample-data/office.js index 3ab0228a17..46cd52dad8 100644 --- a/server/lib/generate-alerts/sample-data/office.js +++ b/server/lib/generate-alerts/sample-data/office.js @@ -69,11 +69,11 @@ export const arrayIp = [ '140.82.113.3', ]; export const arrayUserId = [ - 'ale@communities.net', - 'samu@vacaciones.santillana', - 'franco@stress.very', - 'manu@draw.pile', - 'antuan@god.net', + 'smith@wazuh.com', + 'williams@wazuh.com', + 'frank@wazuh.com', + 'jones@wazuh.com', + 'brown@wazuh.com', ]; export const arrayTargetOffice = [ { From a204cbe4cc59de1850dfb97351b6c3f8810183eb Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Wed, 10 Nov 2021 16:36:52 -0300 Subject: [PATCH 312/493] Update security alerts table when filters changes --- public/components/common/modules/discover/discover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 0d99e5d902..84eb2cd075 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -303,8 +303,8 @@ export const Discover = compose( const range = { range: { timestamp: { - gte: dateParse(this.state.dateRange.from), - lte: dateParse(this.state.dateRange.to), + gte: dateParse(this.timefilter.getTime().from), + lte: dateParse(this.timefilter.getTime().to), format: 'epoch_millis' } } From e40b73967bb88d1276d289df8fa7425725400ba7 Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Wed, 10 Nov 2021 16:42:42 -0300 Subject: [PATCH 313/493] Updated CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 782941bee7..67073d6939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,7 +93,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) - Fixed generation of huge logs from backend errors [#3397](https://github.com/wazuh/wazuh-kibana-app/pull/3397) - Fixed vulnerabilities flyout not showing alerts if the vulnerability had a field missing [#3593](https://github.com/wazuh/wazuh-kibana-app/pull/3593) -- +- Update security alerts table when filters change [#3682](https://github.com/wazuh/wazuh-kibana-app/pull/3682) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 From eb3e7afa7b409f06d4fba065a2a2ffbefca50947 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Thu, 11 Nov 2021 12:29:29 +0100 Subject: [PATCH 314/493] Adding columns of Office and Github Events --- .../common/modules/events-selected-fields.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/public/components/common/modules/events-selected-fields.js b/public/components/common/modules/events-selected-fields.js index 96bf1ce0d7..8633dce4e6 100644 --- a/public/components/common/modules/events-selected-fields.js +++ b/public/components/common/modules/events-selected-fields.js @@ -147,5 +147,22 @@ export const EventsSelectedFiles = { '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' + ], }; From 1ccf5771c3806c60ed6df829fe4be48ce7dac372 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 11 Nov 2021 14:43:52 +0100 Subject: [PATCH 315/493] Fixed default modules config reading --- common/constants.ts | 1 + common/wazu-menu/wz-menu-overview.cy.ts | 3 ++- public/react-services/app-state.js | 21 ++++++-------------- public/services/resolves/get-config.js | 2 ++ public/services/resolves/settings-wizard.js | 22 +++++++-------------- 5 files changed, 18 insertions(+), 31 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index adb04049e7..098cd0c0ae 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -196,6 +196,7 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'extensions.ciscat': false, 'extensions.aws': false, 'extensions.office': false, + 'extensions.github': false, 'extensions.gcp': false, 'extensions.virustotal': false, 'extensions.osquery': false, diff --git a/common/wazu-menu/wz-menu-overview.cy.ts b/common/wazu-menu/wz-menu-overview.cy.ts index 41a4501fe0..2d9dc4071a 100644 --- a/common/wazu-menu/wz-menu-overview.cy.ts +++ b/common/wazu-menu/wz-menu-overview.cy.ts @@ -30,5 +30,6 @@ export enum WAZUH_MENU_MODULES_SECTIONS_CY_TEST_ID { CIS_CAT = 'menuModulesCiscatLink', VIRUSTOTAL = 'menuModulesVirustotalLink', GDPR = 'menuModulesGdprLink', - GITHUB = 'menuModulesGitHubLink' + GITHUB = 'menuModulesGitHubLink', + OFFICE_365 = 'menuModulesOfficeLink' } diff --git a/public/react-services/app-state.js b/public/react-services/app-state.js index f583e1a3d6..4c0a9f2a49 100644 --- a/public/react-services/app-state.js +++ b/public/react-services/app-state.js @@ -57,21 +57,12 @@ export class AppState { const wazuhConfig = new WazuhConfig(); const config = wazuhConfig.getConfig(); if(!Object.keys(config).length) return; - const extensions = { - audit: config['extensions.audit'], - pci: config['extensions.pci'], - gdpr: config['extensions.gdpr'], - hipaa: config['extensions.hipaa'], - nist: config['extensions.nist'], - tsc: config['extensions.tsc'], - oscap: config['extensions.oscap'], - ciscat: config['extensions.ciscat'], - aws: config['extensions.aws'], - gcp: config['extensions.gcp'], - virustotal: config['extensions.virustotal'], - osquery: config['extensions.osquery'], - docker: config['extensions.docker'] - }; + const extensions = Object.keys(config) + .filter(key => key.split('.')[0] == 'extensions') + .reduce((extensions, key) => { + extensions[key.split('.')[1]] = config[key]; + return extensions; + }, {}); AppState.setExtensions(id, extensions); return extensions; } diff --git a/public/services/resolves/get-config.js b/public/services/resolves/get-config.js index ba1ae8220f..c14f566607 100644 --- a/public/services/resolves/get-config.js +++ b/public/services/resolves/get-config.js @@ -44,6 +44,8 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { 'extensions.virustotal': false, 'extensions.osquery': false, 'extensions.docker': false, + 'extensions.office': false, + 'extensions.github': false, timeout: 20000, 'ip.selector': true, 'ip.ignore': [], diff --git a/public/services/resolves/settings-wizard.js b/public/services/resolves/settings-wizard.js index 5ff292c27e..1b468feb69 100644 --- a/public/services/resolves/settings-wizard.js +++ b/public/services/resolves/settings-wizard.js @@ -88,21 +88,13 @@ export function settingsWizard( } const extensions = await AppState.getExtensions(currentApi); if (currentApi && !extensions) { - const extensions = { - audit: config['extensions.audit'], - pci: config['extensions.pci'], - gdpr: config['extensions.gdpr'], - hipaa: config['extensions.hipaa'], - nist: config['extensions.nist'], - tsc: config['extensions.tsc'], - oscap: config['extensions.oscap'], - ciscat: config['extensions.ciscat'], - aws: config['extensions.aws'], - gcp: config['extensions.gcp'], - virustotal: config['extensions.virustotal'], - osquery: config['extensions.osquery'], - docker: config['extensions.docker'], - }; + const extensions = Object.keys(config) + .filter(key => key.split('.')[0] == 'extensions') + .reduce((extensions, key) => { + extensions[key.split('.')[1]] = config[key]; + return extensions; + }, {}); + AppState.setExtensions(currentApi, extensions); } deferred.resolve(); From 9fa664e0e26d9701daea41e2f274803e2e164032 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Mon, 15 Nov 2021 13:47:50 -0300 Subject: [PATCH 316/493] fix(snapshots): fix snapshots of health --- .../__snapshots__/health-check.container.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap index 128d5ec6e4..24eca50be6 100644 --- a/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap +++ b/public/components/health-check/container/__snapshots__/health-check.container.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Health Check container should render a Health check screen 1`] = ` <img alt="" className="health-check-logo" - src="/plugins/wazuh/assets/icon_blue.svg" + src="/plugins/wazuh/assets/undefined" /> <div className="margin-top-30" From a55e0a459dbf06f19ded0ae132303bfd73301d9b Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 16 Nov 2021 12:43:27 +0100 Subject: [PATCH 317/493] Merge branch '4.2-7.10' into 4.3-7.10 --- CHANGELOG.md | 12 ++ kibana.json | 2 +- package.json | 2 +- public/components/agents/sca/inventory.tsx | 7 +- .../common/welcome/management-welcome.js | 8 +- .../common/welcome/overview-welcome.js | 16 +- .../components/wz-menu/wz-menu-management.js | 4 +- public/components/wz-menu/wz-menu-settings.js | 2 +- .../agent/components/agents-preview.js | 21 +- .../agent/components/register-agent.js | 91 ++++---- .../angular/directives/histogram.tsx | 4 +- .../discover/kibana_services.ts | 35 +++- .../discover/saved_searches/_saved_search.ts | 12 +- .../discover/saved_searches/saved_searches.ts | 14 +- public/kibana-integrations/kibana-discover.js | 7 +- .../visualizations/_saved_vis.ts | 10 +- .../visualizations/vis_update_state.js | 195 ++++++++++++++++++ public/services/resolves/get-saved-search.js | 2 +- 18 files changed, 348 insertions(+), 96 deletions(-) create mode 100644 public/kibana-integrations/visualizations/vis_update_state.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e229d2035..41695fefb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,18 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed not showing packages details in agent inventory for a freeBSD agent SO [#3651](https://github.com/wazuh/wazuh-kibana-app/pull/3651) - Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) +## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 + +### Added + +- Support for Kibana 7.13.4 +- Support for Kibana 7.14.2 + +### Fixed + +- Fixed compatibility wazuh 4.2 - kibana 7.13.4 [#3653](https://github.com/wazuh/wazuh-kibana-app/pull/3653) +- Fixed interative register windows agent screen error [#3654](https://github.com/wazuh/wazuh-kibana-app/pull/3654) + ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 ### Added diff --git a/kibana.json b/kibana.json index 1a7d503150..1e678b38e4 100644 --- a/kibana.json +++ b/kibana.json @@ -1,6 +1,6 @@ { "id": "wazuh", - "version": "4.3.0-4301", + "version": "4.3.0-4301-0", "kibanaVersion": "kibana", "configPath": [ "wazuh" diff --git a/package.json b/package.json index a5e460df86..3651a2e796 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "revision": "4301-0", "code": "4301-0", "kibana": { - "version": "7.10.2" + "version": "7.13.4" }, "description": "Wazuh app", "keywords": [ diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 42b808593b..4e99f370f5 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -31,7 +31,8 @@ import { EuiButtonEmpty, EuiToolTip, EuiCallOut, - EuiPopover + EuiPopover, + EuiCard, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { formatUIDate } from '../../../react-services/time-service'; @@ -521,9 +522,9 @@ export class Inventory extends Component { <EuiFlexGroup style={{ 'marginTop': 0 }}> {(this.state.data || []).map((pie, idx) => ( <EuiFlexItem key={idx} grow={false}> - <EuiPanel betaBadgeLabel={pie.name} style={{ paddingBottom: 0 }}> + <EuiCard title description betaBadgeLabel={pie.name} style={{ paddingBottom: 0 }}> <Pie width={325} height={130} data={pie.status} colors={['#00a69b', '#ff645c', '#5c6773']} /> - </EuiPanel> + </EuiCard> </EuiFlexItem> ))} </EuiFlexGroup> diff --git a/public/components/common/welcome/management-welcome.js b/public/components/common/welcome/management-welcome.js index f33dc0c0fc..de73642487 100644 --- a/public/components/common/welcome/management-welcome.js +++ b/public/components/common/welcome/management-welcome.js @@ -54,7 +54,7 @@ class ManagementWelcome extends Component { <EuiPage className="wz-welcome-page"> <EuiFlexGroup> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Administration"> + <EuiCard title description betaBadgeLabel="Administration"> <EuiSpacer size="m" /> <EuiFlexGroup> <EuiFlexItem> @@ -139,10 +139,10 @@ class ManagementWelcome extends Component { </EuiFlexItem> <EuiFlexItem /> </EuiFlexGroup> - </EuiPanel> + </EuiCard> </EuiFlexItem> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Status and reports"> + <EuiCard title description betaBadgeLabel="Status and reports"> <EuiSpacer size="m" /> <EuiFlexGroup> <EuiFlexItem> @@ -220,7 +220,7 @@ class ManagementWelcome extends Component { <EuiFlexItem> </EuiFlexItem> </EuiFlexGroup> - </EuiPanel> + </EuiCard> </EuiFlexItem> </EuiFlexGroup> </EuiPage> diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index f068179a9a..1cd59bda13 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -90,7 +90,7 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends {this.props.agentsCountTotal == 0 && this.addAgent()} <EuiFlexGroup> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Security Information Management"> + <EuiCard title description betaBadgeLabel="Security Information Management"> <EuiSpacer size="s" /> <EuiFlexGrid columns={2}> {this.buildTabCard('general', 'dashboardApp')} @@ -104,10 +104,10 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends {this.props.extensions.github && this.buildTabCard('github', 'logoGithub')} </EuiFlexGrid> - </EuiPanel> + </EuiCard> </EuiFlexItem> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Auditing and Policy Monitoring"> + <EuiCard title description betaBadgeLabel="Auditing and Policy Monitoring"> <EuiSpacer size="s" /> <EuiFlexGrid columns={2}> {this.buildTabCard('pm', 'advancedSettingsApp')} @@ -119,14 +119,14 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends this.buildTabCard('ciscat', 'auditbeatApp')} {this.buildTabCard('sca', 'securityAnalyticsApp')} </EuiFlexGrid> - </EuiPanel> + </EuiCard> </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="xl" /> <EuiFlexGroup> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Threat Detection and Response"> + <EuiCard title description betaBadgeLabel="Threat Detection and Response"> <EuiSpacer size="s" /> <EuiFlexGrid columns={2}> {this.buildTabCard('vuls', 'securityApp')} @@ -139,11 +139,11 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends {this.buildTabCard('mitre', 'spacesApp')} {/* TODO- Change "spacesApp" icon*/} </EuiFlexGrid> - </EuiPanel> + </EuiCard> </EuiFlexItem> <EuiFlexItem> - <EuiPanel betaBadgeLabel="Regulatory Compliance"> + <EuiCard title description betaBadgeLabel="Regulatory Compliance"> <EuiSpacer size="s" /> {!this.props.extensions.pci && !this.props.extensions.gdpr && @@ -183,7 +183,7 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends this.buildTabCard('hipaa', 'emsApp')} </EuiFlexGrid> )} - </EuiPanel> + </EuiCard> </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> diff --git a/public/components/wz-menu/wz-menu-management.js b/public/components/wz-menu/wz-menu-management.js index 3d7105a8f1..e745d306b4 100644 --- a/public/components/wz-menu/wz-menu-management.js +++ b/public/components/wz-menu/wz-menu-management.js @@ -138,7 +138,7 @@ class WzMenuManagement extends Component { name: this.managementSections.administration.text, id: this.managementSections.administration.id, id: 0, - disabled: true, + disabled: false, icon: <EuiIcon type="managementApp" color="primary" />, items: [ this.createItem(this.managementSections.rules), @@ -154,7 +154,7 @@ class WzMenuManagement extends Component { { name: this.managementSections.statusReports.text, id: this.managementSections.statusReports.id, - disabled: true, + disabled: false, icon: <EuiIcon type="reportingApp" color="primary" />, items: [ this.createItem(this.managementSections.status), diff --git a/public/components/wz-menu/wz-menu-settings.js b/public/components/wz-menu/wz-menu-settings.js index 5198b76bce..02b8560132 100644 --- a/public/components/wz-menu/wz-menu-settings.js +++ b/public/components/wz-menu/wz-menu-settings.js @@ -124,7 +124,7 @@ class WzMenuSettings extends Component { { name: availableSettings.settings.text, id: availableSettings.settings.id, - disabled: true, + disabled: false, icon: <EuiIcon type="gear" color="primary" />, items: renderSettings } diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index d2a37f876a..ead8ec5c6d 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -23,6 +23,7 @@ import { EuiSpacer, EuiEmptyPrompt, EuiToolTip, + EuiCard, } from '@elastic/eui'; import { Pie } from '../../../components/d3/pie'; import { ProgressChart } from '../../../components/d3/progress'; @@ -193,7 +194,7 @@ export const AgentsPreview = compose( )) || ( <Fragment> <EuiFlexItem className="agents-status-pie" grow={false}> - <EuiPanel betaBadgeLabel="Status" className="eui-panel"> + <EuiCard title description betaBadgeLabel="Status" className="eui-panel"> <EuiFlexGroup> {this.totalAgents > 0 && ( <EuiFlexItem className="align-items-center"> @@ -217,11 +218,11 @@ export const AgentsPreview = compose( </EuiFlexItem> )} </EuiFlexGroup> - </EuiPanel> + </EuiCard> </EuiFlexItem> {this.totalAgents > 0 && ( <EuiFlexItem> - <EuiPanel betaBadgeLabel="Details"> + <EuiCard title description betaBadgeLabel="Details"> <EuiFlexGroup> <EuiFlexItem> {this.summary && ( @@ -334,7 +335,7 @@ export const AgentsPreview = compose( </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> - </EuiPanel> + </EuiCard> </EuiFlexItem> )} </Fragment> @@ -348,7 +349,9 @@ export const AgentsPreview = compose( height: !this.state.loading ? '182px' : 0, }} > - <EuiPanel + <EuiCard + title + description paddingSize="none" betaBadgeLabel="Evolution" style={{ display: this.props.resultState === 'ready' ? 'block' : 'none' }} @@ -370,8 +373,10 @@ export const AgentsPreview = compose( )} </EuiFlexItem> </EuiFlexGroup> - </EuiPanel> - <EuiPanel + </EuiCard> + <EuiCard + title + description paddingSize="none" betaBadgeLabel="Evolution" style={{ @@ -386,7 +391,7 @@ export const AgentsPreview = compose( title={<h3>No results found in the selected time range</h3>} actions={<WzDatePicker condensed={true} onTimeChange={() => {}} />} /> - </EuiPanel> + </EuiCard> </EuiFlexItem> )} </EuiFlexGroup> diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index bc851a7400..e36be9bd1f 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { Component, Fragment } from 'react'; -import { version } from '../../../../package.json'; +import { version, kibana } from '../../../../package.json'; import { WazuhConfig } from '../../../react-services/wazuh-config'; import { EuiSteps, @@ -115,34 +115,33 @@ const pTextCheckConnectionStyle = { marginTop: '3em', }; -export const RegisterAgent = withErrorBoundary( - class RegisterAgent extends Component { - constructor(props) { - super(props); - this.wazuhConfig = new WazuhConfig(); - this.configuration = this.wazuhConfig.getConfig(); - this.state = { - status: 'incomplete', - selectedOS: '', - selectedSYS: '', - neededSYS: false, - selectedArchitecture: '', - selectedVersion: '', - version: '', - wazuhVersion: '', - serverAddress: '', - wazuhPassword: '', - groups: [], - selectedGroup: [], - udpProtocol: false, - gotErrorRegistrationServiceInfo: false - }; - this.restartAgentCommand = { - rpm: this.systemSelector(), - deb: this.systemSelector(), - macos: 'sudo /Library/Ossec/bin/wazuh-control start', - }; - } +export class RegisterAgent extends Component { + constructor(props) { + super(props); + this.wazuhConfig = new WazuhConfig(); + this.configuration = this.wazuhConfig.getConfig(); + this.state = { + status: 'incomplete', + selectedOS: '', + selectedSYS: '', + neededSYS: false, + selectedArchitecture: '', + selectedVersion: '', + kibanaVersion: (kibana || {}).version || false, + version: '', + wazuhVersion: '', + serverAddress: '', + wazuhPassword: '', + groups: [], + selectedGroup: [], + udpProtocol: false, + }; + this.restartAgentCommand = { + rpm: this.systemSelector(), + deb: this.systemSelector(), + macos: 'sudo /Library/Ossec/bin/wazuh-control start', + }; + } async componentDidMount() { try { @@ -379,17 +378,26 @@ export const RegisterAgent = withErrorBoundary( } } - render() { - const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); - const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; - const textAndLinkToCheckConnectionDocumentation = ( - <p style={pTextCheckConnectionStyle}> - To verify the connection with the Manager, please follow this{' '} - <a href={urlCheckConnectionDocumentation} target="_blank"> - document. - </a> - </p> - ); + getHighlightCodeLanguage(selectedSO){ + if(selectedSO.toLowerCase() === 'win'){ + const iKibanaVersion = parseFloat(this.state.kibanaVersion.split('.').slice(0, 2).join('.'),2); + return iKibanaVersion < 7.14 ? 'ps' : 'powershell'; + }else{ + return 'bash'; + } + } + + render() { + const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); + const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; + const textAndLinkToCheckConnectionDocumentation = ( + <p style={pTextCheckConnectionStyle}> + To verify the connection with the Manager, please follow this{' '} + <a href={urlCheckConnectionDocumentation} target="_blank"> + document. + </a> + </p> + ); const missingOSSelection = this.checkMissingOSSelection(); const ipInput = ( <EuiText> @@ -468,7 +476,7 @@ export const RegisterAgent = withErrorBoundary( const field = `${this.state.selectedOS}Text`; const text = customTexts[field]; - const language = this.state.selectedOS === 'win' ? 'ps' : 'bash'; + const language = this.getHighlightCodeLanguage(this.state.selectedOS); const windowsAdvice = this.state.selectedOS === 'win' && ( <> <EuiCallOut @@ -784,4 +792,3 @@ export const RegisterAgent = withErrorBoundary( ); } } -); \ No newline at end of file diff --git a/public/kibana-integrations/discover/application/angular/directives/histogram.tsx b/public/kibana-integrations/discover/application/angular/directives/histogram.tsx index 4c39c8bb25..ffa15feae1 100644 --- a/public/kibana-integrations/discover/application/angular/directives/histogram.tsx +++ b/public/kibana-integrations/discover/application/angular/directives/histogram.tsx @@ -26,7 +26,6 @@ import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; import { - AnnotationDomainTypes, Axis, Chart, HistogramBarSeries, @@ -49,7 +48,6 @@ import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription, combineLatest } from 'rxjs'; import { getServices } from '../../../kibana_services'; import { Chart as IChart } from '../helpers/point_series'; - export interface DiscoverHistogramProps { chartData: IChart; timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; @@ -315,7 +313,7 @@ export class DiscoverHistogram extends Component<DiscoverHistogramProps, Discove /> <LineAnnotation id="line-annotation" - domainType={AnnotationDomainTypes.XDomain} + domainType="xDomain" dataValues={lineAnnotationData} hideTooltips={true} style={lineAnnotationStyle} diff --git a/public/kibana-integrations/discover/kibana_services.ts b/public/kibana-integrations/discover/kibana_services.ts index 8a9b6742e8..f80027969e 100644 --- a/public/kibana-integrations/discover/kibana_services.ts +++ b/public/kibana-integrations/discover/kibana_services.ts @@ -25,6 +25,10 @@ import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; import { search } from '../../../../../src/plugins/data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; +import { i18n } from '@kbn/i18n'; +import type { estypes } from '@elastic/elasticsearch'; +import type { ISearchSource } from 'src/plugins/data/public'; +import type { RequestStatistics } from 'src/plugins/inspector/common'; let angularModule: any = null; let services: DiscoverServices | null = null; @@ -92,7 +96,7 @@ export const [getScopedHistory, setScopedHistory] = createGetterSetter<ScopedHis 'scopedHistory' ); -export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; +export const { getResponseInspectorStats, tabifyAggResponse } = search; export { unhashUrl, redirectWhenMissing } from '../../../../../src/plugins/kibana_utils/public'; export { formatMsg, formatStack, subscribeWithScope } from '../../../../../src/plugins/kibana_legacy/public'; @@ -107,3 +111,32 @@ export { EsQuerySortValue, SortDirection, } from '../../../../../src/plugins/data/public'; + +export function getRequestInspectorStats(searchSource: ISearchSource) { + const stats: RequestStatistics = {}; + const index = searchSource.getField('index'); + + if (index) { + stats.indexPattern = { + label: i18n.translate('data.search.searchSource.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: index.title, + description: i18n.translate('data.search.searchSource.indexPatternDescription', { + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', + }), + }; + stats.indexPatternId = { + label: i18n.translate('data.search.searchSource.indexPatternIdLabel', { + defaultMessage: 'Index pattern ID', + }), + value: index.id!, + description: i18n.translate('data.search.searchSource.indexPatternIdDescription', { + defaultMessage: 'The ID in the {kibanaIndexPattern} index.', + values: { kibanaIndexPattern: '.kibana' }, + }), + }; + } + + return stats; +} \ No newline at end of file diff --git a/public/kibana-integrations/discover/saved_searches/_saved_search.ts b/public/kibana-integrations/discover/saved_searches/_saved_search.ts index 7e5c139c55..02a42a575f 100644 --- a/public/kibana-integrations/discover/saved_searches/_saved_search.ts +++ b/public/kibana-integrations/discover/saved_searches/_saved_search.ts @@ -16,16 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../../../src/plugins/saved_objects/public'; -export function createSavedSearchClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); +import { SavedObject, SavedObjectsStart } from '../../../../../../src/plugins/saved_objects/public';; - class SavedSearch extends SavedObjectClass { +export function createSavedSearchClass(savedObjects: SavedObjectsStart) { + + class SavedSearch extends savedObjects.SavedObjectClass { public static type: string = 'search'; public static mapping = { title: 'text', diff --git a/public/kibana-integrations/discover/saved_searches/saved_searches.ts b/public/kibana-integrations/discover/saved_searches/saved_searches.ts index d839b95afb..eaabf0feee 100644 --- a/public/kibana-integrations/discover/saved_searches/saved_searches.ts +++ b/public/kibana-integrations/discover/saved_searches/saved_searches.ts @@ -17,12 +17,18 @@ * under the License. */ -import { SavedObjectLoader, SavedObjectKibanaServices } from '../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../../../../src/plugins/saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; -export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { - const SavedSearchClass = createSavedSearchClass(services); - const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); +interface Services { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; +} + +export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) { + const SavedSearchClass = createSavedSearchClass(savedObjects); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', diff --git a/public/kibana-integrations/kibana-discover.js b/public/kibana-integrations/kibana-discover.js index 0adde99731..6cd6ed2993 100644 --- a/public/kibana-integrations/kibana-discover.js +++ b/public/kibana-integrations/kibana-discover.js @@ -803,7 +803,7 @@ function discoverController( $scope.fetchStatus = fetchStatuses.COMPLETE; } - function logInspectorRequest() { + async function logInspectorRequest() { inspectorAdapters.requests.reset(); const title = i18n.translate('discover.inspectorRequestDataTitle', { defaultMessage: 'data', @@ -813,9 +813,8 @@ function discoverController( }); inspectorRequest = inspectorAdapters.requests.start(title, { description }); inspectorRequest.stats(getRequestInspectorStats($scope.searchSource)); - $scope.searchSource.getSearchRequestBody().then((body) => { - inspectorRequest.json(body); - }); + const body = await $scope.searchSource.getSearchRequestBody(); + inspectorRequest.json(body); } $scope.updateTime = function () { diff --git a/public/kibana-integrations/visualizations/_saved_vis.ts b/public/kibana-integrations/visualizations/_saved_vis.ts index 46c1db6c33..59a7155789 100644 --- a/public/kibana-integrations/visualizations/_saved_vis.ts +++ b/public/kibana-integrations/visualizations/_saved_vis.ts @@ -28,11 +28,11 @@ import { SavedObject } from '../../../../../src/plugins/saved_objects/public'; // @ts-ignore import { extractReferences, injectReferences } from './saved_visualization_references'; import { IIndexPattern } from '../../../../../src/plugins/data/public'; -import { ISavedVis, SerializedVis, updateOldState } from '../../../../../src/plugins/visualizations/public'; -import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; +import { updateOldState } from './vis_update_state'; +import { createSavedSearchesLoader } from '../discover/saved_searches'; import { getPlugins } from '../../kibana-services'; -export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { +export const convertToSerializedVis = (savedVis) => { const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; const aggs = searchSourceFields && searchSourceFields.index ? visState.aggs || [] : visState.aggs; @@ -52,7 +52,7 @@ export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { }; }; -export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { +export const convertFromSerializedVis = (vis) => { return { id: vis.id, title: vis.title, @@ -107,7 +107,7 @@ export function createSavedVisClass(services) { version: 1, }, afterESResp: async (savedObject: SavedObject) => { - const savedVis = (savedObject as any) as ISavedVis; + const savedVis = (savedObject as any); savedVis.visState = await updateOldState(savedVis.visState); if (savedVis.searchSourceFields?.index) { await services.indexPatterns.get(savedVis.searchSourceFields.index as any); diff --git a/public/kibana-integrations/visualizations/vis_update_state.js b/public/kibana-integrations/visualizations/vis_update_state.js new file mode 100644 index 0000000000..d0ebe00b1a --- /dev/null +++ b/public/kibana-integrations/visualizations/vis_update_state.js @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { set } from '@elastic/safer-lodash-set'; +import _ from 'lodash'; + +/** + * Will figure out if an heatmap state was saved before the auto coloring + * feature of heatmaps was created. If so it will set the overwriteColor flag + * for the label to true if labels are enabled and a non default color has been used. + * So that those earlier created heatmaps will still use the manual specified color. + */ +function convertHeatmapLabelColor(visState) { + const hasOverwriteColorParam = + _.get(visState, 'params.valueAxes[0].labels.overwriteColor') !== undefined; + if (visState.type === 'heatmap' && visState.params && !hasOverwriteColorParam) { + const showLabels = _.get(visState, 'params.valueAxes[0].labels.show', false); + const color = _.get(visState, 'params.valueAxes[0].labels.color', '#555'); + set(visState, 'params.valueAxes[0].labels.overwriteColor', showLabels && color !== '#555'); + } +} + +/** + * Update old terms aggregation format to new terms aggregation format. This will + * update the following things: + * - Rewrite orderBy: _term to orderBy: _key (new API in Elasticsearch) + */ +function convertTermAggregation(visState) { + if (visState.aggs) { + visState.aggs.forEach((agg) => { + if (agg.type === 'terms' && agg.params && agg.params.orderBy === '_term') { + agg.params.orderBy = '_key'; + } + }); + } +} + +function convertPropertyNames(visState) { + // 'showMeticsAtAllLevels' is a legacy typo we'll fix by changing it to 'showMetricsAtAllLevels'. + if (typeof _.get(visState, 'params.showMeticsAtAllLevels') === 'boolean') { + visState.params.showMetricsAtAllLevels = visState.params.showMeticsAtAllLevels; + delete visState.params.showMeticsAtAllLevels; + } +} + +function convertDateHistogramScaleMetrics(visState) { + if (visState.aggs) { + visState.aggs.forEach((agg) => { + if ( + agg.type === 'date_histogram' && + agg.params && + agg.params.interval !== 'auto' && + agg.params.scaleMetricValues === undefined + ) { + // Set scaleMetricValues to true for existing date histograms, that haven't had it defined and used an interval that's not equal auto, + // so that we keep the previous metric scaling example for existing visualizations that might be effected. + agg.params.scaleMetricValues = true; + } + }); + } +} + +function convertSeriesParams(visState) { + if (visState.params.seriesParams) { + return; + } + + // update value axis options + const isUserDefinedYAxis = visState.params.setYExtents; + const defaultYExtents = visState.params.defaultYExtents; + const mode = ['stacked', 'overlap'].includes(visState.params.mode) + ? 'normal' + : visState.params.mode || 'normal'; + + if (!visState.params.valueAxes || !visState.params.valueAxes.length) { + visState.params.valueAxes = [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ]; + } + + visState.params.valueAxes[0].scale = { + ...visState.params.valueAxes[0].scale, + type: visState.params.scale || 'linear', + setYExtents: visState.params.setYExtents || false, + defaultYExtents: visState.params.defaultYExtents || false, + boundsMargin: defaultYExtents ? visState.params.boundsMargin : 0, + min: isUserDefinedYAxis ? visState.params.yAxis.min : undefined, + max: isUserDefinedYAxis ? visState.params.yAxis.max : undefined, + mode: mode, + }; + + // update series options + const interpolate = visState.params.smoothLines ? 'cardinal' : visState.params.interpolate; + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(visState.params.mode); + visState.params.seriesParams = [ + { + show: true, + type: visState.params.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: interpolate, + drawLinesBetweenPoints: visState.params.drawLinesBetweenPoints, + showCircles: visState.params.showCircles, + radiusRatio: visState.params.radiusRatio, + data: { + label: 'Count', + id: '1', + }, + lineWidth: 2, + valueAxis: 'ValueAxis-1', + }, + ]; +} + +/** + * This function is responsible for updating old visStates - the actual saved object + * object - into the format, that will be required by the current Kibana version. + * This method will be executed for each saved vis object, that will be loaded. + * It will return the updated version as Kibana would expect it. It does not modify + * the passed state. + */ +export const updateOldState = (visState) => { + if (!visState) return visState; + const newState = _.cloneDeep(visState); + + convertTermAggregation(newState); + convertPropertyNames(newState); + convertDateHistogramScaleMetrics(newState); + + if (visState.params && ['line', 'area', 'histogram'].includes(visState.params.type)) { + convertSeriesParams(newState); + } + + if (visState.type === 'gauge' && visState.fontSize) { + delete newState.fontSize; + set(newState, 'gauge.style.fontSize', visState.fontSize); + } + + // update old metric to the new one + // Changed from 6.0 -> 6.1 + if ( + ['gauge', 'metric'].includes(visState.type) && + _.get(visState.params, 'gauge.gaugeType', null) === 'Metric' + ) { + newState.type = 'metric'; + newState.params.addLegend = false; + newState.params.type = 'metric'; + newState.params.metric = newState.params.gauge; + delete newState.params.gauge; + delete newState.params.metric.gaugeType; + delete newState.params.metric.gaugeStyle; + delete newState.params.metric.backStyle; + delete newState.params.metric.scale; + delete newState.params.metric.type; + delete newState.params.metric.orientation; + delete newState.params.metric.verticalSplit; + delete newState.params.metric.autoExtend; + newState.params.metric.metricColorMode = newState.params.metric.gaugeColorMode; + delete newState.params.metric.gaugeColorMode; + } else if ( + visState.type === 'metric' && + _.get(visState.params, 'gauge.gaugeType', 'Metric') !== 'Metric' + ) { + newState.type = 'gauge'; + newState.params.type = 'gauge'; + } + + convertHeatmapLabelColor(newState); + + return newState; +}; diff --git a/public/services/resolves/get-saved-search.js b/public/services/resolves/get-saved-search.js index c8fc1f8640..1163c16f86 100644 --- a/public/services/resolves/get-saved-search.js +++ b/public/services/resolves/get-saved-search.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import { healthCheck } from './health-check'; -import { createSavedSearchesLoader } from '../../../../../src/plugins/discover/public'; +import { createSavedSearchesLoader } from '../../kibana-integrations/discover/saved_searches'; import { getToasts, getChrome, From fbbaeb485011677f789890b3195c4a7d96bfc329 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 16 Nov 2021 15:36:54 +0100 Subject: [PATCH 318/493] Adding Changelog.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41695fefb4..a66a2a7c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed group agent management table does not update on error [#3605](https://github.com/wazuh/wazuh-kibana-app/pull/3605) - Fixed not showing packages details in agent inventory for a freeBSD agent SO [#3651](https://github.com/wazuh/wazuh-kibana-app/pull/3651) - Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) +- Fixed compatibility wazuh 4.3 - kibana 7.13.4 [#3685](https://github.com/wazuh/wazuh-kibana-app/pull/3685) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 093d05e1b4fa37d2525d8733a5f1fe2e8c9f16c5 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 16 Nov 2021 15:39:08 +0100 Subject: [PATCH 319/493] Fixing some custom changes --- kibana.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kibana.json b/kibana.json index 1e678b38e4..1a7d503150 100644 --- a/kibana.json +++ b/kibana.json @@ -1,6 +1,6 @@ { "id": "wazuh", - "version": "4.3.0-4301-0", + "version": "4.3.0-4301", "kibanaVersion": "kibana", "configPath": [ "wazuh" diff --git a/package.json b/package.json index 3651a2e796..a5e460df86 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "revision": "4301-0", "code": "4301-0", "kibana": { - "version": "7.13.4" + "version": "7.10.2" }, "description": "Wazuh app", "keywords": [ From 902fab888cdef0263e720d25391167851abddd64 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 16 Nov 2021 17:12:47 +0100 Subject: [PATCH 320/493] Repairing issue 3683 --- .../common/welcome/overview-welcome.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 1cd59bda13..1510fb36cf 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -34,7 +34,7 @@ import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { withErrorBoundary } from '../hocs'; import office_logo from '../../../assets/office365.svg'; -export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends Component { +export const OverviewWelcome = withErrorBoundary(class OverviewWelcome extends Component { constructor(props) { super(props); this.strtools = new StringsTools(); @@ -71,13 +71,15 @@ export const OverviewWelcome = withErrorBoundary (class OverviewWelcome extends addAgent() { return ( - <EuiFlexGroup > - <EuiFlexItem > - <EuiCallOut style={{height:"65%"}} title="No agents were added to this manager. " color="warning" iconType="alert"> - <EuiButtonEmpty style={{margin: "-58px 286px"}} href='#/agents-preview?'>Add agent</EuiButtonEmpty> - </EuiCallOut> - </EuiFlexItem > - </EuiFlexGroup> + <> + <EuiFlexGroup > + <EuiFlexItem > + <EuiCallOut title={<>No agents were added to this manager. <EuiButtonEmpty href='#/agents-preview?'>Add agent</EuiButtonEmpty></>} color="warning" iconType="alert"> + </EuiCallOut> + </EuiFlexItem > + </EuiFlexGroup> + <EuiSpacer size="xl" /> + </> ); } From 79330df64933578a910f68ad6138b4b273af8505 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Tue, 16 Nov 2021 15:53:08 -0300 Subject: [PATCH 321/493] feat(agents-table.js):change of request --- .../management/groups/multiple-agent-selector.tsx | 1 - .../mitre/components/techniques/techniques.tsx | 1 - .../controllers/agent/components/agents-table.js | 11 +++-------- public/controllers/management/groups.js | 15 ++------------- server/start/monitoring/index.ts | 1 - 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/public/components/management/groups/multiple-agent-selector.tsx b/public/components/management/groups/multiple-agent-selector.tsx index 4282b46d1f..c10000c9cf 100644 --- a/public/components/management/groups/multiple-agent-selector.tsx +++ b/public/components/management/groups/multiple-agent-selector.tsx @@ -130,7 +130,6 @@ export const MultipleAgentSelector = withErrorBoundary( async loadAllAgents(searchTerm, start) { try { const params = { - limit: 500, offset: !start ? this.state.availableAgents.offset : 0, select: ['id', 'name'].toString(), }; diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index fc2f02ddfb..bcce843296 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -461,7 +461,6 @@ export const Techniques = withWindowSize( const response = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { search: searchValue, - limit: 500, }, }); const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index f4c8b852e6..fe96602500 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -163,17 +163,12 @@ export const AgentsTable = withErrorBoundary( async UNSAFE_componentWillMount() { const managerVersion = await WzRequest.apiReq('GET', '//', {}); - const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); - const agentActive = await WzRequest.apiReq('GET', '/agents', { - params: { - q: 'status=active', - }, - }); + const agentStatus = await WzRequest.apiReq('GET', '/agents/summary/status', {}) this.setState({ managerVersion: managerVersion.data.data.api_version, - agentActive: agentActive.data.data.totalItems, - avaibleAgents: totalAgent.data.data.affected_items, + agentActive: agentStatus.data.data.active + agentStatus.data.data.disconnected, + avaibleAgents: agentStatus.data.data.total, }); } diff --git a/public/controllers/management/groups.js b/public/controllers/management/groups.js index 73973c92f7..e15ecbfcc3 100644 --- a/public/controllers/management/groups.js +++ b/public/controllers/management/groups.js @@ -75,12 +75,7 @@ export class GroupsController { if (this.globalAgent) { const globalGroup = this.shareAgent.getSelectedGroup(); // Get ALL groups - const data = await WzRequest.apiReq('GET', - '/groups', { - params: { - limit: 500 - }, - }); + const data = await WzRequest.apiReq('GET', '/groups', {}); const filtered = data.data.data.affected_items.filter(group => group.name === globalGroup); if (Array.isArray(filtered) && filtered.length) { // Load that our group @@ -93,13 +88,7 @@ export class GroupsController { this.shareAgent.deleteAgent(); } else { - const loadedGroups = await WzRequest.apiReq('GET', - '/groups', { - params: { - limit: 500 - } - } - ); + const loadedGroups = await WzRequest.apiReq('GET', '/groups', {}); this.buildGroupsTableProps(loadedGroups.data.data.affected_items); this.load = false; } diff --git a/server/start/monitoring/index.ts b/server/start/monitoring/index.ts index 8534db1c7d..ecbdd0f108 100644 --- a/server/start/monitoring/index.ts +++ b/server/start/monitoring/index.ts @@ -467,7 +467,6 @@ async function fetchAllAgentsFromApiHost(context, apiHost){ let payload = { offset: 0, - limit: 500, q: 'id!=000' }; From bc67aef3d4609ad1c627db54f37a673b99da3af0 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Tue, 16 Nov 2021 16:39:55 -0300 Subject: [PATCH 322/493] feat(agents-table.js):change of request --- public/controllers/agent/components/agents-table.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index fe96602500..f64229901e 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -163,12 +163,14 @@ export const AgentsTable = withErrorBoundary( async UNSAFE_componentWillMount() { const managerVersion = await WzRequest.apiReq('GET', '//', {}); - const agentStatus = await WzRequest.apiReq('GET', '/agents/summary/status', {}) + const totalAgent = await WzRequest.apiReq('GET', '/agents', {}); + const agentActive = await WzRequest.apiReq('GET', '/agents/summary/status', {}); + this.setState({ managerVersion: managerVersion.data.data.api_version, - agentActive: agentStatus.data.data.active + agentStatus.data.data.disconnected, - avaibleAgents: agentStatus.data.data.total, + avaibleAgents: totalAgent.data.data.affected_items, + agentActive: agentActive.data.data.active + agentActive.data.data.disconnected, }); } From d7d409f218ae994ff72ddefe4d31da9177d128ca Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Tue, 16 Nov 2021 17:22:48 -0300 Subject: [PATCH 323/493] feat(changelog): Add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e229d2035..102d2f58a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) +- Changed of request for one that does not return data that is not necessary to optimize times. [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3686) ### Fixed From 60243a481b42f2b36403677129385acac2c03a23 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Tue, 16 Nov 2021 18:04:19 -0300 Subject: [PATCH 324/493] feat(changelog): fixed changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 102d2f58a4..0209696e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,7 +60,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) -- Changed of request for one that does not return data that is not necessary to optimize times. [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3686) +- Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) ### Fixed From c79cc0019ca67d61fab2a24ebfb9388f5d624784 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 17 Nov 2021 08:19:47 +0100 Subject: [PATCH 325/493] Resolving comments --- .../agent/components/register-agent.js | 372 +++++++++--------- .../discover/saved_searches/_saved_search.ts | 2 +- 2 files changed, 185 insertions(+), 189 deletions(-) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index e36be9bd1f..e3282183fd 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -115,33 +115,35 @@ const pTextCheckConnectionStyle = { marginTop: '3em', }; -export class RegisterAgent extends Component { - constructor(props) { - super(props); - this.wazuhConfig = new WazuhConfig(); - this.configuration = this.wazuhConfig.getConfig(); - this.state = { - status: 'incomplete', - selectedOS: '', - selectedSYS: '', - neededSYS: false, - selectedArchitecture: '', - selectedVersion: '', - kibanaVersion: (kibana || {}).version || false, - version: '', - wazuhVersion: '', - serverAddress: '', - wazuhPassword: '', - groups: [], - selectedGroup: [], - udpProtocol: false, - }; - this.restartAgentCommand = { - rpm: this.systemSelector(), - deb: this.systemSelector(), - macos: 'sudo /Library/Ossec/bin/wazuh-control start', - }; - } +export const RegisterAgent = withErrorBoundary( + + class RegisterAgent extends Component { + constructor(props) { + super(props); + this.wazuhConfig = new WazuhConfig(); + this.configuration = this.wazuhConfig.getConfig(); + this.state = { + status: 'incomplete', + selectedOS: '', + selectedSYS: '', + neededSYS: false, + selectedArchitecture: '', + selectedVersion: '', + kibanaVersion: (kibana || {}).version || false, + version: '', + wazuhVersion: '', + serverAddress: '', + wazuhPassword: '', + groups: [], + selectedGroup: [], + udpProtocol: false, + }; + this.restartAgentCommand = { + rpm: this.systemSelector(), + deb: this.systemSelector(), + macos: 'sudo /Library/Ossec/bin/wazuh-control start', + }; + } async componentDidMount() { try { @@ -378,26 +380,26 @@ export class RegisterAgent extends Component { } } - getHighlightCodeLanguage(selectedSO){ - if(selectedSO.toLowerCase() === 'win'){ - const iKibanaVersion = parseFloat(this.state.kibanaVersion.split('.').slice(0, 2).join('.'),2); - return iKibanaVersion < 7.14 ? 'ps' : 'powershell'; - }else{ - return 'bash'; + getHighlightCodeLanguage(selectedSO) { + if (selectedSO.toLowerCase() === 'win') { + const iKibanaVersion = parseFloat(this.state.kibanaVersion.split('.').slice(0, 2).join('.'), 2); + return iKibanaVersion < 7.14 ? 'ps' : 'powershell'; + } else { + return 'bash'; + } } - } - render() { - const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); - const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; - const textAndLinkToCheckConnectionDocumentation = ( - <p style={pTextCheckConnectionStyle}> - To verify the connection with the Manager, please follow this{' '} - <a href={urlCheckConnectionDocumentation} target="_blank"> - document. - </a> - </p> - ); + render() { + const appVersionMajorDotMinor = this.state.wazuhVersion.split('.').slice(0, 2).join('.'); + const urlCheckConnectionDocumentation = `https://documentation.wazuh.com/${appVersionMajorDotMinor}/user-manual/agents/agent-connection.html`; + const textAndLinkToCheckConnectionDocumentation = ( + <p style={pTextCheckConnectionStyle}> + To verify the connection with the Manager, please follow this{' '} + <a href={urlCheckConnectionDocumentation} target="_blank"> + document. + </a> + </p> + ); const missingOSSelection = this.checkMissingOSSelection(); const ipInput = ( <EuiText> @@ -415,30 +417,30 @@ export class RegisterAgent extends Component { const groupInput = ( <> - {!this.state.groups.length &&( - <> - <EuiCallOut - color="warning" - title='This section could not be configured because you do not have permission to read groups.' - iconType="iInCircle" + {!this.state.groups.length && ( + <> + <EuiCallOut + color="warning" + title='This section could not be configured because you do not have permission to read groups.' + iconType="iInCircle" + /> + <EuiSpacer /> + </> + )} + <EuiText> + <p>Select one or more existing groups</p> + <EuiComboBox + placeholder={!this.state.groups.length ? "Default" : "Select group"} + options={this.state.groups} + selectedOptions={this.state.selectedGroup} + onChange={(group) => { + this.setGroupName(group); + }} + isDisabled={!this.state.groups.length} + isClearable={true} + data-test-subj="demoComboBox" /> - <EuiSpacer /> - </> - )} - <EuiText> - <p>Select one or more existing groups</p> - <EuiComboBox - placeholder={!this.state.groups.length ? "Default" : "Select group"} - options={this.state.groups} - selectedOptions={this.state.selectedGroup} - onChange={(group) => { - this.setGroupName(group); - }} - isDisabled={!this.state.groups.length} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiText> + </EuiText> </> ); @@ -455,23 +457,16 @@ export class RegisterAgent extends Component { }; const customTexts = { rpmText: `sudo ${this.optionalDeploymentVariables()}yum install ${this.optionalPackages()}`, - debText: `curl -so wazuh-agent-${ - this.state.wazuhVersion - }.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent-${ - this.state.wazuhVersion - }.deb`, - macosText: `curl -so wazuh-agent-${ - this.state.wazuhVersion - }.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${ - this.state.wazuhVersion - }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${ - this.state.wazuhVersion - }.pkg -target /`, - winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${ - this.state.wazuhVersion - }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${ - this.state.wazuhVersion - }.msi /q ${this.optionalDeploymentVariables()}`, + debText: `curl -so wazuh-agent-${this.state.wazuhVersion + }.deb ${this.optionalPackages()} && sudo ${this.optionalDeploymentVariables()}dpkg -i ./wazuh-agent-${this.state.wazuhVersion + }.deb`, + macosText: `curl -so wazuh-agent-${this.state.wazuhVersion + }.pkg https://packages.wazuh.com/4.x/macos/wazuh-agent-${this.state.wazuhVersion + }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${this.state.wazuhVersion + }.pkg -target /`, + winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${this.state.wazuhVersion + }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${this.state.wazuhVersion + }.msi /q ${this.optionalDeploymentVariables()}`, }; const field = `${this.state.selectedOS}Text`; @@ -508,39 +503,39 @@ export class RegisterAgent extends Component { iconType="iInCircle" /> ) : - this.state.selectedOS && ( - <EuiText> - <p> - You can use this command to install and enroll the Wazuh agent in one or more hosts. - </p> - <EuiCallOut - color="warning" - title={ - <> - Running this command on a host with an agent already installed upgrades the - agent package without enrolling the agent. To enroll it, see the{' '} - <EuiLink href="https://documentation.wazuh.com/current/user-manual/registering/index.html"> - Wazuh documentation - </EuiLink> - . - </> - } - iconType="iInCircle" - /> - <EuiSpacer /> - <EuiCodeBlock style={codeBlock} language={language}> - {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} - </EuiCodeBlock> - {windowsAdvice} - <EuiCopy textToCopy={text}> - {(copy) => ( - <EuiButton fill iconType="copy" onClick={copy}> - Copy command - </EuiButton> - )} - </EuiCopy> - </EuiText> - )} + this.state.selectedOS && ( + <EuiText> + <p> + You can use this command to install and enroll the Wazuh agent in one or more hosts. + </p> + <EuiCallOut + color="warning" + title={ + <> + Running this command on a host with an agent already installed upgrades the + agent package without enrolling the agent. To enroll it, see the{' '} + <EuiLink href="https://documentation.wazuh.com/current/user-manual/registering/index.html"> + Wazuh documentation + </EuiLink> + . + </> + } + iconType="iInCircle" + /> + <EuiSpacer /> + <EuiCodeBlock style={codeBlock} language={language}> + {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} + </EuiCodeBlock> + {windowsAdvice} + <EuiCopy textToCopy={text}> + {(copy) => ( + <EuiButton fill iconType="copy" onClick={copy}> + Copy command + </EuiButton> + )} + </EuiCopy> + </EuiText> + )} </div> ); @@ -606,52 +601,52 @@ export class RegisterAgent extends Component { }, ...(this.state.selectedOS == 'rpm' ? [ - { - title: 'Choose the version', - children: ( - <EuiButtonGroup - color="primary" - legend="Choose the version" - options={versionButtonsCentos} - idSelected={this.state.selectedVersion} - onChange={(version) => this.setVersion(version)} - /> - ), - }, - ] + { + title: 'Choose the version', + children: ( + <EuiButtonGroup + color="primary" + legend="Choose the version" + options={versionButtonsCentos} + idSelected={this.state.selectedVersion} + onChange={(version) => this.setVersion(version)} + /> + ), + }, + ] : []), ...(this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos5' ? [ - { - title: 'Choose the architecture', - children: ( - <EuiButtonGroup - color="primary" - legend="Choose the architecture" - options={architectureCentos5} - idSelected={this.state.selectedArchitecture} - onChange={(architecture) => this.setArchitecture(architecture)} - /> - ), - }, - ] + { + title: 'Choose the architecture', + children: ( + <EuiButtonGroup + color="primary" + legend="Choose the architecture" + options={architectureCentos5} + idSelected={this.state.selectedArchitecture} + onChange={(architecture) => this.setArchitecture(architecture)} + /> + ), + }, + ] : []), ...(this.state.selectedOS == 'deb' || - (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6') + (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6') ? [ - { - title: 'Choose the architecture', - children: ( - <EuiButtonGroup - color="primary" - legend="Choose the architecture" - options={architectureButtons} - idSelected={this.state.selectedArchitecture} - onChange={(architecture) => this.setArchitecture(architecture)} - /> - ), - }, - ] + { + title: 'Choose the architecture', + children: ( + <EuiButtonGroup + color="primary" + legend="Choose the architecture" + options={architectureButtons} + idSelected={this.state.selectedArchitecture} + onChange={(architecture) => this.setArchitecture(architecture)} + /> + ), + }, + ] : []), { title: 'Wazuh server address', @@ -659,11 +654,11 @@ export class RegisterAgent extends Component { }, ...(!(!this.state.needsPassword || this.state.hidePasswordInput) ? [ - { - title: 'Wazuh password', - children: <Fragment>{passwordInput}</Fragment>, - }, - ] + { + title: 'Wazuh password', + children: <Fragment>{passwordInput}</Fragment>, + }, + ] : []), { title: 'Assign the agent to a group', @@ -671,24 +666,24 @@ export class RegisterAgent extends Component { }, { title: 'Install and enroll the agent', - children: this.state.gotErrorRegistrationServiceInfo ? + children: this.state.gotErrorRegistrationServiceInfo ? calloutErrorRegistrationServiceInfo - : missingOSSelection.length ? ( - <EuiCallOut - color="warning" - title={`Please select the ${missingOSSelection.join(', ')}.`} - iconType="iInCircle" - /> - ) : ( - <div>{guide}</div> - ), + : missingOSSelection.length ? ( + <EuiCallOut + color="warning" + title={`Please select the ${missingOSSelection.join(', ')}.`} + iconType="iInCircle" + /> + ) : ( + <div>{guide}</div> + ), }, ...(this.state.selectedOS == 'rpm' || this.state.selectedOS == 'deb' ? [ - { - title: 'Start the agent', - children: this.state.gotErrorRegistrationServiceInfo ? - calloutErrorRegistrationServiceInfo + { + title: 'Start the agent', + children: this.state.gotErrorRegistrationServiceInfo ? + calloutErrorRegistrationServiceInfo : missingOSSelection.length ? ( <EuiCallOut color="warning" @@ -702,19 +697,19 @@ export class RegisterAgent extends Component { onTabClick={onTabClick} /> ), - }, - ] + }, + ] : []), ...(!missingOSSelection.length && - this.state.selectedOS !== 'rpm' && - this.state.selectedOS !== 'deb' && - restartAgentCommand + this.state.selectedOS !== 'rpm' && + this.state.selectedOS !== 'deb' && + restartAgentCommand ? [ - { - title: 'Start the agent', - children: this.state.gotErrorRegistrationServiceInfo ? - calloutErrorRegistrationServiceInfo + { + title: 'Start the agent', + children: this.state.gotErrorRegistrationServiceInfo ? + calloutErrorRegistrationServiceInfo : ( <EuiFlexGroup direction="column"> <EuiText> @@ -731,8 +726,8 @@ export class RegisterAgent extends Component { </EuiText> </EuiFlexGroup> ), - }, - ] + }, + ] : []), ]; return ( @@ -792,3 +787,4 @@ export class RegisterAgent extends Component { ); } } +); \ No newline at end of file diff --git a/public/kibana-integrations/discover/saved_searches/_saved_search.ts b/public/kibana-integrations/discover/saved_searches/_saved_search.ts index 02a42a575f..4fedaa834e 100644 --- a/public/kibana-integrations/discover/saved_searches/_saved_search.ts +++ b/public/kibana-integrations/discover/saved_searches/_saved_search.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject, SavedObjectsStart } from '../../../../../../src/plugins/saved_objects/public';; +import { SavedObject, SavedObjectsStart } from '../../../../../../src/plugins/saved_objects/public'; export function createSavedSearchClass(savedObjects: SavedObjectsStart) { From a1d2825245f494b003dbd52485c533e5a9f2e1f2 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 17 Nov 2021 13:07:39 +0100 Subject: [PATCH 326/493] Fixed visualization when agent is pinned --- public/react-services/vis-factory-handler.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/public/react-services/vis-factory-handler.js b/public/react-services/vis-factory-handler.js index ea33c53d22..c823255484 100644 --- a/public/react-services/vis-factory-handler.js +++ b/public/react-services/vis-factory-handler.js @@ -70,9 +70,9 @@ export class VisFactoryHandler { const data = tab !== 'sca' ? await GenericRequest.request( - 'GET', - `/elastic/visualizations/overview-${tab}/${currentPattern}` - ) + 'GET', + `/elastic/visualizations/overview-${tab}/${currentPattern}` + ) : false; data && rawVisualizations.assignItems(data.data.raw); if (!fromDiscover) { @@ -101,11 +101,11 @@ export class VisFactoryHandler { try { const data = - (!['sca', 'office'].some(moduleID => tab !== moduleID)) + (!['sca', 'office'].includes(tab)) ? await GenericRequest.request( - 'GET', - `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` - ) + 'GET', + `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` + ) : false; data && rawVisualizations.assignItems(data.data.raw); if (!fromDiscover) { From 2acac2de47cdad17eabba8cc44ecd68e2820e555 Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Wed, 17 Nov 2021 15:10:34 +0100 Subject: [PATCH 327/493] Fix handler error on dev-tools (#3687) * feat(dev-tools): Removed unnecessary error handler. * doc(changelog): Updated. --- CHANGELOG.md | 1 + public/controllers/dev-tools/dev-tools.ts | 47 ++++++++--------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e229d2035..a27bb94fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed group agent management table does not update on error [#3605](https://github.com/wazuh/wazuh-kibana-app/pull/3605) - Fixed not showing packages details in agent inventory for a freeBSD agent SO [#3651](https://github.com/wazuh/wazuh-kibana-app/pull/3651) - Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) +- Fixed handler of error on dev-tools [#3687](https://github.com/wazuh/wazuh-kibana-app/pull/3687) ## Wazuh v4.2.4 - Kibana 7.10.2, 7.11.2, 7.12.1 - Revision 4205 diff --git a/public/controllers/dev-tools/dev-tools.ts b/public/controllers/dev-tools/dev-tools.ts index 67c25de2d8..4e58229155 100644 --- a/public/controllers/dev-tools/dev-tools.ts +++ b/public/controllers/dev-tools/dev-tools.ts @@ -334,18 +334,6 @@ export class DevToolsController { noHScroll: true }) }); - - const options: UIErrorLog = { - context: `${DevToolsController.name}.checkJsonParseError`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error parsing query`, - }, - }; - getErrorOrchestrator().handleError(options); } } } @@ -499,7 +487,7 @@ export class DevToolsController { const spaceLineStart = (line.match(reLineStart) || [])[1] || ''; const inputKeyBodyParam = (line.match(reLineStart) || [])[2] || ''; - const renderBodyParam = (parameter, spaceLineStart) => { + const renderBodyParam = (parameter, spaceLineStart) => { let valueBodyParam = ''; if (parameter.type === 'string') { valueBodyParam = '""' @@ -675,10 +663,18 @@ export class DevToolsController { ); // Place play button at first line from the selected group - const cords = this.apiInputBox.cursorCoords({ - line: desiredGroup[0].start, - ch: 0 - }); + let cords; + try { + cords = this.apiInputBox.cursorCoords({ + line: desiredGroup[0].start, + ch: 0 + }); + } catch { + $('#play_button').hide(); + $('#wazuh_dev_tools_documentation').hide(); + return null; + } + if (!$('#play_button').is(':visible')) $('#play_button').show(); if (!$('#wazuh_dev_tools_documentation').is(':visible')) $('#wazuh_dev_tools_documentation').show(); const currentPlayButton = $('#play_button').offset(); @@ -713,7 +709,7 @@ export class DevToolsController { $('#wazuh_dev_tools_documentation').hide(); const options: UIErrorLog = { context: `${DevToolsController.name}.calculateWhichGroup`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + level: UI_LOGGER_LEVELS.WARNING as UILogLevel, severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, error: { error: error, @@ -793,21 +789,10 @@ export class DevToolsController { : '/'; let JSONraw = {}; - try { + try { JSONraw = JSON.parse(paramsInline || desiredGroup.requestTextJson); } catch (error) { JSONraw = {}; - const options: UIErrorLog = { - context: `${DevToolsController.name}.send`, - level: UI_LOGGER_LEVELS.ERROR as UILogLevel, - severity: UI_ERROR_SEVERITIES.UI as UIErrorSeverity, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); } if (typeof extra.pretty !== 'undefined') delete extra.pretty; @@ -820,7 +805,7 @@ export class DevToolsController { if (typeof JSONraw === 'object') JSONraw.devTools = true; if (!firstTime) { const output = await this.wzRequest.apiReq(method, path, JSONraw); - + if (typeof output === 'string' && output.includes('3029')) { this.apiOutputBox.setValue('This method is not allowed without admin mode'); } From 4672864223bfb6684b9c3b32282b2a4a3ec9b3e9 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Thu, 18 Nov 2021 15:56:34 -0300 Subject: [PATCH 328/493] fix(registryValues): add id when agent is not pinned --- .../agents/fim/inventory/fileDetail.tsx | 6 +++++- .../registryValues/registryValues.tsx | 18 +++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index aad97431db..163603d75a 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -425,7 +425,11 @@ export class FileDetails extends Component { > <EuiFlexGroup className="flyout-row"> <EuiFlexItem> - <RegistryValues currentFile={currentFile} agent={agent} /> + <RegistryValues + currentFile={currentFile} + agent={agent} + agentId={implicitFilters[2]['agent.id']} + /> </EuiFlexItem> </EuiFlexGroup> </EuiAccordion>{' '} diff --git a/public/components/agents/fim/inventory/registryValues/registryValues.tsx b/public/components/agents/fim/inventory/registryValues/registryValues.tsx index 9f3c9600ba..3cba24131a 100644 --- a/public/components/agents/fim/inventory/registryValues/registryValues.tsx +++ b/public/components/agents/fim/inventory/registryValues/registryValues.tsx @@ -34,14 +34,18 @@ export const RegistryValues = (props) => { }, []); const getValues = async () => { - const { agent, currentFile } = props; + const { agent, currentFile, agentId } = props; try { - const values = await WzRequest.apiReq('GET', `/syscheck/${agent.id}`, { - params: { - q: `type=registry_value;file=${currentFile.file}`, - sort: '-date', - }, - }); + const values = await WzRequest.apiReq( + 'GET', + `/syscheck/${agent.id ? agent.id : agentId}`, + { + params: { + q: `type=registry_value;file=${currentFile.file}`, + sort: '-date', + }, + } + ); setValues((((values || {}).data || {}).data || {}).affected_items || []); } catch (error) { From 5f4c19e293fdf3bf39ad5cdb0d4498d4b78509dc Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Thu, 18 Nov 2021 16:40:12 -0300 Subject: [PATCH 329/493] changelog: add PR to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d172bed6..c3cc4147e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) - Fixed handler of error on dev-tools [#3687](https://github.com/wazuh/wazuh-kibana-app/pull/3687) - Fixed compatibility wazuh 4.3 - kibana 7.13.4 [#3685](https://github.com/wazuh/wazuh-kibana-app/pull/3685) +- Fixed registry values ​​without agent pinned in FIM>Events [#3689](https://github.com/wazuh/wazuh-kibana-app/pull/3689) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 7bd3dc9aa8efcf7a841f179fbad6b4e5dcff4151 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Thu, 18 Nov 2021 17:33:47 -0300 Subject: [PATCH 330/493] fix(fileDetail): I change the place where I got the id --- public/components/agents/fim/inventory/fileDetail.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 163603d75a..334f653028 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -394,7 +394,7 @@ export class FileDetails extends Component { } render() { - const { fileName, type, implicitFilters, view, currentFile, agent } = this.props; + const { fileName, type, implicitFilters, view, currentFile, agent, agentId } = this.props; const inspectButtonText = view === 'extern' ? 'Inspect in FIM' : 'Inspect in Events'; return ( <Fragment> @@ -428,7 +428,7 @@ export class FileDetails extends Component { <RegistryValues currentFile={currentFile} agent={agent} - agentId={implicitFilters[2]['agent.id']} + agentId={agentId} /> </EuiFlexItem> </EuiFlexGroup> From dfd8834f4e7d5765702b7559cb2ad3d5185ea0b1 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 19 Nov 2021 17:43:16 +0100 Subject: [PATCH 331/493] add filter columns in agent table --- .../agent/components/agents-preview.scss | 10 + .../agent/components/agents-table.js | 243 ++++++++++++------ 2 files changed, 170 insertions(+), 83 deletions(-) diff --git a/public/controllers/agent/components/agents-preview.scss b/public/controllers/agent/components/agents-preview.scss index 1195d3440f..ed734f055b 100644 --- a/public/controllers/agent/components/agents-preview.scss +++ b/public/controllers/agent/components/agents-preview.scss @@ -40,3 +40,13 @@ text-align: center; padding: 30px; } + +.columnsSelectedCheckboxs { + display: flex; + align-items: baseline; + margin: 15px 0px; +} + +.columnsSelectedCheckboxs div { + margin-right: 20px; +} diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index f4c8b852e6..ab11b399ad 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -29,6 +29,8 @@ import { EuiOverlayMask, EuiConfirmModal, EuiLoadingSpinner, + EuiCheckboxGroup, + EuiIcon, } from '@elastic/eui'; import { CheckUpgrade } from './checkUpgrade'; import { getToasts } from '../../../kibana-services'; @@ -61,6 +63,7 @@ export const AgentsTable = withErrorBoundary( selectedItems: [], allSelected: false, purgeModal: false, + isFilterColumnOpen: false, filters: sessionStorage.getItem('agents_preview_selected_options') ? JSON.parse(sessionStorage.getItem('agents_preview_selected_options')) : [], @@ -449,13 +452,28 @@ export const AgentsTable = withErrorBoundary( .map((field) => ({ name: field, value: filters[field] })); this.props.downloadCsv(formatedFilters); }; + + openColumnsFilter = () =>{ + this.setState({ + isFilterColumnOpen: !this.state.isFilterColumnOpen, + }); + } formattedButton() { return ( + <> <EuiFlexItem grow={false}> <EuiButtonEmpty iconType="importAction" onClick={this.downloadCsv}> Export formatted </EuiButtonEmpty> </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip content="Filter columns table" position="left"> + <EuiButtonEmpty> + <EuiIcon type="managementApp" color="primary" onClick={this.openColumnsFilter}/> + </EuiButtonEmpty> + </EuiToolTip> + </EuiFlexItem> + </> ); } @@ -765,88 +783,110 @@ export const AgentsTable = withErrorBoundary( }; columns() { - return [ - { - field: 'id', - name: 'ID', - sortable: true, - width: '6%', - }, - { - field: 'name', - name: 'Name', - sortable: true, - width: '15%', - truncateText: true, - }, - { - field: 'ip', - name: 'IP', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'group', - name: 'Group(s)', - width: '20%', - truncateText: true, - sortable: true, - render: (groups) => (groups !== '-' ? this.renderGroups(groups) : '-'), - }, - { - field: 'os_name', - name: 'OS', - sortable: true, - width: '15%', - truncateText: true, - render: this.addIconPlatformRender, - }, - { - field: 'node_name', - name: 'Cluster node', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'version', - name: 'Version', - width: '5%', - truncateText: true, - sortable: true, - /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ - }, - { - field: 'dateAdd', - name: 'Registration date', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'lastKeepAlive', - name: 'Last keep alive', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'status', - name: 'Status', - truncateText: true, - sortable: true, - width: '15%', - render: this.addHealthStatusRender, - }, - { - align: 'right', - width: '5%', - field: 'actions', - name: 'Actions', - render: (agent) => this.actionButtonsRender(agent), - }, - ]; + const selectedColumns = window.localStorage.getItem('columns') + const defaultColumns = [ + { + field: 'id', + name: 'ID', + sortable: true, + width: '6%', + }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '15%', + truncateText: true, + }, + { + field: 'ip', + name: 'IP', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'group', + name: 'Group(s)', + width: '20%', + truncateText: true, + sortable: true, + render: (groups) => (groups !== '-' ? this.renderGroups(groups) : '-'), + }, + { + field: 'os_name', + name: 'OS', + sortable: true, + width: '15%', + truncateText: true, + render: this.addIconPlatformRender, + }, + { + field: 'node_name', + name: 'Cluster node', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'version', + name: 'Version', + width: '5%', + truncateText: true, + sortable: true, + /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ + }, + { + field: 'dateAdd', + name: 'Registration date', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'lastKeepAlive', + name: 'Last keep alive', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'status', + name: 'Status', + truncateText: true, + sortable: true, + width: '15%', + render: this.addHealthStatusRender, + }, + { + align: 'right', + width: '5%', + field: 'actions', + name: 'Actions', + render: (agent) => this.actionButtonsRender(agent), + }, + ]; + + if(selectedColumns){ + const newSelectedColumns = [] + JSON.parse(selectedColumns).forEach(item =>{ + if(item.show){ + const column = defaultColumns.find(column => column.field === item.field) + newSelectedColumns.push(column) + } + }) + return newSelectedColumns + }else{ + const fieldColumns = defaultColumns.map(item => { + return { + field:item.field, + name: item.name, + show: true + } + }) + window.localStorage.setItem('columns', JSON.stringify(fieldColumns)) + return defaultColumns + } } headRender() { @@ -903,6 +943,42 @@ export const AgentsTable = withErrorBoundary( ); } + selectColumnsRender(){ + const columnsSelected = JSON.parse(window.localStorage.getItem('columns')) || [] + const onChange = optionId => { + let item = columnsSelected.find(item=> item.field === optionId); + item.show = !item.show + window.localStorage.setItem('columns',JSON.stringify(columnsSelected)); + this.forceUpdate(); + }; + + const options = () => { + return columnsSelected.map(item =>{ + return { + id: item.field, + label: item.name, + checked: item.show + } + }) + } + + return ( + this.state.isFilterColumnOpen ? + <EuiFlexGroup> + <EuiFlexItem> + <EuiCheckboxGroup + options={options()} + onChange={onChange} + className="columnsSelectedCheckboxs" + idToSelectedMap={{}} + /> + </EuiFlexItem> + </EuiFlexGroup> + : + '' + ) + } + tableRender() { const getRowProps = (item) => { const { id } = item; @@ -1011,6 +1087,7 @@ export const AgentsTable = withErrorBoundary( const { allSelected, purgeModal, selectedItems, loadingAllItem } = this.state; const title = this.headRender(); const filter = this.filterBarRender(); + const selectColumnsRender = this.selectColumnsRender(); const upgradeButton = this.renderUpgradeButton(); const restartButton = this.renderRestartButton(); const purgeButton = this.renderPurgeButton(); @@ -1020,7 +1097,6 @@ export const AgentsTable = withErrorBoundary( const table = this.tableRender(); const callOut = this.callOutRender(); let renderPurgeModal, loadItems, barButtons; - if (purgeModal) { renderPurgeModal = ( <EuiOverlayMask> @@ -1068,6 +1144,7 @@ export const AgentsTable = withErrorBoundary( {loadItems} {selectedItems.length > 0 && barButtons} {callOut} + {selectColumnsRender} {table} {renderPurgeModal} </EuiPanel> From 2f3c9dde40d49af789d32bdbeca5f845da863cbe Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 19 Nov 2021 17:45:36 +0100 Subject: [PATCH 332/493] add filter columns in agent table --- public/controllers/agent/components/agents-table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index ab11b399ad..89d8ed08a8 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -467,7 +467,7 @@ export const AgentsTable = withErrorBoundary( </EuiButtonEmpty> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiToolTip content="Filter columns table" position="left"> + <EuiToolTip content="Select columns table" position="left"> <EuiButtonEmpty> <EuiIcon type="managementApp" color="primary" onClick={this.openColumnsFilter}/> </EuiButtonEmpty> From 38d6b6fe0a9e569eac01959e1e1233ab3411ea9f Mon Sep 17 00:00:00 2001 From: Gabriel Wassan <gabriel.wassan@wazuh.com> Date: Fri, 19 Nov 2021 20:07:15 +0100 Subject: [PATCH 333/493] Fixed styles of globalBreadcrumb for 7.14 (#3688) * feat(global-breadcrumb): Fixed styles of globalBreadcrumb for 7.14 * feat(global-breadcrumb): Rollback * doc(changelog): Updated. * fix(global-breadcrumb): Cherrypick main-overview. Co-authored-by: Federico Rodriguez <federico.rodriguez@wazuh.com> --- CHANGELOG.md | 1 + .../globalBreadcrumb/globalBreadcrumb.scss | 39 ++++++++-------- .../globalBreadcrumb/globalBreadcrumb.tsx | 45 +++++++++---------- .../common/modules/main-overview.tsx | 40 ++++++++--------- 4 files changed, 61 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d172bed6..95adae0a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed wazuh token deleted twice [#3652](https://github.com/wazuh/wazuh-kibana-app/pull/3652) - Fixed handler of error on dev-tools [#3687](https://github.com/wazuh/wazuh-kibana-app/pull/3687) - Fixed compatibility wazuh 4.3 - kibana 7.13.4 [#3685](https://github.com/wazuh/wazuh-kibana-app/pull/3685) +- Fixed breadcrumbs style compatibility for Kibana 7.14.2 [#3688](https://github.com/wazuh/wazuh-kibana-app/pull/3688) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/globalBreadcrumb/globalBreadcrumb.scss b/public/components/common/globalBreadcrumb/globalBreadcrumb.scss index 50d1dd6501..e291b87de0 100644 --- a/public/components/common/globalBreadcrumb/globalBreadcrumb.scss +++ b/public/components/common/globalBreadcrumb/globalBreadcrumb.scss @@ -1,28 +1,31 @@ -.wz-global-breadcrumb { - font-size: 14px; - font-family: "Inter UI"; - padding: 0px; +nav.euiBreadcrumbs.wz-global-breadcrumb span.euiBreadcrumb[title=""]:first-child{ + display: none; } -.wz-global-breadcrumb-btn{ - max-width: unset!important; - height: 20px; +.euiBreadcrumbs--truncate .wz-global-breadcrumb .euiBreadcrumb:not(.euiBreadcrumb--collapsed) span.euiToolTipAnchor{ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: text-bottom; } -.wz-global-breadcrumb-btn{ - height: 20px; - font-family: "Inter UI"; +.wz-global-breadcrumb { + font-size: 14px; + padding: 0px; } +.wz-global-breadcrumb-btn{ + max-width: unset!important; +} -@media screen and (-webkit-min-device-pixel-ratio: 0) { - _::-webkit-full-page-media, _:future, :root , .wz-global-breadcrumb { - line-height: 1.5; - } +@media screen and (-webkit-min-device-pixel-ratio: 0) { + _::-webkit-full-page-media, _:future, :root , .wz-global-breadcrumb { + line-height: 1.5; + } } @-moz-document url-prefix() { - .wz-global-breadcrumb #breadcrumbNoTitle { - height: auto!important; - } -} \ No newline at end of file + .wz-global-breadcrumb #breadcrumbNoTitle { + height: auto!important; + } +} diff --git a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx index 1762d5fd69..95ccddb80b 100644 --- a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx +++ b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx @@ -1,10 +1,9 @@ import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import { EuiBreadcrumbs, EuiToolTip, EuiSelect } from '@elastic/eui'; +import { EuiBreadcrumbs, EuiToolTip } from '@elastic/eui'; import { connect } from 'react-redux'; import './globalBreadcrumb.scss'; import { AppNavigate } from '../../../react-services/app-navigate'; -import { getAngularModule, getChrome } from '../../../kibana-services'; +import { getAngularModule } from '../../../kibana-services'; class WzGlobalBreadcrumb extends Component { props: { state: { breadcrumb: [] } }; @@ -25,26 +24,24 @@ class WzGlobalBreadcrumb extends Component { return ( <div> {!!this.props.state.breadcrumb.length && ( - <EuiBreadcrumbs - className='wz-global-breadcrumb' - responsive={false} - truncate={false} - max={6} - breadcrumbs={this.props.state.breadcrumb.map(breadcrumb => breadcrumb.agent ? { - text: ( - <a - style={{ margin: '0px 0px -5px 0px', height: 20 }} - className="euiLink euiLink--subdued euiBreadcrumb " - onClick={(ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": breadcrumb.agent.id }); this.router.reload(); }} - id="breadcrumbNoTitle" - > - <EuiToolTip position="top" content={"View agent summary"}> - <span>{breadcrumb.agent.name}</span> - </EuiToolTip> - </a>) - } : breadcrumb)} - aria-label="Wazuh global breadcrumbs" - /> + <EuiBreadcrumbs + className='wz-global-breadcrumb' + responsive={false} + truncate={false} + max={6} + breadcrumbs={this.props.state.breadcrumb.map(breadcrumb => breadcrumb.agent ? { + className: "euiLink euiLink--subdued ", + onClick: (ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": breadcrumb.agent.id }); this.router.reload(); }, + id: "breadcrumbNoTitle", + truncate: true, + text: ( + <EuiToolTip position="top" content={"View agent summary"}> + <span>{breadcrumb.agent.name}</span> + </EuiToolTip> + ) + } : breadcrumb)} + aria-label="Wazuh global breadcrumbs" + /> )} </div> ) @@ -57,4 +54,4 @@ const mapStateToProps = state => { }; }; -export default connect(mapStateToProps, null)(WzGlobalBreadcrumb); \ No newline at end of file +export default connect(mapStateToProps, null)(WzGlobalBreadcrumb); diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index 8bd863539a..0230e6f680 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -73,33 +73,29 @@ export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverv }, ]; if (currentAgent.id) { - breadcrumb.push( { + breadcrumb.push({ + className: "euiLink euiLink--subdued ", + onClick: (ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": currentAgent.id }); this.router.reload(); }, + id: "breadcrumbNoTitle", + truncate: true, text: ( - <a - style={{ margin: '0px 0px -5px 0px', height: 20 }} - className="euiLink euiLink--subdued euiBreadcrumb " - onClick={(ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": currentAgent.id }); this.router.reload(); }} - id="breadcrumbNoTitle" - > - <EuiToolTip position="bottom" content={"View agent summary"} display="inlineBlock"> - <span>{currentAgent.name}</span> - </EuiToolTip> - </a>), + <EuiToolTip position="bottom" content={"View agent summary"} display="inlineBlock"> + <span>{currentAgent.name}</span> + </EuiToolTip> + ), }) } breadcrumb.push({ text: ( - <EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}> - <div style={{ margin: '0.8em 0em 0em 0.09em' }}> - <EuiToolTip position="top"> - <div className="euiBreadcrumb euiBreadcrumb--last" title=""> - {WAZUH_MODULES[this.props.section].title} - </div> - </EuiToolTip> - </div> + <EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}> + <EuiToolTip position="top"> + <> + {WAZUH_MODULES[this.props.section].title} + </> + </EuiToolTip> <EuiToolTip content={WAZUH_MODULES[this.props.section].description}> - <EuiIcon style={{ margin: '0px 0px 1px 5px' }} type='iInCircle' /> - </EuiToolTip> + <EuiIcon style={{ margin: '0px 0px 1px 5px' }} type='iInCircle' /> + </EuiToolTip> </EuiFlexGroup> ), truncate: false, @@ -141,7 +137,7 @@ export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverv {this.props.renderTabs()} <EuiFlexItem grow={false} style={{ marginTop: 6, marginRight: 5 }}> <EuiFlexGroup> - {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => + {ModuleTabView && ModuleTabView.buttons && ModuleTabView.buttons.map((ModuleViewButton, index) => typeof ModuleViewButton !== 'string' ? <EuiFlexItem key={`module_button_${index}`}><ModuleViewButton {...{ ...this.props, ...this.props.agentsSelectionProps }} moduleID={section} /></EuiFlexItem> : null)} </EuiFlexGroup> </EuiFlexItem> From 92505f4a872086cbdf5bc6582d407fc1e10e4205 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 23 Nov 2021 16:26:20 +0100 Subject: [PATCH 334/493] fix methods --- .../agent/components/agents-table.js | 273 +++++++++--------- 1 file changed, 141 insertions(+), 132 deletions(-) diff --git a/public/controllers/agent/components/agents-table.js b/public/controllers/agent/components/agents-table.js index 89d8ed08a8..be576ec311 100644 --- a/public/controllers/agent/components/agents-table.js +++ b/public/controllers/agent/components/agents-table.js @@ -453,26 +453,27 @@ export const AgentsTable = withErrorBoundary( this.props.downloadCsv(formatedFilters); }; - openColumnsFilter = () =>{ + openColumnsFilter = () => { this.setState({ isFilterColumnOpen: !this.state.isFilterColumnOpen, }); - } + }; + formattedButton() { return ( <> - <EuiFlexItem grow={false}> - <EuiButtonEmpty iconType="importAction" onClick={this.downloadCsv}> - Export formatted - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip content="Select columns table" position="left"> - <EuiButtonEmpty> - <EuiIcon type="managementApp" color="primary" onClick={this.openColumnsFilter}/> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="importAction" onClick={this.downloadCsv}> + Export formatted </EuiButtonEmpty> - </EuiToolTip> - </EuiFlexItem> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip content="Select columns table" position="left"> + <EuiButtonEmpty onClick={this.openColumnsFilter}> + <EuiIcon type="managementApp" color="primary" /> + </EuiButtonEmpty> + </EuiToolTip> + </EuiFlexItem> </> ); } @@ -674,6 +675,14 @@ export const AgentsTable = withErrorBoundary( } } + getTableColumnsSelected() { + return JSON.parse(window.localStorage.getItem('columnsSelectedTableAgent')) || []; + } + + setTableColumnsSelected(data) { + window.localStorage.setItem('columnsSelectedTableAgent', JSON.stringify(data)); + } + setUpgradingState(agentID) { const { agents } = this.state; agents.forEach((element) => { @@ -783,110 +792,110 @@ export const AgentsTable = withErrorBoundary( }; columns() { - const selectedColumns = window.localStorage.getItem('columns') - const defaultColumns = [ - { - field: 'id', - name: 'ID', - sortable: true, - width: '6%', - }, - { - field: 'name', - name: 'Name', - sortable: true, - width: '15%', - truncateText: true, - }, - { - field: 'ip', - name: 'IP', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'group', - name: 'Group(s)', - width: '20%', - truncateText: true, - sortable: true, - render: (groups) => (groups !== '-' ? this.renderGroups(groups) : '-'), - }, - { - field: 'os_name', - name: 'OS', - sortable: true, - width: '15%', - truncateText: true, - render: this.addIconPlatformRender, - }, - { - field: 'node_name', - name: 'Cluster node', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'version', - name: 'Version', - width: '5%', - truncateText: true, - sortable: true, - /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ - }, - { - field: 'dateAdd', - name: 'Registration date', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'lastKeepAlive', - name: 'Last keep alive', - width: '10%', - truncateText: true, - sortable: true, - }, - { - field: 'status', - name: 'Status', - truncateText: true, - sortable: true, - width: '15%', - render: this.addHealthStatusRender, - }, - { - align: 'right', - width: '5%', - field: 'actions', - name: 'Actions', - render: (agent) => this.actionButtonsRender(agent), - }, - ]; - - if(selectedColumns){ - const newSelectedColumns = [] - JSON.parse(selectedColumns).forEach(item =>{ - if(item.show){ - const column = defaultColumns.find(column => column.field === item.field) - newSelectedColumns.push(column) - } - }) - return newSelectedColumns - }else{ - const fieldColumns = defaultColumns.map(item => { - return { - field:item.field, - name: item.name, - show: true - } - }) - window.localStorage.setItem('columns', JSON.stringify(fieldColumns)) - return defaultColumns - } + const selectedColumns = this.getTableColumnsSelected(); + const defaultColumns = [ + { + field: 'id', + name: 'ID', + sortable: true, + width: '6%', + }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '15%', + truncateText: true, + }, + { + field: 'ip', + name: 'IP', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'group', + name: 'Group(s)', + width: '20%', + truncateText: true, + sortable: true, + render: (groups) => (groups !== '-' ? this.renderGroups(groups) : '-'), + }, + { + field: 'os_name', + name: 'OS', + sortable: true, + width: '15%', + truncateText: true, + render: this.addIconPlatformRender, + }, + { + field: 'node_name', + name: 'Cluster node', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'version', + name: 'Version', + width: '5%', + truncateText: true, + sortable: true, + /* render: (version, agent) => this.addUpgradeStatus(version, agent), */ + }, + { + field: 'dateAdd', + name: 'Registration date', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'lastKeepAlive', + name: 'Last keep alive', + width: '10%', + truncateText: true, + sortable: true, + }, + { + field: 'status', + name: 'Status', + truncateText: true, + sortable: true, + width: '15%', + render: this.addHealthStatusRender, + }, + { + align: 'right', + width: '5%', + field: 'actions', + name: 'Actions', + render: (agent) => this.actionButtonsRender(agent), + }, + ]; + + if (selectedColumns.length != 0) { + const newSelectedColumns = []; + selectedColumns.forEach((item) => { + if (item.show) { + const column = defaultColumns.find((column) => column.field === item.field); + newSelectedColumns.push(column); + } + }); + return newSelectedColumns; + } else { + const fieldColumns = defaultColumns.map((item) => { + return { + field: item.field, + name: item.name, + show: true, + }; + }); + this.setTableColumnsSelected(fieldColumns); + return defaultColumns; + } } headRender() { @@ -943,27 +952,27 @@ export const AgentsTable = withErrorBoundary( ); } - selectColumnsRender(){ - const columnsSelected = JSON.parse(window.localStorage.getItem('columns')) || [] - const onChange = optionId => { - let item = columnsSelected.find(item=> item.field === optionId); - item.show = !item.show - window.localStorage.setItem('columns',JSON.stringify(columnsSelected)); + selectColumnsRender() { + const columnsSelected = this.getTableColumnsSelected(); + + const onChange = (optionId) => { + let item = columnsSelected.find((item) => item.field === optionId); + item.show = !item.show; + this.setTableColumnsSelected(columnsSelected); this.forceUpdate(); }; const options = () => { - return columnsSelected.map(item =>{ + return columnsSelected.map((item) => { return { id: item.field, label: item.name, - checked: item.show - } - }) - } + checked: item.show, + }; + }); + }; - return ( - this.state.isFilterColumnOpen ? + return this.state.isFilterColumnOpen ? ( <EuiFlexGroup> <EuiFlexItem> <EuiCheckboxGroup @@ -974,9 +983,9 @@ export const AgentsTable = withErrorBoundary( /> </EuiFlexItem> </EuiFlexGroup> - : + ) : ( '' - ) + ); } tableRender() { From 906ad5ee3fc168a41a91f846f39f92f24999e5da Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 24 Nov 2021 10:39:00 +0100 Subject: [PATCH 335/493] Fixing error that shows we're using X-Pack when we have Basic --- server/controllers/wazuh-api.ts | 2 +- .../lib/security-factory/security-factory.ts | 39 ++++++++++++------- server/plugin.ts | 6 ++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/server/controllers/wazuh-api.ts b/server/controllers/wazuh-api.ts index 275ffefaa3..134bb4b30c 100644 --- a/server/controllers/wazuh-api.ts +++ b/server/controllers/wazuh-api.ts @@ -1060,7 +1060,7 @@ export class WazuhApiCtrl { const disabledRoles = ( await getConfiguration() )['disabled_roles'] || []; const logoSidebar = ( await getConfiguration() )['customization.logo.sidebar'] || 'icon_blue.png'; - const wazuhSecurity = SecurityObj(context.wazuh.plugins); + const wazuhSecurity = await SecurityObj(context.wazuh.plugins, context); const data = (await wazuhSecurity.getCurrentUser(request, context)).authContext; const isWazuhDisabled = +(data.roles || []).some((role) => disabledRoles.includes(role)); diff --git a/server/lib/security-factory/security-factory.ts b/server/lib/security-factory/security-factory.ts index 6b3340098e..21e4cabb1f 100644 --- a/server/lib/security-factory/security-factory.ts +++ b/server/lib/security-factory/security-factory.ts @@ -1,23 +1,34 @@ import { OpendistroFactory, XpackFactory, DefaultFactory } from './factories'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { PluginSetup } from '../../types' +import { PluginSetup } from '../../types'; type CurrentUser = { - username?: string - authContext: {[key:string]: any} -} + username?: string; + authContext: { [key: string]: any }; +}; export interface ISecurityFactory { - platform?: string - getCurrentUser(request: KibanaRequest, context?:RequestHandlerContext): Promise<CurrentUser> + platform?: string; + getCurrentUser(request: KibanaRequest, context?: RequestHandlerContext): Promise<CurrentUser>; } -export function SecurityObj({security, opendistroSecurityKibana}:PluginSetup): ISecurityFactory { - if (!!security) { - return new XpackFactory(security); - } else if (!!opendistroSecurityKibana) { - return new OpendistroFactory(opendistroSecurityKibana); - } else { - return new DefaultFactory(); +export async function SecurityObj( + { security, opendistroSecurityKibana }: PluginSetup, + context?: RequestHandlerContext +): Promise<ISecurityFactory> { + const params = { + path: `/_security/user`, + method: 'GET', + }; + + try { + const responseCurl = await context.core.elasticsearch.client.asInternalUser.transport.request( + params + ); + } catch (error) { + return !!opendistroSecurityKibana + ? new OpendistroFactory(opendistroSecurityKibana) + : new DefaultFactory(); } -} \ No newline at end of file + return new XpackFactory(security); +} diff --git a/server/plugin.ts b/server/plugin.ts index 4215de91b3..5bbc8fdece 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -68,10 +68,12 @@ export class WazuhPlugin implements Plugin<WazuhPluginSetup, WazuhPluginStart> { public async setup(core: CoreSetup, plugins: PluginSetup) { this.logger.debug('Wazuh-wui: Setup'); - const wazuhSecurity = SecurityObj(plugins); + const serverInfo = core.http.getServerInfo(); - core.http.registerRouteHandlerContext('wazuh', (context, request) => { + let wazuhSecurity; + core.http.registerRouteHandlerContext('wazuh', async(context, request) => { + !wazuhSecurity && (wazuhSecurity = await SecurityObj(plugins, context)); return { logger: this.logger, server: { From 516ebc95b47532b42a126c9264faa0ce68185846 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 24 Nov 2021 10:43:23 +0100 Subject: [PATCH 336/493] Adding Changelog.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95adae0a49..afff73343a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed handler of error on dev-tools [#3687](https://github.com/wazuh/wazuh-kibana-app/pull/3687) - Fixed compatibility wazuh 4.3 - kibana 7.13.4 [#3685](https://github.com/wazuh/wazuh-kibana-app/pull/3685) - Fixed breadcrumbs style compatibility for Kibana 7.14.2 [#3688](https://github.com/wazuh/wazuh-kibana-app/pull/3688) +- Fixed error that shows we're using X-Pack when we have Basic [#3692](https://github.com/wazuh/wazuh-kibana-app/pull/3692) + ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From f3bad3fe7689fae0b52086f3b79bec93c497fe43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Wed, 24 Nov 2021 10:45:12 +0100 Subject: [PATCH 337/493] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afff73343a..cea3a8a319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,7 +86,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed breadcrumbs style compatibility for Kibana 7.14.2 [#3688](https://github.com/wazuh/wazuh-kibana-app/pull/3688) - Fixed error that shows we're using X-Pack when we have Basic [#3692](https://github.com/wazuh/wazuh-kibana-app/pull/3692) - ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 ### Added From a9740b66343e7acf1a11d49dee4150f0402e8a11 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 24 Nov 2021 11:08:41 +0100 Subject: [PATCH 338/493] Resolving comments --- server/controllers/wazuh-api.ts | 3 +-- server/lib/security-factory/security-factory.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/server/controllers/wazuh-api.ts b/server/controllers/wazuh-api.ts index 134bb4b30c..f252b5df3d 100644 --- a/server/controllers/wazuh-api.ts +++ b/server/controllers/wazuh-api.ts @@ -1060,8 +1060,7 @@ export class WazuhApiCtrl { const disabledRoles = ( await getConfiguration() )['disabled_roles'] || []; const logoSidebar = ( await getConfiguration() )['customization.logo.sidebar'] || 'icon_blue.png'; - const wazuhSecurity = await SecurityObj(context.wazuh.plugins, context); - const data = (await wazuhSecurity.getCurrentUser(request, context)).authContext; + const data = (await context.wazuh.security.getCurrentUser(request, context)).authContext; const isWazuhDisabled = +(data.roles || []).some((role) => disabledRoles.includes(role)); diff --git a/server/lib/security-factory/security-factory.ts b/server/lib/security-factory/security-factory.ts index 21e4cabb1f..89d090210f 100644 --- a/server/lib/security-factory/security-factory.ts +++ b/server/lib/security-factory/security-factory.ts @@ -20,15 +20,18 @@ export async function SecurityObj( path: `/_security/user`, method: 'GET', }; - - try { - const responseCurl = await context.core.elasticsearch.client.asInternalUser.transport.request( - params - ); - } catch (error) { + if (!!security) { + try { + const responseCurl = await context.core.elasticsearch.client.asInternalUser.transport.request( + params + ); + return new XpackFactory(security); + } catch (error) { + return new DefaultFactory(); + } + } else { return !!opendistroSecurityKibana ? new OpendistroFactory(opendistroSecurityKibana) : new DefaultFactory(); } - return new XpackFactory(security); } From ede0bbe00060b2ad3ef551dcc7dbd040887a771e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Wed, 24 Nov 2021 11:11:06 +0100 Subject: [PATCH 339/493] Update plugin.ts --- server/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/plugin.ts b/server/plugin.ts index 5bbc8fdece..e7bba3e48a 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -68,7 +68,6 @@ export class WazuhPlugin implements Plugin<WazuhPluginSetup, WazuhPluginStart> { public async setup(core: CoreSetup, plugins: PluginSetup) { this.logger.debug('Wazuh-wui: Setup'); - const serverInfo = core.http.getServerInfo(); let wazuhSecurity; From 65619a731bc0e06a5cad6f33a21752934f72c307 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 24 Nov 2021 14:08:46 +0100 Subject: [PATCH 340/493] Fixing run failed --- server/lib/security-factory/security-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/security-factory/security-factory.ts b/server/lib/security-factory/security-factory.ts index 89d090210f..61272d00bd 100644 --- a/server/lib/security-factory/security-factory.ts +++ b/server/lib/security-factory/security-factory.ts @@ -34,4 +34,4 @@ export async function SecurityObj( ? new OpendistroFactory(opendistroSecurityKibana) : new DefaultFactory(); } -} +} \ No newline at end of file From c7b8b7d80566f0c39f69a767e32df164b4a0949a Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Wed, 24 Nov 2021 14:13:30 +0100 Subject: [PATCH 341/493] Test --- server/lib/security-factory/security-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/security-factory/security-factory.ts b/server/lib/security-factory/security-factory.ts index 61272d00bd..89d090210f 100644 --- a/server/lib/security-factory/security-factory.ts +++ b/server/lib/security-factory/security-factory.ts @@ -34,4 +34,4 @@ export async function SecurityObj( ? new OpendistroFactory(opendistroSecurityKibana) : new DefaultFactory(); } -} \ No newline at end of file +} From 20dec2a6b172c517414f3f8c9c917aaf58d1366b Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 30 Nov 2021 10:12:03 +0100 Subject: [PATCH 342/493] Fixing blank screen before Health-Check --- public/services/resolves/api-count.js | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/public/services/resolves/api-count.js b/public/services/resolves/api-count.js index 3b2dedd90c..60d0920840 100644 --- a/public/services/resolves/api-count.js +++ b/public/services/resolves/api-count.js @@ -21,21 +21,23 @@ import { AppState } from '../../react-services/app-state'; import { GenericRequest } from '../../react-services/generic-request'; -export async function apiCount($q, $location) { +export function apiCount($q, $location) { const deferred = $q.defer(); - try { - const apis = await GenericRequest.request('GET', '/hosts/apis'); - if (!apis || !apis.data || !apis.data.length) throw new Error('No API entries found'); - if (!AppState.getCurrentAPI()) { - await tryToSetDefault(data.data, AppState); - } - deferred.resolve(); - } catch (error) { - $location.search('_a', null); - $location.search('tab', 'api'); - $location.path('/settings'); - deferred.resolve(); - } + + GenericRequest.request('GET', '/hosts/apis') + .then(async (data) => { + if (!data || !data.data || !data.data.length) throw new Error('No API entries found'); + if (!AppState.getCurrentAPI()) { + await tryToSetDefault(data.data, AppState); + } + deferred.resolve(); + }) + .catch(() => { + $location.search('_a', null); + $location.search('tab', 'api'); + $location.path('/settings'); + deferred.resolve(); + }); return deferred.promise; } From c859704a2783b69f87c216def3df9443c9c46154 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Tue, 30 Nov 2021 10:18:28 +0100 Subject: [PATCH 343/493] Adding Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95adae0a49..881aa7a8e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed handler of error on dev-tools [#3687](https://github.com/wazuh/wazuh-kibana-app/pull/3687) - Fixed compatibility wazuh 4.3 - kibana 7.13.4 [#3685](https://github.com/wazuh/wazuh-kibana-app/pull/3685) - Fixed breadcrumbs style compatibility for Kibana 7.14.2 [#3688](https://github.com/wazuh/wazuh-kibana-app/pull/3688) +- Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 3e3d610aeb688603b2f3afa9cf883b216cb7ada1 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Tue, 30 Nov 2021 16:54:48 +0100 Subject: [PATCH 344/493] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d172bed6..3c5cd5e59e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Adding Pagination and filter to drilldown tables at Office pannel [#3544](https://github.com/wazuh/wazuh-kibana-app/pull/3544). - Simple filters change between panel and drilldown panel [#3568](https://github.com/wazuh/wazuh-kibana-app/pull/3568). - Added new fields in Inventory table and Flyout Details [#3525](https://github.com/wazuh/wazuh-kibana-app/pull/3525) +- Added columns selector in agents table [#3691](https://github.com/wazuh/wazuh-kibana-app/pull/3691) ### Changed From 574b4487e60fd1a640216f80fceaca6b4448b479 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 1 Dec 2021 12:41:24 +0100 Subject: [PATCH 345/493] Added decoder handleFileClick parameters --- .../management/components/management/ruleset/decoder-info.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/controllers/management/components/management/ruleset/decoder-info.js b/public/controllers/management/components/management/ruleset/decoder-info.js index 68f12291bc..6756f7222d 100644 --- a/public/controllers/management/components/management/ruleset/decoder-info.js +++ b/public/controllers/management/components/management/ruleset/decoder-info.js @@ -39,7 +39,7 @@ class WzDecoderInfo extends Component { this.rulesetHandler = new RulesetHandler(RulesetResources.DECODERS); - const handleFileClick = async () => { + const handleFileClick = async (value, item) => { try { const result = await this.rulesetHandler.getFileContent(value); const file = { name: value, content: result, path: item.relative_dirname }; @@ -86,7 +86,7 @@ class WzDecoderInfo extends Component { render: (value, item) => { return ( <EuiToolTip position="top" content={`Show ${value} content`}> - <EuiLink onClick={async () => handleFileClick()}>{value}</EuiLink> + <EuiLink onClick={async () => handleFileClick(value, item)}>{value}</EuiLink> </EuiToolTip> ); }, From c81e6ce3e295851dad42307b2650a80c4c31d512 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 1 Dec 2021 12:56:46 +0100 Subject: [PATCH 346/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9777f205ae..970ef40d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed security alerts table when filters change [#3682](https://github.com/wazuh/wazuh-kibana-app/pull/3682) - Fixed error that shows we're using X-Pack when we have Basic [#3692](https://github.com/wazuh/wazuh-kibana-app/pull/3692) - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) +- Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From a0df5bd025a04f2fbf305af4393a12df0bb02640 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Fri, 3 Dec 2021 08:50:16 +0100 Subject: [PATCH 347/493] Fixing Flyouts in Kibana 7.14.2 --- .../agents/fim/inventory/flyout.tsx | 1 + .../agents/fim/inventory/registry-table.tsx | 141 ++--- .../components/agents/fim/inventory/table.tsx | 155 ++--- .../agents/vuls/inventory/table.tsx | 197 ++++--- .../common/modules/discover/discover.tsx | 29 +- public/components/common/modules/events.tsx | 537 ++++++++++-------- .../fim_events_table/fim_events_table.tsx | 103 ++-- .../components/mitre_top/mitre_top.tsx | 178 +++--- .../requirement-flyout/requirement-flyout.tsx | 1 + .../subrequirements/subrequirements.tsx | 301 ++++++---- .../flyout-technique/flyout-technique.tsx | 1 + .../components/techniques/techniques.tsx | 37 +- .../resource_detail_flyout.tsx | 122 ++-- .../security/policies/create-policy.tsx | 264 ++++----- .../security/policies/edit-policy.tsx | 264 ++++----- .../components/roles-mapping-create.tsx | 120 ++-- .../components/roles-mapping-edit.tsx | 126 ++-- .../components/security/roles/create-role.tsx | 122 ++-- .../components/security/roles/edit-role.tsx | 152 ++--- .../security/users/components/create-user.tsx | 4 +- .../security/users/components/edit-user.tsx | 204 +++---- .../wz-logtest/components/logtest.tsx | 93 +-- 22 files changed, 1701 insertions(+), 1451 deletions(-) diff --git a/public/components/agents/fim/inventory/flyout.tsx b/public/components/agents/fim/inventory/flyout.tsx index fc289616de..1c7f2f1596 100644 --- a/public/components/agents/fim/inventory/flyout.tsx +++ b/public/components/agents/fim/inventory/flyout.tsx @@ -127,6 +127,7 @@ export class FlyoutDetail extends Component { aria-labelledby={this.state.currentFile.file} maxWidth="70%" className="wz-inventory wzApp" + outsideClickCloses = {true} > <EuiFlyoutHeader hasBorder className="flyout-header"> <EuiTitle size="s"> diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index 65bcb027af..e8e0c84b9b 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -1,4 +1,3 @@ - /* * Wazuh app - Integrity monitoring components * Copyright (C) 2015-2021 Wazuh, Inc. @@ -18,7 +17,7 @@ import { EuiBasicTable, EuiOverlayMask, EuiOutsideClickDetector, - Direction + Direction, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,26 +36,26 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export class RegistryTable extends Component { state: { - syscheck: [], - error?: string - pageIndex: number - pageSize: number - totalItems: number - sortField: string - isFlyoutVisible: Boolean - sortDirection: Direction - isLoading: boolean + syscheck: []; + error?: string; + pageIndex: number; + pageSize: number; + totalItems: number; + sortField: string; + isFlyoutVisible: Boolean; + sortDirection: Direction; + isLoading: boolean; currentFile: { - file: string, - type: string - }, - syscheckItem: {} + file: string; + type: string; + }; + syscheckItem: {}; }; props!: { - filters: [] - totalItems: number - } + filters: []; + totalItems: number; + }; constructor(props) { super(props); @@ -71,11 +70,11 @@ export class RegistryTable extends Component { isLoading: true, isFlyoutVisible: false, currentFile: { - file: "", + file: '', type: '', }, - syscheckItem: {} - } + syscheckItem: {}, + }; } async componentDidMount() { @@ -92,7 +91,7 @@ export class RegistryTable extends Component { componentDidUpdate(prevProps) { const { filters } = this.props; if (JSON.stringify(filters) !== JSON.stringify(prevProps.filters)) { - this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck) + this.setState({ pageIndex: 0, isLoading: true }, this.getSyscheck); } } @@ -101,38 +100,37 @@ export class RegistryTable extends Component { } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp("&file=" + "[^\&]*", 'g'), ""); + window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); let fileData = false; if (!redirect) { - fileData = this.state.syscheck.filter(item => { + fileData = this.state.syscheck.filter((item) => { return item.file === file; - }) + }); } else { const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { params: { - 'file': file - } + file: file, + }, }); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) - window.location.href = window.location.href += `&file=${file}`; + if (!redirect) window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. const currentFile = { file, - type: item.type - } - this.setState({ isFlyoutVisible: false }, () => this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item })); + type: item.type, + }; + this.setState({ isFlyoutVisible: false }, () => + this.setState({ isFlyoutVisible: true, currentFile, syscheckItem: item }) + ); } async getSyscheck() { const agentID = this.props.agent.id; try { - const syscheck = await WzRequest.apiReq( - 'GET', - `/syscheck/${agentID}`, - { params: this.buildFilter() } - ); + const syscheck = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { + params: this.buildFilter(), + }); this.setState({ syscheck: (((syscheck || {}).data || {}).data || {}).affected_items || {}, @@ -141,7 +139,7 @@ export class RegistryTable extends Component { error: undefined, }); } catch (error) { - this.setState({ error, isLoading: false }) + this.setState({ error, isLoading: false }); const options: UIErrorLog = { context: `${RegistryTable.name}.getSyscheck`, @@ -160,8 +158,8 @@ export class RegistryTable extends Component { buildSortFilter() { const { sortField, sortDirection } = this.state; - const field = (sortField === 'os_name') ? '' : sortField; - const direction = (sortDirection === 'asc') ? '+' : '-'; + const field = sortField === 'os_name' ? '' : sortField; + const direction = sortDirection === 'asc' ? '+' : '-'; return direction + field; } @@ -175,7 +173,7 @@ export class RegistryTable extends Component { offset: pageIndex * pageSize, limit: pageSize, sort: this.buildSortFilter(), - q: 'type=registry_key' + q: 'type=registry_key', }; return filter; @@ -184,13 +182,14 @@ export class RegistryTable extends Component { onTableChange = ({ page = {}, sort = {} }) => { const { index: pageIndex, size: pageSize } = page; const { field: sortField, direction: sortDirection } = sort; - this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, + this.setState( + { + pageIndex, + pageSize, + sortField, + sortDirection, + isLoading: true, + }, () => this.getSyscheck() ); }; @@ -208,12 +207,12 @@ export class RegistryTable extends Component { sortable: true, width: '200px', render: formatUIDate, - } - ] + }, + ]; } renderRegistryTable() { - const getRowProps = item => { + const getRowProps = (item) => { const { file } = item; return { 'data-test-subj': `row-${file}`, @@ -221,14 +220,23 @@ export class RegistryTable extends Component { }; }; - const { syscheck, pageIndex, pageSize, totalItems, sortField, sortDirection, isLoading, error } = this.state; + const { + syscheck, + pageIndex, + pageSize, + totalItems, + sortField, + sortDirection, + isLoading, + error, + } = this.state; const columns = this.columns(); const pagination = { pageIndex: pageIndex, pageSize: pageSize, totalItemCount: totalItems, pageSizeOptions: [15, 25, 50, 100], - } + }; const sorting = { sort: { field: sortField, @@ -263,21 +271,22 @@ export class RegistryTable extends Component { {registryTable} {this.state.isFlyoutVisible && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - fileName={this.state.currentFile.file} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type={this.state.currentFile.type} - view='inventory' - {...this.props} /> - </div> - </EuiOutsideClickDetector> + <div> + {/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + fileName={this.state.currentFile.file} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type={this.state.currentFile.type} + view="inventory" + outsideClickCloses={true} + {...this.props} + /> + </div> </EuiOverlayMask> )} </div> - ) + ); } } diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index bf5609fb8b..78059a8b23 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -34,28 +34,28 @@ import { getErrorOrchestrator } from '../../../../react-services/common-services export class InventoryTable extends Component { state: { - syscheck: [] - error?: string - pageIndex: number - pageSize: number - totalItems: number - sortField: string - isFlyoutVisible: Boolean - sortDirection: Direction - isLoading: boolean + syscheck: []; + error?: string; + pageIndex: number; + pageSize: number; + totalItems: number; + sortField: string; + isFlyoutVisible: Boolean; + sortDirection: Direction; + isLoading: boolean; currentFile: { - file: string - }, - syscheckItem: {} + file: string; + }; + syscheckItem: {}; }; props!: { - filters: IFilter[] - agent: any - items: [] - totalItems: number, - onTotalItemsChange: Function - } + filters: IFilter[]; + agent: any; + items: []; + totalItems: number; + onTotalItemsChange: Function; + }; constructor(props) { super(props); @@ -70,16 +70,16 @@ export class InventoryTable extends Component { isLoading: false, isFlyoutVisible: false, currentFile: { - file: "" + file: '', }, - syscheckItem: {} - } + syscheckItem: {}, + }; } async componentDidMount() { const regex = new RegExp('file=' + '[^&]*'); const match = window.location.href.match(regex); - this.setState({totalItems: this.props.totalItems}); + this.setState({ totalItems: this.props.totalItems }); if (match && match[0]) { const file = match[0].split('=')[1]; this.showFlyout(decodeURIComponent(file), true); @@ -91,24 +91,25 @@ export class InventoryTable extends Component { } async showFlyout(file, item, redirect = false) { - window.location.href = window.location.href.replace(new RegExp("&file=" + "[^\&]*", 'g'), ""); + window.location.href = window.location.href.replace(new RegExp('&file=' + '[^&]*', 'g'), ''); let fileData = false; if (!redirect) { - fileData = this.state.syscheck.filter(item => { + fileData = this.state.syscheck.filter((item) => { return item.file === file; - }) + }); } else { const response = await WzRequest.apiReq('GET', `/syscheck/${this.props.agent.id}`, { params: { - 'file': file - } + file: file, + }, }); fileData = ((response.data || {}).data || {}).affected_items || []; } - if (!redirect) - window.location.href = window.location.href += `&file=${file}`; + if (!redirect) window.location.href = window.location.href += `&file=${file}`; //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. - this.setState({ isFlyoutVisible: false }, () => this.setState({ isFlyoutVisible: true, currentFile: file, syscheckItem: item })); + this.setState({ isFlyoutVisible: false }, () => + this.setState({ isFlyoutVisible: true, currentFile: file, syscheckItem: item }) + ); } async componentDidUpdate(prevProps) { @@ -154,8 +155,8 @@ export class InventoryTable extends Component { buildSortFilter() { const { sortField, sortDirection } = this.state; - const field = (sortField === 'os_name') ? '' : sortField; - const direction = (sortDirection === 'asc') ? '+' : '-'; + const field = sortField === 'os_name' ? '' : sortField; + const direction = sortDirection === 'asc' ? '+' : '-'; return direction + field; } @@ -168,82 +169,84 @@ export class InventoryTable extends Component { offset: pageIndex * pageSize, limit: pageSize, sort: this.buildSortFilter(), - type: 'file' + type: 'file', }; return filter; } - onTableChange = ({ page = {}, sort = {} }) => { const { index: pageIndex, size: pageSize } = page; const { field: sortField, direction: sortDirection } = sort; - this.setState({ - pageIndex, - pageSize, - sortField, - sortDirection, - isLoading: true, - }, + this.setState( + { + pageIndex, + pageSize, + sortField, + sortDirection, + isLoading: true, + }, () => this.getSyscheck() ); }; columns() { let width; - (((this.props.agent || {}).os || {}).platform || false) === 'windows' ? width = '60px' : width = '80px'; + (((this.props.agent || {}).os || {}).platform || false) === 'windows' + ? (width = '60px') + : (width = '80px'); return [ { field: 'file', name: 'File', sortable: true, - width: '250px' + width: '250px', }, { field: 'mtime', name: 'Last Modified', sortable: true, width: '100px', - render: formatUIDate + render: formatUIDate, }, { field: 'uname', name: 'User', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'uid', name: 'User ID', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'gname', name: 'Group', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'gid', name: 'Group ID', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'size', name: 'Size', sortable: true, - width: `${width}` - } - ] + width: `${width}`, + }, + ]; } renderFilesTable() { - const getRowProps = item => { + const getRowProps = (item) => { const { file } = item; return { 'data-test-subj': `row-${file}`, @@ -251,14 +254,23 @@ export class InventoryTable extends Component { }; }; - const { syscheck, pageIndex, pageSize, totalItems, sortField, sortDirection, isLoading, error } = this.state; + const { + syscheck, + pageIndex, + pageSize, + totalItems, + sortField, + sortDirection, + isLoading, + error, + } = this.state; const columns = this.columns(); const pagination = { pageIndex: pageIndex, pageSize: pageSize, totalItemCount: totalItems, pageSizeOptions: [15, 25, 50, 100], - } + }; const sorting = { sort: { field: sortField, @@ -289,26 +301,27 @@ export class InventoryTable extends Component { render() { const filesTable = this.renderFilesTable(); return ( - <div className='wz-inventory'> + <div className="wz-inventory"> {filesTable} - {this.state.isFlyoutVisible && + {this.state.isFlyoutVisible && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - fileName={this.state.currentFile} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type='file' - view='inventory' - showViewInEvents={true} - {...this.props} /> - </div> - </EuiOutsideClickDetector> + <div> + {/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + fileName={this.state.currentFile} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type="file" + view="inventory" + showViewInEvents={true} + outsideClickCloses={true} + {...this.props} + /> + </div> </EuiOverlayMask> - } + )} </div> - ) + ); } } diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 7ef3ad36ed..1613ba41c5 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -11,11 +11,7 @@ */ import React, { Component } from 'react'; -import { - Direction, - EuiOverlayMask, - EuiOutsideClickDetector, -} from '@elastic/eui'; +import { Direction, EuiOverlayMask, EuiOutsideClickDetector } from '@elastic/eui'; import { FlyoutDetail } from './flyout'; import { filtersToObject, IFilter, IWzSuggestItem } from '../../../wz-search-bar'; import { TableWzAPI } from '../../../../components/common/tables'; @@ -24,32 +20,80 @@ import { formatUIDate } from '../../../../react-services/time-service'; export class InventoryTable extends Component { state: { - error?: string - pageIndex: number - pageSize: number - sortField: string - isFlyoutVisible: Boolean - sortDirection: Direction - isLoading: boolean - currentItem: {} + error?: string; + pageIndex: number; + pageSize: number; + sortField: string; + isFlyoutVisible: Boolean; + sortDirection: Direction; + isLoading: boolean; + currentItem: {}; }; suggestions: IWzSuggestItem[] = [ - {type: 'q', label: 'name', description:"Filter by package ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('name', value, this.props.agent.id)}, - {type: 'q', label: 'cve', description:"Filter by CVE ID", operators:['=','!=', '~'], values: async (value) => getFilterValues('cve', value, this.props.agent.id)}, - {type: 'q', label: 'version', description:"Filter by CVE version", operators:['=','!=', '~'], values: async (value) => getFilterValues('version', value, this.props.agent.id)}, - {type: 'q', label: 'architecture', description:"Filter by architecture", operators:['=','!=', '~'], values: async (value) => getFilterValues('architecture', value, this.props.agent.id)}, - {type: 'q', label: 'severity', description:"Filter by Severity", operators:['=','!=', '~'], values: async (value) => getFilterValues('severity', value, this.props.agent.id)}, - {type: 'q', label: 'cvss2_score', description:"Filter by CVSS2", operators:['=','!=', '~'], values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id)}, - {type: 'q', label: 'cvss3_score', description:"Filter by CVSS3", operators:['=','!=', '~'], values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id)}, - {type: 'q', label: 'detection_time', description:"Filter by Detection Time", operators:['=','!=', '~'], values: async (value) => getFilterValues('detection_time', value, this.props.agent.id)} - ] + { + type: 'q', + label: 'name', + description: 'Filter by package ID', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('name', value, this.props.agent.id), + }, + { + type: 'q', + label: 'cve', + description: 'Filter by CVE ID', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('cve', value, this.props.agent.id), + }, + { + type: 'q', + label: 'version', + description: 'Filter by CVE version', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('version', value, this.props.agent.id), + }, + { + type: 'q', + label: 'architecture', + description: 'Filter by architecture', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('architecture', value, this.props.agent.id), + }, + { + type: 'q', + label: 'severity', + description: 'Filter by Severity', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('severity', value, this.props.agent.id), + }, + { + type: 'q', + label: 'cvss2_score', + description: 'Filter by CVSS2', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('cvss2_score', value, this.props.agent.id), + }, + { + type: 'q', + label: 'cvss3_score', + description: 'Filter by CVSS3', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('cvss3_score', value, this.props.agent.id), + }, + { + type: 'q', + label: 'detection_time', + description: 'Filter by Detection Time', + operators: ['=', '!=', '~'], + values: async (value) => getFilterValues('detection_time', value, this.props.agent.id), + }, + ]; props!: { - filters: IFilter[] - agent: any - onTotalItemsChange: Function - } + filters: IFilter[]; + agent: any; + onTotalItemsChange: Function; + }; constructor(props) { super(props); @@ -61,8 +105,8 @@ export class InventoryTable extends Component { sortDirection: 'asc', isLoading: false, isFlyoutVisible: false, - currentItem: {} - } + currentItem: {}, + }; } closeFlyout() { @@ -70,9 +114,10 @@ export class InventoryTable extends Component { } async showFlyout(item, redirect = false) { - //if a flyout is opened, we close it and open a new one, so the components are correctly updated on start. - this.setState({ isFlyoutVisible: false }, () => this.setState({ isFlyoutVisible: true, currentItem: item })); + this.setState({ isFlyoutVisible: false }, () => + this.setState({ isFlyoutVisible: true, currentItem: item }) + ); } async componentDidUpdate(prevProps) { @@ -84,7 +129,7 @@ export class InventoryTable extends Component { buildSortFilter() { const { sortField, sortDirection } = this.state; - const direction = (sortDirection === 'asc') ? '+' : '-'; + const direction = sortDirection === 'asc' ? '+' : '-'; return direction + sortField; } @@ -96,71 +141,73 @@ export class InventoryTable extends Component { ...filters, offset: pageIndex * pageSize, limit: pageSize, - sort: this.buildSortFilter() + sort: this.buildSortFilter(), }; return filter; } columns() { let width; - (((this.props.agent || {}).os || {}).platform || false) === 'windows' ? width = '60px' : width = '80px'; + (((this.props.agent || {}).os || {}).platform || false) === 'windows' + ? (width = '60px') + : (width = '80px'); return [ { field: 'version', name: 'Version', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'architecture', name: 'Architecture', sortable: true, - width: '100px' + width: '100px', }, { field: 'cve', name: 'CVE', sortable: true, truncateText: true, - width: `${width}` + width: `${width}`, }, { field: 'name', name: 'Name', sortable: true, - width: '100px' + width: '100px', }, { field: 'severity', name: 'Severity', sortable: true, - width: `${width}` + width: `${width}`, }, { field: 'cvss2_score', name: 'CVSS2 Score', sortable: true, - width: `${width}` + width: `${width}`, }, { field: 'cvss3_score', name: 'CVSS3 Score', sortable: true, - width: `${width}` + width: `${width}`, }, { field: 'detection_time', name: 'Detection Time', sortable: true, width: `100px`, - render: formatUIDate - } - ] + render: formatUIDate, + }, + ]; } renderTable() { - const getRowProps = item => { + const getRowProps = (item) => { const id = `${item.name}-${item.cve}-${item.architecture}-${item.version}-${item.severity}-${item.cvss2_score}-${item.cvss3_score}-${item.detection_time}`; return { 'data-test-subj': `row-${id}`, @@ -170,46 +217,48 @@ export class InventoryTable extends Component { const { error } = this.state; const columns = this.columns(); - const selectFields = 'select=cve,architecture,version,name,severity,cvss2_score,cvss3_score,detection_time' + const selectFields = + 'select=cve,architecture,version,name,severity,cvss2_score,cvss3_score,detection_time'; return ( - <TableWzAPI - title='Vulnerabilities' - tableColumns={columns} - tableInitialSortingField='name' - searchTable={true} - searchBarSuggestions={this.suggestions} - endpoint={`/vulnerability/${this.props.agent.id}?${selectFields}`} - isExpandable={true} - rowProps={getRowProps} - error={error} - downloadCsv={true} - /> + <TableWzAPI + title="Vulnerabilities" + tableColumns={columns} + tableInitialSortingField="name" + searchTable={true} + searchBarSuggestions={this.suggestions} + endpoint={`/vulnerability/${this.props.agent.id}?${selectFields}`} + isExpandable={true} + rowProps={getRowProps} + error={error} + downloadCsv={true} + /> ); } render() { const table = this.renderTable(); return ( - <div className='wz-inventory'> + <div className="wz-inventory"> {table} - {this.state.isFlyoutVisible && + {this.state.isFlyoutVisible && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - vulName={this.state.currentItem.cve} - agentId={this.props.agent.id} - item={this.state.currentItem} - closeFlyout={() => this.closeFlyout()} - type='vulnerability' - view='inventory' - showViewInEvents={true} - {...this.props} /> - </div> - </EuiOutsideClickDetector> + <div> + {/* EuiOutsideClickDetector needs a static first child */} + <FlyoutDetail + vulName={this.state.currentItem.cve} + agentId={this.props.agent.id} + item={this.state.currentItem} + closeFlyout={() => this.closeFlyout()} + type="vulnerability" + view="inventory" + showViewInEvents={true} + outsideClickCloses={true} + {...this.props} + /> + </div> </EuiOverlayMask> - } + )} </div> - ) + ); } } diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 4e1a98ba59..b9cf7f99a6 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -391,11 +391,11 @@ export const Discover = compose( const range = { range: { timestamp: { - gte: dateParse(this.timefilter.getTime().from), - lte: dateParse(this.timefilter.getTime().to), - format: 'epoch_millis' - } - } + gte: dateParse(this.timefilter.getTime().from), + lte: dateParse(this.timefilter.getTime().to), + format: 'epoch_millis', + }, + }, }; elasticQuery.bool.must.push(range); @@ -760,17 +760,14 @@ export const Discover = compose( const noResultsText = `No results match for this search criteria`; let flyout = this.state.showMitreFlyout ? ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> - <div> - {/* EuiOutsideClickDetector needs a static first child */} - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} - /> - </div> - </EuiOutsideClickDetector> + <div> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + onChangeFlyout={this.onMitreChangeFlyout} + currentTechnique={this.state.selectedTechnique} + /> + </div> </EuiOverlayMask> ) : ( <></> diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 5f3c407aee..710e8eefc3 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -15,7 +15,13 @@ 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 { + 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/kibana_react/public'; @@ -28,293 +34,344 @@ import { getErrorOrchestrator } from '../../../react-services/common-services'; export const Events = compose( withAgentSupportModule, withModuleTabLoader -)(class Events extends Component { - intervalCheckExistsDiscoverTableTime: number = 200; - isMount: boolean; - state: { - flyout: false | {component: any, props: any } - discoverRowsData: any[], - hasRefreshedKnownFields: boolean, - isRefreshing: boolean, - } - constructor(props) { - super(props); - this.isMount = true; - this.state = { - flyout: false, - discoverRowsData: [], - hasRefreshedKnownFields: false, - isRefreshing: false, +)( + class Events extends Component { + intervalCheckExistsDiscoverTableTime: number = 200; + isMount: boolean; + state: { + flyout: false | { component: any; props: any }; + discoverRowsData: any[]; + hasRefreshedKnownFields: boolean; + isRefreshing: boolean; }; - } + constructor(props) { + super(props); + this.isMount = true; + this.state = { + flyout: false, + discoverRowsData: [], + hasRefreshedKnownFields: false, + isRefreshing: false, + }; + } - 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) => { + 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}; + 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 } ); + 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); - } + 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); - }; + 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 }); + 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 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); + } + }); }); - // Add observer to row details - if (addObserverDetails){ - const rowDetails = row.nextElementSibling; - this.enhanceDiscoverTableRowDetailsAddObserver(rowDetails, 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`); - 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'); - if(rowDetailsFields){ - rowDetailsFields.forEach((rowDetailField) => { - this.checkUnknownFields(rowDetailField); - 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); + 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); + } }); }; - } - checkUnknownFields(rowDetailField) { - const fieldCell = rowDetailField.parentNode.childNodes && rowDetailField.parentNode.childNodes[2]; - if(fieldCell.querySelector('svg[data-test-subj="noMappingWarning"]')) { - this.refreshKnownFields(); + 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'); + if (rowDetailsFields) { + rowDetailsFields.forEach((rowDetailField) => { + this.checkUnknownFields(rowDetailField); + 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 + ); + }); + } } - } - refreshKnownFields = async () => { - if (!this.state.hasRefreshedKnownFields) { - try { - this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); - if(satisfyKibanaVersion('<7.11')){ - await PatternHandler.refreshIndexPattern(); - }; - this.setState({ isRefreshing: false }); - this.reloadToast(); - - } catch (error) { - this.setState({ isRefreshing: false }); - throw error; + checkUnknownFields(rowDetailField) { + const fieldCell = + rowDetailField.parentNode.childNodes && rowDetailField.parentNode.childNodes[2]; + if (fieldCell.querySelector('svg[data-test-subj="noMappingWarning"]')) { + this.refreshKnownFields(); } - } else if (this.state.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}); + refreshKnownFields = async () => { + if (!this.state.hasRefreshedKnownFields) { + try { + this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); + if (satisfyKibanaVersion('<7.11')) { + await PatternHandler.refreshIndexPattern(); + } + this.setState({ isRefreshing: false }); + this.reloadToast(); + } catch (error) { + this.setState({ isRefreshing: false }); + throw error; } - }); - }); - observer.observe(element, { childList: true }); - } + } else if (this.state.isRefreshing) { + await new Promise((r) => setTimeout(r, 150)); + await this.refreshKnownFields(); + } + }; - 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); + 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, + }); + } }); - }else{ - // It is a details table row - this.enhanceDiscoverTableRowDetailsAddObserver(mutationElement, discoverRowsData, options); + }); + 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); } - }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 }); - } + setFlyout = (flyout) => { + this.setState({ flyout }); + }; - closeFlyout = () => { - this.setState({flyout: false}); - } + closeFlyout = () => { + this.setState({ flyout: false }); + }; - reloadToast = () => { - const toastLifeTimeMs = 300000; - if(satisfyKibanaVersion('<7.11')){ - getToasts().add({ - color: 'success', - title: 'The index pattern was refreshed successfully.', - text: toMountPoint(<EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> - <EuiFlexItem grow={false}> - There were some unknown fields for the current index pattern. - You need to refresh the page to apply the changes. - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton onClick={() => window.location.reload()} size="s">Reload page</EuiButton> - </EuiFlexItem> - </EuiFlexGroup>), - toastLifeTimeMs - }); - }else if(satisfyKibanaVersion('>=7.11')){ + reloadToast = () => { + const toastLifeTimeMs = 300000; + if (satisfyKibanaVersion('<7.11')) { + getToasts().add({ + color: 'success', + title: 'The index pattern was refreshed successfully.', + text: toMountPoint( + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + There were some unknown fields for the current index pattern. You need to refresh + the page to apply the changes. + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => window.location.reload()} size="s"> + Reload page + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ), + toastLifeTimeMs, + }); + } else if (satisfyKibanaVersion('>=7.11')) { + getToasts().add({ + color: 'warning', + title: 'Found unknown fields in the index pattern.', + text: toMountPoint( + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + There are some unknown fields for the current index pattern. You need to refresh the + page to update the fields. + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => window.location.reload()} size="s"> + Reload page + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ), + toastLifeTimeMs, + }); + } + }; + + errorToast = (error) => { getToasts().add({ - color: 'warning', - title: 'Found unknown fields in the index pattern.', - text: toMountPoint(<EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> - <EuiFlexItem grow={false}> - There are some unknown fields for the current index pattern. - You need to refresh the page to update the fields. - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton onClick={() => window.location.reload()} size="s">Reload page</EuiButton> - </EuiFlexItem> - </EuiFlexGroup>), - toastLifeTimeMs + 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.', }); }; - } - - 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 ( - <Fragment> - {flyout && ( - <EuiOverlayMask - headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => { this.closeFlyout() }}> - <div>{/* EuiOutsideClickDetector needs a static first child */} + render() { + const { flyout } = this.state; + const FlyoutComponent = (flyout || {}).component; + return ( + <Fragment> + {flyout && ( + <EuiOverlayMask headerZindexLocation="below"> + <div> <FlyoutComponent closeFlyout={this.closeFlyout} + outsideClickCloses={true} {...this.state.flyout.props} {...this.props} /> </div> - </EuiOutsideClickDetector> - </EuiOverlayMask> - )} - </Fragment> - ) + </EuiOverlayMask> + )} + </Fragment> + ); + } } -}) +); diff --git a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx index a227690e33..ee7d1c69e5 100644 --- a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx +++ b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx @@ -12,7 +12,7 @@ * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect, Fragment } from 'react' +import React, { useState, useEffect, Fragment } from 'react'; import { EuiBasicTable, EuiFlexItem, @@ -24,13 +24,13 @@ import { EuiToolTip, EuiOverlayMask, EuiOutsideClickDetector, -} from '@elastic/eui' +} from '@elastic/eui'; // @ts-ignore import store from '../../../../../redux/store'; import { updateCurrentAgentData } from '../../../../../redux/actions/appStateActions'; import { getFimAlerts } from './lib'; import { formatUIDate } from '../../../../../react-services/time-service'; -import { FlyoutDetail } from '../../../../agents/fim/inventory/flyout' +import { FlyoutDetail } from '../../../../agents/fim/inventory/flyout'; import { EuiLink } from '@elastic/eui'; import { getDataPlugin } from '../../../../../kibana-services'; @@ -41,13 +41,18 @@ export function FimEventsTable({ agent, router }) { <EuiFlexItem> <EuiFlexGroup> <EuiFlexItem> - <EuiText size="xs"><h2>FIM: Recent events</h2></EuiText> + <EuiText size="xs"> + <h2>FIM: Recent events</h2> + </EuiText> </EuiFlexItem> <EuiFlexItem grow={false}> <EuiToolTip position="top" content="Open FIM"> - <EuiButtonIcon iconType="popout" color="primary" + <EuiButtonIcon + iconType="popout" + color="primary" onClick={() => navigateToFim(agent, router)} - aria-label="Open FIM" /> + aria-label="Open FIM" + /> </EuiToolTip> </EuiFlexItem> </EuiFlexGroup> @@ -60,12 +65,15 @@ export function FimEventsTable({ agent, router }) { } export function useTimeFilter() { - const { timefilter, } = getDataPlugin().query.timefilter; + const { timefilter } = getDataPlugin().query.timefilter; const [timeFilter, setTimeFilter] = useState(timefilter.getTime()); useEffect(() => { - const subscription = timefilter.getTimeUpdate$().subscribe( - () => setTimeFilter(timefilter.getTime())); - return () => { subscription.unsubscribe(); } + const subscription = timefilter + .getTimeUpdate$() + .subscribe(() => setTimeFilter(timefilter.getTime())); + return () => { + subscription.unsubscribe(); + }; }, []); return timeFilter; } @@ -76,7 +84,9 @@ function FimTable({ agent }) { const [file, setFile] = useState(''); const [sort, setSort] = useState({ field: '_source.timestamp', direction: 'desc' }); const timeFilter = useTimeFilter(); - useEffect(() => { getFimAlerts(agent.id, timeFilter, sort).then(setFimAlerts) }, [timeFilter, sort]); + useEffect(() => { + getFimAlerts(agent.id, timeFilter, sort).then(setFimAlerts); + }, [timeFilter, sort]); return ( <Fragment> <EuiBasicTable @@ -86,21 +96,21 @@ function FimTable({ agent }) { sorting={{ sort }} onChange={(e) => setSort(e.sort)} itemId="fim-alerts" - noItemsMessage="No recent events" /> - {isOpen && ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => setIsOpen(false)}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - agentId={agent.id} - closeFlyout={() => setIsOpen(false)} - fileName={file} - view='extern' - {...{ agent }} /> - </div> - </EuiOutsideClickDetector> - </EuiOverlayMask> - )} + noItemsMessage="No recent events" + /> + {isOpen && ( + <EuiOverlayMask headerZindexLocation="below"> + <div> + <FlyoutDetail + agentId={agent.id} + closeFlyout={() => setIsOpen(false)} + fileName={file} + view="extern" + {...{ agent }} + /> + </div> + </EuiOverlayMask> + )} </Fragment> ); } @@ -112,17 +122,38 @@ function navigateToFim(agent, router) { } const columns = (setFile, setIsOpen) => [ - { field: '_source.timestamp', name: "Time", sortable: true, render: (field) => formatUIDate(field), width: '150px' }, - { field: '_source.syscheck.path', name: "Path", sortable: true, truncateText: true, render: (path) => renderPath(path, setFile, setIsOpen) }, - { field: '_source.syscheck.event', name: "Action", sortable: true, width: '100px' }, - { field: '_source.rule.description', name: "Rule description", sortable: true, truncateText: true }, - { field: '_source.rule.level', name: "Rule Level", sortable: true, width: '75px' }, - { field: '_source.rule.id', name: "Rule Id", sortable: true, width: '75px' }, -] + { + field: '_source.timestamp', + name: 'Time', + sortable: true, + render: (field) => formatUIDate(field), + width: '150px', + }, + { + field: '_source.syscheck.path', + name: 'Path', + sortable: true, + truncateText: true, + render: (path) => renderPath(path, setFile, setIsOpen), + }, + { field: '_source.syscheck.event', name: 'Action', sortable: true, width: '100px' }, + { + field: '_source.rule.description', + name: 'Rule description', + sortable: true, + truncateText: true, + }, + { field: '_source.rule.level', name: 'Rule Level', sortable: true, width: '75px' }, + { field: '_source.rule.id', name: 'Rule Id', sortable: true, width: '75px' }, +]; const renderPath = (path, setFile, setIsOpen) => ( - <EuiLink className="euiTableCellContent__text euiTableCellContent--truncateText" - onClick={() => { setFile(path), setIsOpen(true) }}> + <EuiLink + className="euiTableCellContent__text euiTableCellContent--truncateText" + onClick={() => { + setFile(path), setIsOpen(true); + }} + > {path} </EuiLink> -) \ No newline at end of file +); diff --git a/public/components/common/welcome/components/mitre_top/mitre_top.tsx b/public/components/common/welcome/components/mitre_top/mitre_top.tsx index 5e891e1ff8..395474a077 100644 --- a/public/components/common/welcome/components/mitre_top/mitre_top.tsx +++ b/public/components/common/welcome/components/mitre_top/mitre_top.tsx @@ -36,17 +36,17 @@ export class MitreTopTactics extends Component { timefilter: any; indexPattern: any; props!: { - [key: string]: any - } + [key: string]: any; + }; state: { - alertsCount: { key: string, doc_count:number }[] - isLoading: boolean, - time: any, - filterParams: object, - selectedTactic: string | undefined, - flyoutOn: boolean, - selectedTechnique: string - } + alertsCount: { key: string; doc_count: number }[]; + isLoading: boolean; + time: any; + filterParams: object; + selectedTactic: string | undefined; + flyoutOn: boolean; + selectedTechnique: string; + }; subscription: any; constructor(props) { @@ -59,17 +59,21 @@ export class MitreTopTactics extends Component { time: this.timefilter.getTime(), selectedTactic: undefined, flyoutOn: false, - selectedTechnique: '' + selectedTechnique: '', }; } async componentDidMount() { this._isMount = true; - this.subscription = this.timefilter.getTimeUpdate$().subscribe( - () => this._isMount && this.setState({time: this.timefilter.getTime(), isLoading: true})); + this.subscription = this.timefilter + .getTimeUpdate$() + .subscribe( + () => this._isMount && this.setState({ time: this.timefilter.getTime(), isLoading: true }) + ); this.indexPattern = await getIndexPattern(); - getMitreCount(this.props.agentId, this.timefilter.getTime(), undefined) - .then(alertsCount => this.setState({alertsCount, isLoading: false})); + getMitreCount(this.props.agentId, this.timefilter.getTime(), undefined).then((alertsCount) => + this.setState({ alertsCount, isLoading: false }) + ); } async componentWillUnmount() { @@ -79,36 +83,34 @@ export class MitreTopTactics extends Component { shouldComponentUpdate(nextProp, nextState) { const { selectedTactic, isLoading, alertsCount } = this.state; - if (nextState.selectedTactic !== selectedTactic ) - return true; - if (!isLoading ) - return true; - if (JSON.stringify(nextState.alertsCount) !== JSON.stringify(alertsCount)) - return true; + if (nextState.selectedTactic !== selectedTactic) return true; + if (!isLoading) return true; + if (JSON.stringify(nextState.alertsCount) !== JSON.stringify(alertsCount)) return true; return false; } - - async componentDidUpdate(){ + + async componentDidUpdate() { const { selectedTactic, isLoading } = this.state; if (isLoading) { - getMitreCount(this.props.agentId, this.timefilter.getTime(), selectedTactic) - .then(alertsCount => { + getMitreCount(this.props.agentId, this.timefilter.getTime(), selectedTactic).then( + (alertsCount) => { if (alertsCount.length === 0) { - this.setState({selectedTactic: undefined, isLoading: false}) + this.setState({ selectedTactic: undefined, isLoading: false }); } - this.setState({alertsCount, isLoading: false}) - }); + this.setState({ alertsCount, isLoading: false }); + } + ); } } renderLoadingStatus() { const { isLoading } = this.state; - if(!isLoading) return - return( - <div style={{ display: 'block' , textAlign: "center", paddingTop: 100 }}> + if (!isLoading) return; + return ( + <div style={{ display: 'block', textAlign: 'center', paddingTop: 100 }}> <EuiLoadingChart size="xl" /> </div> - ) + ); } renderTacticsTop() { @@ -119,7 +121,7 @@ export class MitreTopTactics extends Component { <div className="wz-agents-mitre"> <EuiText size="xs"> <EuiFlexGroup> - <EuiFlexItem style={{margin: 0, padding: '12px 0px 0px 10px'}}> + <EuiFlexItem style={{ margin: 0, padding: '12px 0px 0px 10px' }}> <h3>Top Tactics</h3> </EuiFlexItem> </EuiFlexGroup> @@ -132,11 +134,10 @@ export class MitreTopTactics extends Component { quantity={tactic.doc_count} onClick={() => { this.setState({ - selectedTactic: tactic.key, - isLoading: true, - }); - } - } + selectedTactic: tactic.key, + isLoading: true, + }); + }} > {tactic.key} </EuiFacetButton> @@ -152,26 +153,25 @@ export class MitreTopTactics extends Component { const { selectedTactic, alertsCount, isLoading } = this.state; if (isLoading) return; return ( - <Fragment> + <Fragment> <EuiText size="xs"> <EuiFlexGroup> - <EuiFlexItem grow={false} > - <EuiButtonIcon - size={'s'} - color={'primary'} - onClick={() => { - this.setState({ + <EuiFlexItem grow={false}> + <EuiButtonIcon + size={'s'} + color={'primary'} + onClick={() => { + this.setState({ selectedTactic: undefined, isLoading: true, - flyoutOn: false + flyoutOn: false, }); - } - } - iconType="sortLeft" - aria-label="Back Top Tactics" - /> + }} + iconType="sortLeft" + aria-label="Back Top Tactics" + /> </EuiFlexItem> - <EuiFlexItem > + <EuiFlexItem> <h3>{selectedTactic}</h3> </EuiFlexItem> </EuiFlexGroup> @@ -196,24 +196,22 @@ export class MitreTopTactics extends Component { renderEmptyPrompt() { const { isLoading } = this.state; if (isLoading) return; - return( + return ( <EuiEmptyPrompt - iconType="stats" - title={<h4>No results</h4>} - body={ - <Fragment> - <p> - No Mitre results were found in the selected time range. - </p> - </Fragment> - } + iconType="stats" + title={<h4>No results</h4>} + body={ + <Fragment> + <p>No Mitre results were found in the selected time range.</p> + </Fragment> + } /> - ) + ); } - onChangeFlyout = (flyoutOn ) => { + onChangeFlyout = (flyoutOn) => { this.setState({ flyoutOn }); - } + }; closeFlyout() { this.setState({ flyoutOn: false }); @@ -222,16 +220,24 @@ export class MitreTopTactics extends Component { showFlyout(tactic) { this.setState({ selectedTechnique: tactic, - flyoutOn: true - }) + flyoutOn: true, + }); } - openDiscover(e,techniqueID){ - AppNavigate.navigateToModule(e, 'overview', {"tab": 'mitre', "tabView": "discover", filters:{ 'rule.mitre.id': techniqueID} }) + 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} } ) + openDashboard(e, techniqueID) { + AppNavigate.navigateToModule(e, 'overview', { + tab: 'mitre', + tabView: 'dashboard', + filters: { 'rule.mitre.id': techniqueID }, + }); } render() { @@ -245,21 +251,21 @@ export class MitreTopTactics extends Component { {loading} {!selectedTactic || alertsCount.length === 0 ? tacticsTop : tecniquesTop} {alertsCount.length === 0 && emptyPrompt} - {flyoutOn && + {flyoutOn && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - implicitFilters={[{ "agent.id": this.props.agentId }]} - agentId={this.props.agentId} - onChangeFlyout={this.onChangeFlyout} - currentTechnique={selectedTechnique} /> - </div> - </EuiOutsideClickDetector> - </EuiOverlayMask>} + <div> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + implicitFilters={[{ 'agent.id': this.props.agentId }]} + agentId={this.props.agentId} + onChangeFlyout={this.onChangeFlyout} + currentTechnique={selectedTechnique} + /> + </div> + </EuiOverlayMask> + )} </Fragment> - ) + ); } -} \ No newline at end of file +} diff --git a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx index 040c0418e7..07f067c5c6 100644 --- a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx +++ b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx @@ -240,6 +240,7 @@ export class RequirementFlyout extends Component { size="l" className="flyout-no-overlap wz-inventory wzApp" aria-labelledby="flyoutSmallTitle" + outsideClickCloses={true} > {currentRequirement && this.renderHeader()} {this.renderBody()} diff --git a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index ef694e8ef1..eb42238880 100644 --- a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -30,54 +30,52 @@ import { } from '@elastic/eui'; import { AppNavigate } from '../../../../../react-services/app-navigate'; import { AppState } from '../../../../../react-services/app-state'; -import { RequirementFlyout } from '../requirement-flyout/requirement-flyout' +import { RequirementFlyout } from '../requirement-flyout/requirement-flyout'; import { WAZUH_ALERTS_PATTERN } from '../../../../../../common/constants'; import { getDataPlugin } from '../../../../../kibana-services'; export class ComplianceSubrequirements extends Component { _isMount = false; - state: { - } + state: {}; - props!: { - }; + props!: {}; constructor(props) { super(props); this.state = { hideAlerts: false, - searchValue: "", - } + searchValue: '', + }; } hideAlerts() { - this.setState({ hideAlerts: !this.state.hideAlerts }) + this.setState({ hideAlerts: !this.state.hideAlerts }); } - onSearchValueChange = e => { + onSearchValueChange = (e) => { this.setState({ searchValue: e.target.value }); - } + }; - /** - * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} - * @param filter - */ + /** + * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} + * @param filter + */ addFilter(filter) { const { filterManager } = getDataPlugin().query; const matchPhrase = {}; matchPhrase[filter.key] = filter.value; const newFilter = { - "meta": { - "disabled": false, - "key": filter.key, - "params": { "query": filter.value }, - "type": "phrase", - "negate": filter.negate || false, - "index": AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN + meta: { + disabled: false, + key: filter.key, + params: { query: filter.value }, + type: 'phrase', + negate: filter.negate || false, + index: AppState.getCurrentPattern() || WAZUH_ALERTS_PATTERN, }, - "query": { "match_phrase": matchPhrase }, - "$state": { "store": "appState" } - } + query: { match_phrase: matchPhrase }, + $state: { store: 'appState' }, + }; filterManager.addFilters([newFilter]); } @@ -97,7 +95,7 @@ export class ComplianceSubrequirements extends Component { if (this.props.section === 'tsc') { return 'rule.tsc'; } - return "pci_dss" + return 'pci_dss'; } openDashboardCurrentWindow(requirementId) { @@ -113,16 +111,25 @@ export class ComplianceSubrequirements extends Component { openDiscover(e, requirementId) { const filters = {}; filters[this.getRequirementKey()] = requirementId; - AppNavigate.navigateToModule(e, 'overview', { "tab": this.props.section, "tabView": "discover", filters, }, () => this.openDiscoverCurrentWindow(requirementId)) + AppNavigate.navigateToModule( + e, + 'overview', + { tab: this.props.section, tabView: 'discover', filters }, + () => this.openDiscoverCurrentWindow(requirementId) + ); } openDashboard(e, requirementId) { const filters = {}; filters[this.getRequirementKey()] = requirementId; - AppNavigate.navigateToModule(e, 'overview', { "tab": this.props.section, "tabView": "panels", filters }, () => this.openDashboardCurrentWindow(requirementId)) + AppNavigate.navigateToModule( + e, + 'overview', + { tab: this.props.section, tabView: 'panels', filters }, + () => this.openDashboardCurrentWindow(requirementId) + ); } - renderFacet() { const { complianceObject } = this.props; const { requirementsCount } = this.props; @@ -133,90 +140,139 @@ export class ComplianceSubrequirements extends Component { const currentTechniques = complianceObject[key]; if (this.props.selectedRequirements[key]) { currentTechniques.forEach((technique, idx) => { - if (!showTechniques[technique] && ((technique.toLowerCase().includes(this.state.searchValue.toLowerCase())) || this.props.descriptions[technique].toLowerCase().includes(this.state.searchValue.toLowerCase()))) { - const quantity = (requirementsCount.find(item => item.key === technique) || {}).doc_count || 0; + if ( + !showTechniques[technique] && + (technique.toLowerCase().includes(this.state.searchValue.toLowerCase()) || + this.props.descriptions[technique] + .toLowerCase() + .includes(this.state.searchValue.toLowerCase())) + ) { + const quantity = + (requirementsCount.find((item) => item.key === technique) || {}).doc_count || 0; if (!this.state.hideAlerts || (this.state.hideAlerts && quantity > 0)) { showTechniques[technique] = true; tacticsToRender.push({ id: technique, label: `${technique} - ${this.props.descriptions[technique]}`, - quantity - }) + quantity, + }); } } }); - } }); - const tacticsToRenderOrdered = tacticsToRender.sort((a, b) => b.quantity - a.quantity).map((item, idx) => { - const tooltipContent = `View details of ${item.id}` - const toolTipAnchorClass = "wz-display-inline-grid" + (this.state.hover === item.id ? " wz-mitre-width" : " "); - return ( - <EuiFlexItem - onMouseEnter={() => this.setState({ hover: item.id })} - onMouseLeave={() => this.setState({ hover: "" })} - key={idx} style={{ border: "1px solid #8080804a", maxWidth: "calc(25% - 8px)", maxHeight: 41 }}> - - <EuiPopover - id="techniqueActionsContextMenu" - anchorClassName="wz-width-100" - button={( - <EuiFacetButton - style={{ width: "100%", padding: "0 5px 0 5px", lineHeight: "40px", maxHeight: "40px" }} - quantity={item.quantity} - className={"module-table"} - onClick={() => { this.showFlyout(item.id) }}> - <EuiToolTip position="top" content={tooltipContent} anchorClassName={toolTipAnchorClass}> - <span style={{ - display: "block", - overflow: "hidden", - whiteSpace: "nowrap", - textOverflow: "ellipsis" - }}> - {item.id} - {this.props.descriptions[item.id]} - </span> - </EuiToolTip> - - {this.state.hover === item.id && - <span style={{ float: "right", position: 'fixed' }}> - <EuiToolTip position="top" content={"Show " + item.id + " in Dashboard"} > - <EuiIcon onMouseDown={(e) => { this.openDashboard(e, item.id); e.stopPropagation() }} color="primary" type="visualizeApp"></EuiIcon> - </EuiToolTip>   - <EuiToolTip position="top" content={"Inspect " + item.id + " in Events"} > - <EuiIcon onMouseDown={(e) => { this.openDiscover(e, item.id); e.stopPropagation() }} color="primary" type="discoverApp"></EuiIcon> - </EuiToolTip> - - </span> - } - </EuiFacetButton> - ) - } - isOpen={this.state.actionsOpen === item.id} - closePopover={() => { }} - panelPaddingSize="none" - style={{ width: "100%" }} - anchorPosition="downLeft">xxx - </EuiPopover> - </EuiFlexItem> - ); - - }) + const tacticsToRenderOrdered = tacticsToRender + .sort((a, b) => b.quantity - a.quantity) + .map((item, idx) => { + const tooltipContent = `View details of ${item.id}`; + const toolTipAnchorClass = + 'wz-display-inline-grid' + (this.state.hover === item.id ? ' wz-mitre-width' : ' '); + return ( + <EuiFlexItem + onMouseEnter={() => this.setState({ hover: item.id })} + onMouseLeave={() => this.setState({ hover: '' })} + key={idx} + style={{ border: '1px solid #8080804a', maxWidth: 'calc(25% - 8px)', maxHeight: 41 }} + > + <EuiPopover + id="techniqueActionsContextMenu" + anchorClassName="wz-width-100" + button={ + <EuiFacetButton + style={{ + width: '100%', + padding: '0 5px 0 5px', + lineHeight: '40px', + maxHeight: '40px', + }} + quantity={item.quantity} + className={'module-table'} + onClick={() => { + this.showFlyout(item.id); + }} + > + <EuiToolTip + position="top" + content={tooltipContent} + anchorClassName={toolTipAnchorClass} + > + <span + style={{ + display: 'block', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {item.id} - {this.props.descriptions[item.id]} + </span> + </EuiToolTip> + + {this.state.hover === item.id && ( + <span style={{ float: 'right', position: 'fixed' }}> + <EuiToolTip position="top" content={'Show ' + item.id + ' in Dashboard'}> + <EuiIcon + onMouseDown={(e) => { + this.openDashboard(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="visualizeApp" + ></EuiIcon> + </EuiToolTip>{' '} +   + <EuiToolTip position="top" content={'Inspect ' + item.id + ' in Events'}> + <EuiIcon + onMouseDown={(e) => { + this.openDiscover(e, item.id); + e.stopPropagation(); + }} + color="primary" + type="discoverApp" + ></EuiIcon> + </EuiToolTip> + </span> + )} + </EuiFacetButton> + } + isOpen={this.state.actionsOpen === item.id} + closePopover={() => {}} + panelPaddingSize="none" + style={{ width: '100%' }} + anchorPosition="downLeft" + > + xxx + </EuiPopover> + </EuiFlexItem> + ); + }); if (tacticsToRender.length) { return ( - <EuiFlexGrid columns={4} gutterSize="s" style={{ maxHeight: "calc(100vh - 420px)", overflow: "overlay", overflowX: "hidden", maxWidth: "82vw", paddingRight: 10 }}> + <EuiFlexGrid + columns={4} + gutterSize="s" + style={{ + maxHeight: 'calc(100vh - 420px)', + overflow: 'overlay', + overflowX: 'hidden', + maxWidth: '82vw', + paddingRight: 10, + }} + > {tacticsToRenderOrdered} </EuiFlexGrid> - ) + ); } else { - return <EuiCallOut title='There are no results.' iconType='help' color='warning'></EuiCallOut> + return ( + <EuiCallOut title="There are no results." iconType="help" color="warning"></EuiCallOut> + ); } } - onChangeFlyout = (flyoutOn) => { this.setState({ flyoutOn }); - } + }; closeFlyout() { this.setState({ flyoutOn: false }); @@ -225,13 +281,10 @@ export class ComplianceSubrequirements extends Component { showFlyout(requirement) { this.setState({ selectedRequirement: requirement, - flyoutOn: true - }) + flyoutOn: true, + }); } - - - render() { return ( <div style={{ padding: 10 }}> @@ -250,7 +303,7 @@ export class ComplianceSubrequirements extends Component { <EuiSwitch label="" checked={this.state.hideAlerts} - onChange={e => this.hideAlerts()} + onChange={(e) => this.hideAlerts()} /> </EuiText> </EuiFlexItem> @@ -263,37 +316,47 @@ export class ComplianceSubrequirements extends Component { fullWidth={true} placeholder="Filter requirements" value={this.state.searchValue} - onChange={e => this.onSearchValueChange(e)} + onChange={(e) => this.onSearchValueChange(e)} isClearable={true} aria-label="Use aria labels when no actual label is in use" /> <EuiSpacer size="s" /> <div> - {this.props.loadingAlerts - ? <EuiFlexItem style={{ height: "calc(100vh - 410px)", alignItems: 'center' }} > - <EuiLoadingSpinner size='xl' style={{ margin: 0, position: 'absolute', top: '50%', transform: 'translateY(-50%)' }} /> - </EuiFlexItem> - : this.props.requirementsCount && this.renderFacet() - } + {this.props.loadingAlerts ? ( + <EuiFlexItem style={{ height: 'calc(100vh - 410px)', alignItems: 'center' }}> + <EuiLoadingSpinner + size="xl" + style={{ + margin: 0, + position: 'absolute', + top: '50%', + transform: 'translateY(-50%)', + }} + /> + </EuiFlexItem> + ) : ( + this.props.requirementsCount && this.renderFacet() + )} </div> - {this.state.flyoutOn && + {this.state.flyoutOn && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.closeFlyout()}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <RequirementFlyout - currentRequirement={this.state.selectedRequirement} - onChangeFlyout={this.onChangeFlyout} - description={this.props.descriptions[this.state.selectedRequirement]} - getRequirementKey={() => { return this.getRequirementKey() }} - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - /> - </div> - </EuiOutsideClickDetector> - </EuiOverlayMask>} + <div> + <RequirementFlyout + currentRequirement={this.state.selectedRequirement} + onChangeFlyout={this.onChangeFlyout} + description={this.props.descriptions[this.state.selectedRequirement]} + getRequirementKey={() => { + return this.getRequirementKey(); + }} + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + /> + </div> + </EuiOverlayMask> + )} </div> - ) + ); } } diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 2f44914782..c2658fb4d5 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -394,6 +394,7 @@ export class FlyoutTechnique extends Component { size="l" className="flyout-no-overlap wz-inventory wzApp" aria-labelledby="flyoutSmallTitle" + outsideClickCloses={true} > {techniqueData && this.renderHeader()} {this.renderBody()} diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 2d5205ed9b..0dd3849855 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -42,7 +42,7 @@ import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; -const MITRE_ATTACK = 'mitre-attack' +const MITRE_ATTACK = 'mitre-attack'; export const Techniques = withWindowSize( class Techniques extends Component { @@ -464,9 +464,7 @@ export const Techniques = withWindowSize( }, }); const filteredTechniques = (((response || {}).data || {}).data.affected_items || []).map( - (item) => - [item].filter((reference) => reference.source === MITRE_ATTACK)[0] - .external_id + (item) => [item].filter((reference) => reference.source === MITRE_ATTACK)[0].external_id ); this._isMount && this.setState({ filteredTechniques, isSearching: false }); } else { @@ -553,24 +551,25 @@ export const Techniques = withWindowSize( <div>{this.renderFacet()}</div> - { isFlyoutVisible && + {isFlyoutVisible && ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => this.onChangeFlyout(false)}> - <div>{/* EuiOutsideClickDetector needs a static first child */} - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} - onChangeFlyout={this.onChangeFlyout} - currentTechniqueData={this.state.currentTechniqueData} - currentTechnique={currentTechnique} - tacticsObject={this.props.tacticsObject} /> - </div> - </EuiOutsideClickDetector> + <div> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + openIntelligence={(e, redirectTo, itemId) => + this.openIntelligence(e, redirectTo, itemId) + } + onChangeFlyout={this.onChangeFlyout} + currentTechniqueData={this.state.currentTechniqueData} + currentTechnique={currentTechnique} + tacticsObject={this.props.tacticsObject} + /> + </div> </EuiOverlayMask> - } + )} </div> ); } } -); \ No newline at end of file +); diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx index 13836f25d0..c12d7a2e95 100644 --- a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx @@ -30,74 +30,74 @@ import { import { Markdown } from '../../common/util'; interface DetailFlyoutType { - details: any, - closeFlyout: () => {onClick: () => void}, - onSelectResource: (resource: any) => void, + details: any; + closeFlyout: () => { onClick: () => void }; + onSelectResource: (resource: any) => void; } -export const ModuleMitreAttackIntelligenceFlyout = ({details, closeFlyout, onSelectResource}: DetailFlyoutType) => { +export const ModuleMitreAttackIntelligenceFlyout = ({ + details, + closeFlyout, + onSelectResource, +}: DetailFlyoutType) => { const startReference = useRef(null); return ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={closeFlyout}> - <EuiFlyout - onClose={closeFlyout} - size="l" - aria-labelledby={``} - > - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="flyoutTitle">Details</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <div ref={startReference}> - <EuiFlexGroup> - {MitreAttackResources[0].mitreFlyoutHeaderProperties.map(detailProperty => ( - <EuiFlexItem key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`}> - <div> - <strong> - {detailProperty.label} - </strong> - </div> - <EuiText color='subdued'> - {detailProperty.render ? detailProperty.render(details[detailProperty.id]) : details[detailProperty.id]} - </EuiText> - </EuiFlexItem> - ))} - </EuiFlexGroup> - </div> + <EuiFlyout onClose={closeFlyout} size="l" aria-labelledby={``} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="flyoutTitle">Details</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <div ref={startReference}> <EuiFlexGroup> - <EuiFlexItem> - <div> - <strong> - Description - </strong> - </div> - <EuiText color='subdued'> - {details.description ? <Markdown markdown={details.description} /> : ''} - </EuiText> - </EuiFlexItem> + {MitreAttackResources[0].mitreFlyoutHeaderProperties.map((detailProperty) => ( + <EuiFlexItem + key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`} + > + <div> + <strong>{detailProperty.label}</strong> + </div> + <EuiText color="subdued"> + {detailProperty.render + ? detailProperty.render(details[detailProperty.id]) + : details[detailProperty.id]} + </EuiText> + </EuiFlexItem> + ))} </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - {MitreAttackResources.filter((item) => details[item.id]).map((item) => - <Fragment key={`resource_${item.id}`}> - <ReferencesTable - referencesName={item.id} - referencesArray={details[item.id]} - columns={item.tableColumnsCreator(onSelectResource)} - backToTop={() => { startReference.current?.scrollIntoView() }} - /> - <EuiSpacer /> - </Fragment> - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + </div> + <EuiFlexGroup> + <EuiFlexItem> + <div> + <strong>Description</strong> + </div> + <EuiText color="subdued"> + {details.description ? <Markdown markdown={details.description} /> : ''} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + {MitreAttackResources.filter((item) => details[item.id]).map((item) => ( + <Fragment key={`resource_${item.id}`}> + <ReferencesTable + referencesName={item.id} + referencesArray={details[item.id]} + columns={item.tableColumnsCreator(onSelectResource)} + backToTop={() => { + startReference.current?.scrollIntoView(); + }} + /> + <EuiSpacer /> + </Fragment> + ))} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> - ) + ); }; diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 81faed2422..3f64378012 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -287,150 +287,156 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { }, []); useEffect(() => { - if (policyName.length || actionValue.length || addedActions.length || addedResources.length || effectValue) { + if ( + policyName.length || + actionValue.length || + addedActions.length || + addedResources.length || + effectValue + ) { setHasChanges(true); } else { setHasChanges(false); } }, [policyName, actionValue, addedActions, addedResources, effectValue]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New policy</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - value={policyName} - onChange={(e) => onChangePolicyName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New policy</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + value={policyName} + onChange={(e) => onChangePolicyName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." + > + <EuiSuperSelect + options={actions} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue} > - <EuiSuperSelect - options={actions} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue} - > - Add + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." - > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." + > + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue} > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue} - > - Add + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton - disabled={ - !policyName || !addedActions.length || !addedResources.length || !effectValue - } - onClick={() => { - createPolicy(); - }} - fill - > - Create policy + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton + disabled={ + !policyName || !addedActions.length || !addedResources.length || !effectValue + } + onClick={() => { + createPolicy(); + }} + fill + > + Create policy </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index c126d4fac7..648e5d878a 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -157,7 +157,7 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { setInitialAddedResources(initResources); setEffectValue(policy.policy.effect); - setInitialEffectValue(policy.policy.effect) + setInitialEffectValue(policy.policy.effect); }; const onEffectValueChange = (value) => { @@ -289,152 +289,156 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { ); } useEffect(() => { - if (initialActionValue != actionValue || !_.isEqual(addedResources, initialAddedResources) || - !_.isEqual(addedActions, initialAddedActions) || initialResourceValue != resourceValue || - initialEffectValue != effectValue) { + if ( + initialActionValue != actionValue || + !_.isEqual(addedResources, initialAddedResources) || + !_.isEqual(addedActions, initialAddedActions) || + initialResourceValue != resourceValue || + initialEffectValue != effectValue + ) { setHasChanges(true); } else { setHasChanges(false); } }, [actionValue, addedResources, addedActions, resourceValue, effectValue]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit policy {policy.name}   + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit policy {policy.name}   {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - disabled={isReserved} - value={policy.name} - readOnly={true} - onChange={() => { }} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + disabled={isReserved} + value={policy.name} + readOnly={true} + onChange={() => {}} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." + > + <EuiSuperSelect + options={actions} + disabled={isReserved} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue || isReserved} > - <EuiSuperSelect - options={actions} - disabled={isReserved} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue || isReserved} - > - Add + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." - > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." + > + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue || isReserved} > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue || isReserved} - > - Add + Add </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton disabled={isReserved} onClick={updatePolicy} fill> - Apply + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton disabled={isReserved} onClick={updatePolicy} fill> + Apply </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> ); -}; \ No newline at end of file +}; diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index f8cf095ccb..1f916b6a55 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -113,70 +113,70 @@ export const RolesMappingCreate = ({ } }, [selectedRoles, ruleName, hasChangeMappingRules]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>Create new role mapping  </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role mapping name" - isInvalid={false} - error={'Please provide a role mapping name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="Role name" - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList()} - isDisabled={false} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => createRule(rule)} - initialRule={false} - isReserved={false} - isLoading={isLoading} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => { - setHasChangeMappingRules(hasChange); - }} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>Create new role mapping  </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role mapping name" + isInvalid={false} + error={'Please provide a role mapping name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="Role name" + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList()} + isDisabled={false} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => createRule(rule)} + initialRule={false} + isReserved={false} + isLoading={isLoading} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => { + setHasChangeMappingRules(hasChange); + }} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index 5ea23e985f..27f54cf05c 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -135,73 +135,73 @@ export const RolesMappingEdit = ({ } }, [selectedRoles, ruleName, hasChangeMappingRules]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit <strong>{rule.name}  </strong> - {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={false} - error={'Please provide a role name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="" - disabled={WzAPIUtils.isReservedID(rule.id)} - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList(roles)} - isDisabled={WzAPIUtils.isReservedID(rule.id)} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => editRule(rule)} - initialRule={rule.rule} - isLoading={isLoading} - isReserved={WzAPIUtils.isReservedID(rule.id)} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit <strong>{rule.name}  </strong> + {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={false} + error={'Please provide a role name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="" + disabled={WzAPIUtils.isReservedID(rule.id)} + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList(roles)} + isDisabled={WzAPIUtils.isReservedID(rule.id)} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => editRule(rule)} + initialRule={rule.rule} + isLoading={isLoading} + isReserved={WzAPIUtils.isReservedID(rule.id)} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index 3d3d6251e6..b1f602fd2e 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -1,25 +1,23 @@ import React, { useState, useEffect } from 'react'; import { - EuiButton, - EuiTitle, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiForm, - EuiFieldText, - EuiOverlayMask, - EuiOutsideClickDetector, - EuiFormRow, - EuiSpacer, - EuiComboBox, - EuiConfirmModal + EuiButton, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiForm, + EuiFieldText, + EuiOverlayMask, + EuiOutsideClickDetector, + EuiFormRow, + EuiSpacer, + EuiComboBox, + EuiConfirmModal, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; -import { WzOverlayMask } from '../../common/util' - - +import { WzOverlayMask } from '../../common/util'; export const CreateRole = ({ closeFlyout }) => { const [policies, setPolicies] = useState([]); @@ -131,58 +129,58 @@ export const CreateRole = ({ closeFlyout }) => { } }, [selectedPolicies, roleName]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New role</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={roleNameError} - error={'Please provide a role name'} - helpText="Introduce a name for this new role." - > - <EuiFieldText - placeholder="" - value={roleName} - onChange={(e) => onChangeRoleName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton fill onClick={createUser}> - Create role + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New role</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={roleNameError} + error={'Please provide a role name'} + helpText="Introduce a name for this new role." + > + <EuiFieldText + placeholder="" + value={roleName} + onChange={(e) => onChangeRoleName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton fill onClick={createUser}> + Create role </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> ); -}; \ No newline at end of file +}; diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index b9d59f66da..62f4223592 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -1,21 +1,21 @@ import React, { useState, useEffect } from 'react'; import { - EuiButton, - EuiTitle, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiForm, - EuiFieldText, - EuiFormRow, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiBadge, - EuiComboBox, - EuiOverlayMask, - EuiOutsideClickDetector, - EuiConfirmModal + EuiButton, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiForm, + EuiFieldText, + EuiFormRow, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiComboBox, + EuiOverlayMask, + EuiOutsideClickDetector, + EuiConfirmModal, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; @@ -25,7 +25,15 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -const reservedRoles = ['administrator', 'readonly', 'users_admin', 'agents_readonly', 'agents_admin', 'cluster_readonly', 'cluster_admin']; +const reservedRoles = [ + 'administrator', + 'readonly', + 'users_admin', + 'agents_readonly', + 'agents_admin', + 'cluster_readonly', + 'cluster_admin', +]; export const EditRole = ({ role, closeFlyout }) => { const [isLoading, setIsLoading] = useState(true); @@ -147,68 +155,70 @@ export const EditRole = ({ role, closeFlyout }) => { ); } - const onClose = () => { (initialSelectedPolicies.length != selectedPolicies.length) ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + initialSelectedPolicies.length != selectedPolicies.length + ? setIsModalVisible(true) + : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose} > - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {role.name} role   + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {role.name} role   {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} - isDisabled={isReserved} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiButton - style={{ marginTop: 20, maxWidth: 45 }} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} isDisabled={isReserved} - fill - onClick={addPolicy} - > - Add policy + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiButton + style={{ marginTop: 20, maxWidth: 45 }} + isDisabled={isReserved} + fill + onClick={addPolicy} + > + Add policy </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> - <EuiSpacer /> - </EuiForm> - <div style={{ margin: 20 }}> - <EditRolesTable - policies={assignedPolicies} - role={currentRole} - onChange={update} - isDisabled={isReserved} - loading={isLoading} - /> - </div> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + <EuiSpacer /> + </EuiForm> + <div style={{ margin: 20 }}> + <EditRolesTable + policies={assignedPolicies} + role={currentRole} + onChange={update} + isDisabled={isReserved} + loading={isLoading} + /> + </div> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index 3dced60449..f6675748cb 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -243,8 +243,7 @@ export const CreateUser = ({ closeFlyout }) => { return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> <EuiFlyoutHeader hasBorder={false}> <EuiTitle size="m"> <h2>Create new user</h2> @@ -339,7 +338,6 @@ export const CreateUser = ({ closeFlyout }) => { </EuiForm> </EuiFlyoutBody> </EuiFlyout> - </EuiOutsideClickDetector> </EuiOverlayMask> {modal} </> diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 67edebfd98..7f7da6c7b2 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -222,8 +222,10 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { useEffect(() => { if ( - initialPassword != password || initialPassword != confirmPassword || - !_.isEqual(userRolesFormatted, selectedRoles) || allowRunAs + initialPassword != password || + initialPassword != confirmPassword || + !_.isEqual(userRolesFormatted, selectedRoles) || + allowRunAs ) { setHasChanges(true); } else { @@ -231,114 +233,114 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { } }, [selectedRoles, password, confirmPassword, allowRunAs]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={onClose}> - <EuiFlyout className="wzApp" onClose={onClose}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {currentUser.username} user     + <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {currentUser.username} user     {WzAPIUtils.isReservedID(currentUser.id) && ( - <EuiBadge color="primary">Reserved</EuiBadge> - )} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiPanel> - <EuiTitle size="s"> - <h2>Run as</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Set if the user is able to use run as"> - <WzButtonPermissions - buttonType="switch" - label="Allow run as" - showLabel={true} - checked={allowRunAs} - permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} - onChange={(e) => onChangeAllowRunAs(e)} - aria-label="" - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Password</h2> - </EuiTitle> - <EuiFormRow - label="" + <EuiBadge color="primary">Reserved</EuiBadge> + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiPanel> + <EuiTitle size="s"> + <h2>Run as</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Set if the user is able to use run as"> + <WzButtonPermissions + buttonType="switch" + label="Allow run as" + showLabel={true} + checked={allowRunAs} + permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} + onChange={(e) => onChangeAllowRunAs(e)} + aria-label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Password</h2> + </EuiTitle> + <EuiFormRow + label="" + isInvalid={!!formErrors.password} + error={formErrors.password} + helpText="Introduce a new password for the user." + > + <EuiFieldPassword + placeholder="Password" + value={password} + onChange={(e) => onChangePassword(e)} + aria-label="" isInvalid={!!formErrors.password} - error={formErrors.password} - helpText="Introduce a new password for the user." - > - <EuiFieldPassword - placeholder="Password" - value={password} - onChange={(e) => onChangePassword(e)} - aria-label="" - isInvalid={!!formErrors.password} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - <EuiFormRow - label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + <EuiFormRow + label="" + isInvalid={!!formErrors.confirmPassword} + error={formErrors.confirmPassword} + helpText="Confirm the new password." + > + <EuiFieldPassword + placeholder="Confirm Password" + value={confirmPassword} + onChange={(e) => onChangeConfirmPassword(e)} + aria-label="" isInvalid={!!formErrors.confirmPassword} - error={formErrors.confirmPassword} - helpText="Confirm the new password." - > - <EuiFieldPassword - placeholder="Confirm Password" - value={confirmPassword} - onChange={(e) => onChangeConfirmPassword(e)} - aria-label="" - isInvalid={!!formErrors.confirmPassword} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Roles</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Assign roles to the selected user"> - <EuiComboBox - placeholder="Select roles" - options={rolesOptions} - selectedOptions={selectedRoles} - isLoading={rolesLoading || isLoading} - onChange={onChangeRoles} - isClearable={true} - data-test-subj="demoComboBox" - isDisabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Roles</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Assign roles to the selected user"> + <EuiComboBox + placeholder="Select roles" + options={rolesOptions} + selectedOptions={selectedRoles} + isLoading={rolesLoading || isLoading} + onChange={onChangeRoles} + isClearable={true} + data-test-subj="demoComboBox" + isDisabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton - fill - isLoading={isLoading} - isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} - onClick={editUser} - > - Apply + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + fill + isLoading={isLoading} + isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} + onClick={editUser} + > + Apply </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> {modal} </> diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index 8893e75219..264053b49e 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -28,7 +28,11 @@ import { EuiOutsideClickDetector, } from '@elastic/eui'; import { WzRequest } from '../../../react-services'; -import { withErrorBoundary, withReduxProvider, withUserAuthorizationPrompt } from '../../../components/common/hocs'; +import { + withErrorBoundary, + withReduxProvider, + withUserAuthorizationPrompt, +} from '../../../components/common/hocs'; import { compose } from 'redux'; import { useDispatch, useSelector } from 'react-redux'; import { updateLogtestToken } from '../../../redux/actions/appStateActions'; @@ -65,18 +69,20 @@ export const Logtest = compose( }; const formatResult = (result, alert) => { - let returnedDataFormatted =`**Phase 1: Completed pre-decoding. \n ` + - `full event: ${result.full_log || '-'} \n ` + - `timestamp: ${(result.predecoder || '').timestamp || '-'} \n ` + - `hostname: ${(result.predecoder || '').hostname || '-'} \n ` + - `program_name: ${(result.predecoder || '').program_name || '-'} \n\n` + - `**Phase 2: Completed decoding. \n ` + - `name: ${(result.decoder || '').name || '-'} \n ` + - `${(result.decoder || '').parent ? `parent: ${(result.decoder || '').parent} \n ` : ''}` + - `data: ${JSON.stringify(result.data || '-', null, 6).replace('}', ' }')} \n\n` ; - - result.rule && ( - returnedDataFormatted += `**Phase 3: Completed filtering (rules). \n ` + + let returnedDataFormatted = + `**Phase 1: Completed pre-decoding. \n ` + + `full event: ${result.full_log || '-'} \n ` + + `timestamp: ${(result.predecoder || '').timestamp || '-'} \n ` + + `hostname: ${(result.predecoder || '').hostname || '-'} \n ` + + `program_name: ${(result.predecoder || '').program_name || '-'} \n\n` + + `**Phase 2: Completed decoding. \n ` + + `name: ${(result.decoder || '').name || '-'} \n ` + + `${(result.decoder || '').parent ? `parent: ${(result.decoder || '').parent} \n ` : ''}` + + `data: ${JSON.stringify(result.data || '-', null, 6).replace('}', ' }')} \n\n`; + + result.rule && + (returnedDataFormatted += + `**Phase 3: Completed filtering (rules). \n ` + `id: ${(result.rule || '').id || '-'} \n ` + `level: ${(result.rule || '').level || '-'} \n ` + `description: ${(result.rule || '').description || '-'} \n ` + @@ -87,16 +93,15 @@ export const Logtest = compose( `hipaa: ${JSON.stringify((result.rule || '').hipaa || '-')} \n ` + `mail: ${JSON.stringify((result.rule || '').mail || '-')} \n ` + `mitre.id: ${JSON.stringify((result.rule || '').mitre || ''.id || '-')} \n ` + - `mitre.technique: ${JSON.stringify((result.rule || '').mitre || ''.technique || '-')} \n ` + + `mitre.technique: ${JSON.stringify( + (result.rule || '').mitre || ''.technique || '-' + )} \n ` + `nist_800_53: ${JSON.stringify((result.rule || '').nist_800_53 || '-')} \n ` + `pci_dss: ${JSON.stringify((result.rule || '').pci_dss || '-')} \n ` + - `tsc: ${JSON.stringify((result.rule || '').tsc || '-')} \n` - ); + `tsc: ${JSON.stringify((result.rule || '').tsc || '-')} \n`); - returnedDataFormatted += `${alert ? `**Alert to be generated. \n\n\n` : '\n\n'}` - return ( - returnedDataFormatted - ); + returnedDataFormatted += `${alert ? `**Alert to be generated. \n\n\n` : '\n\n'}`; + return returnedDataFormatted; }; const runAllTests = async () => { @@ -107,23 +112,23 @@ export const Logtest = compose( let gotToken = Boolean(token); try { - for (let event of events) { + for (let event of events) { const response = await WzRequest.apiReq('PUT', '/logtest', { log_format: 'syslog', location: 'logtest', event, ...(token ? { token } : {}), }); - + token = response.data.data.token; !sessionToken && !gotToken && token && dispatch(updateLogtestToken(token)); token && (gotToken = true); responses.push(response); - }; + } const testResults = responses.map((response) => { - return response.data.data.output || '' - ? formatResult(response.data.data.output, response.data.data.alert) - : `No result found for: ${response.data.data.output.full_log} \n\n\n` + return response.data.data.output || '' + ? formatResult(response.data.data.output, response.data.data.alert) + : `No result found for: ${response.data.data.output.full_log} \n\n\n`; }); setTestResult(testResults); } finally { @@ -252,24 +257,24 @@ export const Logtest = compose( </EuiPage> )) || ( <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={() => { - props.openCloseFlyout(); - }}> - <EuiFlyout className="wzApp" onClose={() => props.openCloseFlyout()}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - {props.isRuleset.includes('rules') ? <h2>Ruleset Test</h2> : <h2>Decoders Test</h2>} - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody style={{ margin: '20px' }}> - <EuiFlexGroup gutterSize="m"> - <EuiFlexItem /> - </EuiFlexGroup> - <EuiSpacer size="s" /> - {buildLogtest()} - </EuiFlyoutBody> - </EuiFlyout> - </EuiOutsideClickDetector> + <EuiFlyout + className="wzApp" + onClose={() => props.openCloseFlyout()} + outsideClickCloses={true} + > + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + {props.isRuleset.includes('rules') ? <h2>Ruleset Test</h2> : <h2>Decoders Test</h2>} + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody style={{ margin: '20px' }}> + <EuiFlexGroup gutterSize="m"> + <EuiFlexItem /> + </EuiFlexGroup> + <EuiSpacer size="s" /> + {buildLogtest()} + </EuiFlyoutBody> + </EuiFlyout> </EuiOverlayMask> )} </Fragment> From 049c96967b2cc66bc93d91a50bf9b4facfc8592a Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Fri, 3 Dec 2021 08:53:23 +0100 Subject: [PATCH 348/493] Adding Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970ef40d69..1189396ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error that shows we're using X-Pack when we have Basic [#3692](https://github.com/wazuh/wazuh-kibana-app/pull/3692) - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) +- Fixing Flyouts in Kibana 7.14.2 [#3708](https://github.com/wazuh/wazuh-kibana-app/pull/3708) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 81cdbd0ef19f0eb8347435707e2ed33259202ea0 Mon Sep 17 00:00:00 2001 From: CPAlejandro <cuellarpeinado@gmail.com> Date: Fri, 3 Dec 2021 12:44:36 +0100 Subject: [PATCH 349/493] Creating a Flyout component to control all flyouts --- .../agents/fim/inventory/flyout.tsx | 16 +- .../agents/fim/inventory/registry-table.tsx | 24 +- .../components/agents/fim/inventory/table.tsx | 26 +- .../agents/vuls/inventory/table.tsx | 27 +- public/components/common/flyouts/index.ts | 13 + .../components/common/flyouts/wz-flyout.tsx | 33 +++ .../common/modules/discover/discover.tsx | 16 +- public/components/common/modules/events.tsx | 15 +- .../fim_events_table/fim_events_table.tsx | 18 +- .../components/mitre_top/mitre_top.tsx | 20 +- .../requirement-flyout/requirement-flyout.tsx | 16 +- .../subrequirements/subrequirements.tsx | 24 +- .../flyout-technique/flyout-technique.tsx | 22 +- .../components/techniques/techniques.tsx | 4 - .../resource_detail_flyout.tsx | 107 ++++---- .../security/policies/create-policy.tsx | 259 +++++++++--------- .../security/policies/edit-policy.tsx | 255 +++++++++-------- .../components/roles-mapping-create.tsx | 117 ++++---- .../components/roles-mapping-edit.tsx | 123 +++++---- .../components/security/roles/create-role.tsx | 89 +++--- .../components/security/roles/edit-role.tsx | 109 ++++---- .../security/users/components/create-user.tsx | 202 +++++++------- .../security/users/components/edit-user.tsx | 199 +++++++------- .../wz-logtest/components/logtest.tsx | 35 +-- 24 files changed, 885 insertions(+), 884 deletions(-) create mode 100644 public/components/common/flyouts/index.ts create mode 100644 public/components/common/flyouts/wz-flyout.tsx diff --git a/public/components/agents/fim/inventory/flyout.tsx b/public/components/agents/fim/inventory/flyout.tsx index 1c7f2f1596..b8f82bf976 100644 --- a/public/components/agents/fim/inventory/flyout.tsx +++ b/public/components/agents/fim/inventory/flyout.tsx @@ -30,6 +30,7 @@ import { } from '../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzFlyout } from '../../../common/flyouts'; export class FlyoutDetail extends Component { state: { @@ -121,13 +122,14 @@ export class FlyoutDetail extends Component { render() { const { type } = this.state; return ( - <EuiFlyout + <WzFlyout onClose={() => this.props.closeFlyout()} - size="l" - aria-labelledby={this.state.currentFile.file} - maxWidth="70%" - className="wz-inventory wzApp" - outsideClickCloses = {true} + flyoutProps={{ + size: 'l', + 'aria-labelledby': this.state.currentFile.file, + maxWidth: '70%', + className: 'wz-inventory wzApp', + }} > <EuiFlyoutHeader hasBorder className="flyout-header"> <EuiTitle size="s"> @@ -156,7 +158,7 @@ export class FlyoutDetail extends Component { /> </EuiFlyoutBody> )} - </EuiFlyout> + </WzFlyout> ); } } diff --git a/public/components/agents/fim/inventory/registry-table.tsx b/public/components/agents/fim/inventory/registry-table.tsx index e8e0c84b9b..4aeb2e4448 100644 --- a/public/components/agents/fim/inventory/registry-table.tsx +++ b/public/components/agents/fim/inventory/registry-table.tsx @@ -270,21 +270,15 @@ export class RegistryTable extends Component { <div> {registryTable} {this.state.isFlyoutVisible && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - {/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - fileName={this.state.currentFile.file} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type={this.state.currentFile.type} - view="inventory" - outsideClickCloses={true} - {...this.props} - /> - </div> - </EuiOverlayMask> + <FlyoutDetail + fileName={this.state.currentFile.file} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type={this.state.currentFile.type} + view="inventory" + {...this.props} + /> )} </div> ); diff --git a/public/components/agents/fim/inventory/table.tsx b/public/components/agents/fim/inventory/table.tsx index 78059a8b23..af27d87a93 100644 --- a/public/components/agents/fim/inventory/table.tsx +++ b/public/components/agents/fim/inventory/table.tsx @@ -304,22 +304,16 @@ export class InventoryTable extends Component { <div className="wz-inventory"> {filesTable} {this.state.isFlyoutVisible && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - {/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - fileName={this.state.currentFile} - agentId={this.props.agent.id} - item={this.state.syscheckItem} - closeFlyout={() => this.closeFlyout()} - type="file" - view="inventory" - showViewInEvents={true} - outsideClickCloses={true} - {...this.props} - /> - </div> - </EuiOverlayMask> + <FlyoutDetail + fileName={this.state.currentFile} + agentId={this.props.agent.id} + item={this.state.syscheckItem} + closeFlyout={() => this.closeFlyout()} + type="file" + view="inventory" + showViewInEvents={true} + {...this.props} + /> )} </div> ); diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 1613ba41c5..163a4ec180 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -241,22 +241,17 @@ export class InventoryTable extends Component { <div className="wz-inventory"> {table} {this.state.isFlyoutVisible && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - {/* EuiOutsideClickDetector needs a static first child */} - <FlyoutDetail - vulName={this.state.currentItem.cve} - agentId={this.props.agent.id} - item={this.state.currentItem} - closeFlyout={() => this.closeFlyout()} - type="vulnerability" - view="inventory" - showViewInEvents={true} - outsideClickCloses={true} - {...this.props} - /> - </div> - </EuiOverlayMask> + <FlyoutDetail + vulName={this.state.currentItem.cve} + agentId={this.props.agent.id} + item={this.state.currentItem} + closeFlyout={() => this.closeFlyout()} + type="vulnerability" + view="inventory" + showViewInEvents={true} + outsideClickCloses={true} + {...this.props} + /> )} </div> ); diff --git a/public/components/common/flyouts/index.ts b/public/components/common/flyouts/index.ts new file mode 100644 index 0000000000..b9ae3347c0 --- /dev/null +++ b/public/components/common/flyouts/index.ts @@ -0,0 +1,13 @@ +/* + * Wazuh app - Index of Wazuh buttons + * Copyright (C) 2015-2021 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 {WzFlyout} from './wz-flyout'; \ No newline at end of file diff --git a/public/components/common/flyouts/wz-flyout.tsx b/public/components/common/flyouts/wz-flyout.tsx new file mode 100644 index 0000000000..fa98c27a45 --- /dev/null +++ b/public/components/common/flyouts/wz-flyout.tsx @@ -0,0 +1,33 @@ +/* + * Wazuh app - Index of Wazuh buttons + * Copyright (C) 2015-2021 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 { EuiFlyout, EuiOverlayMask, EuiOutsideClickDetector } from '@elastic/eui'; +import { satisfyKibanaVersion } from '../../../../common/semver'; + +export const WzFlyout = ({children, flyoutProps = {}, overlayMaskProps = {}, outsideClickDetectorProps = {}, onClose}) => ( + <EuiOverlayMask headerZindexLocation="below" {...overlayMaskProps}> + <EuiOutsideClickDetector + onOutsideClick={onClose} + isDisabled={satisfyKibanaVersion('>7.10')} + {...outsideClickDetectorProps} + > + <EuiFlyout + onClose={onClose} + {...(satisfyKibanaVersion('>7.10') ? { outsideClickCloses: true } : {})} + {...flyoutProps} + > + {children} + </EuiFlyout> + </EuiOutsideClickDetector> + </EuiOverlayMask> +); diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index b9cf7f99a6..5c1cef2f0f 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -759,16 +759,12 @@ export const Discover = compose( }; const noResultsText = `No results match for this search criteria`; let flyout = this.state.showMitreFlyout ? ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} - /> - </div> - </EuiOverlayMask> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + onChangeFlyout={this.onMitreChangeFlyout} + currentTechnique={this.state.selectedTechnique} + /> ) : ( <></> ); diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 710e8eefc3..2a83f56fd1 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -359,16 +359,11 @@ export const Events = compose( return ( <Fragment> {flyout && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - <FlyoutComponent - closeFlyout={this.closeFlyout} - outsideClickCloses={true} - {...this.state.flyout.props} - {...this.props} - /> - </div> - </EuiOverlayMask> + <FlyoutComponent + closeFlyout={this.closeFlyout} + {...this.state.flyout.props} + {...this.props} + /> )} </Fragment> ); diff --git a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx index ee7d1c69e5..c480df2ddf 100644 --- a/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx +++ b/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx @@ -99,17 +99,13 @@ function FimTable({ agent }) { noItemsMessage="No recent events" /> {isOpen && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - <FlyoutDetail - agentId={agent.id} - closeFlyout={() => setIsOpen(false)} - fileName={file} - view="extern" - {...{ agent }} - /> - </div> - </EuiOverlayMask> + <FlyoutDetail + agentId={agent.id} + closeFlyout={() => setIsOpen(false)} + fileName={file} + view="extern" + {...{ agent }} + /> )} </Fragment> ); diff --git a/public/components/common/welcome/components/mitre_top/mitre_top.tsx b/public/components/common/welcome/components/mitre_top/mitre_top.tsx index 395474a077..c71ea72da5 100644 --- a/public/components/common/welcome/components/mitre_top/mitre_top.tsx +++ b/public/components/common/welcome/components/mitre_top/mitre_top.tsx @@ -252,18 +252,14 @@ export class MitreTopTactics extends Component { {!selectedTactic || alertsCount.length === 0 ? tacticsTop : tecniquesTop} {alertsCount.length === 0 && emptyPrompt} {flyoutOn && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - implicitFilters={[{ 'agent.id': this.props.agentId }]} - agentId={this.props.agentId} - onChangeFlyout={this.onChangeFlyout} - currentTechnique={selectedTechnique} - /> - </div> - </EuiOverlayMask> + <FlyoutTechnique + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + implicitFilters={[{ 'agent.id': this.props.agentId }]} + agentId={this.props.agentId} + onChangeFlyout={this.onChangeFlyout} + currentTechnique={selectedTechnique} + /> )} </Fragment> ); diff --git a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx index 07f067c5c6..93ecddd453 100644 --- a/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx +++ b/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx @@ -32,6 +32,7 @@ import { AppState } from '../../../../../react-services/app-state'; import { requirementGoal } from '../../requirement-goal'; import { getUiSettings } from '../../../../../kibana-services'; import { FilterManager } from '../../../../../../../../src/plugins/data/public/'; +import { WzFlyout } from '../../../../../components/common/flyouts'; export class RequirementFlyout extends Component { _isMount = false; @@ -234,18 +235,19 @@ export class RequirementFlyout extends Component { const { currentRequirement } = this.props; const { onChangeFlyout } = this.props; return ( - <EuiFlyout + <WzFlyout onClose={() => onChangeFlyout(false)} - maxWidth="60%" - size="l" - className="flyout-no-overlap wz-inventory wzApp" - aria-labelledby="flyoutSmallTitle" - outsideClickCloses={true} + flyoutProps={{ + maxWidth: '60%', + size: 'l', + className: 'flyout-no-overlap wz-inventory wzApp', + 'aria-labelledby': 'flyoutSmallTitle', + }} > {currentRequirement && this.renderHeader()} {this.renderBody()} {this.state.loading && this.renderLoading()} - </EuiFlyout> + </WzFlyout> ); } } diff --git a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index eb42238880..4ca018bfdf 100644 --- a/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -341,20 +341,16 @@ export class ComplianceSubrequirements extends Component { </div> {this.state.flyoutOn && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> - <RequirementFlyout - currentRequirement={this.state.selectedRequirement} - onChangeFlyout={this.onChangeFlyout} - description={this.props.descriptions[this.state.selectedRequirement]} - getRequirementKey={() => { - return this.getRequirementKey(); - }} - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - /> - </div> - </EuiOverlayMask> + <RequirementFlyout + currentRequirement={this.state.selectedRequirement} + onChangeFlyout={this.onChangeFlyout} + description={this.props.descriptions[this.state.selectedRequirement]} + getRequirementKey={() => { + return this.getRequirementKey(); + }} + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + /> )} </div> ); diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index c2658fb4d5..2db7181b77 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -44,6 +44,7 @@ import { FilterManager } from '../../../../../../../../../../src/plugins/data/pu import { UI_LOGGER_LEVELS } from '../../../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../../../react-services/common-services'; +import { WzFlyout } from '../../../../../../../components/common/flyouts'; export class FlyoutTechnique extends Component { _isMount = false; @@ -134,8 +135,8 @@ export class FlyoutTechnique extends Component { const { currentTechnique } = this.props; const result = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `external_id=${currentTechnique}` - } + q: `external_id=${currentTechnique}`, + }, }); const rawData = (((result || {}).data || {}).data || {}).affected_items; !!rawData && this.formatTechniqueData(rawData[0]); @@ -160,8 +161,8 @@ export class FlyoutTechnique extends Component { findTacticName(tactics) { const { tacticsObject } = this.props; return tactics.map((element) => { - const tactic = Object.values(tacticsObject).find(obj => obj.id === element); - return { id:tactic.external_id, name: tactic.name}; + const tactic = Object.values(tacticsObject).find((obj) => obj.id === element); + return { id: tactic.external_id, name: tactic.name }; }); } @@ -389,17 +390,18 @@ export class FlyoutTechnique extends Component { const { techniqueData } = this.state; const { onChangeFlyout } = this.props; return ( - <EuiFlyout + <WzFlyout onClose={() => onChangeFlyout(false)} - size="l" - className="flyout-no-overlap wz-inventory wzApp" - aria-labelledby="flyoutSmallTitle" - outsideClickCloses={true} + flyoutProps={{ + size: 'l', + className: 'flyout-no-overlap wz-inventory wzApp', + 'aria-labelledby': 'flyoutSmallTitle', + }} > {techniqueData && this.renderHeader()} {this.renderBody()} {this.state.loading && this.renderLoading()} - </EuiFlyout> + </WzFlyout> ); } } diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 0dd3849855..9526624a13 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -552,8 +552,6 @@ export const Techniques = withWindowSize( <div>{this.renderFacet()}</div> {isFlyoutVisible && ( - <EuiOverlayMask headerZindexLocation="below"> - <div> <FlyoutTechnique openDashboard={(e, itemId) => this.openDashboard(e, itemId)} openDiscover={(e, itemId) => this.openDiscover(e, itemId)} @@ -565,8 +563,6 @@ export const Techniques = withWindowSize( currentTechnique={currentTechnique} tacticsObject={this.props.tacticsObject} /> - </div> - </EuiOverlayMask> )} </div> ); diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx index c12d7a2e95..609529ec61 100644 --- a/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_flyout.tsx @@ -28,6 +28,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { Markdown } from '../../common/util'; +import { WzFlyout } from '../../common/flyouts'; interface DetailFlyoutType { details: any; @@ -43,61 +44,59 @@ export const ModuleMitreAttackIntelligenceFlyout = ({ const startReference = useRef(null); return ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout onClose={closeFlyout} size="l" aria-labelledby={``} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder> - <EuiTitle size="m"> - <h2 id="flyoutTitle">Details</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <div ref={startReference}> - <EuiFlexGroup> - {MitreAttackResources[0].mitreFlyoutHeaderProperties.map((detailProperty) => ( - <EuiFlexItem - key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`} - > - <div> - <strong>{detailProperty.label}</strong> - </div> - <EuiText color="subdued"> - {detailProperty.render - ? detailProperty.render(details[detailProperty.id]) - : details[detailProperty.id]} - </EuiText> - </EuiFlexItem> - ))} - </EuiFlexGroup> - </div> + <WzFlyout onClose={closeFlyout} flyoutProps={{ size: 'l', 'aria-labelledby': `` }}> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2 id="flyoutTitle">Details</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <div ref={startReference}> <EuiFlexGroup> - <EuiFlexItem> - <div> - <strong>Description</strong> - </div> - <EuiText color="subdued"> - {details.description ? <Markdown markdown={details.description} /> : ''} - </EuiText> - </EuiFlexItem> + {MitreAttackResources[0].mitreFlyoutHeaderProperties.map((detailProperty) => ( + <EuiFlexItem + key={`mitre_att&ck_intelligence_detail_resource_property_${detailProperty.label}`} + > + <div> + <strong>{detailProperty.label}</strong> + </div> + <EuiText color="subdued"> + {detailProperty.render + ? detailProperty.render(details[detailProperty.id]) + : details[detailProperty.id]} + </EuiText> + </EuiFlexItem> + ))} </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem> - {MitreAttackResources.filter((item) => details[item.id]).map((item) => ( - <Fragment key={`resource_${item.id}`}> - <ReferencesTable - referencesName={item.id} - referencesArray={details[item.id]} - columns={item.tableColumnsCreator(onSelectResource)} - backToTop={() => { - startReference.current?.scrollIntoView(); - }} - /> - <EuiSpacer /> - </Fragment> - ))} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + </div> + <EuiFlexGroup> + <EuiFlexItem> + <div> + <strong>Description</strong> + </div> + <EuiText color="subdued"> + {details.description ? <Markdown markdown={details.description} /> : ''} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + {MitreAttackResources.filter((item) => details[item.id]).map((item) => ( + <Fragment key={`resource_${item.id}`}> + <ReferencesTable + referencesName={item.id} + referencesArray={details[item.id]} + columns={item.tableColumnsCreator(onSelectResource)} + backToTop={() => { + startReference.current?.scrollIntoView(); + }} + /> + <EuiSpacer /> + </Fragment> + ))} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </WzFlyout> ); }; diff --git a/public/components/security/policies/create-policy.tsx b/public/components/security/policies/create-policy.tsx index 3f64378012..a6f4f3bfc6 100644 --- a/public/components/security/policies/create-policy.tsx +++ b/public/components/security/policies/create-policy.tsx @@ -23,6 +23,7 @@ import { ErrorHandler } from '../../../react-services/error-handler'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { WzFlyout } from '../../common/flyouts'; export const CreatePolicyFlyout = ({ closeFlyout }) => { const [isModalVisible, setIsModalVisible] = useState(false); @@ -306,138 +307,136 @@ export const CreatePolicyFlyout = ({ closeFlyout }) => { return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New policy</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - value={policyName} - onChange={(e) => onChangePolicyName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New policy</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + value={policyName} + onChange={(e) => onChangePolicyName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." + > + <EuiSuperSelect + options={actions} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue} > - <EuiSuperSelect - options={actions} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue} - > - Add - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." + Add + </EuiButton> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." + > + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue} > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." - > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue} - > - Add - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton - disabled={ - !policyName || !addedActions.length || !addedResources.length || !effectValue - } - onClick={() => { - createPolicy(); - }} - fill - > - Create policy - </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + Add + </EuiButton> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton + disabled={ + !policyName || !addedActions.length || !addedResources.length || !effectValue + } + onClick={() => { + createPolicy(); + }} + fill + > + Create policy + </EuiButton> + </EuiForm> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/policies/edit-policy.tsx b/public/components/security/policies/edit-policy.tsx index 648e5d878a..2830589dc4 100644 --- a/public/components/security/policies/edit-policy.tsx +++ b/public/components/security/policies/edit-policy.tsx @@ -26,6 +26,7 @@ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; import _ from 'lodash'; +import { WzFlyout } from '../../common/flyouts'; export const EditPolicyFlyout = ({ policy, closeFlyout }) => { const isReserved = WzAPIUtils.isReservedID(policy.id); @@ -308,136 +309,134 @@ export const EditPolicyFlyout = ({ policy, closeFlyout }) => { return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit policy {policy.name}   - {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> - <EuiFieldText - placeholder="" - disabled={isReserved} - value={policy.name} - readOnly={true} - onChange={() => {}} - aria-label="" - /> - </EuiFormRow> - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Action" - helpText="Set an action where the policy will be carried out." + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit policy {policy.name}   + {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow label="Policy name" helpText="Introduce a name for this new policy."> + <EuiFieldText + placeholder="" + disabled={isReserved} + value={policy.name} + readOnly={true} + onChange={() => {}} + aria-label="" + /> + </EuiFormRow> + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Action" + helpText="Set an action where the policy will be carried out." + > + <EuiSuperSelect + options={actions} + disabled={isReserved} + valueOfSelected={actionValue} + onChange={(value) => onChangeActionValue(value)} + itemLayoutAlign="top" + hasDividers + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem></EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addAction()} + iconType="plusInCircle" + disabled={!actionValue || isReserved} > - <EuiSuperSelect - options={actions} - disabled={isReserved} - valueOfSelected={actionValue} - onChange={(value) => onChangeActionValue(value)} - itemLayoutAlign="top" - hasDividers - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem></EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addAction()} - iconType="plusInCircle" - disabled={!actionValue || isReserved} - > - Add - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedActions.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedActions} columns={actions_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label="Resource" - helpText="Select the resource to which this policy is directed." + Add + </EuiButton> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedActions.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedActions} columns={actions_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label="Resource" + helpText="Select the resource to which this policy is directed." + > + <EuiSuperSelect + options={resources} + valueOfSelected={resourceValue} + onChange={(value) => onChangeResourceValue(value)} + itemLayoutAlign="top" + hasDividers + disabled={!addedActions.length || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem> + <EuiFormRow + label="Resource identifier" + helpText="Introduce the resource identifier. Type * for all." + > + <EuiFieldText + placeholder={getIdentifier()} + value={resourceIdentifierValue} + onChange={(e) => onChangeResourceIdentifierValue(e)} + disabled={!resourceValue || isReserved} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow hasEmptyLabelSpace> + <EuiButton + onClick={() => addResource()} + iconType="plusInCircle" + disabled={!resourceIdentifierValue || isReserved} > - <EuiSuperSelect - options={resources} - valueOfSelected={resourceValue} - onChange={(value) => onChangeResourceValue(value)} - itemLayoutAlign="top" - hasDividers - disabled={!addedActions.length || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label="Resource identifier" - helpText="Introduce the resource identifier. Type * for all." - > - <EuiFieldText - placeholder={getIdentifier()} - value={resourceIdentifierValue} - onChange={(e) => onChangeResourceIdentifierValue(e)} - disabled={!resourceValue || isReserved} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton - onClick={() => addResource()} - iconType="plusInCircle" - disabled={!resourceIdentifierValue || isReserved} - > - Add - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - {!!addedResources.length && ( - <> - <EuiSpacer size="s"></EuiSpacer> - <EuiFlexGroup> - <EuiFlexItem> - <EuiInMemoryTable items={addedResources} columns={resources_columns} /> - </EuiFlexItem> - </EuiFlexGroup> - </> - )} - <EuiSpacer></EuiSpacer> - <EuiFormRow label="Select an effect" helpText="Select an effect."> - <EuiSuperSelect - options={effectOptions} - valueOfSelected={effectValue} - onChange={(value) => onEffectValueChange(value)} - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton disabled={isReserved} onClick={updatePolicy} fill> - Apply - </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + Add + </EuiButton> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + {!!addedResources.length && ( + <> + <EuiSpacer size="s"></EuiSpacer> + <EuiFlexGroup> + <EuiFlexItem> + <EuiInMemoryTable items={addedResources} columns={resources_columns} /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + <EuiSpacer></EuiSpacer> + <EuiFormRow label="Select an effect" helpText="Select an effect."> + <EuiSuperSelect + options={effectOptions} + valueOfSelected={effectValue} + onChange={(value) => onEffectValueChange(value)} + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton disabled={isReserved} onClick={updatePolicy} fill> + Apply + </EuiButton> + </EuiForm> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/roles-mapping/components/roles-mapping-create.tsx b/public/components/security/roles-mapping/components/roles-mapping-create.tsx index 1f916b6a55..7b8d980041 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-create.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-create.tsx @@ -22,6 +22,7 @@ import RolesServices from '../../roles/services'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzFlyout } from '../../../common/flyouts'; export const RolesMappingCreate = ({ closeFlyout, @@ -119,65 +120,63 @@ export const RolesMappingCreate = ({ return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>Create new role mapping  </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role mapping name" - isInvalid={false} - error={'Please provide a role mapping name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="Role name" - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList()} - isDisabled={false} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => createRule(rule)} - initialRule={false} - isReserved={false} - isLoading={isLoading} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => { - setHasChangeMappingRules(hasChange); - }} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>Create new role mapping  </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role mapping name" + isInvalid={false} + error={'Please provide a role mapping name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="Role name" + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList()} + isDisabled={false} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => createRule(rule)} + initialRule={false} + isReserved={false} + isLoading={isLoading} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => { + setHasChangeMappingRules(hasChange); + }} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx index 27f54cf05c..6f44f3f2e4 100644 --- a/public/components/security/roles-mapping/components/roles-mapping-edit.tsx +++ b/public/components/security/roles-mapping/components/roles-mapping-edit.tsx @@ -25,6 +25,7 @@ import _ from 'lodash'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzFlyout } from '../../../common/flyouts'; export const RolesMappingEdit = ({ rule, @@ -141,68 +142,66 @@ export const RolesMappingEdit = ({ return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit <strong>{rule.name}  </strong> - {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={false} - error={'Please provide a role name'} - helpText="Introduce a name for this role mapping." - > - <EuiFieldText - placeholder="" - disabled={WzAPIUtils.isReservedID(rule.id)} - value={ruleName} - onChange={(e) => setRuleName(e.target.value)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Roles" - isInvalid={false} - error={'At least one role must be selected.'} - helpText="Assign roles to your users." - > - <EuiComboBox - placeholder="Select roles" - options={getRolesList(roles)} - isDisabled={WzAPIUtils.isReservedID(rule.id)} - selectedOptions={selectedRoles} - onChange={(roles) => { - setSelectedRoles(roles); - }} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - </EuiForm> - <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> - <EuiFlexItem> - <RuleEditor - save={(rule) => editRule(rule)} - initialRule={rule.rule} - isLoading={isLoading} - isReserved={WzAPIUtils.isReservedID(rule.id)} - internalUsers={internalUsers} - currentPlatform={currentPlatform} - onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} - ></RuleEditor> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit <strong>{rule.name}  </strong> + {WzAPIUtils.isReservedID(rule.id) && <EuiBadge color="primary">Reserved</EuiBadge>} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={false} + error={'Please provide a role name'} + helpText="Introduce a name for this role mapping." + > + <EuiFieldText + placeholder="" + disabled={WzAPIUtils.isReservedID(rule.id)} + value={ruleName} + onChange={(e) => setRuleName(e.target.value)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Roles" + isInvalid={false} + error={'At least one role must be selected.'} + helpText="Assign roles to your users." + > + <EuiComboBox + placeholder="Select roles" + options={getRolesList(roles)} + isDisabled={WzAPIUtils.isReservedID(rule.id)} + selectedOptions={selectedRoles} + onChange={(roles) => { + setSelectedRoles(roles); + }} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + </EuiForm> + <EuiFlexGroup style={{ padding: '0px 24px 24px 24px' }}> + <EuiFlexItem> + <RuleEditor + save={(rule) => editRule(rule)} + initialRule={rule.rule} + isLoading={isLoading} + isReserved={WzAPIUtils.isReservedID(rule.id)} + internalUsers={internalUsers} + currentPlatform={currentPlatform} + onFormChange={(hasChange) => setHasChangeMappingRules(hasChange)} + ></RuleEditor> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/roles/create-role.tsx b/public/components/security/roles/create-role.tsx index b1f602fd2e..7ddb27780e 100644 --- a/public/components/security/roles/create-role.tsx +++ b/public/components/security/roles/create-role.tsx @@ -18,6 +18,7 @@ import { import { WzRequest } from '../../../react-services/wz-request'; import { ErrorHandler } from '../../../react-services/error-handler'; import { WzOverlayMask } from '../../common/util'; +import { WzFlyout } from '../../common/flyouts'; export const CreateRole = ({ closeFlyout }) => { const [policies, setPolicies] = useState([]); @@ -135,51 +136,49 @@ export const CreateRole = ({ closeFlyout }) => { return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>New role</h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFormRow - label="Role name" - isInvalid={roleNameError} - error={'Please provide a role name'} - helpText="Introduce a name for this new role." - > - <EuiFieldText - placeholder="" - value={roleName} - onChange={(e) => onChangeRoleName(e)} - aria-label="" - /> - </EuiFormRow> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - <EuiSpacer /> - <EuiButton fill onClick={createUser}> - Create role - </EuiButton> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>New role</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFormRow + label="Role name" + isInvalid={roleNameError} + error={'Please provide a role name'} + helpText="Introduce a name for this new role." + > + <EuiFieldText + placeholder="" + value={roleName} + onChange={(e) => onChangeRoleName(e)} + aria-label="" + /> + </EuiFormRow> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + <EuiSpacer /> + <EuiButton fill onClick={createUser}> + Create role + </EuiButton> + </EuiForm> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/roles/edit-role.tsx b/public/components/security/roles/edit-role.tsx index 62f4223592..40a7979c12 100644 --- a/public/components/security/roles/edit-role.tsx +++ b/public/components/security/roles/edit-role.tsx @@ -24,6 +24,7 @@ import { EditRolesTable } from './edit-role-table'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { WzFlyout } from '../../common/flyouts'; const reservedRoles = [ 'administrator', @@ -163,63 +164,61 @@ export const EditRole = ({ role, closeFlyout }) => { return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {role.name} role   - {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <EuiFormRow - label="Policies" - isInvalid={selectedPoliciesError} - error={'At least one policy must be selected.'} - helpText="Assign policies to the role." - > - <EuiComboBox - placeholder="Select policies" - options={policies} - isDisabled={isReserved} - selectedOptions={selectedPolicies} - onChange={onChangePolicies} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiButton - style={{ marginTop: 20, maxWidth: 45 }} + <WzFlyout flyoutProps={{className:"wzApp"}} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {role.name} role   + {isReserved && <EuiBadge color="primary">Reserved</EuiBadge>} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <EuiFormRow + label="Policies" + isInvalid={selectedPoliciesError} + error={'At least one policy must be selected.'} + helpText="Assign policies to the role." + > + <EuiComboBox + placeholder="Select policies" + options={policies} isDisabled={isReserved} - fill - onClick={addPolicy} - > - Add policy - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> + selectedOptions={selectedPolicies} + onChange={onChangePolicies} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiButton + style={{ marginTop: 20, maxWidth: 45 }} + isDisabled={isReserved} + fill + onClick={addPolicy} + > + Add policy + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> - <EuiSpacer /> - </EuiForm> - <div style={{ margin: 20 }}> - <EditRolesTable - policies={assignedPolicies} - role={currentRole} - onChange={update} - isDisabled={isReserved} - loading={isLoading} - /> - </div> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <EuiSpacer /> + </EuiForm> + <div style={{ margin: 20 }}> + <EditRolesTable + policies={assignedPolicies} + role={currentRole} + onChange={update} + isDisabled={isReserved} + loading={isLoading} + /> + </div> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/users/components/create-user.tsx b/public/components/security/users/components/create-user.tsx index f6675748cb..9ff0a3a072 100644 --- a/public/components/security/users/components/create-user.tsx +++ b/public/components/security/users/components/create-user.tsx @@ -30,6 +30,7 @@ import { useDebouncedEffect } from '../../../common/hooks/useDebouncedEffect'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzFlyout } from '../../../common/flyouts'; export const CreateUser = ({ closeFlyout }) => { const [selectedRoles, setSelectedRole] = useState<any>([]); @@ -229,8 +230,11 @@ export const CreateUser = ({ closeFlyout }) => { useEffect(() => { if ( - initialSelectedRoles.length != selectedRoles.length || initialPassword != password || - initialPassword != confirmPassword || initialUserName != userName || allowRunAs + initialSelectedRoles.length != selectedRoles.length || + initialPassword != password || + initialPassword != confirmPassword || + initialUserName != userName || + allowRunAs ) { setHasChanges(true); } else { @@ -238,107 +242,107 @@ export const CreateUser = ({ closeFlyout }) => { } }, [selectedRoles, userName, password, confirmPassword, allowRunAs]); - const onClose = () => { hasChanges ? setIsModalVisible(true) : closeFlyout(false) }; + const onClose = () => { + hasChanges ? setIsModalVisible(true) : closeFlyout(false); + }; return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2>Create new user</h2> + <WzFlyout onClose={onClose} flyoutProps={{ className: 'wzApp' }}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2>Create new user</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiPanel> + <EuiTitle size="s"> + <h2>User data</h2> </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiPanel> - <EuiTitle size="s"> - <h2>User data</h2> - </EuiTitle> - <EuiSpacer /> - <EuiFormRow - label="User name" - isInvalid={!!formErrors.userName} - error={formErrors.userName} - helpText="Introduce the user name for the user." - > - <EuiFieldText - placeholder="User name" - value={userName} - onChange={(e) => onChangeUserName(e)} - aria-label="" - isInvalid={!!formErrors.userName} - /> - </EuiFormRow> - <EuiFormRow - label="Password" - isInvalid={!!formErrors.password} - error={formErrors.password} - helpText="Introduce a new password for the user." - > - <EuiFieldPassword - placeholder="Password" - value={password} - onChange={(e) => onChangePassword(e)} - aria-label="" - isInvalid={!!formErrors.password} - /> - </EuiFormRow> - <EuiFormRow - label="Confirm Password" - isInvalid={!!formErrors.confirmPassword} - error={formErrors.confirmPassword} - helpText="Confirm the new password." - > - <EuiFieldPassword - placeholder="Confirm Password" - value={confirmPassword} - onChange={(e) => onChangeConfirmPassword(e)} - aria-label="" - isInvalid={!!formErrors.confirmPassword} - /> - </EuiFormRow> - <EuiFormRow label="Allow run as" helpText="Set if the user is able to use run as"> - <WzButtonPermissions - buttonType="switch" - label="Allow run as" - showLabel={false} - checked={allowRunAs} - permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} - onChange={(e) => onChangeAllowRunAs(e)} - aria-label="" - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>User roles</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Assign roles to the selected user"> - <EuiComboBox - placeholder="Select roles" - options={rolesOptions} - selectedOptions={selectedRoles} - isLoading={rolesLoading || isLoading} - onChange={onChangeRoles} - isClearable={true} - data-test-subj="demoComboBox" - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton fill isLoading={isLoading} onClick={editUser} isDisabled={!showApply}> - Apply - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <EuiSpacer /> + <EuiFormRow + label="User name" + isInvalid={!!formErrors.userName} + error={formErrors.userName} + helpText="Introduce the user name for the user." + > + <EuiFieldText + placeholder="User name" + value={userName} + onChange={(e) => onChangeUserName(e)} + aria-label="" + isInvalid={!!formErrors.userName} + /> + </EuiFormRow> + <EuiFormRow + label="Password" + isInvalid={!!formErrors.password} + error={formErrors.password} + helpText="Introduce a new password for the user." + > + <EuiFieldPassword + placeholder="Password" + value={password} + onChange={(e) => onChangePassword(e)} + aria-label="" + isInvalid={!!formErrors.password} + /> + </EuiFormRow> + <EuiFormRow + label="Confirm Password" + isInvalid={!!formErrors.confirmPassword} + error={formErrors.confirmPassword} + helpText="Confirm the new password." + > + <EuiFieldPassword + placeholder="Confirm Password" + value={confirmPassword} + onChange={(e) => onChangeConfirmPassword(e)} + aria-label="" + isInvalid={!!formErrors.confirmPassword} + /> + </EuiFormRow> + <EuiFormRow label="Allow run as" helpText="Set if the user is able to use run as"> + <WzButtonPermissions + buttonType="switch" + label="Allow run as" + showLabel={false} + checked={allowRunAs} + permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} + onChange={(e) => onChangeAllowRunAs(e)} + aria-label="" + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>User roles</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Assign roles to the selected user"> + <EuiComboBox + placeholder="Select roles" + options={rolesOptions} + selectedOptions={selectedRoles} + isLoading={rolesLoading || isLoading} + onChange={onChangeRoles} + isClearable={true} + data-test-subj="demoComboBox" + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton fill isLoading={isLoading} onClick={editUser} isDisabled={!showApply}> + Apply + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 7f7da6c7b2..d3a86e5c8e 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -32,6 +32,7 @@ import _ from 'lodash'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../react-services/common-services'; +import { WzFlyout } from '../../../common/flyouts'; export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { const userRolesFormatted = @@ -239,109 +240,107 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { return ( <> - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout className="wzApp" onClose={onClose} outsideClickCloses={true}> - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - <h2> - Edit {currentUser.username} user     - {WzAPIUtils.isReservedID(currentUser.id) && ( - <EuiBadge color="primary">Reserved</EuiBadge> - )} - </h2> - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <EuiForm component="form" style={{ padding: 24 }}> - <EuiPanel> - <EuiTitle size="s"> - <h2>Run as</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Set if the user is able to use run as"> - <WzButtonPermissions - buttonType="switch" - label="Allow run as" - showLabel={true} - checked={allowRunAs} - permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} - onChange={(e) => onChangeAllowRunAs(e)} - aria-label="" - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Password</h2> - </EuiTitle> - <EuiFormRow - label="" + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={onClose}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + <h2> + Edit {currentUser.username} user     + {WzAPIUtils.isReservedID(currentUser.id) && ( + <EuiBadge color="primary">Reserved</EuiBadge> + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm component="form" style={{ padding: 24 }}> + <EuiPanel> + <EuiTitle size="s"> + <h2>Run as</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Set if the user is able to use run as"> + <WzButtonPermissions + buttonType="switch" + label="Allow run as" + showLabel={true} + checked={allowRunAs} + permissions={[{ action: 'security:edit_run_as', resource: '*:*:*' }]} + onChange={(e) => onChangeAllowRunAs(e)} + aria-label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Password</h2> + </EuiTitle> + <EuiFormRow + label="" + isInvalid={!!formErrors.password} + error={formErrors.password} + helpText="Introduce a new password for the user." + > + <EuiFieldPassword + placeholder="Password" + value={password} + onChange={(e) => onChangePassword(e)} + aria-label="" isInvalid={!!formErrors.password} - error={formErrors.password} - helpText="Introduce a new password for the user." - > - <EuiFieldPassword - placeholder="Password" - value={password} - onChange={(e) => onChangePassword(e)} - aria-label="" - isInvalid={!!formErrors.password} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - <EuiFormRow - label="" + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + <EuiFormRow + label="" + isInvalid={!!formErrors.confirmPassword} + error={formErrors.confirmPassword} + helpText="Confirm the new password." + > + <EuiFieldPassword + placeholder="Confirm Password" + value={confirmPassword} + onChange={(e) => onChangeConfirmPassword(e)} + aria-label="" isInvalid={!!formErrors.confirmPassword} - error={formErrors.confirmPassword} - helpText="Confirm the new password." - > - <EuiFieldPassword - placeholder="Confirm Password" - value={confirmPassword} - onChange={(e) => onChangeConfirmPassword(e)} - aria-label="" - isInvalid={!!formErrors.confirmPassword} - disabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> - <EuiSpacer /> - <EuiPanel> - <EuiTitle size="s"> - <h2>Roles</h2> - </EuiTitle> - <EuiFormRow label="" helpText="Assign roles to the selected user"> - <EuiComboBox - placeholder="Select roles" - options={rolesOptions} - selectedOptions={selectedRoles} - isLoading={rolesLoading || isLoading} - onChange={onChangeRoles} - isClearable={true} - data-test-subj="demoComboBox" - isDisabled={WzAPIUtils.isReservedID(currentUser.id)} - /> - </EuiFormRow> - </EuiPanel> + disabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> + <EuiSpacer /> + <EuiPanel> + <EuiTitle size="s"> + <h2>Roles</h2> + </EuiTitle> + <EuiFormRow label="" helpText="Assign roles to the selected user"> + <EuiComboBox + placeholder="Select roles" + options={rolesOptions} + selectedOptions={selectedRoles} + isLoading={rolesLoading || isLoading} + onChange={onChangeRoles} + isClearable={true} + data-test-subj="demoComboBox" + isDisabled={WzAPIUtils.isReservedID(currentUser.id)} + /> + </EuiFormRow> + </EuiPanel> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton - fill - isLoading={isLoading} - isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} - onClick={editUser} - > - Apply - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + fill + isLoading={isLoading} + isDisabled={WzAPIUtils.isReservedID(currentUser.id) || !showApply} + onClick={editUser} + > + Apply + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </EuiFlyoutBody> + </WzFlyout> {modal} </> ); diff --git a/public/directives/wz-logtest/components/logtest.tsx b/public/directives/wz-logtest/components/logtest.tsx index 264053b49e..8ba9bbcc24 100644 --- a/public/directives/wz-logtest/components/logtest.tsx +++ b/public/directives/wz-logtest/components/logtest.tsx @@ -45,6 +45,7 @@ import { } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { WzFlyout } from '../../../components/common/flyouts'; type LogstestProps = { openCloseFlyout: () => {}; @@ -256,26 +257,20 @@ export const Logtest = compose( </EuiPanel> </EuiPage> )) || ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiFlyout - className="wzApp" - onClose={() => props.openCloseFlyout()} - outsideClickCloses={true} - > - <EuiFlyoutHeader hasBorder={false}> - <EuiTitle size="m"> - {props.isRuleset.includes('rules') ? <h2>Ruleset Test</h2> : <h2>Decoders Test</h2>} - </EuiTitle> - </EuiFlyoutHeader> - <EuiFlyoutBody style={{ margin: '20px' }}> - <EuiFlexGroup gutterSize="m"> - <EuiFlexItem /> - </EuiFlexGroup> - <EuiSpacer size="s" /> - {buildLogtest()} - </EuiFlyoutBody> - </EuiFlyout> - </EuiOverlayMask> + <WzFlyout flyoutProps={{ className: 'wzApp' }} onClose={() => props.openCloseFlyout()}> + <EuiFlyoutHeader hasBorder={false}> + <EuiTitle size="m"> + {props.isRuleset.includes('rules') ? <h2>Ruleset Test</h2> : <h2>Decoders Test</h2>} + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody style={{ margin: '20px' }}> + <EuiFlexGroup gutterSize="m"> + <EuiFlexItem /> + </EuiFlexGroup> + <EuiSpacer size="s" /> + {buildLogtest()} + </EuiFlyoutBody> + </WzFlyout> )} </Fragment> ); From c5eb293932b7f237c45338c6fbba95fc9802567e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Cu=C3=A9llar=20Peinado?= <alejandro.cuellar@wazuh.com> Date: Fri, 3 Dec 2021 17:56:17 +0100 Subject: [PATCH 350/493] Fixing the bug of index patterns in health-check due to bad copy of a PR (#3707) * Fixing the bug of index patterns in health-check due to bad copy of a PR * Adding Changelog --- CHANGELOG.md | 1 + public/components/common/modules/events.tsx | 1 + .../check-index-pattern-object.service.ts | 2 +- public/components/visualize/wz-visualize.js | 46 +++++--- public/react-services/saved-objects.js | 109 +++++++++--------- 5 files changed, 87 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 970ef40d69..2c63b165ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error that shows we're using X-Pack when we have Basic [#3692](https://github.com/wazuh/wazuh-kibana-app/pull/3692) - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) +- Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 5f3c407aee..f19219946d 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -24,6 +24,7 @@ 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'; +import { satisfyKibanaVersion } from '../../../../common/semver'; export const Events = compose( withAgentSupportModule, diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts index b09e3c9a18..5f8d786d4c 100644 --- a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts @@ -54,7 +54,7 @@ export const checkIndexPatternObjectService = async (appConfig, checkLogger: Ch // show error checkLogger.error(`Default index pattern not found`); } - checkLogger.info(`Getting list of valid index patterns [${patternId}]...`); + checkLogger.info(`Getting list of valid index patterns...`); listValidIndexPatterns = await SavedObject.getListOfWazuhValidIndexPatterns(defaultIndexPatterns, HEALTH_CHECK); checkLogger.info(`Valid index patterns found: ${listValidIndexPatterns.length || 0}`); if(!AppState.getCurrentPattern()){ diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 2a44b4055c..25db2c15ce 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -41,6 +41,7 @@ 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'; +import { satisfyKibanaVersion } from '../../../common/semver'; const visHandler = new VisHandlers(); @@ -129,7 +130,9 @@ export const WzVisualize = compose( // Known fields are refreshed only once per dashboard loading try { this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); - await PatternHandler.refreshIndexPattern(this.newFields); + if(satisfyKibanaVersion('<7.11')){ + await PatternHandler.refreshIndexPattern(this.newFields); + }; this.setState({ isRefreshing: false }); this.reloadToast(); this.newFields = {}; @@ -153,23 +156,38 @@ export const WzVisualize = compose( } }; reloadToast = () => { - getToasts().add({ - color: 'success', - title: 'The index pattern was refreshed successfully.', - text: toMountPoint( - <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + const toastLifeTimeMs = 300000; + if(satisfyKibanaVersion('<7.11')){ + getToasts().add({ + color: 'success', + title: 'The index pattern was refreshed successfully.', + text: toMountPoint(<EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> <EuiFlexItem grow={false}> - There were some unknown fields for the current index pattern. You need to refresh the - page to apply the changes. + There were some unknown fields for the current index pattern. + You need to refresh the page to apply the changes. </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiButton onClick={() => window.location.reload()} size="s"> - Reload page - </EuiButton> + <EuiButton onClick={() => window.location.reload()} size="s">Reload page</EuiButton> </EuiFlexItem> - </EuiFlexGroup> - ), - }); + </EuiFlexGroup>), + toastLifeTimeMs + }); + }else if(satisfyKibanaVersion('>=7.11')){ + getToasts().add({ + color: 'warning', + title: 'Found unknown fields in the index pattern.', + text: toMountPoint(<EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + There are some unknown fields for the current index pattern. + You need to refresh the page to update the fields. + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={() => window.location.reload()} size="s">Reload page</EuiButton> + </EuiFlexItem> + </EuiFlexGroup>), + toastLifeTimeMs + }); + }; }; render() { const { visualizations } = this.state; diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index 0f59471253..ffdc997440 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -36,23 +36,32 @@ export class SavedObject { let indexPatterns = ((savedObjects || {}).data || {}).saved_objects || []; let indexPatternsFields; - if(satisfyKibanaVersion('<7.11')){ - indexPatternsFields = indexPatterns.map(indexPattern => JSON.parse(indexPattern.attributes.fields)); - }else if(satisfyKibanaVersion('>=7.11')){ - indexPatternsFields = await Promise.all(indexPatterns.map(async indexPattern => { - try{ - const {data: {fields}} = await GenericRequest.request( - 'GET', - `/api/index_patterns/_fields_for_wildcard?pattern=${indexPattern.attributes.title}`, - {} - ); - return fields; - }catch(error){ - return []; - } - })); + if (satisfyKibanaVersion('<7.11')) { + indexPatternsFields = indexPatterns.map((indexPattern) => + JSON.parse(indexPattern.attributes.fields) + ); + } else if (satisfyKibanaVersion('>=7.11')) { + indexPatternsFields = await Promise.all( + indexPatterns.map(async (indexPattern) => { + try { + const { + data: { fields }, + } = await GenericRequest.request( + 'GET', + `/api/index_patterns/_fields_for_wildcard?pattern=${indexPattern.attributes.title}`, + {} + ); + return fields; + } catch (error) { + return []; + } + }) + ); } - return indexPatterns.map((indexPattern, idx) => ({...indexPattern, _fields: indexPatternsFields[idx]})); + return indexPatterns.map((indexPattern, idx) => ({ + ...indexPattern, + _fields: indexPatternsFields[idx], + })); } catch (error) { throw ((error || {}).data || {}).message || false ? error.data.message @@ -96,11 +105,10 @@ export class SavedObject { const requiredFields = ['timestamp', 'rule.groups', 'manager.name', 'agent.id']; return list.filter((item) => { - if (item.attributes && item.attributes.fields) { - const fields = JSON.parse(item.attributes.fields); - return requiredFields.every((reqField) => { - return fields.find((field) => field.name === reqField); - }); + if (item._fields) { + return requiredFields.every((reqField) => + item._fields.some((field) => field.name === reqField) + ); } return false; }); @@ -110,9 +118,9 @@ export class SavedObject { const result = await SavedObject.existsIndexPattern(patternID); if (!result.data) { let fields = ''; - if(satisfyKibanaVersion('<7.11')){ + if (satisfyKibanaVersion('<7.11')) { fields = await SavedObject.getIndicesFields(patternID, WAZUH_INDEX_TYPE_ALERTS); - }; + } await this.createSavedObject( 'index-pattern', patternID, @@ -127,26 +135,6 @@ export class SavedObject { } } - /** - * - * Given an index pattern ID, checks if it exists - */ - static async getExistingIndexPattern(patternID) { - try { - const result = await GenericRequest.request( - 'GET', - `/api/saved_objects/index-pattern/${patternID}?fields=title&fields=fields` - ); - - return result.data; - } catch (error) { - if (error && error.response && error.response.status == 404) return false; - return ((error || {}).data || {}).message || false - ? error.data.message - : error.message || false; - } - } - /** * * Given an index pattern ID, checks if it exists @@ -167,7 +155,7 @@ export class SavedObject { status: true, statusCode: 200, title, - fields, + id, }; } } catch (error) { @@ -190,24 +178,30 @@ export class SavedObject { true ); let indexPatternFields; - if(satisfyKibanaVersion('<7.11')){ + if (satisfyKibanaVersion('<7.11')) { indexPatternFields = JSON.parse(indexPatternData.data.attributes.fields); - }else if(satisfyKibanaVersion('>=7.11')){ - try{ - const {data: {fields}} = await GenericRequest.request( + } else if (satisfyKibanaVersion('>=7.11')) { + try { + const { + data: { fields }, + } = await GenericRequest.request( 'GET', `/api/index_patterns/_fields_for_wildcard?pattern=${indexPatternData.data.attributes.title}`, {} ); indexPatternFields = fields; - }catch(error){ + } catch (error) { indexPatternFields = []; - }; - }; - return ({...indexPatternData.data, ...({_fields: indexPatternFields})}); + } + } + return { ...indexPatternData.data, ...{ _fields: indexPatternFields } }; } catch (error) { if (error && error.response && error.response.status == 404) return false; - return Promise.reject(((error || {}).data || {}).message || false ? error.data.message : error.message || `Error getting the '${patternID}' index pattern`); + return Promise.reject( + ((error || {}).data || {}).message || false + ? error.data.message + : error.message || `Error getting the '${patternID}' index pattern` + ); } } @@ -219,9 +213,9 @@ export class SavedObject { params ); - if(satisfyKibanaVersion('<7.11') && type === 'index-pattern'){ + if (satisfyKibanaVersion('<7.11') && type === 'index-pattern') { await this.refreshFieldsOfIndexPattern(id, params.attributes.title, fields); - }; + } return result; } catch (error) { @@ -240,7 +234,6 @@ export class SavedObject { fields: JSON.stringify(fields), timeFieldName: 'timestamp', title: title, - retry_on_conflict: 4, }, }); return; @@ -299,7 +292,9 @@ export class SavedObject { */ static async createWazuhIndexPattern(pattern) { try { - const fields = satisfyKibanaVersion('<7.11') ? await SavedObject.getIndicesFields(pattern, WAZUH_INDEX_TYPE_ALERTS) : ''; + const fields = satisfyKibanaVersion('<7.11') + ? await SavedObject.getIndicesFields(pattern, WAZUH_INDEX_TYPE_ALERTS) + : ''; await this.createSavedObject( 'index-pattern', pattern, From 43f0516282db3b4317649fd8635fbd69b0d4280e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 9 Dec 2021 10:20:02 +0100 Subject: [PATCH 351/493] fix(module/pci_dss): Add the `Controls` missing tab --- public/components/common/modules/modules-defaults.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 469aa128ee..cfe7ed16b4 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -169,11 +169,7 @@ export const ModulesDefaults = { tabs: [DashboardTab, EventsTab], availableFor: ['manager', 'agent'], }, - pci: { - init: 'dashboard', - tabs: RegulatoryComplianceTabs, - availableFor: ['manager', 'agent'], - }, + osquery: { init: 'dashboard', tabs: [DashboardTab, EventsTab], @@ -186,7 +182,7 @@ export const ModulesDefaults = { }, pci: { init: 'dashboard', - tabs: [DashboardTab, EventsTab], + tabs: RegulatoryComplianceTabs, availableFor: ['manager', 'agent'], }, hipaa: { From 1b875ac4a76d06ac7e3c2854392da9f6230cdec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 9 Dec 2021 10:20:02 +0100 Subject: [PATCH 352/493] fix(module/pci_dss): Add the `Controls` missing tab --- public/components/common/modules/modules-defaults.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 469aa128ee..cfe7ed16b4 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -169,11 +169,7 @@ export const ModulesDefaults = { tabs: [DashboardTab, EventsTab], availableFor: ['manager', 'agent'], }, - pci: { - init: 'dashboard', - tabs: RegulatoryComplianceTabs, - availableFor: ['manager', 'agent'], - }, + osquery: { init: 'dashboard', tabs: [DashboardTab, EventsTab], @@ -186,7 +182,7 @@ export const ModulesDefaults = { }, pci: { init: 'dashboard', - tabs: [DashboardTab, EventsTab], + tabs: RegulatoryComplianceTabs, availableFor: ['manager', 'agent'], }, hipaa: { From acfa6f26faf06b438511bb8034ef0003848badc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 9 Dec 2021 15:42:08 +0100 Subject: [PATCH 353/493] fix(module/office365-github/panel): Fix when clearing the query filter in the search bar and the data is not updated - Now uses `useQueryManager` instead of `useQuery` --- public/components/common/hooks/use-es-search.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index f9296a94d2..16d83d8af5 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -12,7 +12,7 @@ import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useFilterManager, useIndexPattern, useQuery } from '.'; +import { useFilterManager, useIndexPattern, useQueryManager } from '.'; import { IndexPattern } from 'src/plugins/data/public'; import { UI_ERROR_SEVERITIES, @@ -44,7 +44,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) const data = getDataPlugin(); const indexPattern = useIndexPattern(); const {filters} = useFilterManager(); - const [query] = useQuery(); + const [query] = useQueryManager(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); From 9b83b0e17f7fb56ec34dc8b85e1849873fb7a060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 9 Dec 2021 17:05:30 +0100 Subject: [PATCH 354/493] changelog: Add PR to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c63b165ca..cecdf95e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) +- Fix clearing the query filter doesn't update the data in Office 365 and GitHub Panel tab [#3722](https://github.com/wazuh/wazuh-kibana-app/pull/3722) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From f42c630a00101eb24fdc0dcd4b459ae95544ee09 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 10 Dec 2021 13:09:46 +0100 Subject: [PATCH 355/493] add toast --- CHANGELOG.md | 1 + .../components/management/ruleset/ruleset-editor.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c63b165ca..1bcd2813c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) +- Fixing bug when create filename with spaces and throws bad error [] () ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/management/components/management/ruleset/ruleset-editor.js b/public/controllers/management/components/management/ruleset/ruleset-editor.js index fd89d0cdbd..b6b65ddd76 100644 --- a/public/controllers/management/components/management/ruleset/ruleset-editor.js +++ b/public/controllers/management/components/management/ruleset/ruleset-editor.js @@ -105,7 +105,10 @@ class WzRulesetEditor extends Component { async save(name, overwrite = true) { if (!this._isMounted) { return; - } + }else if(/\s/.test(name)) { + this.showToast('warning', 'Warning', `The ${this.props.state.section} name must not contain spaces.`, 3000); + return; + } try { const { content } = this.state; From a92f60349204938e4bf33cdfced49db7af8dc092 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Fri, 10 Dec 2021 13:12:12 +0100 Subject: [PATCH 356/493] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcd2813c7..ff4945af03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,7 +98,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) -- Fixing bug when create filename with spaces and throws bad error [] () +- Fixing bug when create filename with spaces and throws a bad error [#3724] (https://github.com/wazuh/wazuh-kibana-app/pull/3724) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From e87a065d7aedb18e1aed62b4b60c0b2ad057a127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 14 Dec 2021 10:46:02 +0100 Subject: [PATCH 357/493] fix(modules/mitre): error opening MITRE Att&ck details flyout from Events tabhas no information about the tactics - Move the request to get the tactics to the `FlyoutTechnique` component --- .../flyout-technique/flyout-technique.tsx | 34 +++++++------------ .../components/techniques/techniques.tsx | 7 ++-- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 2f44914782..9a16a36af6 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -57,9 +57,7 @@ export class FlyoutTechnique extends Component { }; props!: { - currentTechniqueData: any; currentTechnique: string; - tacticsObject: any; }; filterManager: FilterManager; @@ -132,13 +130,21 @@ export class FlyoutTechnique extends Component { try { this.setState({ loading: true, techniqueData: {} }); const { currentTechnique } = this.props; - const result = await WzRequest.apiReq('GET', '/mitre/techniques', { + const techniqueResponse = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { q: `external_id=${currentTechnique}` } }); - const rawData = (((result || {}).data || {}).data || {}).affected_items; - !!rawData && this.formatTechniqueData(rawData[0]); + const [techniqueData] = (((techniqueResponse || {}).data || {}).data || {}).affected_items; + const tacticsResponse = await WzRequest.apiReq('GET', '/mitre/tactics', {}); + const tacticsData = (((tacticsResponse || {}).data || {}).data || {}).affected_items; + + techniqueData.tactics && (techniqueData.tactics = techniqueData.tactics.map(tacticID => { + const tactic = tacticsData.find(tacticData => tacticData.id === tacticID); + return { id: tactic.external_id, name: tactic.name } + })); + const { name, mitre_version, tactics } = techniqueData; + this._isMount && this.setState({ techniqueData: { name, mitre_version, tactics }, loading: false }); } catch (error) { const options = { context: `${FlyoutTechnique.name}.getTechniqueData`, @@ -157,20 +163,6 @@ export class FlyoutTechnique extends Component { } } - findTacticName(tactics) { - const { tacticsObject } = this.props; - return tactics.map((element) => { - const tactic = Object.values(tacticsObject).find(obj => obj.id === element); - return { id:tactic.external_id, name: tactic.name}; - }); - } - - formatTechniqueData(rawData) { - const { tactics, name, mitre_version } = rawData; - const tacticsObj = this.findTacticName(tactics); - this.setState({ techniqueData: { name, mitre_version, tacticsObj }, loading: false }); - } - renderHeader() { const { techniqueData } = this.state; return ( @@ -226,8 +218,8 @@ export class FlyoutTechnique extends Component { }, { title: 'Tactics', - description: techniqueData.tacticsObj - ? techniqueData.tacticsObj.map((tactic) => { + description: techniqueData.tactics + ? techniqueData.tactics.map((tactic) => { return ( <> <EuiToolTip diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index 2d5205ed9b..ee15d93c33 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -58,7 +58,6 @@ export const Techniques = withWindowSize( state: { techniquesCount: { key: string; doc_count: number }[]; isFlyoutVisible: Boolean; - currentTechniqueData: {}; currentTechnique: string; hideAlerts: boolean; actionsOpen: string; @@ -72,7 +71,6 @@ export const Techniques = withWindowSize( this.state = { isFlyoutVisible: false, - currentTechniqueData: {}, techniquesCount: [], currentTechnique: '', hideAlerts: false, @@ -502,7 +500,7 @@ export const Techniques = withWindowSize( } closeFlyout() { - this.setState({ isFlyoutVisible: false, currentTechniqueData: {} }); + this.setState({ isFlyoutVisible: false }); } onChangeFlyout = (isFlyoutVisible: boolean) => { @@ -562,9 +560,8 @@ export const Techniques = withWindowSize( openDiscover={(e, itemId) => this.openDiscover(e, itemId)} openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} onChangeFlyout={this.onChangeFlyout} - currentTechniqueData={this.state.currentTechniqueData} currentTechnique={currentTechnique} - tacticsObject={this.props.tacticsObject} /> + /> </div> </EuiOutsideClickDetector> </EuiOverlayMask> From a03a86356caa6047ea25e5079950eed88421cbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 14 Dec 2021 14:40:47 +0100 Subject: [PATCH 358/493] fix(modules/mitre): Changes in the MITRE Att&ck details flyout - `Discover` component: - Removed prop `openIntelligence` - Removed not used variable and render of a flyout related to MITRE Att&ck - `FlyoutTechnique` component: - Removed prop `openIntelligence` and use `AppNavigate.navigateToModule` - Changed the `references.external_id` property in MITRE Att&ck entities to use `external_id` field. --- .../common/modules/discover/discover.tsx | 34 +++---------------- .../flyout-technique/flyout-technique.tsx | 7 ++-- .../components/techniques/techniques.tsx | 6 ---- .../all_resources.tsx | 2 +- .../mitre_attack_intelligence/resource.tsx | 10 ++---- .../resource_detail_references_table.tsx | 2 +- .../mitre_attack_intelligence/resources.tsx | 12 +++---- .../management/ruleset/rule-info.js | 2 +- 8 files changed, 16 insertions(+), 59 deletions(-) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 4e1a98ba59..01c5487508 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -82,7 +82,6 @@ export const Discover = compose( state: { sort: object; selectedTechnique: string; - showMitreFlyout: boolean; alerts: { _source: {}; _id: string }[]; total: number; pageIndex: number; @@ -107,7 +106,6 @@ export const Discover = compose( query?: { language: 'kuery' | 'lucene'; query: string }; type?: any; updateTotalHits: Function; - openIntelligence: Function; includeFilters?: string; initialColumns: ColumnDefinition[]; initialAgentColumns?: ColumnDefinition[]; @@ -123,7 +121,6 @@ export const Discover = compose( this.state = { sort: {}, selectedTechnique: '', - showMitreFlyout: false, alerts: [], total: 0, pageIndex: 0, @@ -536,7 +533,7 @@ export const Discover = compose( width = '15%'; } if (item === 'rule.mitre.id') { - link = (ev, x, e) => this.props.openIntelligence(e, 'techniques', x); + link = (ev, x) => this.openIntelligence(ev, 'techniques', x); } if (arrayCompilance.indexOf(item) !== -1) { width = '30%'; @@ -700,13 +697,6 @@ export const Discover = compose( this.setState({ pageIndex: 0, tsUpdated: Date.now() }); }; - closeMitreFlyout = () => { - this.setState({ showMitreFlyout: false }); - }; - - onMitreChangeFlyout = (showMitreFlyout: boolean) => { - this.setState({ showMitreFlyout }); - }; openDiscover(e, techniqueID) { AppNavigate.navigateToModule(e, 'overview', { @@ -724,6 +714,10 @@ export const Discover = compose( }); } + openIntelligence(e, redirectTo, itemID) { + AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": redirectTo, "idToRedirect": itemID}); + } + render() { if (this.state.isLoading) return ( @@ -758,23 +752,6 @@ export const Discover = compose( pageSizeOptions: [10, 25, 50], }; const noResultsText = `No results match for this search criteria`; - let flyout = this.state.showMitreFlyout ? ( - <EuiOverlayMask headerZindexLocation="below"> - <EuiOutsideClickDetector onOutsideClick={this.closeMitreFlyout}> - <div> - {/* EuiOutsideClickDetector needs a static first child */} - <FlyoutTechnique - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - onChangeFlyout={this.onMitreChangeFlyout} - currentTechnique={this.state.selectedTechnique} - /> - </div> - </EuiOutsideClickDetector> - </EuiOverlayMask> - ) : ( - <></> - ); return ( <div className="wz-discover hide-filter-control wz-inventory"> {this.props.kbnSearchBar && ( @@ -818,7 +795,6 @@ export const Discover = compose( </EuiFlexItem> </EuiFlexGroup> )} - {flyout} </div> ); } diff --git a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx index 9a16a36af6..489c0d03e6 100644 --- a/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/public/components/overview/mitre/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -207,7 +207,7 @@ export class FlyoutTechnique extends Component { > <EuiLink onClick={(e) => { - this.props.openIntelligence(e, 'techniques', currentTechnique); + AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": 'techniques', "idToRedirect": currentTechnique}); e.stopPropagation(); }} > @@ -228,7 +228,7 @@ export class FlyoutTechnique extends Component { > <EuiLink onClick={(e) => { - this.props.openIntelligence(e, 'tactics', tactic.id); + AppNavigate.navigateToModule(e, 'overview', { "tab": 'mitre', "tabView": "intelligence", "tabRedirect": 'tactics', "idToRedirect": tactic.id}); e.stopPropagation(); }} > @@ -353,9 +353,6 @@ export class FlyoutTechnique extends Component { implicitFilters={implicitFilters} initialFilters={[]} updateTotalHits={(total) => this.updateTotalHits(total)} - openIntelligence={(e, redirectTo, itemId) => - this.props.openIntelligence(e, redirectTo, itemId) - } /> </EuiFlexItem> </EuiFlexGroup> diff --git a/public/components/overview/mitre/components/techniques/techniques.tsx b/public/components/overview/mitre/components/techniques/techniques.tsx index ee15d93c33..c48e0c80a4 100644 --- a/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/public/components/overview/mitre/components/techniques/techniques.tsx @@ -418,11 +418,6 @@ export const Techniques = withWindowSize( this.props.onSelectedTabChanged('dashboard'); } - openIntelligence(e, redirectTo, itemId) { - this.props.onSelectedTabChanged('intelligence'); - window.location.href = window.location + `&tabRedirect=${redirectTo}&idToRedirect=${itemId}`; - } - /** * Adds a new filter with format { "filter_key" : "filter_value" }, e.g. {"agent.id": "001"} * @param filter @@ -558,7 +553,6 @@ export const Techniques = withWindowSize( <FlyoutTechnique openDashboard={(e, itemId) => this.openDashboard(e, itemId)} openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - openIntelligence={(e, redirectTo, itemId) => this.openIntelligence(e, redirectTo, itemId)} onChangeFlyout={this.onChangeFlyout} currentTechnique={currentTechnique} /> diff --git a/public/components/overview/mitre_attack_intelligence/all_resources.tsx b/public/components/overview/mitre_attack_intelligence/all_resources.tsx index d6985913f9..bb6951e3df 100644 --- a/public/components/overview/mitre_attack_intelligence/all_resources.tsx +++ b/public/components/overview/mitre_attack_intelligence/all_resources.tsx @@ -23,7 +23,7 @@ export const ModuleMitreAttackIntelligenceAllResources = ({ results, loading }) const [details, setDetails] = useState(null); const selectResource = useCallback((item) => { - setDetails(({...item, ['references.external_id']: item.references.find(reference => reference.source === 'mitre-attack')?.external_id})); + setDetails(item); },[]); const closeFlyout = useCallback(() => { diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index 85ebe6deab..dfc554356b 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -34,7 +34,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ const redirectTab = urlParams.get('tabRedirect'); const idToRedirect = urlParams.get("idToRedirect"); if(redirectTab && idToRedirect){ - const endpoint = `/mitre/${redirectTab}?q=references.external_id=${idToRedirect}`; + const endpoint = `/mitre/${redirectTab}?q=external_id=${idToRedirect}`; getMitreItemToRedirect(endpoint); urlParams.delete('tabRedirect'); urlParams.delete('idToRedirect'); @@ -46,12 +46,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ const getMitreItemToRedirect = async (endpoint) => { try { const res = await WzRequest.apiReq("GET", endpoint, {}); - const data = res?.data?.data.affected_items.map((item) => ({ - ...item, - ["references.external_id"]: item?.references?.find( - (reference) => reference.source === "mitre-attack" - )?.external_id, - })); + const data = res?.data?.data.affected_items; setDetails(data[0]); } catch (error) { const options = { @@ -87,7 +82,6 @@ export const ModuleMitreAttackIntelligenceResource = ({ searchBarSuggestions={searchBarSuggestions} endpoint={apiEndpoint} tablePageSizeOptions={[10]} - mapResponseItem={(item) => ({...item, ['references.external_id']: item?.references?.find(reference => reference.source === 'mitre-attack')?.external_id})} filters={resourceFilters} /> {details && ( diff --git a/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx index 3a89f0b326..55e4187021 100644 --- a/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource_detail_references_table.tsx @@ -54,7 +54,7 @@ export const ReferencesTable = ({referencesName, referencesArray, columns, backT try{ const data = await Promise.all(namesConcatenated.map(async (nameConcatenated) => { const queryResult = await WzRequest.apiReq('GET', `/mitre/${referencesName}?${referencesName.replace(/s\s*$/, '')}_ids=${nameConcatenated}`, {}); - return ((((queryResult || {}).data || {}).data || {}).affected_items || []).map((item) => ({...item, ['references.external_id']: item.references.find(reference => reference.source === 'mitre-attack')?.external_id})); + return ((((queryResult || {}).data || {}).data || {}).affected_items || []); })); setData(data.flat()); } diff --git a/public/components/overview/mitre_attack_intelligence/resources.tsx b/public/components/overview/mitre_attack_intelligence/resources.tsx index e3d9d25d30..0dd0749c76 100644 --- a/public/components/overview/mitre_attack_intelligence/resources.tsx +++ b/public/components/overview/mitre_attack_intelligence/resources.tsx @@ -24,10 +24,6 @@ const getMitreAttackIntelligenceSuggestions = (endpoint: string, field: string) try{ const response = await WzRequest.apiReq('GET', endpoint, {}); return response?.data?.data.affected_items - .map(item => ({ - ...item, - ['references.external_id']: item?.references?.find(reference => reference.source === 'mitre-attack')?.external_id - })) .map(item => item[field]) .filter(item => item && item.toLowerCase().includes(input.toLowerCase())) .sort() @@ -73,10 +69,10 @@ function buildResource(label: string, labelResource: string){ }, { type: 'q', - label: 'references.external_id', + label: 'external_id', description: `${labelResource} ID`, operators: ['=', '!='], - values: getMitreAttackIntelligenceSuggestions(endpoint, 'references.external_id') + values: getMitreAttackIntelligenceSuggestions(endpoint, 'external_id') } ], apiEndpoint: endpoint, @@ -84,7 +80,7 @@ function buildResource(label: string, labelResource: string){ initialSortingField: 'name', tableColumnsCreator: (openResourceDetails) => [ { - field: 'references.external_id', + field: 'external_id', name: 'ID', width: '12%', render: (value, item) => <EuiLink onClick={() => openResourceDetails(item)}>{value}</EuiLink> @@ -107,7 +103,7 @@ function buildResource(label: string, labelResource: string){ mitreFlyoutHeaderProperties: [ { label: 'ID', - id: 'references.external_id', + id: 'external_id', }, { label: 'Name', diff --git a/public/controllers/management/components/management/ruleset/rule-info.js b/public/controllers/management/components/management/ruleset/rule-info.js index 53238d364a..355ffd164a 100644 --- a/public/controllers/management/components/management/ruleset/rule-info.js +++ b/public/controllers/management/components/management/ruleset/rule-info.js @@ -412,7 +412,7 @@ class WzRuleInfo extends Component { compliance.map(async (i) => { const data = await WzRequest.apiReq('GET', '/mitre/techniques', { params: { - q: `references.external_id=${i}`, + q: `external_id=${i}`, }, }); const formattedData = (((data || {}).data.data || {}).affected_items || [])[0] || {}; From e4ad074de0c6d6af77792897389567f9f608d78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 14 Dec 2021 16:42:53 +0100 Subject: [PATCH 359/493] changelog: Add PR to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4945af03..ad77e5be62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ All notable changes to the Wazuh app project will be documented in this file. [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) - Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) -- Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) +- Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) [#3726](https://github.com/wazuh/wazuh-kibana-app/pull/3726) - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) - Added a new hook for getting value suggestions [#3506](https://github.com/wazuh/wazuh-kibana-app/pull/3506) From ce67a1a2c282097ab085585ef152c75a732d90a2 Mon Sep 17 00:00:00 2001 From: eze9252 <eze9252@gmail.com> Date: Wed, 15 Dec 2021 16:49:45 +0100 Subject: [PATCH 360/493] update readme and changelog with new versions --- CHANGELOG.md | 2 +- README.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4945af03..7068fa57ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the Wazuh app project will be documented in this file. -## Wazuh v4.3.0 - Kibana 7.10.2 , 7.11.2 - Revision 4301 +## Wazuh v4.3.0 - Kibana 7.10.2 , 7.11.2, 7.13.4, 7.14.2 - Revision 4301 ### Added diff --git a/README.md b/README.md index a9e965b88d..593f8b674d 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,28 @@ service kibana restart | Wazuh app | Kibana | Open Distro | Package | | :-------: | :----: | :---------: | :------------------------------------------------------------------------- | +| 4.3.0 | 7.14.2 | | <> | +| 4.3.0 | 7.14.1 | | <> | +| 4.3.0 | 7.14.0 | | <> | +| 4.3.0 | 7.13.4 | | <> | +| 4.3.0 | 7.13.3 | | <> | +| 4.3.0 | 7.13.2 | | <> | +| 4.3.0 | 7.13.1 | | <> | +| 4.3.0 | 7.13.0 | | <> | +| 4.3.0 | 7.12.1 | | <> | +| 4.3.0 | 7.11.2 | | <> | +| 4.3.0 | 7.10.2 | 1.13.2 | <> | +| 4.2.5 | 7.14.2 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.14.2-1.zip> | +| 4.2.5 | 7.14.1 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.14.1-1.zip> | +| 4.2.5 | 7.14.0 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.14.0-1.zip> | +| 4.2.5 | 7.13.4 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.13.4-1.zip> | +| 4.2.5 | 7.13.3 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.13.3-1.zip> | +| 4.2.5 | 7.13.2 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.13.2-1.zip> | +| 4.2.5 | 7.13.1 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.13.1-1.zip> | +| 4.2.5 | 7.13.0 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.13.0-1.zip> | +| 4.2.5 | 7.12.1 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.12.1-1.zip> | +| 4.2.5 | 7.11.2 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.11.2-1.zip> | +| 4.2.5 | 7.10.2 | 1.13.2 | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.4_7.10.2-1.zip> | | 4.2.4 | 7.12.1 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.4_7.12.1-1.zip> | | 4.2.4 | 7.11.2 | | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.4_7.11.2-1.zip> | | 4.2.4 | 7.10.2 | 1.13.2 | <https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.4_7.10.2-1.zip> | From 30cc0348a79c6b27d7c8261fdc23abb1e13ac399 Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Thu, 16 Dec 2021 12:09:11 +0100 Subject: [PATCH 361/493] fix wrong deamons in filter list (#3710) * fix wrong deamons in filter list * add changelog Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 3 ++- .../management/components/management/mg-logs/logs.js | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7068fa57ea..304663dea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,7 +98,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) -- Fixing bug when create filename with spaces and throws a bad error [#3724] (https://github.com/wazuh/wazuh-kibana-app/pull/3724) +- Fix wrong deamons in filter list [#3710](https://github.com/wazuh/wazuh-kibana-app/pull/3710) +- Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index 0afe4df99e..6c58b12546 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -139,13 +139,19 @@ export default compose( } async initDaemonsList(logsPath) { + const daemonsNotIncluded = [ + 'wazuh-modulesd:task-manager', + 'wazuh-modulesd:agent-upgrade' + ] try { const path = logsPath + '/summary'; const data = await WzRequest.apiReq('GET', path, {}); const formattedData = (((data || {}).data || {}).data || {}).affected_items; const daemonsList = [...['all']]; for (const daemon of formattedData) { - daemonsList.push(Object.keys(daemon)[0]); + if(!daemonsNotIncluded.includes(Object.keys(daemon)[0])){ + daemonsList.push(Object.keys(daemon)[0]); + } } this.setState({ daemonsList }); } catch (error) { @@ -186,7 +192,7 @@ export default compose( const { logsPath } = this.state; let result = ''; let totalItems = 0; - + try { const tmpResult = await WzRequest.apiReq('GET', logsPath, { params: this.buildFilters(customOffset), From 8ef80ca54833f50ff20c7a5efb7e76e928e4e827 Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Thu, 16 Dec 2021 12:18:18 +0100 Subject: [PATCH 362/493] add target blank in link table (#3732) * add target blank in link table * add changelog Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 3 ++- package.json | 1 + public/components/common/util/markdown/markdown.tsx | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 304663dea2..278b62eb12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +99,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) - Fix wrong deamons in filter list [#3710](https://github.com/wazuh/wazuh-kibana-app/pull/3710) -- Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) +- Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) +- Fixing redirect to new tab when click in a link [#3732](https://github.com/wazuh/wazuh-kibana-app/pull/3732) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/package.json b/package.json index a5e460df86..5065564d2d 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "json2csv": "^4.1.2", "jwt-decode": "^2.2.0", "loglevel": "^1.7.1", + "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", "pdfmake": "0.1.65", diff --git a/public/components/common/util/markdown/markdown.tsx b/public/components/common/util/markdown/markdown.tsx index 9dc3d5d100..8821d78a37 100644 --- a/public/components/common/util/markdown/markdown.tsx +++ b/public/components/common/util/markdown/markdown.tsx @@ -11,6 +11,7 @@ */ import React from 'react'; import MarkdownIt from 'markdown-it'; +import MarkdownItLinkAttributes from 'markdown-it-link-attributes'; import classnames from 'classnames'; const md = new MarkdownIt({ @@ -18,6 +19,10 @@ const md = new MarkdownIt({ linkify: true, breaks: true, typographer: true +}).use(MarkdownItLinkAttributes, { + attrs: { + target: '_blank' + } }); interface MarkdownProps{ From 857690a28e2fdd43a86996c5532d1ccf1b2968ef Mon Sep 17 00:00:00 2001 From: Ezequiel Airaudo <36004787+eze9252@users.noreply.github.com> Date: Thu, 16 Dec 2021 12:23:58 +0100 Subject: [PATCH 363/493] Bugfix/3719 security user nonexistant unsubmitted changes warn (#3731) * fix flyout * fix changelog Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 1 + public/components/security/users/components/edit-user.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278b62eb12..169e3af91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) - Fix wrong deamons in filter list [#3710](https://github.com/wazuh/wazuh-kibana-app/pull/3710) - Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) +- Fixing bug in security User flyout nonexistant unsubmitted changes warning [#3731](https://github.com/wazuh/wazuh-kibana-app/pull/3731) - Fixing redirect to new tab when click in a link [#3732](https://github.com/wazuh/wazuh-kibana-app/pull/3732) ## Wazuh v4.2.4 - Kibana 7.10.2 , 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/security/users/components/edit-user.tsx b/public/components/security/users/components/edit-user.tsx index 67edebfd98..e6a36c416a 100644 --- a/public/components/security/users/components/edit-user.tsx +++ b/public/components/security/users/components/edit-user.tsx @@ -223,7 +223,7 @@ export const EditUser = ({ currentUser, closeFlyout, rolesObject }) => { useEffect(() => { if ( initialPassword != password || initialPassword != confirmPassword || - !_.isEqual(userRolesFormatted, selectedRoles) || allowRunAs + !_.isEqual(userRolesFormatted, selectedRoles) || allowRunAs != currentUser.allow_run_as ) { setHasChanges(true); } else { From 51db18f2a0c9067d8848157f3e80ccb60059e5e2 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 17 Dec 2021 09:58:20 +0100 Subject: [PATCH 364/493] [FIX] [Monitoring] Retrieve data of all agents (#3728) * fix(monitoring): retrieve data of all agents - Fixed the limit of results to retrieve to 500. - This logic could be improved with select data and increase the limit of results. See the commint included in this commit * changelog: Add PR to changelog --- CHANGELOG.md | 2 +- server/start/monitoring/index.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b5ae7b26..3e44237054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) -- Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) +- Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) [#3728](https://github.com/wazuh/wazuh-kibana-app/pull/3728) ### Fixed diff --git a/server/start/monitoring/index.ts b/server/start/monitoring/index.ts index ecbdd0f108..00bc47c586 100644 --- a/server/start/monitoring/index.ts +++ b/server/start/monitoring/index.ts @@ -467,11 +467,26 @@ async function fetchAllAgentsFromApiHost(context, apiHost){ let payload = { offset: 0, + limit: 500, q: 'id!=000' }; while (agents.length < agentsCount && payload.offset < agentsCount) { try{ + /* + TODO: Improve the performance of request with: + - Reduce the number of requests to the Wazuh API + - Reduce (if possible) the quantity of data to index by document + + Requirements: + - Research about the neccesary data to index. + + How to do: + - Wazuh API request: + - select the required data to retrieve depending on is required to index (using the `select` query param) + - increase the limit of results to retrieve (currently, the requests use the recommended value: 500). + See the allowed values. This depends on the selected data because the response could fail if contains a lot of data + */ const responseAgents = await context.wazuh.api.client.asInternalUser.request( 'GET', `/agents`, From 5f305c9d6f3545c1707d5fb76532be365beeff57 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 17 Dec 2021 11:19:40 +0100 Subject: [PATCH 365/493] [FIX] Missing settings in `Management/Configuration/Global configuration/Global/Main settings` (#3737) * fix: Fixed missing settings in Management/Configuration * changelog: Add PR to changelog --- CHANGELOG.md | 1 + .../global-configuration/global-configuration-global.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e44237054..58e48f042d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) - Fixing bug in security User flyout nonexistant unsubmitted changes warning [#3731](https://github.com/wazuh/wazuh-kibana-app/pull/3731) - Fixing redirect to new tab when click in a link [#3732](https://github.com/wazuh/wazuh-kibana-app/pull/3732) +- Fixed missing settings in `Management/Configuration/Global configuration/Global/Main settings` [#3737](https://github.com/wazuh/wazuh-kibana-app/pull/3737) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/management/components/management/configuration/global-configuration/global-configuration-global.js b/public/controllers/management/components/management/configuration/global-configuration/global-configuration-global.js index 4b69a8af15..b994fb1512 100644 --- a/public/controllers/management/components/management/configuration/global-configuration/global-configuration-global.js +++ b/public/controllers/management/components/management/configuration/global-configuration/global-configuration-global.js @@ -104,7 +104,8 @@ class WzConfigurationGlobalConfigurationGlobal extends Component { agent.id === '000' && currentConfig['analysis-global'] && currentConfig['analysis-global'].global && - currentConfig['analysis-global'].logging + currentConfig['com-logging'] && + currentConfig['com-logging'].logging ? { ...currentConfig['analysis-global'].global, plain: currentConfig['com-logging'].logging.plain, From 8079db211dbd71f7cc17f035bbcba1ca1daa5be6 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 17 Dec 2021 11:22:44 +0100 Subject: [PATCH 366/493] [FIX] `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List (#3738) * fix: Fixed call stack size exceeded exportingo to CSV a key-value pairs of CDB List * changelog: Add PR to changelog --- CHANGELOG.md | 1 + .../management/components/management/ruleset/list-editor.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e48f042d..01963d9d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixing bug in security User flyout nonexistant unsubmitted changes warning [#3731](https://github.com/wazuh/wazuh-kibana-app/pull/3731) - Fixing redirect to new tab when click in a link [#3732](https://github.com/wazuh/wazuh-kibana-app/pull/3732) - Fixed missing settings in `Management/Configuration/Global configuration/Global/Main settings` [#3737](https://github.com/wazuh/wazuh-kibana-app/pull/3737) +- Fixed `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List [#3738](https://github.com/wazuh/wazuh-kibana-app/pull/3738) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/management/components/management/ruleset/list-editor.js b/public/controllers/management/components/management/ruleset/list-editor.js index d8e576d429..e559d6ac30 100644 --- a/public/controllers/management/components/management/ruleset/list-editor.js +++ b/public/controllers/management/components/management/ruleset/list-editor.js @@ -536,7 +536,7 @@ class WzListEditor extends Component { const addingNew = name === false || !name; const listName = this.state.newListName || name; - const exportCsv = async () => { + const exportToCsv = async () => { try { this.setState({ generatingCsv: true }); await exportCsv( @@ -558,7 +558,7 @@ class WzListEditor extends Component { this.setState({ generatingCsv: false }); } catch (error) { const options = { - context: `${WzListEditor.name}.exportCsv`, + context: `${WzListEditor.name}.exportToCsv`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { @@ -590,7 +590,7 @@ class WzListEditor extends Component { iconType="exportAction" isDisabled={this.state.generatingCsv} isLoading={this.state.generatingCsv} - onClick={async () => exportCsv()} + onClick={async () => exportToCsv()} > Export formatted </EuiButtonEmpty> From 92ef3369daba52a45b1b046832245f21cca74cd9 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 17 Dec 2021 11:26:18 +0100 Subject: [PATCH 367/493] [FIX] [Styles] Fix filter button styles of Inventory and height `Evolution` card with no data (#3733) * fix: Fixed problem with the filter button of property values in the details flyout for `Modules/Integrity monitoring/Inventory` and `Modules/Vulnerabilities/Inventory` - Styles: - Use position:absolute to ignore the element flow and move others elements - Added a background color (light and dark theme) to the button to contrast with the text when the button is overlapping to the text. - Behavior: - Fixed the filter button wasn't displayed for any properties * fix: Fixed the height of card of Evolution visualization when there is no data to match the height of another cards in the same row * changelog: Add PR to changelog * fix(agent-evolution): Fixed style margin when data is provided * Delete vcs.xml rollback * Delete workspace.xml rollback Co-authored-by: gabiwassan <gabriel.wassan@wazuh.com> --- CHANGELOG.md | 2 ++ public/components/agents/fim/inventory/fileDetail.tsx | 4 ++-- public/components/agents/vuls/inventory/detail.tsx | 4 ++-- public/controllers/agent/components/agents-preview.js | 4 ++-- public/styles/dark_theme/wz_theme_dark.scss | 6 ++++++ public/styles/inventory.scss | 5 ++++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01963d9d64..449a35b842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed blank screen in Kibana 7.10.2 [#3700](https://github.com/wazuh/wazuh-kibana-app/pull/3700) - Fixed related decoder link undefined parameters error [#3704](https://github.com/wazuh/wazuh-kibana-app/pull/3704) - Fixing the bug of index patterns in health-check due to bad copy of a PR [#3707](https://github.com/wazuh/wazuh-kibana-app/pull/3707) +- Fixed styles and behaviour of button filter in the flyout of `Inventory` section for `Integrity monitoring` and `Vulnerabilities` modules [#3733](https://github.com/wazuh/wazuh-kibana-app/pull/3733) +- Fixed height of `Evolution` card in the `Agents` section when has no data for the selected time range [#3733](https://github.com/wazuh/wazuh-kibana-app/pull/3733) - Fix clearing the query filter doesn't update the data in Office 365 and GitHub Panel tab [#3722](https://github.com/wazuh/wazuh-kibana-app/pull/3722) - Fix wrong deamons in filter list [#3710](https://github.com/wazuh/wazuh-kibana-app/pull/3710) - Fixing bug when create filename with spaces and throws a bad error [#3724](https://github.com/wazuh/wazuh-kibana-app/pull/3724) diff --git a/public/components/agents/fim/inventory/fileDetail.tsx b/public/components/agents/fim/inventory/fileDetail.tsx index 334f653028..c3116aa33b 100644 --- a/public/components/agents/fim/inventory/fileDetail.tsx +++ b/public/components/agents/fim/inventory/fileDetail.tsx @@ -290,14 +290,14 @@ export class FileDetails extends Component { <span className={className} onMouseEnter={() => { - this.setState({ hoverAddFilter: item }); + this.setState({ hoverAddFilter: item.field }); }} onMouseLeave={() => { this.setState({ hoverAddFilter: '' }); }} > {value} - {_.isEqual(this.state.hoverAddFilter, item) && ( + {this.state.hoverAddFilter === item.field && ( <EuiToolTip position="top" anchorClassName="detail-tooltip" diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index c23788ed0c..efcdbb415d 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -219,14 +219,14 @@ export class Details extends Component { <span className={className} onMouseEnter={() => { - this.setState({ hoverAddFilter: item }); + this.setState({ hoverAddFilter: item.field }); }} onMouseLeave={() => { this.setState({ hoverAddFilter: '' }); }} > {value} - {_.isEqual(this.state.hoverAddFilter, item) && ( + {this.state.hoverAddFilter === item.field && ( <EuiToolTip position="top" anchorClassName="detail-tooltip" diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index ead8ec5c6d..2bc05e5e54 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -346,7 +346,7 @@ export const AgentsPreview = compose( className="agents-evolution-visualization" style={{ display: !this.state.loading ? 'block' : 'none', - height: !this.state.loading ? '182px' : 0, + margin: !this.state.loading ? '12px' : 0, }} > <EuiCard @@ -380,7 +380,7 @@ export const AgentsPreview = compose( paddingSize="none" betaBadgeLabel="Evolution" style={{ - height: 180, + height: 193, display: this.props.resultState === 'none' ? 'block' : 'none', }} > diff --git a/public/styles/dark_theme/wz_theme_dark.scss b/public/styles/dark_theme/wz_theme_dark.scss index 01f07decda..8f665c83b8 100644 --- a/public/styles/dark_theme/wz_theme_dark.scss +++ b/public/styles/dark_theme/wz_theme_dark.scss @@ -382,6 +382,12 @@ md-divider.md-default-theme, md-divider { border-top: 1px solid #343741!important; } +.wz-inventory{ + .detail-tooltip { + background-color: #16171c; + } +} + .flyout-body .euiAccordion { border-bottom: 1px solid #343741!important; } diff --git a/public/styles/inventory.scss b/public/styles/inventory.scss index 9e9ae54239..5abca1a158 100644 --- a/public/styles/inventory.scss +++ b/public/styles/inventory.scss @@ -35,6 +35,7 @@ margin-left: 36px; float: left; white-space: break-spaces; + position: relative; } .buttonAddFilter { @@ -53,7 +54,9 @@ } .detail-tooltip { - float: right; + position: absolute; + right: 36px; + background-color: #fcfdfe; } .application .euiAccordion, From ea47ca0db6e4aeee7ba2a7e2a20e98264cbe2024 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <matiasmoreno876@gmail.com> Date: Mon, 20 Dec 2021 06:25:55 -0300 Subject: [PATCH 368/493] fix regex lookahead and lookbehind for safari (#3741) * fix regex lookahead and lookbehind for safari * changelog * changelog Co-authored-by: Matias Ezequiel Moreno <matiasezequielmoreno@MacBook-Pro-de-Matias.local> --- CHANGELOG.md | 1 + .../common/custom-search-bar/components/multi-select.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a00c12ab92..eaee2ad0b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixing redirect to new tab when click in a link [#3732](https://github.com/wazuh/wazuh-kibana-app/pull/3732) - Fixed missing settings in `Management/Configuration/Global configuration/Global/Main settings` [#3737](https://github.com/wazuh/wazuh-kibana-app/pull/3737) - Fixed `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List [#3738](https://github.com/wazuh/wazuh-kibana-app/pull/3738) +- Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/custom-search-bar/components/multi-select.tsx b/public/components/common/custom-search-bar/components/multi-select.tsx index 7eabcb3ce1..9c92418900 100644 --- a/public/components/common/custom-search-bar/components/multi-select.tsx +++ b/public/components/common/custom-search-bar/components/multi-select.tsx @@ -91,7 +91,12 @@ export const MultiSelect = ({ }, [suggestedValues, isLoading]); const setSelectedKey = (item: Item) => { - return parseInt(item.label.match(/(?<=\[).+?(?=\])/g) || item.key); + const label = item.label.match(/\[.+?\]/g); + if (label) { + return parseInt(label[0].substring(1, label[0].length - 1)); + } else { + return parseInt(item.key); + } }; const buildSelectedOptions = () => { From ebb5c08453dc93648f5189748996f680bed30756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 20 Dec 2021 12:50:23 +0100 Subject: [PATCH 369/493] fix: styles of filter button in the details flyout in Vulnerabilities/Inventory --- public/components/agents/vuls/inventory/detail.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index efcdbb415d..937a990831 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -209,6 +209,7 @@ export class Details extends Component { if (!item.onlyLinux || (item.onlyLinux && this.props.agent && agentPlatform !== 'windows')) { let className = item.checksum ? 'detail-value detail-value-checksum' : 'detail-value'; className += item.field === 'perm' ? ' detail-value-perm' : ''; + className += ' wz-width-100'; return ( <EuiFlexItem key={idx}> <EuiStat From 7b646bd278c6baf0de48d0d46809ee3464fae238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 20 Dec 2021 12:53:06 +0100 Subject: [PATCH 370/493] fix: update endpoints and security actions with the data of Wazuh API v4.3.0-rc1 --- common/api-info/endpoints.json | 1200 ++++++++++++++++++++----- common/api-info/security-actions.json | 3 +- 2 files changed, 992 insertions(+), 211 deletions(-) diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index 33c88f3a3f..0009280df5 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -7,7 +7,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.default_controller.default_info", "description": "Return basic information about the API", "summary": "Get API info", - "tags": ["API Info"], + "tags": [ + "API Info" + ], "query": [ { "name": "pretty", @@ -24,7 +26,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agents", "description": "Return information about all available agents or a list of them", "summary": "List agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -190,7 +194,12 @@ "type": "array", "items": { "type": "string", - "enum": ["active", "pending", "never_connected", "disconnected"] + "enum": [ + "active", + "pending", + "never_connected", + "disconnected" + ] }, "minItems": 1 } @@ -218,7 +227,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_config", "description": "Return the active configuration the agent is currently using. This can be different from the configuration present in the configuration file, if it has been modified and the agent has not been restarted yet", "summary": "Get active configuration", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -313,7 +324,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_sync_agent", "description": "Return whether the agent configuration has been synchronized with the agent or not. This can be useful to check after updating a group configuration", "summary": "Get configuration sync status", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -351,7 +364,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_key", "description": "Return the key of an agent", "summary": "Get key", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -389,7 +404,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_component_stats", "description": "Return Wazuh's {component} statistical information from agent {agent_id}", "summary": "Get agent's component stats", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -408,7 +425,10 @@ "required": true, "schema": { "type": "string", - "enum": ["logcollector", "agent"] + "enum": [ + "logcollector", + "agent" + ] } } ], @@ -436,7 +456,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_no_group", "description": "Return a list with all the available agents without an assigned group", "summary": "List agents without group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "limit", @@ -516,7 +538,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_outdated", "description": "Return the list of outdated agents", "summary": "List outdated agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "limit", @@ -585,7 +609,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_fields", "description": "Return all the different combinations that agents have for the selected fields. It also indicates the total number of agents that have each combination", "summary": "List agents distinct", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "fields", @@ -665,7 +691,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_summary_os", "description": "Return a summary of the OS of available agents", "summary": "Summarize agents OS", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "pretty", @@ -690,7 +718,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_summary_status", "description": "Return a summary of the status of available agents", "summary": "Summarize agents status", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "pretty", @@ -715,7 +745,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agent_upgrade", "description": "Return the agents upgrade results", "summary": "Get upgrade results", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -730,6 +762,71 @@ } } }, + { + "name": "group", + "description": "Filter by group of agents", + "schema": { + "type": "string", + "description": "Group name", + "format": "group_names" + } + }, + { + "name": "ip", + "description": "Filter by the IP used by the agent to communicate with the manager. If it's not available, it will have the same value as registerIP", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "manager", + "description": "Filter by manager hostname where agents are connected to", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "name", + "description": "Filter by name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "node_name", + "description": "Filter by node name", + "schema": { + "type": "string", + "format": "names" + } + }, + { + "name": "os.name", + "description": "Filter by OS name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.platform", + "description": "Filter by OS platform", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.version", + "description": "Filter by OS version", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, { "name": "pretty", "description": "Show results in human-readable format", @@ -738,6 +835,29 @@ "default": false } }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "registerIP", + "description": "Filter by the IP used when registering the agent", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "version", + "description": "Filter by agents version", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, { "name": "wait_for_complete", "description": "Disable timeout response", @@ -753,7 +873,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.ciscat_controller.get_agents_ciscat_results", "description": "Return the agent's ciscat results info", "summary": "Get results", - "tags": ["Ciscat"], + "tags": [ + "Ciscat" + ], "args": [ { "name": ":agent_id", @@ -915,7 +1037,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_configuration_node", "description": "Return wazuh configuration used in node {node_id}. The 'section' and 'field' parameters will be ignored if 'raw' parameter is provided.", "summary": "Get node config", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1008,7 +1132,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_node_config", "description": "Return the requested configuration in JSON format for the specified node", "summary": "Get node active configuration", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":component", @@ -1101,7 +1227,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_info_node", "description": "Return basic information about a specified node such as version, compilation date, installation path", "summary": "Get node info", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1137,7 +1265,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_log_node", "description": "Return the last 2000 wazuh log entries in the specified node", "summary": "Get node logs", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1155,7 +1285,14 @@ "description": "Filter by log level", "schema": { "type": "string", - "enum": ["critical", "debug", "debug2", "error", "info", "warning"] + "enum": [ + "critical", + "debug", + "debug2", + "error", + "info", + "warning" + ] } }, { @@ -1264,7 +1401,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_log_summary_node", "description": "Return a summary of the last 2000 wazuh log entries in the specified node", "summary": "Get node logs summary", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1300,7 +1439,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_node", "description": "Return Wazuh statistical information in node {node_id} for the current or specified date", "summary": "Get node stats", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1344,7 +1485,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_analysisd_node", "description": "Return Wazuh analysisd statistical information in node {node_id}", "summary": "Get node stats analysisd", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1380,7 +1523,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_hourly_node", "description": "Return Wazuh statistical information in node {node_id} per hour. Each number in the averages field represents the average of alerts per hour", "summary": "Get node stats hour", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1416,7 +1561,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_remoted_node", "description": "Return Wazuh remoted statistical information in node {node_id}", "summary": "Get node stats remoted", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1452,7 +1599,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_stats_weekly_node", "description": "Return Wazuh statistical information in node {node_id} per week. Each number in the averages field represents the average of alerts per hour for that specific day", "summary": "Get node stats week", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1488,7 +1637,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_status_node", "description": "Return the status of all Wazuh daemons in node node_id", "summary": "Get node status", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -1524,7 +1675,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_api_config", "description": "Return the API configuration of all nodes (or a list of them) in JSON format", "summary": "Get nodes API config", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "nodes_list", @@ -1559,7 +1712,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_conf_validation", "description": "Return whether the Wazuh configuration is correct or not in all cluster nodes or a list of them", "summary": "Check nodes config", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "nodes_list", @@ -1594,7 +1749,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_healthcheck", "description": "Return cluster healthcheck information for all nodes or a list of them. Such information includes last keep alive, last synchronization time and number of agents reporting on each node", "summary": "Get nodes healthcheck", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "nodes_list", @@ -1629,7 +1786,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_config", "description": "Return the current node cluster configuration", "summary": "Get local node config", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "pretty", @@ -1654,7 +1813,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_cluster_node", "description": "Return basic information about the cluster node receiving the request", "summary": "Get local node info", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "pretty", @@ -1679,7 +1840,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_cluster_nodes", "description": "Get information about all nodes in the cluster or a list of them", "summary": "Get nodes info", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "limit", @@ -1759,7 +1922,10 @@ "description": "Filter by node type", "schema": { "type": "string", - "enum": ["worker", "master"] + "enum": [ + "worker", + "master" + ] } }, { @@ -1777,7 +1943,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.get_status", "description": "Return information about the cluster status", "summary": "Get cluster status", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "pretty", @@ -1802,7 +1970,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders", "description": "Return information about all decoders included in ossec.conf. This information include decoder's route, decoder's name, decoder's file among others", "summary": "List decoders", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "query": [ { "name": "decoder_names", @@ -1902,7 +2072,11 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": ["enabled", "disabled", "all"], + "enum": [ + "enabled", + "disabled", + "all" + ], "minItems": 1 } }, @@ -1921,7 +2095,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders_files", "description": "Return information about all decoders files used in Wazuh. This information include decoder's file, decoder's route and decoder's status among others", "summary": "Get files", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "query": [ { "name": "filename", @@ -1992,7 +2168,11 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": ["enabled", "disabled", "all"], + "enum": [ + "enabled", + "disabled", + "all" + ], "minItems": 1 } }, @@ -2011,7 +2191,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_file", "description": "Get the content of a specified decoder file", "summary": "Get decoders file content", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "args": [ { "name": ":filename", @@ -2055,7 +2237,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.get_decoders_parents", "description": "Return information about all parent decoders. A parent decoder is a decoder used as base of other decoders", "summary": "Get parent decoders", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "query": [ { "name": "limit", @@ -2128,7 +2312,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_cis_cat_results", "description": "Return CIS-CAT results for all agents or a list of them", "summary": "Get agents CIS-CAT results", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -2283,7 +2469,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_hardware_info", "description": "Return all agents (or a list of them) hardware info. This information include cpu, ram, scan info among others of all agents", "summary": "Get agents hardware", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -2420,7 +2608,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_hotfixes_info", "description": "Return all agents (or a list of them) hotfixes info", "summary": "Get agents hotfixes", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -2513,7 +2703,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_address_info", "description": "Return all agents (or a list of them) IPv4 and IPv6 addresses associated to their network interfaces. This information include used IP protocol, interface, and IP address among others", "summary": "Get agents netaddr", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "address", @@ -2631,7 +2823,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_interface_info", "description": "Return all agents (or a list of them) network interfaces. This information includes rx, scan, tx info and some network information among other", "summary": "Get agents netiface", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "adapter", @@ -2830,7 +3024,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_network_protocol_info", "description": "Return all agents (or a list of them) routing configuration for each network interface. This information includes interface, type protocol information among other", "summary": "Get agents netproto", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -2851,7 +3047,12 @@ "schema": { "type": "string", "description": "DHCP status", - "enum": ["enabled", "disabled", "unknown", "BOOTP"] + "enum": [ + "enabled", + "disabled", + "unknown", + "BOOTP" + ] } }, { @@ -2949,7 +3150,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_os_info", "description": "Return all agents (or a list of them) OS info. This information includes os information, architecture information among other", "summary": "Get agents OS", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -3075,7 +3278,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_packages_info", "description": "Return all agents (or a list of them) packages info. This information includes name, section, size, and priority information of all packages among other", "summary": "Get agents packages", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -3199,7 +3404,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_ports_info", "description": "Return all agents (or a list of them) ports info. This information includes local IP, Remote IP, protocol information among other", "summary": "Get agents ports", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -3349,7 +3556,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.get_processes_info", "description": "Return all agents (or a list of them) processes info", "summary": "Get agents processes", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -3547,7 +3756,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_list_group", "description": "Get information about all groups or a list of them. Returns a list containing basic information about each group such as number of agents belonging to the group and the checksums of the configuration and shared files", "summary": "Get groups", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "query": [ { "name": "groups_list", @@ -3642,7 +3853,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_agents_in_group", "description": "Return the list of agents that belong to the specified group", "summary": "Get agents in a group", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":group_id", @@ -3726,7 +3939,12 @@ "type": "array", "items": { "type": "string", - "enum": ["active", "pending", "never_connected", "disconnected"] + "enum": [ + "active", + "pending", + "never_connected", + "disconnected" + ] }, "minItems": 1 } @@ -3746,7 +3964,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_config", "description": "Return the group configuration defined in the `agent.conf` file", "summary": "Get group configuration", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":group_id", @@ -3804,7 +4024,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_files", "description": "Return the files placed under the group directory", "summary": "Get group files", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":group_id", @@ -3899,7 +4121,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_json", "description": "Return the content of the specified group file parsed to JSON", "summary": "Get a file in group", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":file_name", @@ -3937,7 +4161,12 @@ "type": "array", "items": { "type": "string", - "enum": ["conf", "rootkit_files", "rootkit_trojans", "rcl"] + "enum": [ + "conf", + "rootkit_files", + "rootkit_trojans", + "rcl" + ] } } }, @@ -3956,7 +4185,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.get_group_file_xml", "description": "Return the contents of the specified group file parsed to XML", "summary": "Get a file in group", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":file_name", @@ -3994,7 +4225,12 @@ "type": "array", "items": { "type": "string", - "enum": ["conf", "rootkit_files", "rootkit_trojans", "rcl"] + "enum": [ + "conf", + "rootkit_files", + "rootkit_trojans", + "rcl" + ] } } }, @@ -4013,7 +4249,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_lists", "description": "Return the contents of all CDB lists. Optionally, the result can be filtered by several criteria. See available parameters for more details", "summary": "Get CDB lists info", - "tags": ["Lists"], + "tags": [ + "Lists" + ], "query": [ { "name": "filename", @@ -4105,7 +4343,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_lists_files", "description": "Return the path from all CDB lists. Use this method to know all the CDB lists and their location in the filesystem relative to Wazuh installation folder", "summary": "Get CDB lists files", - "tags": ["Lists"], + "tags": [ + "Lists" + ], "query": [ { "name": "filename", @@ -4186,7 +4426,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.get_file", "description": "Return the content of a CDB list file. Only the filename can be specified. It will be searched recursively if not found", "summary": "Get CDB list file content", - "tags": ["Lists"], + "tags": [ + "Lists" + ], "args": [ { "name": ":filename", @@ -4230,7 +4472,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_api_config", "description": "Return the local API configuration in JSON format", "summary": "Get API config", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4255,7 +4499,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_configuration", "description": "Return wazuh configuration used. The 'section' and 'field' parameters will be ignored if 'raw' parameter is provided.", "summary": "Get configuration", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "field", @@ -4337,7 +4583,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_manager_config_ondemand", "description": "Return the requested active configuration in JSON format", "summary": "Get active configuration", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "args": [ { "name": ":component", @@ -4421,7 +4669,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_conf_validation", "description": "Return whether the Wazuh configuration is correct", "summary": "Check config", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4446,7 +4696,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_info", "description": "Return basic information such as version, compilation date, installation path", "summary": "Get information", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4471,14 +4723,23 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_log", "description": "Return the last 2000 wazuh log entries", "summary": "Get logs", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "level", "description": "Filter by log level", "schema": { "type": "string", - "enum": ["critical", "debug", "debug2", "error", "info", "warning"] + "enum": [ + "critical", + "debug", + "debug2", + "error", + "info", + "warning" + ] } }, { @@ -4587,7 +4848,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_log_summary", "description": "Return a summary of the last 2000 wazuh log entries", "summary": "Get logs summary", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4612,7 +4875,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats", "description": "Return Wazuh statistical information for the current or specified date", "summary": "Get stats", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "date", @@ -4645,7 +4910,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_analysisd", "description": "Return Wazuh analysisd statistical information", "summary": "Get stats analysisd", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4670,7 +4937,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_hourly", "description": "Return Wazuh statistical information per hour. Each number in the averages field represents the average of alerts per hour", "summary": "Get stats hour", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4695,7 +4964,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_remoted", "description": "Return Wazuh remoted statistical information", "summary": "Get stats remoted", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4720,7 +4991,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_stats_weekly", "description": "Return Wazuh statistical information per week. Each number in the averages field represents the average of alerts per hour for that specific day", "summary": "Get stats week", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4745,7 +5018,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.get_status", "description": "Return the status of all Wazuh daemons", "summary": "Get status", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -4770,7 +5045,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_groups", "description": "Return the groups from MITRE database", "summary": "Get MITRE groups", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "group_ids", @@ -4861,7 +5138,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_metadata", "description": "Return the metadata from MITRE database", "summary": "Get MITRE metadata", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "pretty", @@ -4886,7 +5165,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_mitigations", "description": "Return the mitigations from MITRE database", "summary": "Get MITRE mitigations", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "limit", @@ -4977,7 +5258,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_references", "description": "Return the references from MITRE database", "summary": "Get MITRE references", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "limit", @@ -5068,7 +5351,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_software", "description": "Return the software from MITRE database", "summary": "Get MITRE software", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "limit", @@ -5159,8 +5444,10 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_tactics", "description": "Return the tactics from MITRE database", "summary": "Get MITRE tactics", - "tags": ["MITRE"], - "query": [ + "tags": [ + "MITRE" + ], + "query": [ { "name": "limit", "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", @@ -5250,7 +5537,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.mitre_controller.get_techniques", "description": "Return the techniques from MITRE database", "summary": "Get MITRE techniques", - "tags": ["MITRE"], + "tags": [ + "MITRE" + ], "query": [ { "name": "limit", @@ -5341,7 +5630,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.overview_controller.get_overview_agents", "description": "Return a dictionary with a full agents overview", "summary": "Get agents overview", - "tags": ["Overview"], + "tags": [ + "Overview" + ], "query": [ { "name": "pretty", @@ -5366,7 +5657,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.get_rootcheck_agent", "description": "Return the rootcheck database of an agent", "summary": "Get results", - "tags": ["Rootcheck"], + "tags": [ + "Rootcheck" + ], "args": [ { "name": ":agent_id", @@ -5491,7 +5784,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.get_last_scan_agent", "description": "Return the timestamp of the last rootcheck scan of an agent", "summary": "Get last scan datetime", - "tags": ["Rootcheck"], + "tags": [ + "Rootcheck" + ], "args": [ { "name": ":agent_id", @@ -5529,7 +5824,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules", "description": "Return a list containing information about each rule such as file where it's defined, description, rule group, status, etc", "summary": "List rules", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "query": [ { "name": "filename", @@ -5694,7 +5991,11 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": ["enabled", "disabled", "all"], + "enum": [ + "enabled", + "disabled", + "all" + ], "minItems": 1 } }, @@ -5721,7 +6022,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_files", "description": "Return a list containing all files used to define rules and their status", "summary": "Get files", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "query": [ { "name": "filename", @@ -5792,7 +6095,11 @@ "description": "Filter by list status. Use commas to enter multiple statuses", "schema": { "type": "string", - "enum": ["enabled", "disabled", "all"], + "enum": [ + "enabled", + "disabled", + "all" + ], "minItems": 1 } }, @@ -5811,7 +6118,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_file", "description": "Get the content of a specified rule in the ruleset", "summary": "Get rules file content", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "args": [ { "name": ":filename", @@ -5855,7 +6164,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_groups", "description": "Return a list containing all rule groups names", "summary": "Get groups", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "query": [ { "name": "limit", @@ -5917,14 +6228,24 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.get_rules_requirement", "description": "Return all specified requirement names defined in the Wazuh ruleset", "summary": "Get requirements", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "args": [ { "name": ":requirement", "required": true, "schema": { "type": "string", - "enum": ["pci_dss", "gdpr", "hipaa", "nist-800-53", "gpg13", "tsc", "mitre"] + "enum": [ + "pci_dss", + "gdpr", + "hipaa", + "nist-800-53", + "gpg13", + "tsc", + "mitre" + ] } } ], @@ -5989,7 +6310,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.sca_controller.get_sca_agent", "description": "Return the security SCA database of an agent", "summary": "Get results", - "tags": ["SCA"], + "tags": [ + "SCA" + ], "args": [ { "name": ":agent_id", @@ -6093,7 +6416,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.sca_controller.get_sca_checks", "description": "Return the policy monitoring alerts for a given policy", "summary": "Get policy checks", - "tags": ["SCA"], + "tags": [ + "SCA" + ], "args": [ { "name": ":agent_id", @@ -6293,7 +6618,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rbac_actions", "description": "Get all RBAC actions, including the potential related resources and endpoints.", "summary": "List RBAC actions", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "endpoint", @@ -6317,7 +6644,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_security_config", "description": "Return the security configuration in JSON format", "summary": "Get security config", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -6342,7 +6671,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_policies", "description": "Get all policies in the system, including the administrator policy", "summary": "List policies", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "limit", @@ -6427,7 +6758,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rbac_resources", "description": "This method should be called to get all current defined RBAC resources.", "summary": "List RBAC resources", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -6464,7 +6797,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_roles", "description": "For a specific list, indicate the ids separated by commas. Example: ?role_ids=1,2,3", "summary": "List roles", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "limit", @@ -6549,7 +6884,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_rules", "description": "Get a list of security rules from the system or all of them. These rules must be mapped with roles to obtain certain access privileges. For a specific list, indicate the ids separated by commas. Example: ?rule_ids=1,2,3", "summary": "List security rules", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "limit", @@ -6634,7 +6971,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.login_user", "description": "This method should be called to get an API token. This token will expire after auth_token_exp_timeout seconds (default: 900). This value can be changed using PUT /security/config", "summary": "Login", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "raw", @@ -6651,7 +6990,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_users", "description": "Get the information of a specified user", "summary": "List users", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "limit", @@ -6736,7 +7077,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_user_me", "description": "Get the information of the current user", "summary": "Get current user info", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -6761,7 +7104,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.get_user_me_policies", "description": "Get the processed policies information for the current user", "summary": "Get current user processed policies", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -6778,7 +7123,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.get_syscheck_agent", "description": "Return FIM findings in the specified agent", "summary": "Get results", - "tags": ["Syscheck"], + "tags": [ + "Syscheck" + ], "args": [ { "name": ":agent_id", @@ -6798,7 +7145,10 @@ "description": "Filter by architecture", "schema": { "type": "string", - "enum": ["[x32]", "[x64]"] + "enum": [ + "[x32]", + "[x64]" + ] } }, { @@ -6925,7 +7275,11 @@ "description": "Filter by file type. Registry_key and registry_value types are only available in Windows agents", "schema": { "type": "string", - "enum": ["file", "registry_key", "registry_value"] + "enum": [ + "file", + "registry_key", + "registry_value" + ] } }, { @@ -6959,7 +7313,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.get_last_scan_agent", "description": "Return when the last syscheck scan started and ended. If the scan is still in progress the end date will be unknown", "summary": "Get last scan datetime", - "tags": ["Syscheck"], + "tags": [ + "Syscheck" + ], "args": [ { "name": ":agent_id", @@ -6997,7 +7353,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_hardware_info", "description": "Return the agent's hardware info. This information include cpu, ram, scan info among others", "summary": "Get agent hardware", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7046,7 +7404,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_hotfix_info", "description": "Return all hotfixes installed by Microsoft(R) in Windows(R) systems (KB... fixes)", "summary": "Get agent hotfixes", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7146,7 +7506,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_address_info", "description": "Return the agent's network address info. This information include used IP protocol, interface, IP address among others", "summary": "Get agent netaddr", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7279,7 +7641,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_interface_info", "description": "Return the agent's network interface info. This information include rx, scan, tx info and some network information among others", "summary": "Get agent netiface", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7484,7 +7848,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_network_protocol_info", "description": "Return the agent's routing configuration for each network interface", "summary": "Get agent netproto", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7505,7 +7871,12 @@ "schema": { "type": "string", "description": "DHCP status", - "enum": ["enabled", "disabled", "unknown", "BOOTP"] + "enum": [ + "enabled", + "disabled", + "unknown", + "BOOTP" + ] } }, { @@ -7610,7 +7981,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_os_info", "description": "Return the agent's OS info. This information include os information, architecture information among others of all agents", "summary": "Get agent OS", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7659,7 +8032,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_packages_info", "description": "Return the agent's packages info. This information include name, section, size, priority information of all packages among others", "summary": "Get agent packages", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7790,7 +8165,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_ports_info", "description": "Return the agent's ports info. This information include local IP, Remote IP, protocol information among others", "summary": "Get agent ports", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -7947,7 +8324,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscollector_controller.get_processes_info", "description": "Return the agent's processes info", "summary": "Get agent processes", - "tags": ["Syscollector"], + "tags": [ + "Syscollector" + ], "args": [ { "name": ":agent_id", @@ -8152,7 +8531,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.task_controller.get_tasks_status", "description": "Returns all available information about the specified tasks", "summary": "List tasks", - "tags": ["Tasks"], + "tags": [ + "Tasks" + ], "query": [ { "name": "agents_list", @@ -8289,7 +8670,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.vulnerability_controller.get_vulnerability_agent", "description": "Return the vulnerabilities of an agent", "summary": "Get vulnerabilities", - "tags": ["Vulnerability"], + "tags": [ + "Vulnerability" + ], "args": [ { "name": ":agent_id", @@ -8391,6 +8774,14 @@ } } }, + { + "name": "severity", + "description": "Filter by CVE severity", + "schema": { + "type": "string", + "format": "names" + } + }, { "name": "sort", "description": "Sort the collection by a field or fields (separated by comma). Use +/- at the beggining to list in ascending or descending order. Use '.' for nested fields. For example, '{field1: field2}' may be selected with 'field1.field2'", @@ -8399,6 +8790,29 @@ "format": "sort" } }, + { + "name": "status", + "description": "Filter by CVE status", + "schema": { + "type": "string", + "enum": [ + "valid", + "pending", + "obsolete" + ] + } + }, + { + "name": "type", + "description": "Filter by CVE type", + "schema": { + "type": "string", + "enum": [ + "os", + "package" + ] + } + }, { "name": "version", "description": "Filter by CVE version", @@ -8416,6 +8830,46 @@ } } ] + }, + { + "name": "/vulnerability/:agent_id/last_scan", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.vulnerability_controller.get_last_scan_agent", + "description": "Return when the last full and partial vulnerability scan of a specified agent ended.", + "summary": "Get last scan datetime", + "tags": [ + "Vulnerability" + ], + "args": [ + { + "name": ":agent_id", + "description": "Agent ID. All possible values from 000 onwards", + "required": true, + "schema": { + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" + } + } + ], + "query": [ + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] } ] }, @@ -8427,7 +8881,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.active_response_controller.run_command", "description": "Run an Active Response command on all agents or a list of them", "summary": "Run command", - "tags": ["Active-response"], + "tags": [ + "Active-response" + ], "query": [ { "name": "agents_list", @@ -8489,7 +8945,9 @@ } } }, - "required": ["command"] + "required": [ + "command" + ] } ] }, @@ -8498,7 +8956,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_agent_single_group", "description": "Assign an agent to a specified group", "summary": "Assign agent to group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -8553,7 +9013,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agent", "description": "Restart the specified agent", "summary": "Restart agent", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -8591,7 +9053,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_multiple_agent_single_group", "description": "Assign all agents or a list of them to the specified group", "summary": "Assign agents to group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -8646,7 +9110,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents_by_group", "description": "Restart all agents which belong to a given group", "summary": "Restart agents in group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":group_id", @@ -8683,7 +9149,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents_by_node", "description": "Restart all agents which belong to a specific given node", "summary": "Restart agents in node", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":node_id", @@ -8719,7 +9187,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.reconnect_agents", "description": "Force reconnect all agents or a list of them", "summary": "Force reconnect agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -8757,7 +9227,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.restart_agents", "description": "Restart all agents or a list of them", "summary": "Restart agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -8793,23 +9265,24 @@ { "name": "/agents/upgrade", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_upgrade_agents", - "description": "Upgrade agents using a WPK file from online repository", + "description": "Upgrade agents using a WPK file from online repository. When upgrading more than 3000 agents at the same time, it's highly recommended to use the parameter `wait_for_complete` set to `true` to avoid a possible API timeout", "summary": "Upgrade agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", - "description": "List of agent IDs (separated by comma), select a list of agents with size less or equal than 100", + "description": "List of agent IDs (separated by comma), use the keyword `all` to select all agents", "required": true, "schema": { "type": "array", "items": { "type": "string", "minLength": 3, - "description": "Agent ID", - "format": "numbers" - }, - "maxItems": 100 + "description": "Agent ID|all", + "format": "numbers_or_all" + } } }, { @@ -8820,6 +9293,71 @@ "default": false } }, + { + "name": "group", + "description": "Filter by group of agents", + "schema": { + "type": "string", + "description": "Group name", + "format": "group_names" + } + }, + { + "name": "ip", + "description": "Filter by the IP used by the agent to communicate with the manager. If it's not available, it will have the same value as registerIP", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "manager", + "description": "Filter by manager hostname where agents are connected to", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "name", + "description": "Filter by name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "node_name", + "description": "Filter by node name", + "schema": { + "type": "string", + "format": "names" + } + }, + { + "name": "os.name", + "description": "Filter by OS name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.platform", + "description": "Filter by OS platform", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.version", + "description": "Filter by OS version", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, { "name": "pretty", "description": "Show results in human-readable format", @@ -8828,6 +9366,29 @@ "default": false } }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "registerIP", + "description": "Filter by the IP used when registering the agent", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "upgrade_version", + "description": "Wazuh version to upgrade to", + "schema": { + "type": "string", + "format": "wazuh_version" + } + }, { "name": "use_http", "description": "Use http protocol. If it's false use https. By default the value is set to false", @@ -8838,9 +9399,10 @@ }, { "name": "version", - "description": "Wazuh version to upgrade to", + "description": "Filter by agents version", "schema": { - "type": "string" + "type": "string", + "format": "alphanumeric" } }, { @@ -8864,23 +9426,24 @@ { "name": "/agents/upgrade_custom", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_upgrade_custom_agents", - "description": "Upgrade the agents using a local WPK file", + "description": "Upgrade the agents using a local WPK file. When upgrading more than 3000 agents at the same time, it's highly recommended to use the parameter `wait_for_complete` set to `true` to avoid a possible API timeout", "summary": "Upgrade agents custom", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", - "description": "List of agent IDs (separated by comma), select a list of agents with size less or equal than 100", + "description": "List of agent IDs (separated by comma), use the keyword `all` to select all agents", "required": true, "schema": { "type": "array", "items": { "type": "string", "minLength": 3, - "description": "Agent ID", - "format": "numbers" - }, - "maxItems": 100 + "description": "Agent ID|all", + "format": "numbers_or_all" + } } }, { @@ -8892,6 +9455,15 @@ "format": "wazuh_path" } }, + { + "name": "group", + "description": "Filter by group of agents", + "schema": { + "type": "string", + "description": "Group name", + "format": "group_names" + } + }, { "name": "installer", "description": "Installation script. Default is <code>upgrade.sh</code> or <code>upgrade.bat</code> for windows agents", @@ -8900,6 +9472,62 @@ "format": "alphanumeric" } }, + { + "name": "ip", + "description": "Filter by the IP used by the agent to communicate with the manager. If it's not available, it will have the same value as registerIP", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "manager", + "description": "Filter by manager hostname where agents are connected to", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "name", + "description": "Filter by name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "node_name", + "description": "Filter by node name", + "schema": { + "type": "string", + "format": "names" + } + }, + { + "name": "os.name", + "description": "Filter by OS name", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.platform", + "description": "Filter by OS platform", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "os.version", + "description": "Filter by OS version", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, { "name": "pretty", "description": "Show results in human-readable format", @@ -8908,6 +9536,29 @@ "default": false } }, + { + "name": "q", + "description": "Query to filter results by. For example q="status=active"", + "schema": { + "type": "string" + } + }, + { + "name": "registerIP", + "description": "Filter by the IP used when registering the agent", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, + { + "name": "version", + "description": "Filter by agents version", + "schema": { + "type": "string", + "format": "alphanumeric" + } + }, { "name": "wait_for_complete", "description": "Disable timeout response", @@ -8923,7 +9574,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.update_configuration", "description": "Replace wazuh configuration for the given node with the data contained in the API request", "summary": "Update node configuration", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "args": [ { "name": ":node_id", @@ -8959,7 +9612,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cluster_controller.put_restart", "description": "Restart all nodes in the cluster or a list of them", "summary": "Restart nodes", - "tags": ["Cluster"], + "tags": [ + "Cluster" + ], "query": [ { "name": "nodes_list", @@ -8994,7 +9649,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.put_file", "description": "Upload or replace a user decoder file content", "summary": "Update decoders file", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "args": [ { "name": ":filename", @@ -9038,7 +9695,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.put_group_config", "description": "Update an specified group's configuration. This API call expects a full valid XML file with the shared configuration tags/syntax", "summary": "Update group configuration", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "args": [ { "name": ":group_id", @@ -9075,7 +9734,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.put_file", "description": "Replace or upload a CDB list file with the data contained in the API request", "summary": "Update CDB list file", - "tags": ["Lists"], + "tags": [ + "Lists" + ], "args": [ { "name": ":filename", @@ -9119,7 +9780,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.logtest_controller.run_logtest_tool", "description": "Run logtest tool to check if a specified log raises any alert among other information", "summary": "Run logtest", - "tags": ["Logtest"], + "tags": [ + "Logtest" + ], "query": [ { "name": "pretty", @@ -9141,7 +9804,11 @@ "body": [ { "type": "object", - "required": ["event", "log_format", "location"], + "required": [ + "event", + "log_format", + "location" + ], "properties": { "token": { "type": "string", @@ -9168,7 +9835,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.update_configuration", "description": "Replace Wazuh configuration with the data contained in the API request", "summary": "Update Wazuh configuration", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -9193,7 +9862,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.manager_controller.put_restart", "description": "Restart the wazuh manager", "summary": "Restart manager", - "tags": ["Manager"], + "tags": [ + "Manager" + ], "query": [ { "name": "pretty", @@ -9218,7 +9889,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rootcheck_controller.put_rootcheck", "description": "Run rootcheck scan in all agents or a list of them", "summary": "Run scan", - "tags": ["Rootcheck"], + "tags": [ + "Rootcheck" + ], "query": [ { "name": "agents_list", @@ -9256,7 +9929,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.put_file", "description": "Upload or replace a user ruleset file content", "summary": "Update rules file", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "args": [ { "name": ":filename", @@ -9300,7 +9975,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.put_security_config", "description": "Update the security configuration with the data contained in the API request", "summary": "Update security config", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -9334,7 +10011,10 @@ "rbac_mode": { "description": "RBAC mode (white/black)", "type": "string", - "enum": ["white", "black"], + "enum": [ + "white", + "black" + ], "example": "white" } } @@ -9346,7 +10026,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_policy", "description": "Modify a policy, at least one property must be indicated", "summary": "Update policy", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":policy_id", @@ -9410,7 +10092,11 @@ "description": "Effect of the policy" } }, - "required": ["actions", "resources", "effect"] + "required": [ + "actions", + "resources", + "effect" + ] } } } @@ -9421,7 +10107,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_role", "description": "Modify a role, cannot modify associated policies in this endpoint, at least one property must be indicated", "summary": "Update role", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":role_id", @@ -9471,7 +10159,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_rule", "description": "Modify a security rule by specifying its ID", "summary": "Update security rule", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":rule_id", @@ -9525,14 +10215,18 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.revoke_all_tokens", "description": "This method should be called to revoke all active JWT tokens", "summary": "Revoke JWT tokens", - "tags": ["Security"] + "tags": [ + "Security" + ] }, { "name": "/security/users/:user_id", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.update_user", "description": "Modify a user's password by specifying their ID", "summary": "Update users", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":user_id", @@ -9576,7 +10270,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.edit_run_as", "description": "Modify a user's allow_run_as flag by specifying their ID", "summary": "Enable/Disable run_as", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":user_id", @@ -9621,7 +10317,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.put_syscheck", "description": "Run FIM scan in all agents", "summary": "Run scan", - "tags": ["Syscheck"], + "tags": [ + "Syscheck" + ], "query": [ { "name": "agents_list", @@ -9664,7 +10362,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.add_agent", "description": "Add a new agent", "summary": "Add agent", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "pretty", @@ -9701,9 +10401,11 @@ { "name": "/agents/insert", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.insert_agent", - "description": "Add an agent specifying its name, ID and IP. If an agent with the same name, the same ID or the same IP already exists, replace it using `force_time` parameter", + "description": "Add an agent specifying its name, ID and IP. If an agent with the same name, the same ID or the same IP already exists, replace it using the `force` parameter", "summary": "Add agent full", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "pretty", @@ -9791,7 +10493,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.post_new_agent", "description": "Add a new agent with name `agent_name`. This agent will use `any` as IP", "summary": "Add agent quick", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agent_name", @@ -9826,7 +10530,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.post_group", "description": "Create a new group", "summary": "Create a group", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "query": [ { "name": "pretty", @@ -9859,7 +10565,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_policy", "description": "Add a new policy, all fields need to be specified", "summary": "Add policy", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -9881,7 +10589,10 @@ "body": [ { "type": "object", - "required": ["name", "policy"], + "required": [ + "name", + "policy" + ], "properties": { "name": { "description": "Policy name", @@ -9912,7 +10623,11 @@ "description": "Effect of the policy" } }, - "required": ["actions", "resources", "effect"] + "required": [ + "actions", + "resources", + "effect" + ] } } } @@ -9923,7 +10638,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_role", "description": "Add a new role, all fields need to be specified", "summary": "Add role", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -9945,7 +10662,9 @@ "body": [ { "type": "object", - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string", @@ -9962,7 +10681,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_role_policy", "description": "Create a specified relation role-policy, one role may have multiples policies", "summary": "Add policies to role", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":role_id", @@ -10021,7 +10742,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_role_rule", "description": "Create a specific role-rule relation. One role may have multiple security rules", "summary": "Add security rules to role", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":role_id", @@ -10071,7 +10794,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.add_rule", "description": "Add a new security rule", "summary": "Add security rule", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -10093,7 +10818,10 @@ "body": [ { "type": "object", - "required": ["name", "rule"], + "required": [ + "name", + "rule" + ], "properties": { "name": { "type": "string", @@ -10114,7 +10842,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.run_as_login", "description": "This method should be called to get an API token using an authorization context body. This token will expire after auth_token_exp_timeout seconds (default: 900). This value can be changed using PUT /security/config", "summary": "Login auth_context", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "raw", @@ -10137,7 +10867,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.create_user", "description": "Add a new API user to the system", "summary": "Add user", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -10176,7 +10908,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.set_user_role", "description": "Create a specified relation role-policy, one user may have multiples roles", "summary": "Add roles to user", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":user_id", @@ -10240,7 +10974,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_agents", "description": "Delete all agents or a list of them based on optional criteria", "summary": "Delete agents", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -10369,7 +11105,13 @@ "type": "array", "items": { "type": "string", - "enum": ["all", "active", "pending", "never_connected", "disconnected"] + "enum": [ + "all", + "active", + "pending", + "never_connected", + "disconnected" + ] }, "minItems": 1 } @@ -10397,7 +11139,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_multiple_groups", "description": "Remove the agent from all groups or a list of them. The agent will automatically revert to the default group if it is removed from all its assigned groups", "summary": "Remove agent from groups", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -10447,7 +11191,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_single_agent_single_group", "description": "Remove an agent from a specified group. If the agent belongs to several groups, only the specified group will be deleted.", "summary": "Remove agent from group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "args": [ { "name": ":agent_id", @@ -10495,7 +11241,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_multiple_agent_single_group", "description": "Remove all agents assignment or a list of them from the specified group", "summary": "Remove agents from group", - "tags": ["Agents"], + "tags": [ + "Agents" + ], "query": [ { "name": "agents_list", @@ -10544,7 +11292,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.decoder_controller.delete_file", "description": "Delete a specified decoder file", "summary": "Delete decoders file", - "tags": ["Decoders"], + "tags": [ + "Decoders" + ], "args": [ { "name": ":filename", @@ -10621,7 +11371,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.experimental_controller.clear_syscheck_database", "description": "Clear the syscheck database for all agents or a list of them", "summary": "Clear agents FIM results", - "tags": ["Experimental"], + "tags": [ + "Experimental" + ], "query": [ { "name": "agents_list", @@ -10660,7 +11412,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.agent_controller.delete_groups", "description": "Delete all groups or a list of them", "summary": "Delete groups", - "tags": ["Groups"], + "tags": [ + "Groups" + ], "query": [ { "name": "groups_list", @@ -10699,7 +11453,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.cdb_list_controller.delete_file", "description": "Delete a specified CDB list file. Only the filename can be specified. It will be searched recursively if not found", "summary": "Delete CDB list file", - "tags": ["Lists"], + "tags": [ + "Lists" + ], "args": [ { "name": ":filename", @@ -10735,7 +11491,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.logtest_controller.end_logtest_session", "description": "Delete the saved logtest session corresponding to {token}", "summary": "End session", - "tags": ["Logtest"], + "tags": [ + "Logtest" + ], "args": [ { "name": ":token", @@ -10811,7 +11569,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.rule_controller.delete_file", "description": "Delete a specified rule file", "summary": "Delete rules file", - "tags": ["Rules"], + "tags": [ + "Rules" + ], "args": [ { "name": ":filename", @@ -10847,7 +11607,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.delete_security_config", "description": "Replaces the security configuration with the original one", "summary": "Restore default security config", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -10872,7 +11634,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_policies", "description": "Delete a list of policies or all policies in the system, roles linked to policies are not going to be removed", "summary": "Delete policies", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "policy_ids", @@ -10910,7 +11674,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_roles", "description": "Policies linked to roles are not going to be removed", "summary": "Delete roles", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -10948,7 +11714,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_role_policy", "description": "Delete a specified relation role-policy", "summary": "Remove policies from role", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":role_id", @@ -10998,7 +11766,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_role_rule", "description": "Delete a specific role-rule relation", "summary": "Remove security rules from role", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":role_id", @@ -11048,7 +11818,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_rules", "description": "Delete a list of security rules or all security rules in the system, roles linked to rules are not going to be deleted", "summary": "Delete security rules", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -11086,14 +11858,18 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.logout_user", "description": "This method should be called to invalidate all the current user's tokens", "summary": "Logout current user", - "tags": ["Security"] + "tags": [ + "Security" + ] }, { "name": "/security/users", "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.delete_users", "description": "Delete a list of users by specifying their IDs", "summary": "Delete users", - "tags": ["Security"], + "tags": [ + "Security" + ], "query": [ { "name": "pretty", @@ -11131,7 +11907,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.security_controller.remove_user_role", "description": "Delete a specified relation user-roles", "summary": "Remove roles from user", - "tags": ["Security"], + "tags": [ + "Security" + ], "args": [ { "name": ":user_id", @@ -11181,7 +11959,9 @@ "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.syscheck_controller.delete_syscheck_agent", "description": "Clear file integrity monitoring scan results for a specified agent. Only available for agents < 3.12.0, it doesn't apply for more recent ones", "summary": "Clear results", - "tags": ["Syscheck"], + "tags": [ + "Syscheck" + ], "args": [ { "name": ":agent_id", @@ -11216,4 +11996,4 @@ } ] } -] +] \ No newline at end of file diff --git a/common/api-info/security-actions.json b/common/api-info/security-actions.json index 2907b397d4..2707a9506b 100644 --- a/common/api-info/security-actions.json +++ b/common/api-info/security-actions.json @@ -1102,7 +1102,8 @@ "effect": "allow" }, "related_endpoints": [ - "GET /vulnerability/{agent_id}" + "GET /vulnerability/{agent_id}", + "GET /vulnerability/{agent_id}/last_scan" ] } } \ No newline at end of file From c3f94fdbd3515be650a1b5692a2dd088a714b533 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <matiasmoreno876@gmail.com> Date: Mon, 20 Dec 2021 12:06:50 -0300 Subject: [PATCH 371/493] Added workflow wazuh package (#3742) * added workflow wazuh package * Changelog Co-authored-by: Matias Ezequiel Moreno <matiasezequielmoreno@MacBook-Pro-de-Matias.local> --- .github/workflows/create-wazuh-packages.yml | 44 +++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 45 insertions(+) create mode 100644 .github/workflows/create-wazuh-packages.yml diff --git a/.github/workflows/create-wazuh-packages.yml b/.github/workflows/create-wazuh-packages.yml new file mode 100644 index 0000000000..7e235228a2 --- /dev/null +++ b/.github/workflows/create-wazuh-packages.yml @@ -0,0 +1,44 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Create Wazuh Packages + +on: + workflow_dispatch: + inputs: + wazuh_kibana_app_branch: + description: 'Branch Wazuh-Kibana-App' + required: true + wazuh_packages_branch: + description: 'Branch Wazuh/Packages' + required: true + default: 'master' +jobs: + setup-wazuh-kibana-app: + name: Run setup environment wazuh kibana app + runs-on: ubuntu-18.04 + steps: + - name: Step 01 - Set up environment + run: | + mkdir packages + - name: Step 02 - Download Project wazuh-packages + uses: actions/checkout@v2 + with: + repository: wazuh/wazuh-packages + ref: ${{ github.event.inputs.wazuh_packages_branch }} + path: wazuh-packages + - name: Step 03 - Building package + run: | + cd $GITHUB_WORKSPACE/wazuh-packages/wazuhapp + echo fixing command... + sed -i -e 's/'\|' cut -d \"\/\" \-f2//g' ./generate_wazuh_app.sh + echo run command... + ./generate_wazuh_app.sh -b ${{ github.event.inputs.wazuh_kibana_app_branch }} -s $GITHUB_WORKSPACE/packages -r 1 + cd $GITHUB_WORKSPACE/packages + ls -a + - name: Archive packages + uses: actions/upload-artifact@v2 + with: + name: wazuh-packages + path: packages/* + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index eaee2ad0b4..65c0e38362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Simple filters change between panel and drilldown panel [#3568](https://github.com/wazuh/wazuh-kibana-app/pull/3568). - Added new fields in Inventory table and Flyout Details [#3525](https://github.com/wazuh/wazuh-kibana-app/pull/3525) - Added columns selector in agents table [#3691](https://github.com/wazuh/wazuh-kibana-app/pull/3691) +- Added a new workflow for create wazuh packages [#3742](https://github.com/wazuh/wazuh-kibana-app/pull/3742) ### Changed From 3d986b5e56d4037379cc1b898ec3f182e1853a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 20 Dec 2021 16:12:51 +0100 Subject: [PATCH 372/493] fix: Asset URL in the Readme file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77eaedc348..f266520ebd 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ This plugin for Kibana allows you to visualize and analyze Wazuh alerts stored i **Agent summary** -![Overview](/public/asstes/app7.png) +![Overview](/public/assets/app7.png) ## Branches From 2167d38bde891746c46dcc57d1450129e02fa97f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 21 Dec 2021 14:58:07 +0100 Subject: [PATCH 373/493] Added filters to flyout details --- public/components/agents/fim/inventory.tsx | 1 - public/components/agents/vuls/inventory.tsx | 32 ++++++++++++------- .../agents/vuls/inventory/detail.tsx | 8 ++--- .../agents/vuls/inventory/table.tsx | 6 +++- .../components/common/tables/table-wz-api.tsx | 2 ++ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/public/components/agents/fim/inventory.tsx b/public/components/agents/fim/inventory.tsx index de9263d3c7..b46fb96efc 100644 --- a/public/components/agents/fim/inventory.tsx +++ b/public/components/agents/fim/inventory.tsx @@ -137,7 +137,6 @@ export class Inventory extends Component { } onFiltersChange = (filters) => { - // this.setStoreFilters(filters); this.setState({ filters }); } diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 3cd5d0701b..7188e8d9c8 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -27,17 +27,18 @@ import { ICustomBadges } from '../../wz-search-bar/components'; export class Inventory extends Component { _isMount = false; state: { + filters: []; isLoading: Boolean; customBadges: ICustomBadges[]; }; - props: any; constructor(props) { super(props); this.state = { isLoading: true, - customBadges: [] + customBadges: [], + filters: [], } } @@ -51,20 +52,28 @@ export class Inventory extends Component { } async loadAgent() { - if (this._isMount){ - this.setState({ isLoading: false }); + if (this._isMount) { + this.setState({ isLoading: false }); } } + onFiltersChange = (filters) => { + this.setState({ filters }); + } + renderTable() { + const { filters } = this.state; return ( <div> - <InventoryTable - {...this.props}/> + <InventoryTable + {...this.props} + filters={filters} + onFiltersChange={this.onFiltersChange} + /> </div> ) } - + loadingInventory() { return <EuiPage> <EuiFlexGroup> @@ -84,10 +93,9 @@ export class Inventory extends Component { const table = this.renderTable(); return <EuiPage> - <EuiPanel> - <EuiSpacer size={(((this.props.agent || {}).os || {}).platform || false) === 'windows' ? 's' : 'm'} /> - {table} - </EuiPanel> - </EuiPage> + <EuiPanel> + {table} + </EuiPanel> + </EuiPage> } } diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 937a990831..921e799c61 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -93,25 +93,25 @@ export class Details extends Component { field: 'name', name: 'Name', icon: 'dot', - link: false, + link: true, }, { field: 'cve', name: 'CVE', icon: 'securitySignal', - link: false, + link: true, }, { field: 'version', name: 'Version', icon: 'package', - link: false, + link: true, }, { field: 'architecture', name: 'Architecture', icon: 'node', - link: false, + link: true, }, { field: 'last_full_scan', diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 163a4ec180..086b466ed3 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -92,7 +92,8 @@ export class InventoryTable extends Component { props!: { filters: IFilter[]; agent: any; - onTotalItemsChange: Function; + items: []; + onFiltersChange: Function; }; constructor(props) { @@ -216,6 +217,7 @@ export class InventoryTable extends Component { }; const { error } = this.state; + const { filters, onFiltersChange } = this.props; const columns = this.columns(); const selectFields = 'select=cve,architecture,version,name,severity,cvss2_score,cvss3_score,detection_time'; @@ -231,6 +233,8 @@ export class InventoryTable extends Component { rowProps={getRowProps} error={error} downloadCsv={true} + filters={filters} + onFiltersChange={onFiltersChange} /> ); } diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx index ec91da0b1c..96ee216eea 100644 --- a/public/components/common/tables/table-wz-api.tsx +++ b/public/components/common/tables/table-wz-api.tsx @@ -31,6 +31,7 @@ export function TableWzAPI({endpoint, ...rest}){ 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 onSearch = useCallback(async function(filters, pagination, sorting){ try { @@ -38,6 +39,7 @@ export function TableWzAPI({endpoint, ...rest}){ const { field, direction } = sorting.sort; setIsLoading(true); setFilters(filters); + onFiltersChange(filters); const params = { ...filtersToObject(filters), offset: pageIndex * pageSize, From 7f8b33b84d0b375163bdeb66073ffc8e087f3665 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 21 Dec 2021 15:09:13 +0100 Subject: [PATCH 374/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c0e38362..12991be5b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed missing settings in `Management/Configuration/Global configuration/Global/Main settings` [#3737](https://github.com/wazuh/wazuh-kibana-app/pull/3737) - Fixed `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List [#3738](https://github.com/wazuh/wazuh-kibana-app/pull/3738) - Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) +- Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From adbf9546f157c39c1d53a50beb80f01ace84dfb3 Mon Sep 17 00:00:00 2001 From: yenienserrano <ian.serrano@wazuh.com> Date: Tue, 21 Dec 2021 17:48:23 -0300 Subject: [PATCH 375/493] feat(printer): horizontal-vertical shift --- .../visualizations/overview/overview-vuls.ts | 18 +++++++++--------- server/lib/reporting/printer.ts | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/server/integration-files/visualizations/overview/overview-vuls.ts b/server/integration-files/visualizations/overview/overview-vuls.ts index cee2e2ab5d..b04b437a48 100644 --- a/server/integration-files/visualizations/overview/overview-vuls.ts +++ b/server/integration-files/visualizations/overview/overview-vuls.ts @@ -156,10 +156,10 @@ export default [ schema: 'bucket', params: { field: 'agent.id', - size: 1, + size: 100, order: 'desc', orderBy: '1', - otherBucket: false, + otherBucket: true, otherBucketLabel: 'Other', missingBucket: false, missingBucketLabel: 'Missing', @@ -167,13 +167,13 @@ export default [ }, }, { - id: '6', + id: '4', enabled: true, type: 'terms', schema: 'bucket', params: { field: 'data.vulnerability.rationale', - size: 1, + size: 10, order: 'desc', orderBy: '1', otherBucket: false, @@ -200,7 +200,7 @@ export default [ customLabel: 'Title', }, },{ - id: '7', + id: '6', enabled: true, type: 'terms', schema: 'bucket', @@ -216,7 +216,7 @@ export default [ customLabel: 'CVE', }, },{ - id: '8', + id: '7', enabled: true, type: 'terms', schema: 'bucket', @@ -232,7 +232,7 @@ export default [ customLabel: 'Name', }, },{ - id: '9', + id: '8', enabled: true, type: 'terms', schema: 'bucket', @@ -248,7 +248,7 @@ export default [ customLabel: 'Condition', }, },{ - id: '10', + id: '9', enabled: true, type: 'terms', schema: 'bucket', @@ -264,7 +264,7 @@ export default [ customLabel: 'References', }, },{ - id: '11', + id: '10', enabled: true, type: 'terms', schema: 'bucket', diff --git a/server/lib/reporting/printer.ts b/server/lib/reporting/printer.ts index b3700daa44..04604128de 100644 --- a/server/lib/reporting/printer.ts +++ b/server/lib/reporting/printer.ts @@ -241,7 +241,8 @@ export class ReportPrinter{ this.addContent({ text: table.title, style: 'h3', - pageBreak: 'before' + pageBreak: 'before', + pageOrientation: table.columns.length >= 9 ? 'landscape' : 'portrait', }); this.addNewLine(); const full_body = []; @@ -256,6 +257,7 @@ export class ReportPrinter{ const modifiedRows = rows.map(row => row.map(cell => ({ text: cell || '-', style: 'standard' }))); + // the width of the columns is assigned const widths = Array(table.columns.length - 1).fill('auto'); widths.push('*'); From e14416986ea45ddc096b2ebe4fa10e3a2b833fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 17 Dec 2021 15:30:38 +0100 Subject: [PATCH 376/493] fix: Changed `telemetry` dependency to `optionalPlugins` --- kibana.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kibana.json b/kibana.json index 3b1917e3dd..28e6b92049 100644 --- a/kibana.json +++ b/kibana.json @@ -17,14 +17,14 @@ "savedObjects", "kibanaReact", "kibanaUtils", - "securityOss", - "telemetry" + "securityOss" ], "optionalPlugins": [ "security", "opendistroSecurityKibana", "searchguard", - "spaces" + "spaces", + "telemetry" ], "server": true, "ui": true From d9bbb3e8248ef6bcb0b0dc977bbede05a39286b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 17 Dec 2021 15:30:38 +0100 Subject: [PATCH 377/493] fix: Changed `telemetry` dependency to `optionalPlugins` --- kibana.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kibana.json b/kibana.json index 3b1917e3dd..28e6b92049 100644 --- a/kibana.json +++ b/kibana.json @@ -17,14 +17,14 @@ "savedObjects", "kibanaReact", "kibanaUtils", - "securityOss", - "telemetry" + "securityOss" ], "optionalPlugins": [ "security", "opendistroSecurityKibana", "searchguard", - "spaces" + "spaces", + "telemetry" ], "server": true, "ui": true From de86a55952831956da3f04c747691bbde95f3a40 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <matiasezequielmoreno@MacBook-Pro-de-Matias.local> Date: Mon, 27 Dec 2021 13:00:56 -0300 Subject: [PATCH 378/493] bump: kibana 4.3.0-7.13.4-RC2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5065564d2d..9a0cdde1c4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "revision": "4301-0", "code": "4301-0", "kibana": { - "version": "7.10.2" + "version": "7.13.4" }, "description": "Wazuh app", "keywords": [ From fc8cd6a7649c3fe5375a62c709c01b7ab1dbc229 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <matiasezequielmoreno@MacBook-Pro-de-Matias.local> Date: Mon, 27 Dec 2021 13:11:28 -0300 Subject: [PATCH 379/493] Revert "bump: kibana 4.3.0-7.13.4-RC2" This reverts commit de86a55952831956da3f04c747691bbde95f3a40. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a0cdde1c4..5065564d2d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "revision": "4301-0", "code": "4301-0", "kibana": { - "version": "7.13.4" + "version": "7.10.2" }, "description": "Wazuh app", "keywords": [ From 0134f58e8ad2100f41de677208bf61ad88fa7b08 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 7 Jan 2022 17:42:36 +0100 Subject: [PATCH 380/493] Changed copy codeblock button --- .../agent/components/agents-preview.scss | 47 ++++++++++ .../agent/components/register-agent.js | 94 ++++++++++--------- 2 files changed, 99 insertions(+), 42 deletions(-) diff --git a/public/controllers/agent/components/agents-preview.scss b/public/controllers/agent/components/agents-preview.scss index ed734f055b..d4973ceb28 100644 --- a/public/controllers/agent/components/agents-preview.scss +++ b/public/controllers/agent/components/agents-preview.scss @@ -50,3 +50,50 @@ .columnsSelectedCheckboxs div { margin-right: 20px; } +.copy-codeblock-wrapper { + position: relative; + + .euiToolTipAnchor { + opacity: 0; + transition: 150ms opacity ease-in-out; + position: absolute; + top: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(255, 255, 255, 0.75); + + &:hover { + opacity: 1; + } + .copy-overlay { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + cursor: pointer; + width: 100%; + p { + margin: 0; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + } + } + + code.euiCodeBlock__code { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } +} diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index e3282183fd..5ceaa051e5 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -24,7 +24,6 @@ import { EuiText, EuiCodeBlock, EuiTitle, - EuiButton, EuiButtonEmpty, EuiCopy, EuiPage, @@ -34,6 +33,7 @@ import { EuiProgress, EuiCode, EuiLink, + EuiIcon, } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { withErrorBoundary } from '../../../components/common/hocs'; @@ -477,7 +477,9 @@ export const RegisterAgent = withErrorBoundary( <EuiCallOut title="You will need administrator privileges to perform this installation." iconType="iInCircle" - /> + > + <p>Keep in mind you need to run this command in a Windows powershell terminal</p> + </EuiCallOut> <EuiSpacer></EuiSpacer> </> ); @@ -523,17 +525,19 @@ export const RegisterAgent = withErrorBoundary( iconType="iInCircle" /> <EuiSpacer /> - <EuiCodeBlock style={codeBlock} language={language}> - {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} - </EuiCodeBlock> + <div className="copy-codeblock-wrapper"> + <EuiCodeBlock style={codeBlock} language={language}> + {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} + </EuiCodeBlock> + <EuiCopy textToCopy={text}> + {(copy) => ( + <div className="copy-overlay" onClick={copy}> + <p><EuiIcon type="copy"/> Copy command</p> + </div> + )} + </EuiCopy> + </div> {windowsAdvice} - <EuiCopy textToCopy={text}> - {(copy) => ( - <EuiButton fill iconType="copy" onClick={copy}> - Copy command - </EuiButton> - )} - </EuiCopy> </EuiText> )} </div> @@ -547,16 +551,18 @@ export const RegisterAgent = withErrorBoundary( <Fragment> <EuiSpacer /> <EuiText> - <EuiCodeBlock style={codeBlock} language={language}> - {this.systemSelector()} - </EuiCodeBlock> - <EuiCopy textToCopy={this.systemSelector()}> - {(copy) => ( - <EuiButton fill iconType="copy" onClick={copy}> - Copy command - </EuiButton> - )} - </EuiCopy> + <div className="copy-codeblock-wrapper"> + <EuiCodeBlock style={codeBlock} language={language}> + {this.systemSelector()} + </EuiCodeBlock> + <EuiCopy textToCopy={this.systemSelector()}> + {(copy) => ( + <div className="copy-overlay" onClick={copy}> + <p><EuiIcon type="copy" /> Copy command</p> + </div> + )} + </EuiCopy> + </div> {textAndLinkToCheckConnectionDocumentation} </EuiText> </Fragment> @@ -569,16 +575,18 @@ export const RegisterAgent = withErrorBoundary( <Fragment> <EuiSpacer /> <EuiText> - <EuiCodeBlock style={codeBlock} language={language}> - {this.systemSelector()} - </EuiCodeBlock> - <EuiCopy textToCopy={this.systemSelector()}> - {(copy) => ( - <EuiButton fill iconType="copy" onClick={copy}> - Copy command - </EuiButton> - )} - </EuiCopy> + <div className="copy-codeblock-wrapper"> + <EuiCodeBlock style={codeBlock} language={language}> + {this.systemSelector()} + </EuiCodeBlock> + <EuiCopy textToCopy={this.systemSelector()}> + {(copy) => ( + <div className="copy-overlay" onClick={copy}> + <p><EuiIcon type="copy" /> Copy command</p> + </div> + )} + </EuiCopy> + </div> {textAndLinkToCheckConnectionDocumentation} </EuiText> </Fragment> @@ -713,16 +721,18 @@ export const RegisterAgent = withErrorBoundary( : ( <EuiFlexGroup direction="column"> <EuiText> - <EuiCodeBlock style={codeBlock} language={language}> - {restartAgentCommand} - </EuiCodeBlock> - <EuiCopy textToCopy={restartAgentCommand}> - {(copy) => ( - <EuiButton fill iconType="copy" onClick={copy}> - Copy command - </EuiButton> - )} - </EuiCopy> + <div className="copy-codeblock-wrapper"> + <EuiCodeBlock style={codeBlock} language={language}> + {restartAgentCommand} + </EuiCodeBlock> + <EuiCopy textToCopy={restartAgentCommand}> + {(copy) => ( + <div className="copy-overlay" onClick={copy}> + <p><EuiIcon type="copy" /> Copy command</p> + </div> + )} + </EuiCopy> + </div> </EuiText> </EuiFlexGroup> ), From 1b37f493913d293a96dec9b46cf6e3bffba2e794 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Fri, 7 Jan 2022 18:19:50 +0100 Subject: [PATCH 381/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12991be5b0..0d0684b1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) - Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) [#3728](https://github.com/wazuh/wazuh-kibana-app/pull/3728) +- Changed agent install codeblock copy button and powershell terminal warning [#3792](https://github.com/wazuh/wazuh-kibana-app/pull/3792) ### Fixed From af4350fd6fc919fe3e6ed92a068bfad09dafcada Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Mon, 10 Jan 2022 17:03:33 +0100 Subject: [PATCH 382/493] Added switch to show password --- .../agent/components/register-agent.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 5ceaa051e5..4f6ae2ef55 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -34,6 +34,7 @@ import { EuiCode, EuiLink, EuiIcon, + EuiSwitch } from '@elastic/eui'; import { WzRequest } from '../../../react-services/wz-request'; import { withErrorBoundary } from '../../../components/common/hocs'; @@ -137,6 +138,7 @@ export const RegisterAgent = withErrorBoundary( groups: [], selectedGroup: [], udpProtocol: false, + showPassword: false, }; this.restartAgentCommand = { rpm: this.systemSelector(), @@ -266,6 +268,10 @@ export const RegisterAgent = withErrorBoundary( this.setState({ wazuhPassword: event.target.value }); } + setShowPassword(event) { + this.setState({ showPassword: event.target.checked }); + } + obfuscatePassword(text) { let obfuscate = ''; const regex = /WAZUH_REGISTRATION_PASSWORD=?\040?\'(.*?)\'/gm; @@ -478,7 +484,7 @@ export const RegisterAgent = withErrorBoundary( title="You will need administrator privileges to perform this installation." iconType="iInCircle" > - <p>Keep in mind you need to run this command in a Windows powershell terminal</p> + <p>Keep in mind you need to run this command in a Windows PowerShell terminal</p> </EuiCallOut> <EuiSpacer></EuiSpacer> </> @@ -527,7 +533,7 @@ export const RegisterAgent = withErrorBoundary( <EuiSpacer /> <div className="copy-codeblock-wrapper"> <EuiCodeBlock style={codeBlock} language={language}> - {this.state.wazuhPassword ? this.obfuscatePassword(text) : text} + {this.state.wazuhPassword && !this.state.showPassword ? this.obfuscatePassword(text) : text} </EuiCodeBlock> <EuiCopy textToCopy={text}> {(copy) => ( @@ -537,6 +543,12 @@ export const RegisterAgent = withErrorBoundary( )} </EuiCopy> </div> + <EuiSwitch + label="Show password" + checked={this.state.showPassword} + onChange={(active) => this.setShowPassword(active)} + /> + <EuiSpacer /> {windowsAdvice} </EuiText> )} @@ -563,6 +575,7 @@ export const RegisterAgent = withErrorBoundary( )} </EuiCopy> </div> + <EuiSpacer /> {textAndLinkToCheckConnectionDocumentation} </EuiText> </Fragment> @@ -587,6 +600,12 @@ export const RegisterAgent = withErrorBoundary( )} </EuiCopy> </div> + <EuiSwitch + label="Show password" + checked={this.state.showPassword} + onChange={(active) => this.setShowPassword(active)} + /> + <EuiSpacer /> {textAndLinkToCheckConnectionDocumentation} </EuiText> </Fragment> From e0d72f5a964552b0157c711ceb2cf3f698003f8e Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Tue, 11 Jan 2022 12:12:05 -0300 Subject: [PATCH 383/493] Changed user to manage sample-data indexs --- .../add-modules-data/sample-data.tsx | 24 ++++++++++----- server/controllers/wazuh-elastic.ts | 29 +++++++++++++++---- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index a92e8757ed..6692e361f4 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -13,7 +13,7 @@ import React, { Component, Fragment } from 'react'; import { WzButtonPermissions } from '../../components/common/permissions/button'; -import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { getToasts } from '../../kibana-services'; import { WzRequest } from '../../react-services/wz-request'; @@ -70,6 +70,7 @@ export default class WzSampleData extends Component { exists: false, addDataLoading: false, removeDataLoading: false, + havePermissions: false }; }); } @@ -127,7 +128,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: error.name || error, + title: 'Error checking sample data', }, }; getErrorOrchestrator().handleError(options); @@ -175,7 +176,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `${error.name}: Error trying to add sample data`, + title: `Error trying to add sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -215,7 +216,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `${error.name}: Error trying to delete sample data`, + title: `Error trying to delete sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -267,9 +268,16 @@ export default class WzSampleData extends Component { } render() { return ( - <EuiFlexGrid columns={3}> - {this.categories.map((category) => this.renderCard(category))} - </EuiFlexGrid> + <> + <EuiCallOut + title="You will need privileges to perform this actions." + iconType="iInCircle" + /> + <EuiSpacer /> + <EuiFlexGrid columns={3}> + {this.categories.map((category) => this.renderCard(category))} + </EuiFlexGrid> + </> ); } } @@ -292,5 +300,5 @@ const PromiseAllRecursiveObject = function (obj) { } return value; }) - ).then((result) => zipObject(keys, result)); + ).then((result) => zipObject(keys, result)) }; diff --git a/server/controllers/wazuh-elastic.ts b/server/controllers/wazuh-elastic.ts index b89121ad3c..c4e80e6a09 100644 --- a/server/controllers/wazuh-elastic.ts +++ b/server/controllers/wazuh-elastic.ts @@ -646,7 +646,14 @@ export class WazuhElasticCtrl { 'wazuh-elastic:haveSampleAlertsOfCategory', `Error checking if there are sample alerts indices: ${error.message || error}` ); - return ErrorResponse(`Error checking if there are sample alerts indices: ${error.message || error}`, 1000, 500, response); + + const statusCode = error.meta.statusCode || 500; + + if(statusCode === 403){ + error.message = 'Permission denied'; + } + + return ErrorResponse(`Error checking if there are sample alerts indices: ${error.message || error}`, 1000, statusCode, response); } } /** @@ -720,7 +727,7 @@ export class WazuhElasticCtrl { } }; - await context.core.elasticsearch.client.asInternalUser.indices.create({ + await context.core.elasticsearch.client.asCurrentUser.indices.create({ index: sampleAlertsIndex, body: configuration }); @@ -731,7 +738,7 @@ export class WazuhElasticCtrl { ); } - await context.core.elasticsearch.client.asInternalUser.bulk({ + await context.core.elasticsearch.client.asCurrentUser.bulk({ index: sampleAlertsIndex, body: bulk }); @@ -748,7 +755,13 @@ export class WazuhElasticCtrl { 'wazuh-elastic:createSampleAlerts', `Error adding sample alerts to ${sampleAlertsIndex} index: ${error.message || error}` ); - return ErrorResponse(error.message || error, 1000, 500, response); + const statusCode = error.meta.statusCode || 500; + + if(statusCode === 403){ + error.message = 'Permission denied'; + } + + return ErrorResponse(error.message || error, 1000, statusCode, response); } } /** @@ -809,7 +822,13 @@ export class WazuhElasticCtrl { 'wazuh-elastic:deleteSampleAlerts', `Error deleting sample alerts of ${sampleAlertsIndex} index: ${error.message || error}` ); - return ErrorResponse(error.message || error, 1000, 500, response); + const statusCode = error.meta.statusCode || 500; + + if(statusCode === 403){ + error.message = 'Permission denied'; + } + + return ErrorResponse(error.message || error, 1000, statusCode, response); } } From a4a701d3fbd450f48a4ced8f786fcf640798348f Mon Sep 17 00:00:00 2001 From: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Date: Tue, 11 Jan 2022 12:17:34 -0300 Subject: [PATCH 384/493] Revert "Changed user to manage sample-data indexs" This reverts commit e0d72f5a964552b0157c711ceb2cf3f698003f8e. --- .../add-modules-data/sample-data.tsx | 24 +++++---------- server/controllers/wazuh-elastic.ts | 29 ++++--------------- 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index 6692e361f4..a92e8757ed 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -13,7 +13,7 @@ import React, { Component, Fragment } from 'react'; import { WzButtonPermissions } from '../../components/common/permissions/button'; -import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup } from '@elastic/eui'; import { getToasts } from '../../kibana-services'; import { WzRequest } from '../../react-services/wz-request'; @@ -70,7 +70,6 @@ export default class WzSampleData extends Component { exists: false, addDataLoading: false, removeDataLoading: false, - havePermissions: false }; }); } @@ -128,7 +127,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: 'Error checking sample data', + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); @@ -176,7 +175,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `Error trying to add sample data`, + title: `${error.name}: Error trying to add sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -216,7 +215,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `Error trying to delete sample data`, + title: `${error.name}: Error trying to delete sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -268,16 +267,9 @@ export default class WzSampleData extends Component { } render() { return ( - <> - <EuiCallOut - title="You will need privileges to perform this actions." - iconType="iInCircle" - /> - <EuiSpacer /> - <EuiFlexGrid columns={3}> - {this.categories.map((category) => this.renderCard(category))} - </EuiFlexGrid> - </> + <EuiFlexGrid columns={3}> + {this.categories.map((category) => this.renderCard(category))} + </EuiFlexGrid> ); } } @@ -300,5 +292,5 @@ const PromiseAllRecursiveObject = function (obj) { } return value; }) - ).then((result) => zipObject(keys, result)) + ).then((result) => zipObject(keys, result)); }; diff --git a/server/controllers/wazuh-elastic.ts b/server/controllers/wazuh-elastic.ts index c4e80e6a09..b89121ad3c 100644 --- a/server/controllers/wazuh-elastic.ts +++ b/server/controllers/wazuh-elastic.ts @@ -646,14 +646,7 @@ export class WazuhElasticCtrl { 'wazuh-elastic:haveSampleAlertsOfCategory', `Error checking if there are sample alerts indices: ${error.message || error}` ); - - const statusCode = error.meta.statusCode || 500; - - if(statusCode === 403){ - error.message = 'Permission denied'; - } - - return ErrorResponse(`Error checking if there are sample alerts indices: ${error.message || error}`, 1000, statusCode, response); + return ErrorResponse(`Error checking if there are sample alerts indices: ${error.message || error}`, 1000, 500, response); } } /** @@ -727,7 +720,7 @@ export class WazuhElasticCtrl { } }; - await context.core.elasticsearch.client.asCurrentUser.indices.create({ + await context.core.elasticsearch.client.asInternalUser.indices.create({ index: sampleAlertsIndex, body: configuration }); @@ -738,7 +731,7 @@ export class WazuhElasticCtrl { ); } - await context.core.elasticsearch.client.asCurrentUser.bulk({ + await context.core.elasticsearch.client.asInternalUser.bulk({ index: sampleAlertsIndex, body: bulk }); @@ -755,13 +748,7 @@ export class WazuhElasticCtrl { 'wazuh-elastic:createSampleAlerts', `Error adding sample alerts to ${sampleAlertsIndex} index: ${error.message || error}` ); - const statusCode = error.meta.statusCode || 500; - - if(statusCode === 403){ - error.message = 'Permission denied'; - } - - return ErrorResponse(error.message || error, 1000, statusCode, response); + return ErrorResponse(error.message || error, 1000, 500, response); } } /** @@ -822,13 +809,7 @@ export class WazuhElasticCtrl { 'wazuh-elastic:deleteSampleAlerts', `Error deleting sample alerts of ${sampleAlertsIndex} index: ${error.message || error}` ); - const statusCode = error.meta.statusCode || 500; - - if(statusCode === 403){ - error.message = 'Permission denied'; - } - - return ErrorResponse(error.message || error, 1000, statusCode, response); + return ErrorResponse(error.message || error, 1000, 500, response); } } From 92c2462e113f9f7c9ec3f6587f9630aee2e387e6 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Tue, 11 Jan 2022 17:17:04 +0100 Subject: [PATCH 385/493] Dashboards PDF report error when switching pinned agent (#3748) * Added report button useCallback dependencies * Added changelog --- CHANGELOG.md | 1 + public/components/common/modules/buttons/generate_report.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12991be5b0..36b8feaf28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List [#3738](https://github.com/wazuh/wazuh-kibana-app/pull/3738) - Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) - Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) +- Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/modules/buttons/generate_report.tsx b/public/components/common/modules/buttons/generate_report.tsx index f56622bab6..b98d6469cc 100644 --- a/public/components/common/modules/buttons/generate_report.tsx +++ b/public/components/common/modules/buttons/generate_report.tsx @@ -45,7 +45,7 @@ export const ButtonModuleGenerateReport = ({agent, moduleID, disabledReport}) => } else { await reportingService.startVis2Png(moduleID, agent?.id || false) } - }); + }, [agent]); return ( <WzButton From 0f50a0529d4c0a8e91780cfeb17a95c139f6ce09 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 11 Jan 2022 17:19:26 +0100 Subject: [PATCH 386/493] [FIX] [API Console] Styles of action buttons overlaying the request text (#3772) * fix: API Console buttons overlays to request text. - Added a background color to the buttons of same color or highlighted request line * changelog: Add PR to changelog --- CHANGELOG.md | 2 ++ public/templates/tools/tools.html | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b8feaf28..fb693e32aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) - Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) - Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) +- Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) + ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/templates/tools/tools.html b/public/templates/tools/tools.html index fb0bc7f939..05c1120e90 100644 --- a/public/templates/tools/tools.html +++ b/public/templates/tools/tools.html @@ -22,7 +22,7 @@ <i ng-click="ctrl.send()" title="Click to send the request" - class="fa fa-play wz-play-dev-color cursor-pointer pull-right fa-fw wz-always-top" + class="fa fa-play wz-play-dev-color cursor-pointer pull-right fa-fw wz-always-top CodeMirror-styled-background" id="play_button" aria-hidden="true" ></i> @@ -30,7 +30,7 @@ href="" target="__blank" title="Open documentation" - class="fa fa-info-circle cursor-pointer pull-right fa-fw wz-always-top" + class="fa fa-info-circle cursor-pointer pull-right fa-fw wz-always-top CodeMirror-styled-background" id="wazuh_dev_tools_documentation" > </a> From 40ed715727170803769eae13104d3c170963e34d Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 11 Jan 2022 17:22:13 +0100 Subject: [PATCH 387/493] [FIX] [REPORTING] [MODULES] `Rule ID` value in tables related to top results (#3775) * fix(reporting): `Rule ID` value in tables related to top results - Modules affected: - Intregrity monitoring - GDPR - PCI DSS - TSC * changelog: Add PR to changelog --- CHANGELOG.md | 2 +- server/controllers/wazuh-reporting.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb693e32aa..fb93e528e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) - Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) - Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) - +- Fix `Rule ID` value in reporting tables related to top results [#3774](https://github.com/wazuh/wazuh-kibana-app/issues/3774) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/server/controllers/wazuh-reporting.ts b/server/controllers/wazuh-reporting.ts index 7fa8c4381c..b22480fbea 100644 --- a/server/controllers/wazuh-reporting.ts +++ b/server/controllers/wazuh-reporting.ts @@ -576,7 +576,7 @@ export class WazuhReportingCtrl { rules.length && printer.addSimpleTable({ columns: [ - { id: 'ruleId', label: 'Rule ID' }, + { id: 'ruleID', label: 'Rule ID' }, { id: 'ruleDescription', label: 'Description' }, ], items: rules, @@ -619,7 +619,7 @@ export class WazuhReportingCtrl { rules.length && printer.addSimpleTable({ columns: [ - { id: 'ruleId', label: 'Rule ID' }, + { id: 'ruleID', label: 'Rule ID' }, { id: 'ruleDescription', label: 'Description' }, ], items: rules, @@ -662,7 +662,7 @@ export class WazuhReportingCtrl { rules.length && printer.addSimpleTable({ columns: [ - { id: 'ruleId', label: 'Rule ID' }, + { id: 'ruleID', label: 'Rule ID' }, { id: 'ruleDescription', label: 'Description' }, ], items: rules, @@ -726,7 +726,7 @@ export class WazuhReportingCtrl { if (rules && rules.length) { printer.addContentWithNewLine({ text: 'Top 3 FIM rules', style: 'h2' }).addSimpleTable({ columns: [ - { id: 'ruleId', label: 'Rule ID' }, + { id: 'ruleID', label: 'Rule ID' }, { id: 'ruleDescription', label: 'Description' }, ], items: rules, From 84a1cd37250c1d5c1789fe0cc574fd553f070c03 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 11 Jan 2022 17:38:12 +0100 Subject: [PATCH 388/493] [FIX] [HEALTH CHECK] Run `template` and `fields` checks depends on the app configuration (#3783) * fix: Run `template` and `fields` checks related to the index pattern when these are enabled in the app configuration - Created a decorator to wrpas the same logic to apply to the `tempalte` and `fields` checks - Fixed a bug comparing the `checks.patterns` value to run some actions - Added message to display the `template` and `fields` chacks are skipped when are disabled - Added message to display that some minimal tasks will be done when the `checks.patterns` is disabled * fix: Added `checks.fields` to the initial app configuration file and fixed typo related with `Known fields` * chagelog: add PR to the changelog --- CHANGELOG.md | 1 + .../check-index-pattern.service.ts | 28 +++++++++++++------ public/utils/config-equivalences.js | 2 +- server/lib/initial-wazuh-config.ts | 1 + 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb93e528e5..6dba72ff4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new fields in Inventory table and Flyout Details [#3525](https://github.com/wazuh/wazuh-kibana-app/pull/3525) - Added columns selector in agents table [#3691](https://github.com/wazuh/wazuh-kibana-app/pull/3691) - Added a new workflow for create wazuh packages [#3742](https://github.com/wazuh/wazuh-kibana-app/pull/3742) +- Run `template` and `fields` checks in the health check depends on the app configuration [#3783](https://github.com/wazuh/wazuh-kibana-app/pull/3783) ### Changed diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts index 81bf9ccf12..146026055b 100644 --- a/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts @@ -20,15 +20,25 @@ import { satisfyKibanaVersion } from '../../../../../common/semver'; export const checkIndexPatternService = (appConfig) => async (checkLogger: CheckLogger) => await checkPattern(appConfig, checkLogger); const checkPattern = async (appConfig, checkLogger: CheckLogger) => { - if(appConfig.data['check.pattern'] === 'false'){ - checkLogger.info('Index pattern check Disabled'); - await checkIndexPatternObjectService(appConfig, checkLogger); - }else{ - await checkIndexPatternObjectService(appConfig, checkLogger); - await checkTemplateService(appConfig, checkLogger); - if(satisfyKibanaVersion('<7.11')){ - await checkFieldsService(appConfig, checkLogger); + if(!appConfig.data['checks.pattern']){ + checkLogger.info('Check [pattern]: disabled. Some minimal tasks will be done.'); + }; + await checkIndexPatternObjectService(appConfig, checkLogger); + await checkTemplate(appConfig, checkLogger); + if(satisfyKibanaVersion('<7.11')){ + await checkFields(appConfig, checkLogger); + }; +}; + +const decoratorHealthCheckRunCheckEnabled = (checkKey, fn) => { + return async (appConfig: any, checkLogger: CheckLogger) => { + if(appConfig.data[`checks.${checkKey}`]){ + await fn(appConfig, checkLogger); + }else{ + checkLogger.info(`Check [${checkKey}]: disabled. Skipped.`); }; } - return; }; + +const checkTemplate = decoratorHealthCheckRunCheckEnabled('template', checkTemplateService); +const checkFields = decoratorHealthCheckRunCheckEnabled('fields', checkFieldsService); diff --git a/public/utils/config-equivalences.js b/public/utils/config-equivalences.js index ce3682054c..6e165e3f19 100644 --- a/public/utils/config-equivalences.js +++ b/public/utils/config-equivalences.js @@ -83,7 +83,7 @@ export const nameEquivalence = { 'checks.template': 'Index template', 'checks.api': 'API connection', 'checks.setup': 'API version', - 'checks.fields': 'Know fields', + 'checks.fields': 'Known fields', 'checks.metaFields': 'Remove meta fields', 'checks.timeFilter': 'Set time filter to 24h', 'checks.maxBuckets': 'Set max buckets to 200000', diff --git a/server/lib/initial-wazuh-config.ts b/server/lib/initial-wazuh-config.ts index 3f2a81f5e4..ec6ca44154 100644 --- a/server/lib/initial-wazuh-config.ts +++ b/server/lib/initial-wazuh-config.ts @@ -47,6 +47,7 @@ export const initialWazuhConfig: string = `--- # step once the Wazuh app starts. Values must to be true or false. #checks.pattern : true #checks.template: true +#checks.fields : true #checks.api : true #checks.setup : true #checks.metaFields: true From 287abf21fb10e5d06f18ea54a99b053941a33272 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Tue, 11 Jan 2022 17:41:07 +0100 Subject: [PATCH 389/493] [FIX] [Modules] [Panel] Update aggregation data when the time filter is changed (#3790) * fix: Update the aggregation sumary tables of `Modules/<Module>/Panel` section - Added the `timeFilter` as dependency to update the data * changelog: Add PR to the changelog --- CHANGELOG.md | 1 + public/components/common/hooks/use-es-search.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dba72ff4c..b54ee589a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) - Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) - Fix `Rule ID` value in reporting tables related to top results [#3774](https://github.com/wazuh/wazuh-kibana-app/issues/3774) +- Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/hooks/use-es-search.ts b/public/components/common/hooks/use-es-search.ts index 16d83d8af5..a9e35c629a 100644 --- a/public/components/common/hooks/use-es-search.ts +++ b/public/components/common/hooks/use-es-search.ts @@ -12,7 +12,7 @@ import { SetStateAction, useEffect, useState } from 'react'; import { getDataPlugin } from '../../../kibana-services'; -import { useFilterManager, useIndexPattern, useQueryManager } from '.'; +import { useFilterManager, useIndexPattern, useQueryManager, useTimeFilter } from '.'; import { IndexPattern } from 'src/plugins/data/public'; import { UI_ERROR_SEVERITIES, @@ -45,6 +45,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) const indexPattern = useIndexPattern(); const {filters} = useFilterManager(); const [query] = useQueryManager(); + const { timeFilter } = useTimeFilter(); const [esResults, setEsResults] = useState<SearchResponse>({} as SearchResponse); const [error, setError] = useState<Error>({} as Error); const [isLoading, setIsLoading] = useState<boolean>(true); @@ -72,7 +73,7 @@ const useEsSearch = ({ preAppliedFilters = [], preAppliedAggs = {}, size = 10 }) setIsLoading(false); } })(); - }, [indexPattern, query, filters, page, preAppliedAggs]); + }, [indexPattern, query, filters, timeFilter, page, preAppliedAggs]); const search = async (): Promise<SearchResponse> => { if (indexPattern) { From c548827ccf43e7894fe0799c987ad9dbb06246bd Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 12 Jan 2022 08:47:12 +0100 Subject: [PATCH 390/493] [FIX] Component rendering of command to deploy new Windows agent (#3753) * fix: component rendering of command to deploy new Windows agent - Set language as `powershell` that is compatible with older and newer versions of `@elastic/eui` that changed the method to highlighting the text * changelog: Add PR to changelog --- CHANGELOG.md | 1 + public/controllers/agent/components/register-agent.js | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b54ee589a4..b8518c4915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) - Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) - Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) +- Fixed the rendering of the command to deploy new Windows agent not working in some Kibana versions [#3753](https://github.com/wazuh/wazuh-kibana-app/pull/3753) - Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) - Fix `Rule ID` value in reporting tables related to top results [#3774](https://github.com/wazuh/wazuh-kibana-app/issues/3774) - Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index e3282183fd..47f39ae215 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { Component, Fragment } from 'react'; -import { version, kibana } from '../../../../package.json'; +import { version } from '../../../../package.json'; import { WazuhConfig } from '../../../react-services/wazuh-config'; import { EuiSteps, @@ -129,7 +129,6 @@ export const RegisterAgent = withErrorBoundary( neededSYS: false, selectedArchitecture: '', selectedVersion: '', - kibanaVersion: (kibana || {}).version || false, version: '', wazuhVersion: '', serverAddress: '', @@ -382,8 +381,7 @@ export const RegisterAgent = withErrorBoundary( getHighlightCodeLanguage(selectedSO) { if (selectedSO.toLowerCase() === 'win') { - const iKibanaVersion = parseFloat(this.state.kibanaVersion.split('.').slice(0, 2).join('.'), 2); - return iKibanaVersion < 7.14 ? 'ps' : 'powershell'; + return 'powershell'; } else { return 'bash'; } From 810a46d824bcea11e684bac823b860aba6cb7fc7 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Wed, 12 Jan 2022 10:54:35 +0100 Subject: [PATCH 391/493] Fixed dark theme style and removed extra switch --- .../agent/components/register-agent.js | 17 +++++++---------- public/styles/dark_theme/wz_theme_dark.scss | 6 +++++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 4f6ae2ef55..61f19468c1 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -543,11 +543,13 @@ export const RegisterAgent = withErrorBoundary( )} </EuiCopy> </div> - <EuiSwitch - label="Show password" - checked={this.state.showPassword} - onChange={(active) => this.setShowPassword(active)} - /> + {this.state.needsPassword && ( + <EuiSwitch + label="Show password" + checked={this.state.showPassword} + onChange={(active) => this.setShowPassword(active)} + /> + )} <EuiSpacer /> {windowsAdvice} </EuiText> @@ -600,11 +602,6 @@ export const RegisterAgent = withErrorBoundary( )} </EuiCopy> </div> - <EuiSwitch - label="Show password" - checked={this.state.showPassword} - onChange={(active) => this.setShowPassword(active)} - /> <EuiSpacer /> {textAndLinkToCheckConnectionDocumentation} </EuiText> diff --git a/public/styles/dark_theme/wz_theme_dark.scss b/public/styles/dark_theme/wz_theme_dark.scss index 8f665c83b8..fe0dafcc96 100644 --- a/public/styles/dark_theme/wz_theme_dark.scss +++ b/public/styles/dark_theme/wz_theme_dark.scss @@ -442,4 +442,8 @@ svg .legend text { .application .euiAccordion, .flyout-body .euiAccordion { border-bottom: 1px solid #343741!important; -} \ No newline at end of file +} + +.copy-codeblock-wrapper .euiToolTipAnchor { + background-color: rgba(0, 0, 0, 0.7); +} From 3fbbff15dd6d40854d255ae5d6aa724fcd347650 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <maximilianoaibarra@gmail.com> Date: Thu, 13 Jan 2022 05:07:40 -0300 Subject: [PATCH 392/493] Changed user for sample data management (#3795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed user to manage sample-data indexs * Refactoring get code and message from error * Updated CHANGELOG * fix(sample_data): Changed the user that does the request to check if the sample data index exists - Changed the info message about the permissions for sample data Co-authored-by: Ibarra Maximiliano <maximiliano.ibarra@wazuh.com> Co-authored-by: Antonio David Gutiérrez <antonio.gutierrez@gmail.com> --- CHANGELOG.md | 1 + .../add-modules-data/sample-data.tsx | 24 ++++++++++----- server/controllers/wazuh-elastic.ts | 30 +++++++++++++++---- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ce2e58a6..484bb1a2f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) - Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) [#3728](https://github.com/wazuh/wazuh-kibana-app/pull/3728) +- Changed user for sample data management [#3795](https://github.com/wazuh/wazuh-kibana-app/pull/3795) - Changed agent install codeblock copy button and powershell terminal warning [#3792](https://github.com/wazuh/wazuh-kibana-app/pull/3792) ### Fixed diff --git a/public/components/add-modules-data/sample-data.tsx b/public/components/add-modules-data/sample-data.tsx index a92e8757ed..6f4a5b7570 100644 --- a/public/components/add-modules-data/sample-data.tsx +++ b/public/components/add-modules-data/sample-data.tsx @@ -13,7 +13,7 @@ import React, { Component, Fragment } from 'react'; import { WzButtonPermissions } from '../../components/common/permissions/button'; -import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiCard, EuiFlexGrid, EuiFlexGroup, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { getToasts } from '../../kibana-services'; import { WzRequest } from '../../react-services/wz-request'; @@ -70,6 +70,7 @@ export default class WzSampleData extends Component { exists: false, addDataLoading: false, removeDataLoading: false, + havePermissions: false }; }); } @@ -127,7 +128,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: error.name || error, + title: 'Error checking sample data', }, }; getErrorOrchestrator().handleError(options); @@ -175,7 +176,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `${error.name}: Error trying to add sample data`, + title: `Error trying to add sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -215,7 +216,7 @@ export default class WzSampleData extends Component { error: { error: error, message: error.message || error, - title: `${error.name}: Error trying to delete sample data`, + title: `Error trying to delete sample data`, }, }; getErrorOrchestrator().handleError(options); @@ -267,9 +268,16 @@ export default class WzSampleData extends Component { } render() { return ( - <EuiFlexGrid columns={3}> - {this.categories.map((category) => this.renderCard(category))} - </EuiFlexGrid> + <> + <EuiCallOut + title="These actions require permissions on the managed indices." + iconType="iInCircle" + /> + <EuiSpacer /> + <EuiFlexGrid columns={3}> + {this.categories.map((category) => this.renderCard(category))} + </EuiFlexGrid> + </> ); } } @@ -292,5 +300,5 @@ const PromiseAllRecursiveObject = function (obj) { } return value; }) - ).then((result) => zipObject(keys, result)); + ).then((result) => zipObject(keys, result)) }; diff --git a/server/controllers/wazuh-elastic.ts b/server/controllers/wazuh-elastic.ts index b89121ad3c..d1b552e8a1 100644 --- a/server/controllers/wazuh-elastic.ts +++ b/server/controllers/wazuh-elastic.ts @@ -646,7 +646,9 @@ export class WazuhElasticCtrl { 'wazuh-elastic:haveSampleAlertsOfCategory', `Error checking if there are sample alerts indices: ${error.message || error}` ); - return ErrorResponse(`Error checking if there are sample alerts indices: ${error.message || error}`, 1000, 500, response); + + const [statusCode, errorMessage] = this.getErrorDetails(error); + return ErrorResponse(`Error checking if there are sample alerts indices: ${errorMessage || error}`, 1000, statusCode, response); } } /** @@ -705,7 +707,7 @@ export class WazuhElasticCtrl { // Index alerts // Check if wazuh sample alerts index exists - const existsSampleIndex = await context.core.elasticsearch.client.asInternalUser.indices.exists({ + const existsSampleIndex = await context.core.elasticsearch.client.asCurrentUser.indices.exists({ index: sampleAlertsIndex }); if (!existsSampleIndex.body) { @@ -720,7 +722,7 @@ export class WazuhElasticCtrl { } }; - await context.core.elasticsearch.client.asInternalUser.indices.create({ + await context.core.elasticsearch.client.asCurrentUser.indices.create({ index: sampleAlertsIndex, body: configuration }); @@ -731,7 +733,7 @@ export class WazuhElasticCtrl { ); } - await context.core.elasticsearch.client.asInternalUser.bulk({ + await context.core.elasticsearch.client.asCurrentUser.bulk({ index: sampleAlertsIndex, body: bulk }); @@ -748,7 +750,10 @@ export class WazuhElasticCtrl { 'wazuh-elastic:createSampleAlerts', `Error adding sample alerts to ${sampleAlertsIndex} index: ${error.message || error}` ); - return ErrorResponse(error.message || error, 1000, 500, response); + + const [statusCode, errorMessage] = this.getErrorDetails(error); + + return ErrorResponse(errorMessage || error, 1000, statusCode, response); } } /** @@ -809,7 +814,9 @@ export class WazuhElasticCtrl { 'wazuh-elastic:deleteSampleAlerts', `Error deleting sample alerts of ${sampleAlertsIndex} index: ${error.message || error}` ); - return ErrorResponse(error.message || error, 1000, 500, response); + const [statusCode, errorMessage] = this.getErrorDetails(error); + + return ErrorResponse(errorMessage || error, 1000, statusCode, response); } } @@ -853,4 +860,15 @@ export class WazuhElasticCtrl { return Promise.reject(error); } }; + + getErrorDetails(error){ + const statusCode = error?.meta?.statusCode || 500; + let errorMessage = error.message; + + if(statusCode === 403){ + errorMessage = error?.meta?.body?.error?.reason || 'Permission denied'; + } + + return [statusCode, errorMessage]; + } } From 159c41db81cef86801498ac2082491387b0aad50 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez <federico.rodriguez@wazuh.com> Date: Thu, 13 Jan 2022 09:18:04 +0100 Subject: [PATCH 393/493] Github/Office365 multi-select filters fix to get suggested values (#3787) * Added getValueSuggestions toSpec parameter * Added changelog --- CHANGELOG.md | 1 + public/components/common/hooks/use-value-suggestion.ts | 2 +- public/react-services/saved-objects.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484bb1a2f9..2fe15d3fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the rendering of the command to deploy new Windows agent not working in some Kibana versions [#3753](https://github.com/wazuh/wazuh-kibana-app/pull/3753) - Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) - Fix `Rule ID` value in reporting tables related to top results [#3774](https://github.com/wazuh/wazuh-kibana-app/issues/3774) +- Fixed github/office365 multi-select filters suggested values [#3787](https://github.com/wazuh/wazuh-kibana-app/pull/3787) - Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/hooks/use-value-suggestion.ts b/public/components/common/hooks/use-value-suggestion.ts index 86e2e92a34..446461e859 100644 --- a/public/components/common/hooks/use-value-suggestion.ts +++ b/public/components/common/hooks/use-value-suggestion.ts @@ -69,7 +69,7 @@ export const useValueSuggestion = ( : await data.autocomplete.getValueSuggestions({ query, indexPattern: indexPattern as IIndexPattern, - field, + field: { ...field, toSpec: (options) => field }, boolFilter: boolFilter, }); }; diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index 03baa3ddfc..c19781023d 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -282,6 +282,7 @@ export class SavedObject { "data.vulnerability.reference":{"id":"url"}, "data.url":{"id":"url"} }`, + fields: '[]', sourceFilters: '[{"value":"@timestamp"}]', }, }, From 74ff7d30862d3e1b92cddc07cb7ce76c65b224e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 24 Jan 2022 11:00:55 +0100 Subject: [PATCH 394/493] fix: Fixed bug of replacement fake agent properties when the Office 365 sample alerts are generated due to agent property replacement --- server/lib/generate-alerts/generate-alerts-script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/lib/generate-alerts/generate-alerts-script.js b/server/lib/generate-alerts/generate-alerts-script.js index 1d4fc740a8..65cb88e4e3 100644 --- a/server/lib/generate-alerts/generate-alerts-script.js +++ b/server/lib/generate-alerts/generate-alerts-script.js @@ -314,6 +314,11 @@ function generateAlert(params) { } if (params.office) { + alert.agent = { + id: '000', + ip: alert.agent.ip, + name: alert.agent.name + }; if (params.manager && params.manager.name) { alert.agent.name = params.manager.name; From 35326029898d75ee42d8d4a81e51c17a4293eee8 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 24 Jan 2022 11:03:01 +0100 Subject: [PATCH 395/493] [FEAT]: Add new options of auth for the Registration service configuration (#3806) * feat: Add new options of auth for the Registration service configuration * changelog: Add PR to changelog --- CHANGELOG.md | 2 +- .../registration-service.js | 23 +++++++++++++++---- .../configuration-settings-group.js | 6 +++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fe15d3fa5..8c1fdb55fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Moved the filterManager subscription to the hook useFilterManager [#3517](https://github.com/wazuh/wazuh-kibana-app/pull/3517) - Change filter from is to is one of in custom searchbar [#3529](https://github.com/wazuh/wazuh-kibana-app/pull/3529) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) -- Updated depracated and new references authd [#3663](https://github.com/wazuh/wazuh-kibana-app/pull/3663) +- Updated the deprecated and added new references authd [#3663](https://github.com/wazuh/wazuh-kibana-app/pull/3663) [#3806](https://github.com/wazuh/wazuh-kibana-app/pull/3806) - Added time subscription to Discover component [#3549](https://github.com/wazuh/wazuh-kibana-app/pull/3549) - Refactored as module tabs and buttons are rendered [#3494](https://github.com/wazuh/wazuh-kibana-app/pull/3494) - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) diff --git a/public/controllers/management/components/management/configuration/registration-service/registration-service.js b/public/controllers/management/components/management/configuration/registration-service/registration-service.js index e10013c768..ce2c7a0832 100644 --- a/public/controllers/management/components/management/configuration/registration-service/registration-service.js +++ b/public/controllers/management/components/management/configuration/registration-service/registration-service.js @@ -46,10 +46,25 @@ const mainSettings = [ label: 'Limit registration to maximum number of agents' }, { - field: 'force', - label: 'Force registration when using an existing IP address', - render: (value) => value.enabled - } + field: 'force.enabled', + label: 'Force registration when using an existing IP address' + }, + { + field: 'force.after_registration_time', + label: 'Specifies that the agent replacement will be performed only when the time (seconds) passed since the agent registration is greater than the value configured in the setting' + }, + { + field: 'force.key_mismatch', + label: 'Avoid re-registering agents that already have valid keys' + }, + { + field: 'force.disconnected_time.enabled', + label: 'Specifies that the replacement will be performed only for agents that have been disconnected longer than a certain time' + }, + { + field: 'force.disconnected_time.value', + label: 'Seconds since an agent is in a disconnected state' + }, ]; const sslSettings = [ { field: 'ssl_verify_host', label: 'Verify agents using a CA certificate' }, diff --git a/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js b/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js index 4f535bec13..ac00f7678b 100644 --- a/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js +++ b/public/controllers/management/components/management/configuration/util-components/configuration-settings-group.js @@ -12,6 +12,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; +import { get } from 'lodash'; import { EuiFlexGroup, @@ -56,6 +57,7 @@ class WzSettingsGroup extends Component { const keyItem = `${title || ''}-${item.label}-${ item.value }-${key}`; + const value = get(config, item.field); return ( <WzConfigurationSetting key={keyItem} @@ -63,8 +65,8 @@ class WzSettingsGroup extends Component { label={item.label} value={ item.render - ? item.render(config[item.field]) - : config[item.field] + ? item.render(value) + : value } /> ); From 37689369903403e8c693eb16ea6d1a72486702af Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 24 Jan 2022 11:04:33 +0100 Subject: [PATCH 396/493] [FIX] [GROUPS] Fixes creating a new group with invalid name and no display the remove button in the agents table for the `default` group (#3804) * fix: Added a toast message when there is an error when creating a new group * fix: Don't display the remove button in the table of a group's agents when the group name is the `default` group * changelog: Add PR to changelog --- CHANGELOG.md | 2 + .../management/groups/actions-buttons-main.js | 13 +++++ .../management/groups/group-agents-table.js | 50 ++++++++++--------- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1fdb55fb..bb4bfd6b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added columns selector in agents table [#3691](https://github.com/wazuh/wazuh-kibana-app/pull/3691) - Added a new workflow for create wazuh packages [#3742](https://github.com/wazuh/wazuh-kibana-app/pull/3742) - Run `template` and `fields` checks in the health check depends on the app configuration [#3783](https://github.com/wazuh/wazuh-kibana-app/pull/3783) +- Added a toast message when there is an error creating a new group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) ### Changed @@ -120,6 +121,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix `Rule ID` value in reporting tables related to top results [#3774](https://github.com/wazuh/wazuh-kibana-app/issues/3774) - Fixed github/office365 multi-select filters suggested values [#3787](https://github.com/wazuh/wazuh-kibana-app/pull/3787) - Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) +- Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/management/components/management/groups/actions-buttons-main.js b/public/controllers/management/components/management/groups/actions-buttons-main.js index 826131dcfe..38a3ac020e 100644 --- a/public/controllers/management/components/management/groups/actions-buttons-main.js +++ b/public/controllers/management/components/management/groups/actions-buttons-main.js @@ -179,6 +179,19 @@ class WzGroupsActionButtons extends Component { this.closePopover(); } } catch (error) { + const options = { + context: `${WzGroupsActionButtons.name}.createGroup`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: false, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error creating a new group`, + }, + }; + getErrorOrchestrator().handleError(options); this.props.updateLoadingStatus(false); throw new Error(error); } diff --git a/public/controllers/management/components/management/groups/group-agents-table.js b/public/controllers/management/components/management/groups/group-agents-table.js index 6d138ea806..ac6fe1ee13 100644 --- a/public/controllers/management/components/management/groups/group-agents-table.js +++ b/public/controllers/management/components/management/groups/group-agents-table.js @@ -206,30 +206,32 @@ class WzGroupAgentsTable extends Component { }} color="primary" /> - <WzButtonPermissionsModalConfirm - buttonType="icon" - permissions={[ - [ - { action: 'agent:modify_group', resource: `agent:id:${item.id}` }, - ...(item.group || []).map((group) => ({ - action: 'agent:modify_group', - resource: `agent:group:${group}`, - })), - ], - ]} - tooltip={{ position: 'top', content: 'Remove agent from this group' }} - aria-label="Remove agent from this group" - iconType="trash" - onConfirm={async () => { - this.removeItems([item]); - }} - color="danger" - isDisabled={item.name === 'default'} - modalTitle={`Remove ${item.file || item.name} agent from this group?`} - modalProps={{ - buttonColor: 'danger', - }} - /> + {this.props?.state?.itemDetail?.name !== 'default' && ( + <WzButtonPermissionsModalConfirm + buttonType="icon" + permissions={[ + [ + { action: 'agent:modify_group', resource: `agent:id:${item.id}` }, + ...(item.group || []).map((group) => ({ + action: 'agent:modify_group', + resource: `agent:group:${group}`, + })), + ], + ]} + tooltip={{ position: 'top', content: 'Remove agent from this group' }} + aria-label="Remove agent from this group" + iconType="trash" + onConfirm={async () => { + this.removeItems([item]); + }} + color="danger" + isDisabled={item.name === 'default'} + modalTitle={`Remove ${item.file || item.name} agent from this group?`} + modalProps={{ + buttonColor: 'danger', + }} + /> + )} </div> ); }, From 8a90a3f63e73d1bd018f2e82ab1c59a783ac0f57 Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno <matiasmoreno876@gmail.com> Date: Tue, 25 Jan 2022 07:02:31 -0300 Subject: [PATCH 397/493] [Feature] Rebranding (#3788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(color icon): icons and colors were changed * feat(assets): Change logos * feat(colorIcon): icon color change * feat(managment-welcome): change icon colors and add changelog * feat(changelog): add changelog * feat(printer): change the report color * feat(health-check logo): change of logo in health-check * changelog * the colors was change * fix: removed fixed value of brand color in some icons * fix: Apply brand logos depending on custom or default images with the selected theme - Apply brand images depending on app configuration with custom (no themes) or default (with themes) - Changed default values of `customization` app setting to empty. - customization.logo.app - customization.logo.sidebar - customization.logo.healthcheck - customization.logo.reports - Moved the app screenshots images to `screenshots` directory - Organize the default assets in the next structure: - fonts - images - icons: moved the icons (office365 and gmail) - themes: moved the default brand images. Added images for dark theme. - Moved some values to `contants.ts` file - Created functions related to the assets that create the path of them * fix: set some butotns as primary * feat(rebranding): Add brand colors and replace some module icons in Management directory - Add logos (line art) for Docker, GitHub, Google Cloud, Office 365 as ReactJS components - Replaced logos for Docker, GitHub, Google Cloud and Office 365 in `Management directory` - Add app styles for `light` and `dark` theme. - Created mixin to apply the brand color in `light` and `dark` themes. - Centralized the brand colors using SCSS variables - Changed some styles in the health check - Apply `primary` color for icons in the app menu * fix: Fixed path to app screenshot in Readme.md * feat: Add app screenshots images Co-authored-by: yenienserrano <yenienserrano75@gmail.com> Co-authored-by: yenienserrano <ian.serrano@wazuh.com> Co-authored-by: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Co-authored-by: Antonio David Gutiérrez <antonio.gutierrez@wazuh.com> --- CHANGELOG.md | 4 +- README.md | 14 ++--- common/constants.ts | 14 +++-- public/assets/app.png | Bin 163287 -> 0 bytes public/assets/app2.png | Bin 223147 -> 0 bytes public/assets/app3.png | Bin 172155 -> 0 bytes public/assets/app4.png | Bin 218225 -> 0 bytes public/assets/app5.png | Bin 173499 -> 0 bytes public/assets/app6.png | Bin 157311 -> 0 bytes public/assets/app7.png | Bin 157311 -> 0 bytes .../{ => fonts}/opensans/Montserrat-Light.ttf | Bin .../{ => fonts}/opensans/OpenSans-Bold.ttf | Bin .../opensans/OpenSans-BoldItalic.ttf | Bin .../{ => fonts}/opensans/OpenSans-Italic.ttf | Bin .../{ => fonts}/opensans/OpenSans-Light.ttf | Bin public/assets/icon_blue.png | Bin 2353 -> 0 bytes public/assets/icon_blue.svg | 1 - public/assets/icon_fail.svg | 33 ------------ .../{ => images/icons}/icon_google_groups.svg | 0 .../assets/{ => images/icons}/office365.svg | 0 public/assets/images/logo_reports.png | Bin 0 -> 3195 bytes public/assets/images/themes/dark/icon.svg | 17 ++++++ public/assets/images/themes/dark/logo.svg | 32 +++++++++++ public/assets/images/themes/light/icon.svg | 13 +++++ public/assets/images/themes/light/logo.svg | 23 ++++++++ public/assets/logo.png | Bin 3659 -> 0 bytes public/assets/logo.svg | 1 - public/assets/logotype.svg | 18 ------- public/assets/new_logo_white.svg | 1 - public/assets/new_logo_white_wolf.svg | 4 -- .../components/common/hocs/withUserLogged.tsx | 7 +-- public/components/common/logos/docker.tsx | 50 ++++++++++++++++++ public/components/common/logos/github.tsx | 28 ++++++++++ .../components/common/logos/google_cloud.tsx | 47 ++++++++++++++++ public/components/common/logos/index.ts | 4 ++ public/components/common/logos/office_365.tsx | 29 ++++++++++ .../common/welcome/management-welcome.js | 2 +- .../common/welcome/overview-welcome.js | 12 ++--- .../container/health-check.container.tsx | 3 +- .../overview/github-panel/views/stats.tsx | 3 +- .../office-panel/views/office-stats.tsx | 4 +- .../components/wz-menu/wz-menu-management.js | 4 +- public/components/wz-menu/wz-menu-settings.js | 4 +- public/components/wz-menu/wz-menu-tools.js | 2 +- public/components/wz-menu/wz-menu.js | 6 ++- public/plugin.ts | 11 ++-- public/services/resolves/get-config.js | 8 +-- public/styles/common.scss | 49 ++++------------- public/styles/component.scss | 2 +- public/styles/index.ts | 3 +- public/styles/mixins.scss | 15 ++++++ .../dark/index.dark.scss} | 9 ++++ public/styles/theme/dark/variables.scss | 2 + public/styles/theme/light/index.light.scss | 4 ++ public/styles/theme/light/variables.scss | 2 + public/utils/add_help_menu_to_app.tsx | 5 +- public/utils/assets.ts | 13 +++++ public/utils/config-equivalences.js | 10 ++-- screenshots/app.png | Bin 0 -> 134233 bytes screenshots/app2.png | Bin 0 -> 325622 bytes screenshots/app3.png | Bin 0 -> 128138 bytes screenshots/app4.png | Bin 0 -> 237119 bytes screenshots/app5.png | Bin 0 -> 303033 bytes screenshots/app6.png | Bin 0 -> 214994 bytes screenshots/app7.png | Bin 0 -> 151661 bytes server/controllers/wazuh-api.ts | 2 +- server/lib/initial-wazuh-config.ts | 19 +++---- server/lib/reporting/printer.ts | 15 +++--- 68 files changed, 387 insertions(+), 162 deletions(-) delete mode 100644 public/assets/app.png delete mode 100644 public/assets/app2.png delete mode 100644 public/assets/app3.png delete mode 100644 public/assets/app4.png delete mode 100644 public/assets/app5.png delete mode 100644 public/assets/app6.png delete mode 100644 public/assets/app7.png rename public/assets/{ => fonts}/opensans/Montserrat-Light.ttf (100%) rename public/assets/{ => fonts}/opensans/OpenSans-Bold.ttf (100%) rename public/assets/{ => fonts}/opensans/OpenSans-BoldItalic.ttf (100%) rename public/assets/{ => fonts}/opensans/OpenSans-Italic.ttf (100%) rename public/assets/{ => fonts}/opensans/OpenSans-Light.ttf (100%) delete mode 100644 public/assets/icon_blue.png delete mode 100644 public/assets/icon_blue.svg delete mode 100644 public/assets/icon_fail.svg rename public/assets/{ => images/icons}/icon_google_groups.svg (100%) rename public/assets/{ => images/icons}/office365.svg (100%) create mode 100644 public/assets/images/logo_reports.png create mode 100644 public/assets/images/themes/dark/icon.svg create mode 100644 public/assets/images/themes/dark/logo.svg create mode 100644 public/assets/images/themes/light/icon.svg create mode 100644 public/assets/images/themes/light/logo.svg delete mode 100644 public/assets/logo.png delete mode 100644 public/assets/logo.svg delete mode 100644 public/assets/logotype.svg delete mode 100644 public/assets/new_logo_white.svg delete mode 100644 public/assets/new_logo_white_wolf.svg create mode 100644 public/components/common/logos/docker.tsx create mode 100644 public/components/common/logos/github.tsx create mode 100644 public/components/common/logos/google_cloud.tsx create mode 100644 public/components/common/logos/index.ts create mode 100644 public/components/common/logos/office_365.tsx create mode 100644 public/styles/mixins.scss rename public/styles/{dark_theme/wz_theme_dark.scss => theme/dark/index.dark.scss} (98%) create mode 100644 public/styles/theme/dark/variables.scss create mode 100644 public/styles/theme/light/index.light.scss create mode 100644 public/styles/theme/light/variables.scss create mode 100644 public/utils/assets.ts create mode 100644 screenshots/app.png create mode 100644 screenshots/app2.png create mode 100644 screenshots/app3.png create mode 100644 screenshots/app4.png create mode 100644 screenshots/app5.png create mode 100644 screenshots/app6.png create mode 100644 screenshots/app7.png diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4bfd6b4f..eb6ce7766b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Testing logs using the Ruletest Test don't display the rule information if not matching a rule. [#3446](https://github.com/wazuh/wazuh-kibana-app/pull/3446) - Changed format permissions in FIM inventory [#3649](https://github.com/wazuh/wazuh-kibana-app/pull/3649) - Changed of request for one that does not return data that is not necessary to optimize times. [#3686](https://github.com/wazuh/wazuh-kibana-app/pull/3686) [#3728](https://github.com/wazuh/wazuh-kibana-app/pull/3728) +- Rebranding. Replaced the brand logos, set module icons with brand colors [#3788](https://github.com/wazuh/wazuh-kibana-app/pull/3788) - Changed user for sample data management [#3795](https://github.com/wazuh/wazuh-kibana-app/pull/3795) - Changed agent install codeblock copy button and powershell terminal warning [#3792](https://github.com/wazuh/wazuh-kibana-app/pull/3792) @@ -115,6 +116,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed `Maximum call stack size exceeded` error exporting key-value pairs of a CDB List [#3738](https://github.com/wazuh/wazuh-kibana-app/pull/3738) - Fixed regex lookahead and lookbehind for safari [#3741](https://github.com/wazuh/wazuh-kibana-app/pull/3741) - Fixed Vulnerabilities Inventory flyout details filters [#3744](https://github.com/wazuh/wazuh-kibana-app/pull/3744) +- Removed api selector toggle from settings menu since it performed no useful function [#3604](https://github.com/wazuh/wazuh-kibana-app/pull/3604) +- Fixed the requests get [#3661](https://github.com/wazuh/wazuh-kibana-app/pull/3661) - Fixed Dashboard PDF report error when switching pinned agent state [#3748](https://github.com/wazuh/wazuh-kibana-app/pull/3748) - Fixed the rendering of the command to deploy new Windows agent not working in some Kibana versions [#3753](https://github.com/wazuh/wazuh-kibana-app/pull/3753) - Fixed action buttons overlaying to the request text in Tools/API Console [#3772](https://github.com/wazuh/wazuh-kibana-app/pull/3772) @@ -218,7 +221,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed Wazuh main menu and breadcrumb render issues [#3347](https://github.com/wazuh/wazuh-kibana-app/pull/3347) - Fixed generation of huge logs from backend errors [#3397](https://github.com/wazuh/wazuh-kibana-app/pull/3397) - Fixed vulnerabilities flyout not showing alerts if the vulnerability had a field missing [#3593](https://github.com/wazuh/wazuh-kibana-app/pull/3593) -- Removed api selector toggle from settings menu since it performed no useful function [#3604](https://github.com/wazuh/wazuh-kibana-app/pull/3604) ## Wazuh v4.2.1 - Kibana 7.10.2 , 7.11.2 - Revision 4202 diff --git a/README.md b/README.md index f266520ebd..e85ec1703e 100644 --- a/README.md +++ b/README.md @@ -56,31 +56,31 @@ This plugin for Kibana allows you to visualize and analyze Wazuh alerts stored i **Modules overview** -![Overview](/public/assets/app.png) +![Overview](screenshots/app.png) **Security events** -![Overview](/public/assets/app2.png) +![Overview](screenshots/app2.png) **Integrity monitoring** -![Overview](/public/assets/app3.png) +![Overview](screenshots/app3.png) **Vulnerability detection** -![Overview](/public/assets/app4.png) +![Overview](screenshots/app4.png) **Regulatory compliance** -![Overview](/public/assets/app5.png) +![Overview](screenshots/app5.png) **Agents overview** -![Overview](/public/assets/app6.png) +![Overview](screenshots/app6.png) **Agent summary** -![Overview](/public/assets/app7.png) +![Overview](screenshots/app7.png) ## Branches diff --git a/common/constants.ts b/common/constants.ts index 098cd0c0ae..27347fff48 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -223,10 +223,10 @@ export const WAZUH_DEFAULT_APP_CONFIG = { hideManagerAlerts: false, 'logs.level': 'info', 'enrollment.dns': '', - 'customization.logo.app':'logotype.svg', - 'customization.logo.sidebar':'icon_blue.png', - 'customization.logo.healthcheck':'icon_blue.svg', - 'customization.logo.reports':'logo.png' + 'customization.logo.app': '', + 'customization.logo.sidebar': '', + 'customization.logo.healthcheck':'', + 'customization.logo.reports': '' }; // Wazuh errors @@ -347,3 +347,9 @@ export const UI_TOAST_COLOR = { DANGER: 'danger', }; +export const ASSETS_BASE_URL_PREFIX = '/plugins/wazuh/assets/'; + +export const REPORTS_PRIMARY_COLOR = '#256BD1'; + +export const REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH = 'images/logo_reports.png'; + diff --git a/public/assets/app.png b/public/assets/app.png deleted file mode 100644 index cfb2a32427832a6cf4207f6aae1bf05a54355781..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163287 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 ?{tZSiFv+dwM+vh&1poKF<DP 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 Q+=g7gs(GbA>Gp&F0#!f*?EnA( diff --git a/public/assets/app2.png b/public/assets/app2.png deleted file mode 100644 index 78d46c7ce9787c9198c8875008f7aa2a384c1988..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 223147 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 SmeVN8_cziDi=GJKCVzF0x@**F!eia)%SO znf_$l$C};us&+NSF#?s9K*`<KPb`6%)3!)S7lr+Dbc1|66w(CyazIQsjn81hcU0J3 zH{{}<%|hq$CULe8%Iq2kX>fws&t;CwX`NKW5jRf@?@+_VN794ODs|dpr9CF{G<2!w zQuWbu<!MzV)xfqflZ*JdXh<CV{DLQ6*PH6If$Q7PdXDjLL><KJ%83*++%Pigubnoe zVbwXl7CQpP659Vw!j9*m!Y7l1)EveZ{muh(=@c_1Y(3*>^pcc(E~gD}l3eJ)js{J9 z+RvH_Lc3%l$J1Comho3sX>Q&Cz{xMFavkld-=>FZnZ;ZG0s(<{WFQCj=VJ<o00HdW zHM_47Bzr%$fbHWJRV|;ay`OxbdMcb(U7TpvHnX0%*~(F>m&JnDO_t4~`(nJ9i0X}V z;*HQgkNt~kuSxo^n@a$3aa%KXwj>e>0}H%3WNLV$@P>zjs-lRxN3Zj0O^3nSacrKP zyk-*YZHR|waWacW(qN%>rC#d3R&tzQfh0u`#)LW7#V(p_)%_zT>H%Gz@VPv7RH~e1 zJ&0SFj0ZQcmcR>6Ek-{5s~IdrED-tFoPtNoqk2o=SALD8HXSh^I{$yhFfkY7bkvOo zluqDvoD`|ru03&yz(=W;k1Zq=L*Y-EDQ@lF4tX0hN-EqCbc-}Snf9q85tZAv97<9_ z;&noFi6tAd-p?X@+>(z3ZJb{XPsZ_+_EqNheA`KRqQ_lxn(?NR>Qq;G+UXcCwR2TD z$*FvweA9dEJA>XtseUnw>Ft$z(ivI2nKPi{Hm$rJ{8q7bY(2VbPmWC-L0aCXtzCF9 zj=vvO+V>_8Fn(Tl%-#S581@g5_kmxV-&N$z&K77xo~9i)lLCNWga~mczvXsEb~Nml ziJ!VHA03iQiBG({8|lHF<#$LMWVl`tE=l#~Op4%t8ZPy$>+GjO_Z2;50IjIuEW1+Q z=RvikP*V~>1>HV8JTp}O-KG7xGGbgv+3Qtu@3$X{H{17yn%3qd<Ei{^Blbd`eA06Y z1ULEYB}ht{6=({rTc;$k`>}DO?cxpLR*O$emMpAO4k0SIkZ22R(dch^N?JFD^H#5? zGg^DL^G(`d<!h-DqU{!^(INy4hUL12EZrAJ`79{rVCUU$21B_g7O&YJzIMFmIa+`g ztg@tJqY;<D&!{YWvuSw7vDzz<QfWbShG}KQ!=G^DM^w((d3apXl6YM1-#O}jX5<zq zIyx&nDcYfCeIbk6t}G)eMR8Sc@LNUGzGnc@c*UK<0mp@ubTt5btY{4{aHO!-Vt(n) z>hNRD^FYjg9>DN?LAst6CT4}zn#ar+C~LvZ9TDG`<8Qyf&$cd^E4L$wD%J+#l`1W= zz(~Y`Mlra&gj-BzI=pXCJ?Y9HZJVf;h=8eIczmhzwx8`?=YjnS3!9Q>doN_dDw(Zq zDEPRo2>I*z)z}L&Ln@Hx*>mWd=-zJx=D?`MVbQ_?)gw-IMrxdy^V+P7bDYZC#_>w? z+wQ(5^x~ajb|wC8LE6^&!y;?(!zL;f6^@n7+RUi1uBl)4okQChex>fm@TRkhAtw{} z%MNGb`6i6QZTSzVs0LpbO3*VrbDlIG<I<PPQLgK!9-m`<1X@lA_TSUS{9-!49&NHs z4SkOX{N`8Q<fR0O3T3FWWjwEic+$~PHy6EFU-o&4GkpE5*v;m>g!&^E!nbF+H49SV zzFn5(!U4dVTA_w_*4|ZT?C?rstczReg@uj)o_}3$VCJ-Mztk!PUYsh(ad#9Q8&ijI z>miT0)#*g^{K*xLZfpX6B#a)rwULqDX!C9U>MvMSC-TP_DXu40QYm|TU^#2RqN|br zB+ZK-7^AQu=h^f+2J#6j08XR;Y$@%tpOB`r@0?cDVqkSh#MJ7X>0M}ISGwqeXz-$D zIA(&u_+V`OFs1aemvDR}h7rhNwde|mlbzmS+Q9R*jbP2I&Gc~2i=U~)chjB`I(1}d z240kv?UHS#JjUmx(;)Ao=sr#?j{%T*uq>9XUOE6mP-T+A8*R26i=E)X$_zixOPeke z->b*t-1(vCaIrQ?{LAERRQe@m!{P;{#o7zw#VW#L`V4fbdH{fmDpia|ojlNEq@yEq z`%cwy0;k#dppSK|+V<t+MiDH>?5Kd@!w-n@=vT<<<ixxA4;f$D)pY~<Isr!Qw!GB# zdv{~5j2HpScf_$T<rS^t_?+;Ad`^W`Q1EI^aj7$GJW5%TCq{~2>y6cunk7d(H38vU zp;Mt-MW3SH`Y*rv1dl~@5#$?B`5Sg`1#}+dzSS<|S?6*-@M~`Ov+llA`b_xzaI7bS zW)}hou4@hGj1`|Cm<+jbV;#F(TeB?B>d*>`#}<7`Y3PHAXmDfl{k3i^B)Thm(YuSC z-yS8{bp3qU_99wnobb%GX6|!mxnS*DAG>JVThXC+pFZI)0lmq=>u~2X6kGWh8pMD< znG-Di_$i+XgEq$py;7O(S*W4nudf{WrU;k~eeV;`xqZZ0XXC=VK_|g@zIrvxv66*x zchYMs)&9I#^F?0t;2DnoT#Wj~vn4>vZc@)VPX7RQIXD3Y5(YgaP_As_nnLzPBcnNv zoRpQ-vyatZ7A{U3=)B(d_bvl`zzQ!lbTgW@rot>!Lm0&C_CNMH1q*HD*JIxS0pw^I zaLTn{3hdI>%nLO+{#6<>#Oby)(EZWMrBSyj4t#vSB%|9(J!!AUVwYs}HvAjioamU} zJotFzjH#L=U<vUMLg7Ku-UO!z7Cz;Q>mozb_x8VdONBdL%<GBR=dqmsD1Nn*eIc@R z+ala2|AC51yX%HXcDjmp1a`XZFcCpHdN|hqd@&n+!V}_RD<^@{(Ao)9^uti88#C<K zV_RK~-7v4Dwkr&Cs0%3OxmsQ+Wi*bJ%uG%H$-ogU>N`J?V?b~S<ooBv#BIeD&pTTo zk56bG324B6q)|q<8rpu;Vc*D*JjR8&%k*R`sxDg;XY)G!3t#w`cKvlqj4t{+EC+nT z7L66k+r*Ftw&KMy7RqbK$7I;(84V6^41~?8qV|%)wR~vCo>dZ0+rMi1bw-!Y?_D(} znReS=%TQ3L5@j7ZzT}G=K-o;xEd*n*ySwlyS!jAmMCL>0Tepi%7Q1+@CWW#U8{NNg zdf+n;isgAxEv)G7VCL@2PEMb=jwGMSIk>RdCclFzD&AU}lzPRZl0R{1Fa3oqqoD&3 zV0X3l0Dt;AVCJrx`^ShL;&sgS+_FEb<i=sy@8F9x%G&pd21Fx(w@l!J3`coll;HUd z?X`*asHkwk=H@hWnBx?t+zIqnMbFW58L-736>SBF(SSzQIVYLq_P45gAe-1ZKRg>$ zmVoIohp?3dfae3ECS=VFxS7Xb^Lt%SVU-R|<ja7AaJsgUf%LXf{(9n*wtXcDanMz{ zL;(uL{3d=BnnZGb_&x>?{kmtRKW>{$!MhN#@pf2~!hz8}`x5eXb2XYfi0|DYKuht+ zO!uZ=2;b-CrJgG+%BrH7Tv0TTwN^j4VX{BQb4mOeVud;4)->@=TdT4tB8UPiM5RAX zr37Eq)pBk9<`WcR#}ES?jjP3DMjZ*53seZ;5xEbc@O&UOsK0S6-)QmZF+`BOcYP?x zauz7trkg>Ufl_VSB5H?xV@u95!Bwtw;mlKX!wDwt43YJ8RBxLb>P~mXN$^-j2SCyA z?bQ$E)8U4-B`6gk{AJ+Ys9b~<+3)NE{)>%I8;SPiUnO*+NaFl{DeSY>a2+?Ode)|t z2$>l%-9lwmwg?eUH%+|LpiX#w4bMO`vvc@r4%RIMV1?#Ume=@Zc^T%mRw<mYnyveW zfmvhs(T@bO$TKI*Jl`7ZZjq{c0mmu%ER?BDgt=y)_1-p79*IDpqUX=MrMMS=ri6Tm z=iw*k*VJQ`jf{_PS+u{a1siZ~d$~&t<7oRR8k8?i<r&mbq#?H!TL-8uccyVrVo;fq zGxse<p^!OHH_8;t=LT7J8de<6Qd_0*9;^>@Zk1Q_eLnm`#%=!EQ{?)a%TuhdCEu6D zL%@4-h^5`PByrnotf`2yI3bF&)nq=KJymHk&Ih=mp?y!#M;E<bZJ#kGQa~`ee}FEY zE;)?5!UnqMDF%HcYbX7&c=fO}B^Xuc^uA^{Wdt6`Br{=r&gFG<dV@N*ytBpJpylhd zs1ij(>~;`4%&~?K4cAqjlk@R{&@G>`ReJivrt^nLv7lxfEi_%`a`U(4jW3Ije}rA) z2uIG=E(8b;@oDl;Ux*T}MARB~eBso8@|(p7`JID$>gm*W(FfINv#83HvU_0AY-91W zJ-myD_{tqV;!-))c-jtkt}10d!3or1S4c0;<#!@|Gpi^2MtiDa+YyDQQkHAgO}u=$ zsV;OD9K6pV08)}UR&3EluV+UP_lmeD$-x~O)@ss<yek8h_19M!rjFZ|ki_H0z73@) znJ~*cp)PU!1+K>d;Ew{Q33S^jHVnoaqa$>A43Mv-zuZFSAlrV!n6;M6)|So-)d>C& zZcYU5<2mNbD#yMtup<OaLuD1dlTIey51G!f)>_QN0@b)~@Tj=hxw-t>P<aOAN0-T0 z`1&*F9;to39T45@)0xB2FGC*%?PJ))xisGygBgVLj029A;q~%%VX5MHA!4n(8DDoU zzkXQjdX;NaxciyNi#tkWd0dE{>~FWY?Gw$ge={uNIudG0^;F&yf)sGHLHdmp=r(nF z+e9ygL`5lk-ajtk3Y(jXE&mCLQ*js!;k@3>6XeioNSd6OTA5=Pv2lHq8WK`aZ2S+$ z{=Iv@9gDKGb4Z_CmVFYZToByJ@3QZe-^afeQ8z`)<dt*nRhFoBEEu5%#Ro)JElMI6 zl1eL853EDRkIxNI)1|ACW(xs1nbD)E;e5{wgoSbPUmw*dYh7K-Xh9EluMZKXrw0-N z4qADlh3i-D%G#C^pQ^C=L*<w8QT7cKDzB7;Ygy`LB-wO7e|SiJ>X5dxPbDuhO2ZSk zzr;3$bgX8++5Xc|>)<*e<CdRuQX+#K6{x1DIf}1OHqp$h2V#aUbVqi<FT6Sf!K3}z zOr#5EB0@YZ&&5{0d;Iz`M^)T*e8bxMN`0Dfziaj_Do(W6drJa1X&z3uwzv4gu<NP4 zZ5R>~L6+O?Fli|Fo1moSTzemTG2?DF`=TBzHE9LdWf@&4sa+$LqY*qkl=dyBgGNnF zeWlp7xP$Pw-T5<|t)SizTxOo6*8+48C>QxqNg>b5TuYyE33jf~#Z3b1L2IkJF~mzm z&f(D7*0!UyMf^;yR96#Z_JK#)9qXcYAgdr#;*GbxP*!jOR>`G=tE~M7q8>x`L_vJ@ zb&)x{*{*YquZw?PS?VYR#w6EYQ`!gSE}5@k=uZ(rn9lD5m-?Hg^r4|4SG7=qHN&dp z^oI(<``%`d&$}jO+QW@LY2XM!_!F~P5)3J_LYC5bUgrLQ(HZj#0Wt^tYf2iMLs;Cl zCGRuxyb|5o8&N!5S5{$_v4q_5Lye@;Rlym|l2`P8!z6o@?^mOR3<6MmIZ69Jv{tG4 z_HoNXZlJ8n`H%hR567s$W-*B@$yg~W_8d=Fb`VtK@kkw!(fks2X+xfbhf}J6rCLg^ ztLsq~s0(k`#yU<cVA)3@C774;z@698j9w10jzV02(m_cZ$}v{v=C~f4wSGo=@q>b! zYhU382G_j~X6~P&v)$Y%|5>M`9X-&amUOumMUur*oD45y(KYFv$1t~iJrO$^Le8Ny zahu9xC*C{olEc<wF=i-A1z^7s5@BP8;*`>c*Okj&pA}b&;R|-UiUU`HBsm$=yJ}lK zmkm0|%#WWmlE5yS>J0s!BEJ9vSn>l)=!Ci;LgyT>!zrI7u}qQd{8jHG&Qfdpey%m2 z-gX^4<!NX4B!IYI>1HXZkA$5Vi8Wjm@TvQdR8?TXPAeUoPq<W(Zo30!y1~R;{vHK6 zBLB?8v3$T`E4p*7SgFQpJ#%zR?4mBvWkR}}Y*bW28wkmB6}ukP*3DxH-aLXiZ)C+r z3$dtcBTckp!8QuHv!P~b@X)bSu`6-~?wD;Wi_S6<n;DHPnq;J_ilXv7_=dFawCAGw zdO7_NlxKN%GQ4~Tk<tc^^m!zra!@qn&L!ZIJD6gfm389f)<lUGwwvoCt{^_~@TK-k z6%;?BJq2&NH)MsxYT8Qh<rUNJiWa{Ay#QsuGAs|R4%~MX@SBiP1(pVz_LvD~11+t; zX<MH9{Qa`+43#qKDSfYPX8W+8pa-t|nsVqx#>5=b6Ss$+>*_4z{KRR;5V?t}0p&gW zfW0wPQc`kx5E&CDaq-X&D}2YeSY9DOr`GLzG>Wv+_lAp(-`A?CU0^HGM6Se$1lWp! z%64)@?h-c};N%6B)k1@#Yl|KgI;<fhQXu;<QMNMZ0SL%>wj1nt9x*H--yhb&F}AyQ zD}fR`%;X;fI5eCVaeb^wnsA9uhuvC;2F1eGH9aFnj`zGpGKtHSHs?#I651AbO#MiV zk>YMLs%x&56pNP%cV=aL=Qy}T@gmaWCRwO1o#Nss_G{}bI`%wSkMpFLF#_9+|Gj{l zbx=>4+t@uMw85&lvh#SSoi?zb44h?HPV;QQ%9qcI>M3hb`{Nc47d~%n(x*DoHGRUo zPnX=awG{;pKbobC2ki}SR%`2gToCB%Q@XlzF%v!qh4z87_&BR927#EXB}FPzX(sHf zevUPwsp*bOfhf`Ua7>80IB7F)4Y9f-ZClnQ7f!NqA<O9-L4Jr&mWN}aD@B$bOR~WE z$g|^S^(iQ&3cuw5r|g5N3n*p9!A0$=jtYFYyYG$h!71uX?|iy$oryw$lW}vC#m0*} zigff*r=rrRoJ4<21k1(1P@rzZ76j~`g<^^qg1j9^hx=|Z(1IDn_;|T!#*q#KMU96A zV4Z}^tvU$BfX*)!ZP6m#GL-YA0{L+Mt}1zN=K7xX=+>e7&GbR_I&2jwS#8fS9FHT{ z`c<U2LRY_TH=CPdmV}Z}ks|D7NpYI7T_Tz?K0N&{=xfrCHyi|aUi7WTZtI4W<$;}} z*A;4_W$f*>;b)1LUPA}@EKs5z=iZHD>%q^|RXW^u{FciZ?yUSZiZ2?>x*27*0Du~7 zid{kb2%+W^=AShT7-ukgt~-Me2A9jaJM2w5-_v0GY;S~~@-OzAF8!tR;g%|Ij0xq( z8wPIKGvIOx<P|iA8L>l7OmuC3ai~Zk>w3P34C=P|uy8UeEwaT|X;zMkU~#=RMo;Dj zW^Pv?^4O5QqSDqaL3`C90>H(oH1QE-(D#sn`Atox{y4=Yu}ed_gv+jkAp`~Oc>aAW zEGPRW{fVPQ&oRc4?W>@Cg@z^RX|~$x-3+@krGd0u>!fTB#ofv@kEKT@;f5cNMWlr+ z$-M1|c<x}IXCxF?yBMYylQX0a<CZhSLd1`%(GA>CDcE27KEj!!Ag3X>-{hR~Jm3D2 z{)`jUw$HOqn}{stBOR!0onqIM4n53}RsY*DZz*0iEUw%v8gWKlb1)R3K;$>yl|fT; z;JRfT5EGa1EVw-?t?BNWhWWVI(Jvv21%Y#BH4RNFx~{Ks(MI#pD7?Y>-%_h;Hv}4b zh}$<awssMuKLUd3E&?>466$*PTFO<Y|FC&Sn<xd9X>(%^SJwI5g<9_RoD|Qm<e~6| z4tJOpS<039{XFkV3c{awsVKt2XOlx*dY<HMo;}#9?8UC0s0|kttwHp`H))GWrd^-; zlQDL|(=)fUMDa6PdT5&&DBs=`<#4^4=f0szd(5MHTW_Q&X1wb8&)m3Zcqx8vKvnEo z+kQ;;$V7d}yYT(=i^<k~z1BOIx2F5BeG_#6L{4nFZaf2-gloUw{E_%#ky!!UgN=+p zeZ<^VHRw^|c2J2v<mo)6*AoI!?_!!TFJbt@xF`zNs3lFcFTA^YY!jshL1DnH7Te0# zDgw<sJlGUz1c}Y;ZBwwLZ)WROwES;&N-ak^c|^eP7EtsdqTObu#y%kmhSrdPzcc<e zb80ss>tp{Gq9DD$#^<d|;Y(up75YdRKVv9E<79~*_BfdK-$d$fo!JiU>7-1TeRCYb zahN6RTWD<f_RyUid%||szcCruoiPnezPieqmq)(es^5m;*uBfukQ3c@Al|-v;<d!H z+i-!VhKoBwODAT{(KCz17b%&5<igRVtV4GNAhQN+MNTfE&wqFs4Oew=c4w~D8X6BT zM}bq9Th~uzwq;ey>YZR8eJgVJhL<bvly@5hw*@yF=8*vQ*Nz?1jzN$ImoG013rFa3 zGjE}Wi5WxH)>H4PA(Z+mZ@4@mZ<Z^}wiOO;4)AOMCw6t#OwZq5OF#2mRp0!M%E+M( z2$)mfbut!PWn-CXr1&-9+9M+^ZlZ>Nz{ZPU8t4`l9ccMrcI`oML<$gz+MkpngCy%T z3}FrSbw-m;gL1%RhUJF2&zylQ)%zx!)-e~&DnBuP<^mnJp@t^wW#l7PuI}q`#UrW> zUr-^-s2{4p4BzrODvRg$)=(=9?w=GqId5p9f={Su3GZ4~$R?PE)T+oln2z5jNb(|{ zCY|7Yd2GP_3}e)5Tg`glcPYz$`y>Ym6kBHUM9&HP7KG<%(x-iwc)Rs*O>aN^W6Je` zi3JzafXB`^NiX)=uH$*8Q(&}4`{z?=ToRzh`@C}1>!w-NbG`MmCl#Kudk<h;9UpRD zH1<|LYm})ya7uMId*nFOJoZahDk#rUq+D~iS_9sy>-4@*)zZ~#m^s#K9LsxE-E3xO zD}~3lZOY3o*k^hvOGSO|$*!K4SCBZ4z=Hq-U$oelFF`>eZHr~K%kNf;hpE=%m`9<l zQm9n(Mu*vw9DC+<cJ2LgsY2B<DHWggebwk+l#;b_?5ch8oVGJxZB>tgZPNfnF9LO_ zf4s9|;jYt=yb^eJmxmxi#dBh_yj`eL5?j^$OwU#_W$M@=T6!UGEJ>k}T(hiUYDKEe z>#q3*@H=10o7V4*^pt+T@>EbLXm7*1?Bb@Q{uAT2UW5o#<cYpsdI5^hI6yJ^>+zz5 z(wsSUTLlv*+;yKVs&#}shs4u)cN~<*>$p1u>&&ls8JL)HxzFl1+_kEQZ^l%|PMUwR zwS}Y~q52sTa`g}C^C;33sP(NcEH8M%TGkoED0P0l)4V_Iw0-Zz22q;P&xYp&rDDj1 zhk6`TnA_O>&)=K%Z@f+uz5Dg#r)FYihlStxuB5jfP1Meh5azC}wSprIR1qKfL48?9 z4GEx>S>02vnZvQ7z!u09j(2>g{MC>*AzRIpouZbg($SM_mL6joRc9i#(G@Nn%CMk) z_62nPQ&H)B+JsLoJW;?$0f#0^7+X8pAgEwqDw;>>mmRBY(=AzEV*d&Ydmx>_W93CJ zDzk;u9zNs;i(Dsb_ros<2oB6H{-4JE%lF+}X;Emd;~81l#3^13=c-LXl};ruuIr;x z`FqoE?Pnw6?3=^~m*rMjm^EH}zdg{SiQs=co2(bKLm}LeF&H0T)Me=VA@Jgjy$+71 zM7>Mw*@xt~s2$(n*RBihIT=n&6AfFGEG3#ht!VoI=~&}Mn`1Dhz-i<7p~Rq_44)Y9 zxm{^NAMau*o+lIL38=Kra9Thg5a4g?CZd<P*>|a%oD}*^S&I5_z&XMAHZ+7}&WfeI zYE$l{c0!{#sf-#tk@G3ya1m!Zk}TZrG*nh!PH2q#GMJT=!~ERShLO>~S1+Fiu%1(6 z2)y|CF=h^c_xPFQH+Uu$YT|N-{vY+Oqu1iWC;<51E_|o|&#GDf2R0{heDgH)lIQeh zqg_p@aBuo=<*z`H>{F4utb9dn(F9^7Dp&m5m}b2Qxvm|=Sh+RQG+=18_j&&`=;f0? z#ly<4ZUXcH_linFwbZ6YLyfwc|LDo3;NcsDt?Gjr%H`V?Cl4d=jFP$II{|}P$v^ie zVyRq6I=XgniSQo%sA3DOLOyKw>$*<<Q-3Vb-`dq&s&H>wbn(EYURlG~|25r<(;#W_ zJy%)bf7*nrECm+SE8w0tu9=C=ctQWDLnl{cMA+};-!?0o3(Q>C%3~PuKQ1fb6w6ko z7UtKmd5sGHQI~*AAJhKIse5d?ky6IjqmYLj>xO^s?AaVf0wzsnOKw**ZviQ#*qgoP zf9`;WUUla<*)Vk%$d)i64Zm_AX#cqZG8$dFu2sh{3USPZ7?i)+#tnx2`v%qDHxR)< zv{|&2Q}#bie-wO$>YePpe)@#-UO%tA?|(YIvog_Lh<L@v@vq#KD=e+y^Lwu|H+?&q zB>z@o<I^M%tB0wh2|wiex6x1OY$E}>{12->cG745(IbP!wdusYFUULe|9ptz_d`GK zl>wgq{T>5D)#~ncjS*b#k4oO>MxNS}dj4A#?izuo$d5@KC9(^g#$<0|t3f%_-&bI8 zyn-+(wG<FOynllo=A*8VoZsnEnXY{0>2u;P)WcrXw$hYpg8AoZ<&|EH)n(sQR$279 z3dlp@pYfTa7MU8wnJSMyGg$oBXz9UNH3VeP%0^l%!u$U%g2%^49o|>i`04Y{f65mA z`FcERX)6P}wp3@+zxC_N`SEzoZj0wJ41Yd}8%W82>IH?UiWjc`XC3^HB9R-|!dt~m z!{sH}AFa=`e`k<?Uv2Mw&Y-hMd5LCUpM-{}FrU=Fh4LVUaji4WsRj7o5iOpTQ@K%I zl6wP~564(3Ycclkef_(D9d-GOtM>jKKp=-vwX<4ka{q@F7|kLcglq`W-OJ`cl|nz) zW5!EW08dz0jdRLJ4(_DIXD#(<lcU@A1+1d|hcQjjK^V*G?q(Mp&+3lw@Iy%cjIJTV zrru?RFu%WQ-EombwdoXNr6@4vd`gl9A?69;!DA+oY(8?zZ5m<fv#oh*k3$l4AGJTA zlk$0VfQTEWz%3lBdegkrivlg@cw@-pk{AE+Y7VTdGofFST7Tk?T3z!hj&A0)IZ^qP z-`AtWAz4J9fY5y#9UdZ}STr-cpgz-Jn}0LZ1K}Y`hY!EPSn}QXXuO;JfDiv^{n}T3 zPb14{pT+xn%1g_J%Xm|h(zL#jA3!1EZtWW`tSA4AaLxHf$lk}_(CtR?C}xNr**L5@ ziR8FxJQMj@q3==9Pzaq!?}~CXg5e;55U}j5%$#i^@(y01JjdinT6qS8CajCVeF^#& z`wU>SF#U<<M1SDsxh&97WSO@Z?%E6qss*qmO!X-n<f0eJBY>|h54>VhHEx#A%LPBq zh$#9}9vcr$Y54qEP649(;ohfh!a5M&94AJBCg->j<grFZ=^dCDY!t)vX95e1$mD0R zP6_bdFz>HCtiZ)=0<yERcU}LYTYGGI*zN(t@<Wv^c|(ttiG6O)uTP<Y*Qb-koVVco ziZLqt_y=?JhTRjE(A6Y4L?rq$<HI$wf<-Wf;Ktx4sz1;+x}kl|le<Yi#7yyN-$I}k z2h>CF%R!*KSt8o)N8zVA&dZmBD#+6)_3KlGU2|LXvCBb7ejb-=gaQBZ1U+SIL?Xg% zPYWh|6z6;O#TNS_N!@+ZUzrb9=~nqp;HbTutpk^c>*{BR+vz;^%)T{`%qG2N+YW~b zOmdU-{Y{X%R)tg)9J+{XIX1?(yD?C}N4siU&N*+4N<M0Qo-7JwTk<?()N|_#boa^z zqc~{PRiHoc8DNBma7Iw<5zmD1yO9TvmwQn#Gm+1T9OI_@+~!`}B{%+ERzfa|>}dLt z4o`Hoc~G~(J8<(}XUUx^h@L#ulr4U$`FptNzE=y+iq;uLp6`B3A|*0X8#vfia%|W} zTfEecY=Y8r5s>~`Ez<U2(77b;+EaBLOUQQ&)f3%QXuO)OCN4Rcc+#!njhOk6V91Sy z*$j*<Z$gqE8#JOJy^fgiJAQmU<&}kbgCFj1vVlpHR|G8`dkg8`Eq{kr0k=e?796*4 z?eDSZ=3aCyR_rnFA{D!3sMnQF)>W^UnS|}!Z`{0T$$62R{V{ap(6MO5hD~u!MigVT zUP%ddvnxqc!=AL+9p&Y6dua5k+@{y{<KZ7WEpE|};Lgj%0NPW$<(4r+M3*5@=J>uK zhbhL}KlU4+cr^{4NOneC_gpZ?*=XU1`}Gp~$wC#7W4~H=M05F73-zCgFIM(8z8Q=y ze=nImEtr4I$s3HuoMiJ&*fK`#d$eIP`8C|Rv{EUxeW$%2{gq%o5f}%_E0WxI#-WK! z;lMWi6?$1y5a(}F|Mmb~GaGfze7bR+!Vm<Z{Dk^~*f;$}qUolj(IzEO`gq+Yf)a1K z0VR~r(WB8_y-whpdD@v3gTPKonfEtl+iNK_Em~AKi$Ww~w>!DHRo1}w#B8hpR*aQv zeN^w217eERF5Wk1=LEttNEc#=9aE4*V1%$u#o5;Ve{2c45dSG@O|M~ZR<s|w@c|?t zhUXYJ;%F8#8+j};F!OZd^X<Cq-x+tY(P3{+2QVW?Etcr;w<lt<cFt(TAX03^pN1S9 z%iP{T&lBFcQIa6nYNDr&&buC$TSNCd=P2f+8CH*3$FGE;U4pN-MK7tlS8LSJef4fI zGzMJn@n(cqI*tWhHRhwbXnGx*{Cbd&lKC@BXSC!>&|8JC@azkc8*&ok^5JUF4lrXi zHs`}CjB2w!+5Wv|^z)^gud6TQ{%8f;skZ|&!Q-nGnNQrA6(7CB^-ct_R&=&qYGgAH zTN|9+uno&6;@Y7v_j+2>mRKh2pJ@~Fz+|m+oB8}+G?@?h_iHCvQ;rxb`k0*Cyp|s8 zTpI*iS<)V*t@KXj^ORf0j-lXb(Xl9!;M1|Q@Z)AFzji+`?Xc-trg5FK0j8gI(M8{K zNN4d=C>=L6S%8FIF(%2-H-ZwAyds&sBP|)L{i|0q6n*uU+HpW!nCGL<L)F}Cc+k2e zbLbdK1W8e&a`W`LgeH4GhE37f+Iy+*6hA|Sk95caBi*-hrz@ShCB30t`zZlr3tlDg z4tFvB9cRC)>vcw<>p7Nz8za=VXWPr5k-b<t)~7u1phT}Lm}JheG5pcku-86gBbPmT z4SQ@7yWHiO>SiiJ|E~lNqmXdS=DRnf{m5HX0pGrH{#z5Hes7_aaZD*iZH7mQ^eZ`z z*=ZRhfBmw+#1EAN*#7X(3Y*;ro3!O_N!W7th*)40&9eQ>QWU$#u0<j3C+iByNtm4L z(RcoH1L3Fe2tiKoO=`Zo9%a7OhVthgi)rrH{$!P6rcoXBs)|xaU5XC<{Kv);<1dZU zp&R)rZ>$K8NO+%0rC-~7<9cw4c$}EfYBPy)3ty>uH0gRwLu6*ZKz>Usx{FcsdKmbn zpYIuwsI2G($^spA?!aK@e6Wt64*iXM3nz21y44=YGU`ZH1q5`wE+-hR8F6gjt>ylh z?MAdxhUdaW$itD0r_mKv?Pjs-D|4zA0la0*NLPx|EEB|{|LBb)NVgEGtvCh1`3sRL ze!M`lR_w+aXKOdwBx~om-79|%SR7Ar`s}>@>d$y|$aiWG5~*CG0S=>=Poqb72f#s0 z&yMCR7bVE);_>Obxu1MT>oNA1dD^x<=YED#|CKLxaPNH|-DM-)>T|C@|K{mq(wgK< z^Di&BNU$cX2-KJoXPg|cUE$1_;?EYc9J&6P#<;HEuR5B^`OqC;yMk{}$5pfM&SpR- zmWD@HdkwV(o6-MF;io0<I$Fp{r2d8n(inaJ+j2$RYV!YQs|x=I#$kWgTo#CVD_0Pp zHU|1S4gh8~Zn)M*{h2r+Ap%jq*NS#{#(RrUqAE~@P%Fj4i#Y1+^JYH$Gf=9^P10Vf z^4>+-x=w;V8{V1Vahk0#e3E!6F2Bx1B;-GzfBc+Qn^Nz5!IY0v8^2I){(lAHg&j!& z?f*tz{xV)`hF5<eFzBll+YNJkZ7-~<L#-N3n@A{-*U($H@u+L&VP)*U%_HfgXgQ2Y zPNtu_N=#2@n)>JWHdSx$5-sa=p^kGwk9~<kk7+20zfAp|#O}L&LqUvY3K+AUsGiz$ z?iPxcU<y;?Kcl(Da+g95f*NHX@1ci~J^8h0pggCk;`G8r=Ldsp)7+eZg$oHWC|d&S z(FX}LZWB|}%;=Ek-1tv&%{Haw<^3cF?_)mb+&>4Elsrg%X$cB?(7NlARa1stIK~Zp z&}~xPy6N=#wTQxm(yNK@IW=D=jJP@~Sf5K1sH^C{lC;9y5vyfgIrUUBE_t5Yb-6z+ zRl=>1v0Yog)Qr_D_P*J5q8C)v{K6XvL|5`Ise)AC4{i7nyA8Qt*z4Pf>)*p`Y0WCs z;?ub$#k1b|j%>ZnK_(_7P&2>PpHQk`j7Ln-($c-lp9~{;N;k<(S16n-=AxERkY!|2 zqqFLWI##{?J2or7Z&_KXn9kE<`G>`zwKt#4D<hwm^f5EV5>ul8U~@m?uhNt+mAR=6 z4aHvKK?&iJkX<xGbxE!l%Vo;FBXOq@(w8q8Wnr@N@>oTflCG|-RRX2>5#1pqPrsd= z$#!&-J};aONBsd>Q~8vWFGIvb_Z6*k{}NP}L$(NhD~|QZzkOAW<n^v4<8SHN)U@q7 zG4bv&I22i<<b@JZ2N)%<P02#m=gifQs8I5HRSWI!>k}_@)YjHEDRs<8ilDLW+RIz- zOjr$w9(gOz8Db;>5ZqsAf&P`$rb;*aTtW!5Hx!5yo@c9fMsXbX$;iHk1!6r$TU%?` zLeo1t9?1ZyY`Cl+D^xHsvPy-)E1IG(R*clN)dQq`9-3TNm?#y!)m`~gADcqVZpDzo z_d`KVJuN17$P2HvD6@jNo`0}gZNfvgKcY{#Amj<%*8pX8S1c+QT$K{sMq88+oaz?s zl&!Sopjvy?^awM_z<<W8zY?9Ko)amXfbA>riw4u4o9NZkaX_o4+<=MTAM1yrrh@)b zQ%_SeI7^SQ#>Se4Nb<GL!{vv>f0QaD{5v5EIR8eb$}Q<JbMhtCB{x32#<%KjEBnGA z3*E?aQD>6_4(vkNKW1Khg6yviU@)TFypn335m&bQ-Wc6zKmOR{%(>fmanO8yt(dK| z?mR<m@ltUWG_{@l*GYyn(ER&uX5KThNt+j1OLWKMaUn}Y>!;d3Iqnj1IP915-FH4+ z{iQ2V<qsHt%`d)^w({df)Tpj&nto0@TdZ1~nB2%V-8;YGqr*(a^P%M5jw{RkwW}cP z*Epvv^j_PPZcS%#(`l4ff5HM(&#?*ruZ;SW(}xwLY{6Q#LH(<Q)-dZW_g&&-tv}2P z)l-Uyz&{-=u>6~jvQHYOZ}u)I{P~)dUt@xT$qs+WhZM>l+M3#~l1MUto0@Wai*s$( zOEsXc7J8u^{8}vEX0G@XE&{gEodSb8SppA34n&^`cj(rZA|K;4^PwQg9r_j<_iHbA z(VX6HugDbW+PVAX_U5%|tBK9TW&7x&DIA?UyP<(Zr&J^1iU1D}EmD$N_Q$ZD+q+U_ zF-qLmRt(Qs2uUwuape&dO0bE^RAY{gccmJkBm!Dz)r~_o@AcB(qfCyw5XB5RHn9l^ zWxo8!cm2Ef->a$R6lA?qntNbX$eL-c`znS-qmcELPkz<{VGL4Ii-l0xO3&sXE9y}Q ziKc;>q={aFldx)`S|78L058Ip!Nqj-3FFcPYWhxGrsVL^i9}8J8<{1oW$o-RSa6XM zvlGwSeA#UP#g<XS3>O%PdpN&O@0&#s=4^Q3@h~pEC3d%`(wh9f_ndN==R?IC-IBgb z))bV9NglM*MA-V*N4{ECjYTTdXjmUtR?#D{{F07%y!kgDc~|I$^C_cs3wWQ<a-5EO zom^1ktc3nNxc=0|Q(0+%@wu@UJHQS|b?A(f0ZjN>zYNEUZ<OI*ip0z@-CU>S&m$f; z-)58|K=6_GKVJBjoG-hb;rTiakP#}u-S>Bw0+86Z%D-Ghwc-<+uP|c}{#OP5W=e#I zoeb}D%|+4s0iD01Wfi^+L6%1}XwxbLd{~Bh@HY{roLyDe#rrvzk!eaC-Yk;hH@uyZ z<bej(Q^w2@S`apZ{PYa~m}Jj@&#dZY|8&}&*&pBn67&DBFIq&oW=~NuNj~G;EbU_d zIR!$V_#&JjPpO$MV`t`Zg$Pnpzs%UYE39G_XlUi2-TU~dLwY_Zv$pC(_|D^OHM1P^ zHYQ5c{Klut^S;2(YlJ<_^LZoxn`=>)Hex|#(eahknQO8=H?e2Jip8-|Kf5dO>r()= z?Bj%>vD5piThIRxX_wJ2C_ik5bGX_u<>_%Msr4LaosTl7q@nU;qVht8diHljbsm#S zjiW}$&2a;X&2PG#HiO0P=<ytzK+}kn%Z&#cB7FG$h@aThhUwpoO)xcl{0a%^d1r`v z`p<D<t3~sjo9W8~b2dKh_*<~m{p(}-iNs&2t7%a4@B}$Cqib2F;uaS6jueQUdn6js z48K>>FbExpxGPI@Q6f_y`snH^EwAu`yLI>6UZlNbq1r*xeT1I<K2i|*x{{W!*SV-c zkBz1S`SzOj(O04E7$Nhg+a=f~VN57DLS6mngS(xX$I#(Z(?AC9ql)$`IUZcsIIMZO zk+*^}^%;eeerafY3~S3lAGipklFEWB9|thFvYs6)$rR#hr=q;PzkGe=i_p;1_paqZ zS4Cx<GKY-hpzIEZNBC+38q7j$idqB4R<k<aK(*WL`qmO>mhCYjr^J=CF8sOI^?5be zmW-`F_8?|vCPuL@pBosID;UcQ#6NfoQMuSqKg|BFgXbbZZUPFZjZ{+B#u!Fqb8%-^ zL$&7ckU=gF>MrtmPgQzP-NlOpSAf4h-KWU-5yCi4$(Z4tgf^cV(tIdD)_jM#pvgg~ z2u#bi@pB_z@?gN}7Ud??1*S(Q{Gv0Ba6hYeISNm)tHX99pZ8v2LT@{Bn@HK%_+*i5 z*k`27%$>tM_9%ka2dx<Nf#$2&b)G#36DzBfj@8o%lHcxo`w~=z0z8ec36vCqF{_;( z@bv_gpo@AuNP}q@?=*b0IW!C!js_^$U?>oVr{DWR_4>8JZwMrd^*Ivd(urpXpU`X? zbMW6Md)Z)nzkFhemn0H2Q77W#8O{wFnoPuN$$F02Ej=wJX0t<&J|`w~FByeu!g<{S z)U`bx#o^&)ZVoPHs_E+&FX)D1uciIcPo1Tr|GF5W_~b6wC0rCnz9s)iY2UTtm<I$! zZ9UM3br`&<uRv6^`Otw01)7Qq3h-xVUxLJ2B|W5BM6eq|R(iKb47j<gM|P=lw5UVy zS_lYiW$)EqQOG6B-b+SMJ!+JYf7lrIV!$0vYEcd$FqgBRuJ)j#K+xjhEC@Vxj6$V? zPvwbES7^+Rvil^C*Ji=-rx%?NB|eyhcjk`>MssPv!QwoJ&mPsr!s|zU7;9DXF#$_G zJ&(mKxnsV6FA_?8&ax3yMYhpFVuAuiA3Qv0*VeE?CU}W?Be8Xq9U0$@jptyJ;<flu zLGQ7#F-r0B@SydrIxs0_<>u#xd|%-@MXiG~iTEZ6A*!J*?+6qOUNxMC%h_OweEvzG zkc=l2$_js-$5WU@GN2PVK^S6&GX5LyIN3C;FvWxHbl=$>CnqL`s;q;-;0_Yy?Y$z5 z6{XOBVbT+de__)8#xl8;ii{<{N?d%V=>t9nb&bhaD(uS(o9$t(1w%G|^P3u;qrna8 zNKdre1os)a;#Jp%k;69A7>}(dmE2tXf6)`g0+g-+?B8axvBi=1RO8Gd;Y_&TH+WL{ z1D@V5W@R-tk{$8TeAV@e&J{kjxA?5aM7SD2DQ7f)gGS+X@N$8^dFhrk@r=i#u2`{F zhB=iFIhQqaeWr%IDz|dz3RG!fKF+2LE2gtLJv3F6na^lbl>>9!lo*^5Zlv?Sq~>i7 zozIu<781Zq%+HuNX+b3=@R_XEelQHmcfAyie3zpiU^_%r85qKF(xj#JmDJE6bo>~p zk75#>$Z@OjeuogCuLas^IC8%Aaqz@K7oCX`JMSVi)Xz%z0>WQ)3(t~yPt6uw>pV7= z^7-_!d0lS1GWJ~L=Bvhdlx?I1x@gJA=eC7r&{Ag5>P#!p9abvQ9lD<cQnV}&z#o^> z4|E>_mkSci$?q5k9uqY+?5O9GMf`oV+X!a`62@$sBH)8pLogZo-RH7DsxzS%EjDrC zqC!3qlcM!-+$}@}B2u?i-|b)mW>86<GV{Dwt5VU~c5r`yqNO!L_-g>E*FE%Fhu!;) z+w-&x8J9#{Sb6r28k5Es)6j4g3lp4`m2T9Nk$;|aL$z$Mhk3+g<f{+qNhEqBnC^jr zHt7R{-#xBy-p4NR_98`hq!T`fHBd&CZ>Mj?HNWvlYgBZOi-wTeH(g&XO{*q@w%Hg* z_P?u!y{HcP?f#Vawa;WT$E?JAhizRm?e-~9uTnj=HBP=q;dtrFm%;lRFJmj=8-{=T z4pHRISI=pEvHpUV3e*x4>~6<)qpP@-Kf9Vq{VYU&23Dkd%N=Hy5rON)ZYn-;?;jma z73(*X!k|CGq`6uev&Da-<`1OZYADFJN7Wa{B3-y3C~m57`Q%zn1YW;`qbw%waza?f z9af!G?P<78nSxNDTyRlvQ~q6yDLx@m1CpJQEyCWp&yxH*aRYMrXLlrJB4yHJajTo9 zviFr`4C1p@Db2=N1+S?D+;4ITE2qFyM`_4AE}Qxc2;eus+*#57lKA?~VGDo6CPi!B zU?AcFN)jReY->^w?yw^)UAoIE>+2c(*t+2y!<_1=NsZ{qCCtZR{NW#54GJ({RL7Q| z@QYt9zC^)3xz!@uJG4kt3tPi}2Mr*$mC@P^r8vl<d~hg9bIBU9#i9-NbLEx)pzEvS zqUzdqNs*RDI;0!vl9rGdx>Hn!F6ovoX(XjPM!FjWq`SMj8_ouw=Y79(e&5;u@f(Jj zz1Cu{>%QV%XMX}Q%$Usjo37uLT|Uc$@Zoc?Q@geyBTW@-k!83Q9x_<K`wcqzn}ENj z>ovRV*OlqhOM?!sR2P*)F$aYq059y@v@Pcw6`L>}^esHgRoDg&vmxmtd;E`9f)?i3 zPuLtA_Zh|Sz!Xq2^}03RhE!*@WS<6OVhpz?*^4sYm>i~9HPD?1oiz0u9zLsCNN?1` z;H%n@@*Kiwywa7P-G7GOfA8n##FDUiihRsPFDt6I`o3@N1S1$&7z@CfPm+V_d#WKu zPBYM1_uc*(p_VdSRbfIrK;b)bGVEc&f(PzusPl;%0^u11*^pNqv@qL4O1_wU*&^Ne zgUcx{j=;jei)1zLfn7qnC@X!j!vO|pz&;1y2}AmX*fPX=s&X+2?!D39Pb$CPG3a3d z8Y=ydzZr5uHh@hja~o_8E~})_2NoL4dy*AR`HO+aLz`Y%JuB0xXK6Ph+=!vz+u-~e zhZK;%8UVi~TIYN+n$U7Q-|!gx@6)?3?I;OT{b4nxP`>E@sEjl~pYyzPEdZm7XFyl2 z&_DmD$UWD7e!NMYMSo5i1}|svi?#jQiuj&he7cYnCV&B@OeBHP${7JGKI}fff+R<u z+qa6fD|d43cJzPCsN;QoeZKPt*xBw>R5W6R77snY#ClY;5yIqs6Sls9#Jm+XHDSff zInFQ;1|ZV=>S~|F!7}V)1VW{lv_hqLCS5W>p$~8laJi%K$|<kudV(^jof<C3U9V~% zfmGWzCqNkisG1n5ZgRQNIQ120YdiRvC=q%lN)kROX$u1&%?LmVS-k*a1IiN@_pK`U z1|`?#j118j0&eRiL!+=fJBukjpVVpT-t!N|Y9iT6K}-k%$5yi(ILPn2J6a4_1C{V7 z@6lqVY(9LFOw9XfU>viO-q8W~rIG+7)|!>m^`<h{J}@{KlN0*`;}dE_Us*Ahkoz+2 z)N>m6$MnG?P(Ao?_5J1hnGR!dxvlT3n=@K(m6UuA7vQdO;$z4GmL-OEN<~G5?((O7 z-7|owtfbyvlnmI~TC$*2QB!MM{Tiawr}eyjv0$}8Qy?@7aXk3tzM)}gj%gLOlrqag z)?l={c@Z<L;Wt%-FTQgQa3JY56-3+lbR&LYh)cr9E{(KIh2II10IVg27JyZm!<UuK zCG^Mkv&&7&X4zdCh0dkLD&i7n?dj2F9)+9ZpAcZM6PRparh+=YIXz7lC1(OBK|Yg# z0kGkRD*ePa`#u8`nLdmAefD-;GLx3G#IZE>X*AjXAx7BP0>e=I*IHVmJOhGddm~di zlj>UwD&_*$I>Z;|P?c22D|=xb*hJjysu>E)e<K=jV`7RPlIwmpA)Z(~;-e@}ikO|p z<u|(dls?#q7nnG-Y|jVM)ebl#Yv2~~a1E9jX5tqEMlR+x!*_hrh(KIPq@M#IU1f&K z#V73LH2YFA6T^}&&b)YH!PpR{2CGNJ>f^^O+I<F2PR=~cmT@I9Ee0AdRJNzFYG}Z; zIer0P(xSW??AP0-0FC!O87j_73IchpVGE-nU+@<l*~}(gX3{pI#rHSMjP03yZj-s= zvE=m-Jk|wa3H<rQkjzXIg^BgVM$H*a^dc^4RKdgK&(Q*_FuO`PkjCr3jMINdYJH*E z#zxl;4*l9us;M?<G_0AwwIHj4h^|K(_!@N1DzQ?$(0{^_Me?6vk))B)mSC24IW>Ax z>K*1y)&{BK$&{|5v_%L8>7Dl2H#A;o9aRfaNtdJfB(moJAfs#($#oLoaoU<+Q1=1@ zfZBol95M0n?eqR5Eya!9BdJ&MuW^8<5UaXbS-XK{-EyKS2};`7Ct4<E9N>}md@XlY z<z}cVhvrjOl;NlY4`2+?muGW3?VlgUbF2wTsnHxW#Q|NJZFlge5<qSEbb*{82l-xu zt2o&?J&m56kr-w1Y9W9}AY<q`)B#E;m#oLI0()4ZG>ZAAaKZzv$DS{2Jw|@n*oDU8 z^sq3W$|2e((kVUQ`5Df-8G1xEkxVNS76RE)#4yAK9-mKF#t{H&)v9d|tSa>o%nbdW z9(aBv&pcCGviiQ5e{C}@Cv#iAq;#Fczob`NNM=LMxS*H2B|3O=Bk1X#ww{v`w4wdC zhG;56IG7gsfXz`_HS#Fm?LS{m$Xp_Jh=2k*R%VPtA(dgu*zx~*>wVQJ%NGGAgc^tg z2-t;zw_)SM>$^q7axVldDm=b>-VdH{Ga$-Tnf&KOpi<u$Usx0eKq4Ti{(Ta&rC~2h zPHUmr8Z#YRZ#Mvc!`|AwGh5~t_hE_xsG12~FJAizM5Z*zE#z`0_ocS4Z6}HMlv=ji z3%Jc@C%BELI9(6UXT$+GAu!K|q#i3#DuuT4L-u$ZNRQ!v4(F$lSqu|1=kQE%ZAT{# zBACQL7~|2g_?VAY%4-%ULj48P;m)In9pje|KE<49AiC)b+!ALu?ymVq=$u9;)L)+# zyaMo4f}T(Vu=)LpS7kW8sfv>+R@y-Rsx}eyJlSo}8z%nS_xu>frq^_URTXWG&TFzn zhY8qyXMp{!wsy@*&^ay6-&dZ*mT<w%vVr!x_|cTw|MAu`v_Di>yiHige)r?r;}JiP zFzM6E;_R%OeHmMIE+*W^!R7*#2oeA!qOJJm1T%%I?mGY_XTv*ZWZ~IQ#6sL|`VbUI zJEoaAA6Qj22dlBKEV2qRo}P&X_RBo?P~><3rE%6<iRy>)H1Hc;)_#j6v$7*SJ0vFW zjXLmUHQGj7vmvnCQNl+C)S*hh#u`kpZXd)(Z0F^(B!vA-^bPc$<nkQ1&FOpB&4*tA zMRz%Wi9+IWLpOK5ND67C=HgKK_42XNs-R*Jx#kpt4#^WAlT8#@FzaZ*%UQXg(Qj!- z7xayPWDQ0I1~I*xWZS!yh)$+I#JT|V(R4mJSzKHWvW6E3ph*+`W~Du?Y4pYsaZsc; zNv30Zg02@GBOC71;(>ebm)|ewrAm+U44r<7l0i41>dQMrlkQDW$d9!^&`903yWk@P zymGTADZ8<aZ}#p3elWC#UQQSe?JH-wAGDrEW3pZ*H5*28bK1ErM~p(uTjZRacLbeW zcKoCsoT+igX_KD7Z@BEHL=mT2Od_7?WQL3x27sjvV!jnY&ni|Z{Hn(8G-Hp9A`2Eb z^nHfsWWVhm>vqf)cW2_DE?fl4o-qnJJsf7sJu3)FI5&9_eK_L<A$PjLl}2db3EpZ~ zxM(ToZrQm08N>9q@oi%p$sF2C(h#;B2Mwmd*~PVE$s)b@U}brkb1TKmHoDAyji!Y? z7GQq-@FTiHoMi7w+c$c|Bqk-QjRd4AGCp;j{lNhcYhRN`k5mtksi~#y1<QO-BOB+& z$4R6WVEsFMu9~P7Y{i7H3(0E{$}ANAK11TZC&SiMv;}T$z3{9U|M5EzVw@IPn&l2~ zYC}O3--G{rFeNq3&57WanXL$%2yu~?bfGRfc>?<6ITd;MLO9<)ZBv31a~;1Z#{LHW z5r~t=up~pCr}7<amps1T_Tiv7gnhI0^=__$Sf|P7pUuBgXd9kq6wJ>HT{KZ`@u2@h ztI)gnZv^oFgSv`$rLh(N!@0f6e%Bc6$cm^ekEK8(+qzRkgGYARDveJKE_*<pYXH+O ze7`8IcAF{8OSWe4FE-t|O4qnZbHKkg@;xoOfl+3Vw=84%9$)?VO4~zCdzHICIx6bt zcvWNMFz7?2!_ZbmU=VJEyRxu~+C<AMLR5FS`ZPEtfLKwAj{Z6EZS9m^qz7Pl=QR96 z&#(amv1DIoQ1F(LiWf9y-{fq!xs++?xs;<&iow~h&G!Yj>b$V)Hj@O8GiC4}826CY z%0dVAff}HgBWGaP%#q=gOkx4b0r=r&$O8Fy`<$u@*SjAFOCqoxTBB+UPNX|koC}{y z@McT#!d-SvLEx?#b=H*#W5`_`rMTZ~?cBwn+2*K{T989a+3fKjK^n?J3(6Z1{?uPt zF>Ck73vi{(b~e^a5sXJJ><oyty*&rYzpS09COrGw3lB$ZozO0sH`LU^vt8H1+e3Nz zY$BxTV%Ue;@Khn#na(<v9i7#d1?dKgr?W2j%~~*WckhhB72)=Rj@T*&Et`&k!AmsV zgO!y^{Jd072XaGAPv|K22ek~gIX!(rjH@D_?$Mgvm(m%n=rgO#Xrr*dD3BiV(X4WV zB%}!7Fr=ux)`icSd>~FYb3~6^hkOmEiX`ElJ9CW+5AlpG?d4c(>o=60(Sx=I{8-08 zmVULG+u=^b+}&14=^!zbZKY>(q?W~~Zkcb+!Ti*_kmzOMSQt&7Qj~HGK-`gYXH|~C ze`pes_BQ5FBj*x&5s86Hl9cPKrlaM5Hm0)O)&*z%^EcvB6g%awx1&$0K>KzK0l9;e ztQ;<9%3-)d4jD@|?xg2z9o$i!<vZh^t@#9pJ%)^&Q%y!L9$oo2)E!S!9zlL<(YTZa zH0&&l_s1BR&M6IGrqSoCyyg~IxVeb*uZ0C&&t93ymE*)40ozgH2N<A<g}3rUSY8nd zXcUcvW=8-caJ}47S55r`RDcqrc_5qAWXF&+8eG&Jzlw^Iza@fQSSFe(eWzHDbWlSr zVOBwbS@wmBxZ>j6e(xV#hB@XZ%(+QEFL&t<*B6c{{)s9mal$S}#c(T$o05WxL#UMK zox1uYxggW?L|tIV{?z_TQ_YeuZTmT^qr*uY4kQ!+Ft_=!4;B<Ybz;85U%t(*;%3J^ zPt@)kFXG*!uGJ*gl~(EAIf>Yih0byln$_(P*Ps-j$>i}B_mG||!$90ZiP`A~fje1h zk{So>l|<eq>x+;pVgA?315)sF{|njIwHXDd#;65w)|F%5<FHyF{`gM_T~9Bl#z{<S z@eEZ`qBHxqlmfu3d9A^f-BlP?n=gDvdK1u||7Lt-)F=1zc}l6!H{$t1oAC(ski?C3 zs*(=$+|rg(nkGsxU)r>}D$G<?2kyfw<)q)JpLn4eX&C6Nd&2;+`I+YX3R6>5^dW`- zVEU*gwA79|FIky&kYqi)mEtz<VguX<QtZQ(sQK`9*`bL<A!T#7+{`6r;Y;L?X!~^Z z67t*t{8yvOUYC-VH-h1j>d7ns))M|#otH8WpF*Ob;;`PN1M@v-IHU4XSKAdzR;Lmd z#mSmtC7|Nqh5KhazLdNIE}OAxGBrhw(7EJ^ZWa44J?KF_mp>^e5}?o|RmVAHV0aS# z!rOgIcbJ-gNbgUQ67R6p<;GTKyHnfTM|Sh6G!xw7UOKHWmk7;M7k^y2GYc?U?NiQn zPi=MAA72VZ&h}DY$o;WuH;^0}nY7peY>&nyDM7u;gBEN{0OmxJ+@M9sFfVS-j|B^% z2*si^(<RUVTQKi11Ny1k+}&VIe8a#R%dp6^a5DA@D7Et(_6bjPiPQZWE3}>&r1x8A zP?j-(l89UPB88FO(-}*@OE)0t{XygMzHLpmJ3&3P$I|iR_i!8O?urXubim<o0d;`C z2TkO(2R+B;13@^-PP-?JL_;08*?kn3>(l#j#&uAc8ZbI2AU0;t{P&mV*Z_7AIM$S1 z(2Ee~4#MgRRgPfIY7FplK;Q0GCTv$Imf9t{?90+R;>Uj1J^8FOTev#?uNirWmcS?7 zO~LZi0Gy&V@P?8h*eg<m7e&tSS||i%hoq-nuzH$U(4|GF&;SbrV*Lf6d1K32{kH&n z5LUmwAO>uL*bRSiQ(XBBN@bP1pjV?M`ku+&QzkQ=X0L86tmcM~h?D^d_*LBmu|x0H zk*h6ixnX_|+DnxuSdHO_)YS&Oh+rEtDNI<5FvF`9C{yhJ@9vBi{-j7_=Okm0h-;%} zjPac$w^q6&{fF*g2KMx^$bHgT(F0y{7DIG^)<P2ei?QiSX9AqeW;bGks=$WfHcutZ z(*@d!!fK(HyS4HzbRgiN|L_F+vH6}^C?qk7dgLW#=+|*V895FpxJ-;}mj9thbT3B0 zEi_2`=}ID4nE}q)k$Hek2?_P@XQjW3WAvXX^cg6`;4H>6oNOt%4E*osZ62yt+C40~ z%;ddvB4U1KWd40$m@0p|UFH3tn~@y54qxei@2fBwYNB$#bqi>xY~QOm0({BzZ%+jh z62IN%eV~c#BW`v{&kBU1ZTf<YyEPFBFlqy@J{3v^OI1^b)2hreV`>9PSSmZdyU>i6 zWM7|6=j6}7g+$m1?`pjqs(oDa;5xVKgg)tRUp+N)u92Y&Kxjee<WKvhe~5KdHOT?| z{&eNN#r8Nyh|@X3o|u1&D$9Jw5{f(-!ZfA@5L06}FF|0^(%JSWfBfGJOh%c_WYp%; znDbC^7QbZCRygsz+0oXlLU6He$UEpFtn7(VSNq<%))3r#=6^n6OOfHq4dcSC9v`GB z9Vo>?mh+yVO5mOoJh2uem`7EjRXB2c`4f0xbk7FUKi2{r&o^Ya{KGNry|h2PX6(K7 zLxv50`8^7XZhYuEuWJX1ZU|>@7{2T*RttNvb{H|$H89s>9?fnbug{h`LHzRn67|5U z;4B7DbM4)HfwqSXEACdvq|^`ihuXY1ygXZsVXyoH(t~x#(u<i<C?0Lm)1rDVu%0<b zB;=6MQ|qZXxKM4EqC99Nly<|2GlQKZg`mWe_Vi1#u8}5T^+Uz>tzv)&%&01)t)lB$ z&^%l-9IWE2(bfBxJ50lME+})mxv#k{kY&5Bx@B{xvTAr*tXF+$7v&?T>e<tm6U8Qu zyaHS_G8;v&ILC<UpB9KaIO`s0+Vk%+CU{!L8Q(AG9T!^+CRWN)x<d$>$vrQ{OB;BK zWP$jaZou2N5PY*x+Df??Ib`K5UEn!2V>vS>C-bYC4&X4;Hh3Wq_u9ufn??c;7r6-+ zziac{He5a|(s4am=ls$~oEZ#R@^5lCt;9$QVIlYBAa>UudFdNGFdSB;?4cEsu<BtF zPl_`9yT4f;>T5S4r$!;bXRwB+YG}Rq0pho_zP{dSZrXq+reuU!5ymmYCu?CrT%pBe zWR&rwYnhm!qCkd+-itpOgnpeMWxb&hDPL4B9rMsD#ar*92dG1UGP+FjQxM)&THae^ z#^7}Ae|N30W@6duTOy5bf29Z_bdt|S&fzORyG^78^G!ZlxIlfr=OLIcKmbrclHFds zwOOOn#TbHw+$Kb8&$?DOFd_m6Fl|*nAyPwavLX7ytSaHqG2EemXW>g-jq6?W^bmQ! zzWN$@>bhQ2vCYMKqjhhV4rnIr^)*Aa*t@ltAA?FOu5JmBLN`DS9Nxz-p`Q(mRde!V z!c2<^s>E}59_RRvRdUMz3;0w5pcY{!HaZn`hZvz(syDV1C#S>E`>j_v#B|`gIO0ba zyq%X!V<5R|I(IZYCF*dAz28k48$%GONw?}*VoWxz>uzMj8#BTR#C=zuI&#M_`1u1q zgviDb2{~)qTAjRNPD&~&9srYi%gFRb{`eP@X6<Sd*^9&srZ=w5#mN_n*?%~y!k}j; z%+eTPl3_CYpVqfCFh`g^wvy5_kOV0kEBO$K{b}X)_`<^L6&ex|=@a9drv2@Lo|#U} z#f6>&{Osa7l?juh{8jG5|HZUe@VeecuIJHu#R`@+YmG-EOSl`va32}_@IsPW3e{%{ zzrl6k7C#{^G`GZEoCpV)LM%=t$~R1$nq&4PNu9xVlM8;k&2<Ek2UU;vn$}m7v^^Co zr-&?pi37%wLLtgX2J3|O*gTw-06Pq3B|rhHiYO2g_G*?{xE?8>B1cU6p8T=}sPW4K zIV$t}wdItg-TPU^Y7a=wW^tx?KD@&$n|rVF(Bd^T^A7b+o-6>ecmS>#fTmMZCD%~# z0Q7v*$u|Fs9ayGGBkYt>(OBuDCmWWm1qCG%S$e_ifDKS?5+q=ZDD&w7URUQdO_#x9 zl7M3P&^tt;Ki#-W18774{rf8#u+QWM-KgQL#W${tB~mwsr2Q?AeN!o+Pe1bz<pCvj zeB41wVfW4zYpkX%1L#Fn?*6)HZ9od}69f*yEBwW!st*=MMn2VH1{?&j2-#YN|I=<a zeWi?$4`ia@hv74|z*2pJQO3@r+&_E!7%=dxzWZPUQ<MLf4gJd@N4i4c#m8i7e|4Js zUU%SI55lqPHaRU-iE6H5Cp8+-Mh_@~Ik1WlGmj$)ksK7&tV3N)eJtRiKOzDsN%JV5 zqJcxr`}RH3BOgOt%iEO~pi!>3?>+s0p&)OByUGHySFR}!#UB}yKnD1d)?Sp)!SZm$ zS$GT6bIVwG1Es;M*KLRf`HlK&YSc1e0FjDm7FNG{witWup)FRZyy|z|9i*hJwz{5q z*kS+CXih9*Wf{@E^;k^`s8+1M(BlF?*~X?djcDuTzasQMUOjcmM5Dvhr8f};nE4Se zcarLfTX(Gud*BtwDR|Kmf5#nCPFS#X<!w7s(C-dW-|YMWSaIO+DWCb1LVcgXeZQO^ z3uDD5ETs~6KXp(%R@gyU$!h_lg#~hkoIBbzbhM0;mhz`;I1VJ&<ZRa@D&oM|B&~T* zENtCNx`~&78`9a?ISI3cY2G-0@*f%&$<%q?jLpCfsbPdv&9xXS(e6R~yd}+Gzn&m5 z6{k#XcG%d_bL<UI2gb71ZEDI;-K-~;KsDKGON)@ffT!-=@0Tu`JvvVo)7oo_BUVyt zb*=OqfsG%@v;H06YkwnF7}fl1D;Znm4k8_s$2QQ*mj}x+R!=Px@EZlK@CldsJ_89& zb$2%!duE)Q_|(uk0%W&*99DGAm)#SQti?p|eY|Py#=~Te3h(mVT6Pb=dPGc!8R~w3 z=19Gu@lxLTwKQB&a!G<Q{J`!~)%4l9{MIFTq;j4PDJt+osNm&CY?dp+{y`<+!^E+* zl2sk@bbh`5(*6Zks2Tw$<Oh^x@Z`3?Oktd!w=`6rV6nM`xgRC~%$`q-5ml3jLUsT8 zhHT<Be0)EA_v?3UjT=NY6JY^-RzyouzNOlTdk$z>D&j_!Ujum98zeM210fZjd5Ijd zo>wZu7IMXaqzZ1NRFHaNgud$cqq(_{6JNPR(8>l+miQPOyt9^k`!rlMRCa1Do5#Xf z(>LkCmHxD277RY^ziW!w!jYn*;LxyR)sVHNW1kQi{JSTfnf48n{s|tSve)6EC~rKR zcm7yXl4z;40OWS-dmAR5&v#JO5`M{o5ZZyVK&It>%zeqw?EBLn5-t=!FAB<zsuuxE z6=*kwoeppQrZZS+bj^LuJ&Eg8bQ(X}t|du;O}u_^&*zACLGA9<eufvp`@rCdFQ<R$ z!n~Scdl@b8267w`25gYLy=FhrmWWbM6h}T(0j*|fpDAt8k=vwCWcl=%?q)Zw%k1r( zV7fh7>fmAz=A;)*fpAwD#ZrrcFRN&|gOP(fCXP(PzEaM(SbLXD6n9;$xB-|HoK%>L zV$OxgXcc{FDlL8(a(q?foN#$Dn{Y8N@S)jL;1|r>;3sYtq)3}mjhUDis5W>%`&|IV z{@RBV_*&C_W>5~dxso$!GMQ>kRuSXR;L!)LqX26l>cmcNtMM%~S`sQr08)4;IFFdl zXjd;)6|ep6N5QBQWekh)mGkMtAg1L$61qz$%lje2uwni&R+ntAAAxzqO20C!2escX z@9nzM_2?DL7e9H?<)2)oV9*P3*5jwJsA1mG?z>rzODnON1SyTLvF!utLFh}g(JPvz z3NbKZ4^8_g&H#xl3Rt3_fT~d91Nod5i%TpKdi^}g<q=L<DL=@3KE9Cxw<KMA$>I9D zCT7^aX{?d#m93<cVObS9WF(L_375K2?g=#gRc)JCA9(-_$Ng^a?bFCif-2qkbt=`U zAX9pg+lZ_W1BO2@KQpn?w{syxX_fjm7Cje+{|t(4lhieG(6~$11i@0lQ2X>}8V*(8 zjtN*^<Zv(U{=lM@H!4)q5*Alc(W`86fc=&fc+`g5JQqz*i*lwU!oypu6es)QxNgnx z=VUPEczLXfST?FTj7GNFwJR>re2XovK`CZHKWGNEUINt*=6M;f{T9@@wUG19o<Op4 zd0jLcfvZihIc<aTN;zP!zdg3rg4Fd^8bT%DJT8<q;}WEFi~omDBXjPlPI|h$-z<Kp zUW*s`%u_R*R59%0L?o}mG)oPZ+TfTFB?c0SgS)G*hQ!C#6S!~;+S^A#7o*^!>%scG zE4%0^(bn+iXAhl1<8?(;2eG0XdomioPWL^u+_n^?k`7#hHj5<31Z0`Q^03aAHxH>i zMb=?4_W+f~-H6e}g!!*0p8}~N<A-dzLO4#gZ>CbAArnV1W=}Hbr#u(|I1UxBV%odh zSYSoxZPjNFbxE{ZRJ9K=5YOuW=md^E65pcnBDSpH5YCpJ$+a1Qph4=I)slgTH(F!t zw#08HR<aCv)U@|Vw#3D<_I#8XP=$Iw5Nc@W8Nuy?iE_K?kxlWwCS?RMsT06pU}rs5 zzkZDIJ;70=@nXgNHOpzc&MC|)cdgD|yFH7g$;Gi)tKL}?U_*vaBff!k;WOv;icQvy zX8~O_V3ncH_r`<Ovf@8{G`Fa|ERtC7A-UF@qby6y0jaHho*XAJ+C3C^mreOj8Z(;% z-9jB1m>?F&UjZVtLXNN7)3f76($o^C(E_rv&24o5(|z!Drs~4866$P0>S^|H85&@@ zTBEro%`H;hH$g`^v6Pv#@HsrWd8qHQCv3NL5FR%LClX=)mX8@f+cgpbY1b?gZ+Iz2 zv0%C07jsc#a_)X@HMZE<PA%yK=^@Ky$;ZIF2kIchqNkGPNWGj7U7GSHI4bSVx<Azl zk3sphqBZg~aMaQCTSLB8G=az8PwE2I?<)st-2Jy1pZ|5@`s4y+%zvD?J^v9e$Shoe zNSbo!cKK;cCX#1zxCyLfTc7g!sb1h(TK1_e7y&rE>8SbIO7w7OsVo@7dRoW&g8th~ zBx4nqg7PHnLKFY@I+H-9)Z>2cUiP`MFHF6T_GRphus~(+kaI-L3<w;w*iZaBl7&zN zo`W{f(H|pWp&2O_3HYDQyK^nf+nU*1=Y*Cm;eL}gs{7e*a32GZNtcUV&gI2TsDeiH z{Dm}WwaX-9Xf^!IVW!q?#Rze>S)|fdygt{-a2Yz>Xe39{_1<?Ig_;tp^?4t6$<g8q z;ZkPYC9V7;eBk_}Z(z0u6}T?P41Z$OCv6e~HbK9reZhiZM^_jf2ed1D`Gmfk5T@eQ z-V(2g43T6y7-R;u;F%+%U|ZT#BGMu$N4POP9FN;`b-<7WxO%(*F$VM{+Eq^CO_yjf zx;46LDSFo;ccAFx25lj<+j)EJi@1qb@bzvUfEN6@heg@n>#G{c6mQ3-J%b&r^7E`M zozjrs1gIZ(<t{Fz#DB^-`etY2Ry0Lh$eN)<I!%TzFYzgdn2S)6-XbI%7rOjIN>C`i zX%hBXoZ1(RV!ZRH(6bL3>p)+>JmtpwR&7Ar+){NDU>sL-a@q6AG5<C-!xxt!GQzo- z`3JUvMo_@DN-J|N5CXR$Z3~p>`<+iE`hV^OpadcX{ES*mjVM5kZuPVd789bBDv9B> z`%E+kfUzp0<fo-8#!7&_mHmO8Llzo_G=!B|3kYD~=O;5OghYt_Kegx0`yhUWZ3U~9 z?EOa8?YZ>883?{U^VrJW?Lg<!jhDkeu4S7}VGuN9+66s5&0MgdnO-M<!Z*6ypxmTv z1ILMI<nOmjij22|e4%bOnEpXb8t3piYRPj3`eZDF@5Cn_J~*gBs!BUL#J4J-i6+*` zeth@@1cW-ogSwmO$mk2f27zJzdU)hupV9Vrf+6#~)Y%AYZ*JLmHm<Nb<CauD6$#7O zVsh+!ch{rZa&p&;AuRkJ8AkyYZQqwo^`hzuD{N|lsqC!GBfKOm+8w~r*6z+dA(Jad zq{<(^x?dx{S}W`BjPu>JuT@n4EMlwVlebO8&5{;Oa&3cOe?SBp^+-#;WP7oeyL5s4 zNCid{EbK>%Kb!CB=4>dJL+DeXsf6bMxZp$poEH?u0n!Zc14Yd&O#iV{V_s1ac6Jvk zzgw9G<&MtZ;&x0vrsOlY5{X{b;2;OV_-<A)ucK{vczA@9O){nqH|?zX*Ds`xS%>!r zqOsi{CDt81+Z))?hhgi;x91`Qe}BN%7K?}cLP9(~9&%`T1+hN@e{cvK+J2?`Zbk$@ z|K33`RR1n0u)+z1rEvg8X15}a$ZP*4A-AMy&gG;-($NXz$&Tn;K2axkst)19Jo2xx z%|_ZU&~}i%lNy7=1CmRen%|@5YA>ntOsPyHKl+OhlIs8-a)y5lIrg}#o1QJYrv8N$ zT4Q^CyFgPKY~MZCK`c(<QZ+E;mQO>NqNZwgM^y3nj4R`Qkf#_2pt+LH)gt+(rF_Gn zpo!r=ef_7e4wgFt`_VA&f7fR`y>F4aR<`ZzVwjFf+WFDfm#=cWjCP|>BW))?hGMkZ zZO`Gc&07;6@Ks$)h7rBoO_aLJ&dCvGxL0}+lDMO_egDOf)xO`LArt?(fb-H@T!LJa z*=^RN8;Ui{5JC80>s_u`=Jb<qqkf^Nb#|KchG97%2-Z&HTsP=p$Kyw)q%Z?3DBp7~ za5%({?Mu5&*^P|ho?$>$-9EIs`QYm#X3~(?k2fS(mnhxi@p{0alSH}gL4eDAgpU{E zF){eq7Fo#7=5`4*g}N6ac!X98138@*JO(9-4H`Ygu_)LgTg}ME#{k9WU*mA-zj-X* zYNjDfz`)JIz<`(CwZ3MLWhS0dXh!WNMuQu-+CNpp1KKglT1*#|Fxi@55>xp`&1*Sz z)1huXuC>IjiSZ-#WkkPsJ4q^>uIQI)av9maScI^3S4wQRf&%cX{0L$D4;6Wcq@Mam z`d?5ZhdS&(xDH$VB6r?{I%_a($U_t;pN6UK3;Wg)E^UO+R$!(FDltsVYk8MIhwhj7 zbq8b1X5Tb}x~3-iIkJ^XqJ@6%Nag28rSt(u2OWPz&%{LX4G@vT!w|Sm9{6=pRgDpw z0<(L-4zeTDHuM|e`am?TFlJx0vqK;*1$iGz4w#d=fIQbMtGXMIP_a*kvqt<R@PJw| zxe#0)TW!=G<(S&)Kdfwh|H=fRr|GtaYp|oe`QwVW_IvUH2%42Z6Cx!^jL!z`Put;o zr>eb=0`4{}*aIFIq+e)^dtc`(6`B&}DCmAm=4Z<IcmW0VcqT0_qLL+C>vucw6tw%N z^6ynfY|YSm<7NB<3N6nz5-J>J1n#K7q|c^wTM$J%Ivg@P*gArd1ZlXWAS$QBQ+_-z zn_{@a#)+q(6qKt21biY|Jt}COaLeg*JF%Ud%R>p%U+|ku2`|+lh~bnE{<UT9f;yBp z1K!KInQW&QueU1sNlNGLDu9SC5e0PeLJ#7SGOkWUv?rk#bl0l5+-*PK&08KyfdmiJ zO+i{CTttJ~V>xeSCd?KHV1!bsm4bTjZw;gI<S^$Xp6mJ7VV;P%H1yYDUiSq?<AHca zStU5xvh}Qf^eepnU3e5}oCsn0<)XTL-W$}z5Iyc>a<Hg!kQ7w8qk?tRdyQzU7B<5; zsDa0&&oOs3L^o^U^<2H^=2D*6HfNt5LlRpGu@IL!$mF3Ood;Hjayvxkp&Rek5sX$| ztQjoD$coEZ+XTZ|zrbn{DSS@xh35OH<^x~V9tenaA9SQ)k%+aG=~+*53NnSx?g&q4 zQbyVxLo#xVRfFKl$bpE!x(v+J6OFbn7zYGM$rC|69k1gr+N&nh+T0@qeQ!xzpqt;w zwVBwqFrYi0Z*!TXot6>@hp{M}AD*hB4%8?jIjy&!i`k5eGc~z?5PF~!sC<JQJ-9<< zB{J|Mn$7k!sm49n{!ivxkn>J0(~Ac2bc2~4t8}~k*tJsgvlD0@b6JXs&++so83t9A z>NqzRa{jF(h^3RLAqDr+4B9;gR}{&==|Uwy{@6qTuOqqw;LHsCZ-a|R>Dn!^Fe)bt zd{~awdkK#>4s#h;oOMemGcJr6KLq$UW+B0+F@KY6KI~VB)e7@rc@&<nq5x1x@p2xk z;U!J82;nG1xom62c!^8Xa8{WdTt`wmiyYCpK;8p*9di9Dkv*Hm$2iAI7U9Jfi#|m+ zv&U-r^>!vn1o_DQ9)P|>4<KZ2R70_VS9v(0ZVIipJUmjzjIW50pV<@!F#GmY7oX{# z<3d~+0xP^L`qHtTRojwW4}Q7DVZz<I=J?g!6^o5(D_hOo34^$WwITXa3vwk%OT7CH z=@i90&;H=)E5HKMxi9Kq@0gde-%R<sR4+U#hTzUwl*EJgni%A2?URmhbit%;?Y@0l zv)yNgu`GDCfYRr3=QaL5#^9i)TD%`H^0W~93VQLsaD_TlHDl;%kW)4+ijD>$$b{_& z*udtJ2^Fl{@x}`V6jq0IpY^zIzAe__MP+fXfVy^gaVBsLbmxx(0d-TjuKXk*_z8dS z25k34GlE`@?pMh4k2Cb1_@Hv219LcWf^SoGDiv51{n?AZ2i<VuKe<#?ASSL<l()@e zi#~3N7cNna+!dVIf^<RBQneye)bPM5ZbAdB@snCX8Bx!@%U0e*65#FcE}+l=;cu^c zwQ{;Fe&PwnbIN_63EpT>3Qf6wRpB4*{#zPQ4s|mStYL5AbN-5fR<KRSUg{I#EevL2 zEwb2qdhVN?Q<U>O;7vHtYP?!<U<X+6!QSE(r+Dl7Lp6znO%6OPmkD`40Si34TFuR& zK;ByEpLut5rE0Wq>ig}<)8!HtTiU<OUvrSC3Ilu<kDb6@`KwaT4p<>iwl_`ASvISi zl$x6b{j$m~a(d`Ze|76yetDg{-v|j07oIj>`^#&#Pxp-&KzMB$34DRGE5gU<eM=5p z9;>~Z$FyQ%{JmxX<-+;7J!Ts)CXz<2m3}w~Q+;wb@3`zyjVr!~qo%eDl_>=I3&aMz ze>(VfLNNA7YO)SDUnQ_tMs$2puKRJ!xvfNYKCrN~{Wu^;Py(BMLVS%f8)!TqffhHz zr-GxJE{P;GXNl*sPW6*^l=0#X63L_MChwht21dBnkMPWk<QD<+XldGEbvqtL5f|mt z(dngO;sBO^hsTUlnQrCSPqLNly$cinaXTM{PELFb_vF=JKb7Y$5OC)pvOoDgd$eJ< zD$z5D!H;w)<ihFy<~<aKG`>x1r<7(bMywG(NWD`d0VaE8Fl<O%Qn=VGLpNWEE{Jh8 zElC_6gHuu-vscj5Y;uE=of0(mRls?{a&)6@V!j4l%=VXVlf1(lU32WcPE`PRV|y@O zXIm#~@j9exg{@3`hQPpM0rKk@{9ezmpNHYt8oPhWVsBr$?^#|M?J9oeSPi^8E|aW> z7#-^yS!E1dw#-Ht=`|PQS5d>mA}N4&{y0<~q3EK^jT>aUvCfSL(49hkj*-k{ZImDL zZ=x@L4Z3Kd@>1TPR=oByk(~~t%$S(9)S;cwn%HYXbZ1*RItZLEg?(>+3Br$hz0BhD zXR7YVaVkO(hWPDoQh;kwK3TQRqwI93LOhvElvrd2oT4-O<BQtIlT?GWV(_HBYo(&u zK0;Jzu_LZq!9{+O+YDP%aAnl=FjbfU5kf1_ql^gn)(SX_>$_<q1Pm86DP*ue016tC zT@ttXx3_J+6$R~vr8eYs)mTkfkD_DEn{CFIBC6Y>p%MY5lYcPYzj8<&XHYj>nXER{ zOfy2!j|_i+&m|i+RIIu#(wb6+9c`AiQ1E3L_!e`*GuG)pSSFVPcnY!)=@bF$DF+E> zT&X@Cpnqzqne^a2yVq{DA7`ZT*)Gflny#@01k&sCfC}4pxuCM8B47B2dJ%8p7r|uJ zPgB7>4=^CHN<v5D`+HKO)v>|!x8n3cQ}WBONX=Ke*mbyO-WUZrlB_RZ#Yz^VaLS2e zhcjNA3{hPjc31fWPhV=8h(w3|h{JqiKt#pfSkOP?MfpUmbor)bS0(eH<40b_TB}Am z;m&Z9A$RR}U2I?CALcbmo4|p^u<G*7u#RJmh{{FC_f;2dOY^zp?JH@!lRC->R%#e~ z^$IGz-b(}xh(Y`aK#*hk_QzWZi!bFJH8GD1u+oP^Kc@__fqPnFTZ_|&xE%`!*v&`^ z+<A>PVVpl{v+K>p${RLj{YUwAS6^c8e@VswJ;1M<{bISB{Le2vztt5>1Rlh}6qC!C z3!d5AfMh5t7?d`kCLHF7TG-Pjfd{fUcGZ(D=BKRoZ%RrPrd5HKRzUO4V2zJg^|f?G zu};ucJZJP~U;^^k^bOkbekOYYP8I}f*g2Ve5VHYPv6S8Ph_sH5QmgK9zeh2LWLV0$ z&y<bUujI8|=~q(Nj96MeakdW^DsI=#V!9TiX7ihseNIBesRu7PxB>^fOq;(BWC@dS zvw-uULyjOHT3?<sX6#3woKypiaAs&(Jy9lcRGnwo&+~nr?C^<amt{Zk<ws&WwyxMa zpY=aLr#s?`ky?h>4oR~-$_m<)W*J}*Qg>qc%g(2*jyE+U3^%pyme{9X#r}YE2RBS* zp5|2$0MIw@u5cF>c0VuA!UgSAIFdOU2+4rRD3Dly6C?C;wPUoT>zLK<ARug74SQ?A zMw=wC`K5gkr<yn?1Dqfp=`)M|?oV6ep@0wnf;0Z1`$<z?wxUw>lCZoXw^I>#YTzE` zILD?;O}rKIAnqiWe{@bST;1RP^EmJ5M7uOFGmvirc9f*M=9=5cw0b`97ns#e0@wB= zyd$vTt@T0huo26<!ps*<DwKaz#l+=C=ag2nX0}?scev?Hd?vBC{+UyA%<D0)3cEbo zBe;|aO28mFh2lRW=}5e!9)J}gp=e^JcJuF{&Iq66K8FL+QdN7}ES_DD%c+NsAyFR( z!9`_gk_wMMF~nP2%_I#YHMJm_*4u7wtgbjn77udbtuhBpUl<C2(~Fe=S2xMdphZ=> zWgiJh4Ete?e56{48`e|a2_-4VBRrAwTBc1sjR-;4eDnOuA~YF}>!C_MrtU?=JYAzs z@8?6QotLNqEi@TrCJS}BoW8AlVh7NBr%s?g!=Jt~-Be!0UlVpFmVN04mweYhnd&yg zdHUX_CPfH92;p<5%bU7+%OCvkj6NBgxxjzT9a{=1YG(5|kW08Yb~R!_`a0NjEl^>} zY3qpM&5|u+nV#|S!N!PT>E;%*Cj1tMBniQE9i)NNj?Cduv?=6*nEPlCtM(bk_{d-U zW6T<v*`Rgkl=BPuX#o_z!=`qQS|T)!OQ;Ew&LIt335(GWCb%7Da`=fA#Ncg~xvS;Q zvYGL+aiS?-)&1ar>RFPH*dN=l@kkKmt0w^OOaU#tq`58}<M5Mb1w0*D8~vSNRv{*l z`jGN)R_&g}gT#1pE;xFi2|JqQ9U+Zi8OEw!BgQ>{O(p_pw-(|y^Y-d6G>p|EH6Xr8 zD|Eeipu=<VHVIu+Sur}m)3VRairqP3JgSyKJc!m!m2n|7mb)<><yfZ?Omk*>9Qa2{ z#bx^InRWX<)naI=Mx7=w1<qHWEh_Y4_`>h3ELlpQT**g9^UA{|rYI_4PDWk(8*hVg zL&T*KAYI!;_ZA|ewRklCcBIb9y%IGrNFv#j8%dPg%^F=V@D^ID8tJ_SzTaEi)QIB{ z=9a)pnxC_&_-->6uZ8YDCQc>!OwZJaevFZ9H?eECy8>^04Fe7ZStDsFfkSrtO8hHi zMKN)}5^>CCEj@K}!@TYLxwI6+q4!zuaLv}`F}~&yDaJzAW^r*oXB+QV#AFGe1{&4j zqA}+pmEWz>8H0+%nT<>`_sPyVhAVwr`NDMeR(KV(+JCu#U76mqZQYi8Lf&WzsKF?{ zyqFiiy8?!(DZsro-pYyp<k;4aX3L(euOgV^czALte)`R~P*zP$Oz%8UR_lT+#6=7V z)vP#{uHJ@=c1~E7WfMo-S`qoxG&pDwllX=!CPW3;#aYc&`pvBlU)8&91_0ZkpojYW z-HWQmBVwJ(PgL6sj|&K9Bh}>Bg1usuePXJPIDi8x?>t~>Q8L?yS_vTlqNef+!w<n# zGmQPwN_!J34j>y;?kgo?Xz}eeV0idk$s|z*MSfMJ&Y9&^FzXpBboDzX2kAY`(*_FM zE+F$5FND#`6t*W&)z)mq%d&lK$B?R~CYvS+0cqc^p2cDfGq9lP=P--Oai-ZLe_c1_ zTEnJlS+#3TQT3VOZ10F`oJ&$rbP@TwXwL!I!x{wlnIL|&tP{n0nL64H^eitd|41tJ zOC?udVV@QM!FA;r&}c+XO?A#I6i^%E7*sM5bNPsKHsc;0+r44w+n?_}YP%#biRMV5 z!i*EeaGnqi(Hry6xwhLB+gBx@nXe<i)nXXH%+(<T)E^2VdSt8!7i41>-O@L%oZu9o z@yI$KkYgPb85Sq|EvfYeEF6%B&wq0ACy;Omoj2%(c%Skc;wd6>BqYv4$F^O2I>8sS zqQ#SR+%FwHZ<6xJkx2c3f|m2q0}l{vx0C^sNoHr<P`Xln*=ccY2KK{VwyKQnh2oI- zt|3FD2MGoUAF>u-Rd`wYfwB^L9mG!S^fcy{Lp?P+BeK@xCU`gc@xhaR-Nx2YQ$apm z%7+3WwT;}AZTYQivR#H92giYDMEF;xZeXJ74IPu_S<eJ|FdhCqyA??TnABx0%wJ*M zuYK<7sFuXE-f@XqJkBgOU9VaLxdRtyI$$%;H>do73$Q>^MMPe!++p1<CmR8_7j^Tj zYS?Yjn%@XES@U!tMzW{oYApU#=9t-y6>&&e?^WBijPW>Ko#ouKmxcl=etlVQj#sRz zbgE}xbNxy6qn=^%1x3aMklBrkZsaEB)(0Xm`>!v4&{nKvsLOM12%e&R!j?3$ew6xi z>jW(m?P91HJmp9GTjim$Qddy$BRJ(nIJ*#;{IbdH>7wCaC{^H29E`4LXdbGdZPbXK z8k5ZOAdt@F5l?TF$@Fea0Hi1+mbg3iY>P>fle=BplN$e1D1<PerQ7Z6SBDGV98;5h zk{(Bo&fzBl1@lhlkA?-WY8SbPYUxq&9?m>@iZZU*&Wv<HJht;>*lT0U$JMF0mDuJR zYxmyxA!ryl1WvAd{^d3GQ?_KEzw)}FF|o^JmeKgGj(`kQV&YdzR=6Y8NP@;j4Dp-6 z*cA&Kk9%XPlNkkB1P4vCpj{%w%0dUM7gKUyzbs3-%z4}0_xg911dJ|7v8bsF2-NyX zF~|gP&M?u7Dc9x_xV&F_EZP;!&kOtP_>?%Ec)Jv{TheiD9I$d_$I=cDoAD~1|5bFU zATh5IdWM1VoiQv&<bD#ja%O~ZXH*I2Agv!u#JKl4y4$y%aej=*FJCU@jttU+LZRG* z#=b@-9mn~ch+SB1*tss{E$uysW9tDhOTDhCGG3;*tpwczgKEc|I2mfeUsK-_Xzb?Y zifYP=a(0y3J2W(Q&HQ+Vw)T14Xm5pxsC?h8qfU$4erwq;^Sib|OJW7U8?@$-&99|B zPHy|SE=hi&4+v&Lu#H9cVo4|X(J7rL|1l!~W_72TR4iFKU@VH#dM~^+qsj&?02XJR zs;nNoOjHH!Fis;f-qFw!22q(ErmPZ=L5ule;60n^UpYbnzOa3x^}VT<CBt0n?lSDN zxWyKm#0-W7>YdJe#Bh9*@U`{6xTh!+1y~Oew_gi>J`b7g_=-qe`uHeeSP<fH0b`w` z0yu(GsE#yJdfnpo$<bl=?OZ|CVV4NB6}*KzWd}{@u}O<F2`%!)qd(T(933d&1uHp~ z<Ee{f3v!&k?UP2kzL6Hkmj5#w63e?bD=Rc%0dDX`BX$8IwxcU8_(EyudZp+j5eU~3 zs`zu<%I2m$)D5R;EM9#%D>sx9stLV)ePVLOnlTr#2?;Ov$-!<F{yiW4`;EH9RIM0J zB}i&BSo}94FtoL;KF`SCx!s5+S30mVCtE}E_Uizm)R0viXfw^@FVhI#rR3STT-d5| z;sMk2`@2iOZD-msi%c#fXd(_+-A($uv~e=Z3f^NgGTx-;R->Te#Q-}Xyel2?yK>XR z4;z<~-K}|VbD(+0!8mMrHY0#-Tt&nTMt}1T^r@u8SHUcx{*g<iu1-XC{z7vl>>4*Y z5&~rdLO?s!IjoKX<&9>weXlxfklVQu$SUb;8Ikq;MmOMx7v7|AZc2D<7{XT$FD;D- z7~AR&FCWQAN?Nx)_1=JCHZzgmwLzR4t+#8VpPW>cGR=gRcq=Pf+}J#o;rRn7#ge{T zU*vxhNU4qkiUqaP49x!WxeG{BlTId#|0FPnl%A3=hRd8qWN`1z7a$Fy1ti#bQNCSt zH;<e<tirg}Q=>aGAbIPF4HtUcWEO}r4)%4Nv&grxf^e9DUSOG-nHX4Db?rGMC_t1m z+<0VE)cDWgjwXYFUc+m8_gl;s_R>!8DPuIT73WNVH!RF65(T=oRLl$=%)c%H%ROrv zI1XD;We4<cW%M6FnqaK<-I;Z}x@G!eky3Ok3{He@aHTX`;``uX6d<3wrhEE=f;~Io z?$w;w$xL#ubuRB?qskZ@LVO=prz`Fo$B{-vuqMy~=k<0z(4-{iR{?<y1TsV{j>!>& zBXsmy<%H^ttR!<lP<88JBzC9+-s8Y6dQ1{Hkm9N57&=&0Rh8~3bpo_6aa~zNlZTSU ztI%KQ;O|;JE=8=JB9!@pfkQa`6W~mLs+U^1)nttU;Se7TfwYUn!2?MVf5e{HNc^&E zDHH}0imHnF2Fm7^R7>1HB#(7@+iv;;_ekaao~@YMqKR}b9Elx?Z#K?wwfW`$X<1^v za=D~<V>4mi@QRx-{zLssA=TIP<xq0x!jMXvVdDkywa-{=xB72~%@kntYPJy}*>*TM z@iJi<9RK7~xGzIwd!ItWV{j8pq;nDbyyN%{9#G-4fC+a#estL@zu-mLZp?COe-DXM z-^>G9a8bz|N@X^|=Sv-WFhO-DJ!+qsKgE6;Kg5-4Y_B8Ld-izKroL?Ho1?hJEDw?V z*jOUpzT_hOTPq2rSQ4zb`?Ng7<n3Jdz~o;w^0mwf{c2BH7b<oq)U2IEtuLHf9bI4h z2~UY7nx;wdfG%`1yx%^t0`X}R6BD3o&EB3lL~^XwhYeciX0$MYZ;}@106@TnsG!64 zYWZRdFoc%18VKv??B<;EXpA#B-o11E3_bPRp>kh6G>-9YdE=VNhp*g(139fZriom1 z+@?tnCOH-Qy>#6A(WQo82tLVwjb?o7-18@n@l$f-*&y-YM7>#*MNeGgrF3o~@7t*C zAN=tPt^?k;8hD2g;gj&~jaah7mCo;<J`9dio8(oOl4er{#iwkqwZuQ-4I98Iop;Q# zU~;Ly!7=V^U&LhG4$EKkN!}~6`)bQQ53w_r$QDHlqrS%KZWj^ojJV%J6fYwAUH*Bq zZS?1G_(;-3=H5gITwez{a+;>UU+Ygi#_jg1Fh^14NS3|qIE`--zgODoCOyeDMtr5- zzyyr?`rQ4100)R<)prN}8lf<Zh_Za;o%%<lXT4myQ3hp1jp)Yn5y50(ZDUD8|J8b0 zJ@$1xwdZzW`?ik`^=21=^62}7m$)JDj7d%ft>T*+)a(y(kPKUx(k4UE$A#SEQ~txQ zh==;d(ABor<%qVeqwcqJo=qVG6x_I+AC32$B0k96Q89v>P%xaa_1*4V+aDV97Y>~6 zy07A0us%X#6lhmk=fPZocjvF}S3OBC?xyQ3*bn>%C<8h=Q9CwMh?MWBSQn5wV#$Tf z?G>6nPYq@h&Uei`JZuT_fK0jz8YL)$o4KOlQx8}ZPULyw&)VBdQp>JnUtUw>jXEH^ zQYy!qN_>l@=tUhF@Mqc;I|%Yq{6=-P3+qXGQJsB@t6~Fd{TWQn%jrQ!^xE6-k$HYg zvf9onQ<0WZxony1?xAE?Buam5@eKB$vh4mw{>~XUxH%WUq(tgmQecLUbpD$z=`B;@ z!;t{vA&F0OW7=X7DM{zT-~;@<)gIwRqdnmq1sA%H;DmHgGaTaOgJLtXVTkKgvlw?> z_AV|m8_&r;!mFyf&&5}=-%aui-P{k$CJn00(>-W9g7ZT<DC1-?BD(zVne8(P=eM}9 zstrm7>F8dq*t9_EO{bbgs?)XUr6Ew}Ey<4?_u0H&xzS>`=u3%{hhAc&ZO-+Dkc$pf z+S3n0W8#r4gm#3eTZ2gtH3$06X8~X6Yz*$JFd3~HU>Igg#MJDe<4AvZ*00N5twb+k z4J6;6t+|4}jF3?Wd-VjqoES!|E)s5N`R21=Uc+umV$gdvU2le`$MDXdDxe&+=vZq& z++kYYK4T%lF$lLg;^9PDhygUm<}u`$dWkHZ+?i~iY4G|rgeP3b4-5LUVi^i1=$E2x zXH(Yukm745ABRt^zm10Al;0K?o%byD7<>~%@)J!5CLu#nG#(US)^rX#_cb*x0;$D{ zV8<lk!_6X}moE1`$-UJ<z=vFa0S4d83#w9SdC8Uj0_J|PO{~Mr?Fg2ahZ!gV?v7Y% zygkP&(I()fsd?Hx5bd@C<DaE11rpd@bdg9wP7LH6WTp4NnqHan%fF19e*bee7<(`i zT_hula>@+rf)*Re<P+8&<T!nwCPbXh_q63tvVMw1mIl;7XMq{_im_N#r<B|hsMlKH z?u_!P=~kbW2S;P9;zi#KGj4cj*yO(d{1@-yZlt7Pq|$CCqSqe{Z`T`+j&^T7STu4{ z-@UtV<WS|Yei%?<+-)St9w4IC<Q4kmOojVZKPXBMl`nYC>3n|jo8uqLKrihte?*dR zSqus>iZUtD)=zjfPQzJ*KM-{%l{Qfjr7L2t#+P)1CW<9Y7X1_@2X!3GR(CMCI@+4E za4dU_I@}%FB72q{;eJLtldtr;P^f&w$2dbjheC746~$J`$jHUFbT<m29tq2PgBG?( zwnv3mtPteIcRWOe{WKlPqgN5Doz3%M2p1z&Z>L`wUQP|B3D3G)3FnZ$^5~>oD-QFc z{Jza$ePAVVy0|s<=}nP#_z8)5kCzi`!s0uMr=geQ`15o5eFVjji8qCG2-yg+=Klc( zLHWLh3hn=TTiBa@_?r44n&R|fBC#00_I_cHLZMJ*Hh~^%Qu>8rl467V81uV_1Pf)l zNO7y!j3>?r{XDxvK&K<Wc<8ryqF^WHpD<wQiA%Avd4X_bbi%<Ptj2zXydf|py<qCP z!It(Q>||i;vj}GmkQ>lrPE#g!R&)uLspWXnNxWHlocHuy*835`I!H!Dg|!E-MqP6z z?h(I^){Zve$n4|~_x};%m<R<eixDg9fykD)(8g-xP<T|3M<jUBwoR~7C}VTot6$^G zU3L4~GolMH3<GPQ88L$$u1W<Wj`M^g$$<?rs8O{Gdt+0rGa|xD5RaWH81oAtDmziq zJ{N^1H9qm&H-sZI2NCy^YMrZvz0vV`y>L4md~Tpb&;~SMWKm&{LZMJ76w0V*7UPn& zx#&K806ktGJpbLev6XNNW&HR%%-DbXJ@6dshO#gV64lrti-|Wd@0Gf%5sklpPOwr= z!FmD+nVd-p^+ynM_k@Iijs|H&DN1_7aAlw5zV>&Tp~yWq#9x9;uL0{1qWAe7f@OB$ zYc)b7o4n`RxDWx}re0x(LZMJ*HvY~tb)U%~)vNi?{FLb>#R6~{F8%b4`24Bu`qe1a zXi<OVDpUz)P*TS?9{DM@RdwLJUHMqjdL=3yiTPbSELM2?gJ9=0!C8_IPiz^y!R7Eo zm%(1CLtN#Dt=B4?JwSciWniT--1g|yhgtq%>pi@O@4W7zA!kof;5VSUdp$B;a(u1j zZoxV`>5Vz~aIKOci)eK_L`>p?8Gq{v#5I0YbY{S%r-OD%V%+-FxA^D3q_Yb3<_f_I z1OoxMEDo@hU2v)9A<kq9N0LK~jUvd%P?7BtEMudPMv)=)!O=T3nn5Jfhe#xV`r~t9 z$qnMkC!Y|Gq(7V9c?VK<>?S=Hi}Ce$$jTxT_rTK~gx!2*eLfsIe=g!Q?I(pop-?CZ z(vF=wtni$C1HYpSp_78^NCysn?LI#5uOe^!Vhs_2?R%PW{FhG(77D?6s15OWoEOTI zfmkZ$y+$0|wWd(gm7vkVi@&Tl2xVS!$EY&_saA!yf4#*o{+Mk9dTd-hG_@%gh?kqU zkqW>R3T1W>?6D4b?<N7#yR>dSI<P~THc~7AYf-gq5muhsHe7`t-%y5QTTg;!M@TKV zKmJ3UD6?Yijv_4Xxe8gK4B^OV#CfmZsKgJ?vJQB?8wC3xqL>`ctV+aXVc7czMFS$R z*0f~8nCBNPQ^OY@`yMYm&23(f>@j|0t*scDu6ew7>u3M{9l<*L=q>P~$f!X)C_^Zk z$bBC~;;uuiq7#L^IW!OOETgTl89#aOF5L9|H?VQ@SMizVpX1Wax8wR(zlLu-^b_pg zy+=56eBAcf_hHQrVo}3fY;3qluma(52tJ1w;W@4FMpg;d<l?snkT17DB2Fy)^C>Da z`Vk){nn{Gshsc_Z`PNmaZqwnZ%w58f^k;YTAt=IQuXJ|w*%4O-5MG!IXLbc*D{P1d zBx8!e1^)TGz}((CyCW!2C=?2Xk{k?}vw|D{4%sS^Eu29~9Ct$p8h-FF<e3^?$ek$4 zJUv{8+tGT@xsetq5v<#eCr$i41s?`_GYtbx86PBvYX92@(ER6@cyac`Xa9jiH~$g) zKL0nI_|?<sdub;h1z@sRK4`$fJMQHdDkyShch29DkSOJReL5Q##DuMGh^N>Cm<T2Q zuCW(YQz#Tlx<ewkkX-S}sz)Vi1%iDJge{3X?og6XiUnXF+7C8h-+#A*cyUKN;P=8K zoIy!Gw><qVbQQR8{>~yS@3|I=vvP3`M@R_y94$2Qq5$h$E*u#icTE6spByf`L$Ck~ z+Al#^5yc<x|BGNrEl)i6Z?u=YvHU<W=2^=H>*!pyZ#5PiFGf$X55IbF`f?Vh5|1u| zd}A)+svuXv&K0bQ!<Lwk(>M<?u6B6sWtuB%rt_<Z|A1>>`x?H~|3@4x=tq`A&WB51 zx4j4}_7$S6ON-tt556CI5FdT{|KYwz?-SlRHeY}6$LJ_=W5xaotnOGRSO>yBd$@sD zWKJ6bzO{li`LMwdDplhTJzuWu1<MRofMeddh}O2i<u{|g=@LXGF?{vG9|}j)8<X4# zbzt-rdUlfqaYcx$<ICX}@*ulh)o?E|Bkq;MVI6sCmLe*HG9<xs@4YNoC=?2XLZOUI zAvWA{BR+rQCD^c}4l6I-h?}py9-qHuHA;muD2YMB&>j8R6HsL9Ak9z-)`_FcGr;ur z0dzjLS+JbjICeKdEFV6H@<fqnlyDww74|5Uk#RS4qV<1Y$F5KO9{a!izvy`AUD)?D zAsPrnmaT<6SC4os2G5CZ^gh26hranQ?ETDN(EXneCnN@2w;V&`@1EmblH>-hU_Cb> z`O_mVA5Q%GX~8l(AbFjqIs`NkN*L~1QYaM4Ovkq4B;+~cb0!-Ld6t%M+bPpWiUr_q zY<l8(Jo4Y|Wo&8>LA7KN<_Kp{lFRk4{6BcK5v<-_fn~kd2-eYYSZwfGeY{xX5iJ$0 zqZ9MiA-uE!Oe`Q+fMRbppJ#V#(bNp~{*$pCa~m_UvTJDG-_cmraRrtg%)tlM?eGO> zS10Z1r=Q~gEo@teSa}yD?izl7JoqEoP&hTvd1TmZnwVE8W%~K{BR|7so4$-4#T}^V z)L`C;BJQ5nA=kVTW!4R->%I*0`z}S9aUF6z3sBi!id?G#&y^p<wXfZQKR)nh;gg1k zc+sD%v}4uYV(u9n48P8@EZim+nA$e@{pSnT<P&3KTv=71B5VAs$g(p+T;mr*#6Q%_ zJ)GqS69}WiosZ@FOVL>4z@Pp#`n5<0oVpM+{*fo|NI34ePYKsBYh|L|d5BiGAsQRq z>z{mnr*!hG?kN<?xkba%-FW#c2k`I}TltN=CN>IXwi7)1Jbv_(zv531K93iV8nNYH zcj0&MQV*f=5M!b^@vEmG&(tPG{N)h=s=_Q@Kr+3#Uof7VknqpMGb7pFa^&;Gaw+d^ zO`)6(<MN~H@y*zE^B-{R7ypKB+i^ahx4I;U7bS@>OUk84DuGBM=Dk&AIyIzPCI9Ra z`rkW*)_=Z^V|Q}z-*J++jm7a_Jjv$)Cn4m|#jwxAA=hi+*xQ1Wzj;Qm%xpqt7bFT9 zMB)@rb)N+(t0b&8$$w6vP$)AQ5)~%db!a>eSl(|C_9)Xqid)6AasYGNH^282{Qk4^ zk)+`pMd0nX-^3N~ei@3e7z-Mzv8-?8e7L9Mve^;yO2O2%!tWjQ;7bI*cRg1#MBzNm zYiv%(T14bg+&zYPG+Dmz*mwDSlyllI7A$8H@cZF$xey5t6*R=$8&!eDDCk$=)9?I9 zu+9#)xsUL9jzysen1YE-Fhwo%5YKdR*GG+>e6L`o%mj?xeYoSXAK{YsZ%0d>18a8Y zz~CrHmVG5II(ij8zGXd@_tv1$XMj8^g)X8-jiU%3*|`eW?zsvzJsY@l%TU~>!KU(N zK14JTdWisP>$l+ekz@Gj|J}vaHD7^KAHwRL)mYVeweZ)uvJ++DG;+@!SD*NTsS|$* z0cQYNQU?^Ws9+f%`AQpto}u+a(6<C&rF>L`6+IiV_(%@knA3>AJ@9wojP%5t!63ss za<m7Z%LAsq5%J*gen)Q6GQ^d^GeY~HjovMX%@7p!C=?21bUel&FZS-eza6$_FJerL z-^goXBge=&<Du!PFXPtVy$QRpPobnQ?Rep-2B_D54mT_y`v$CBS&E)_HskOxX(%Ye z)AYv|5ND&1sK$<HH6Dnlq%1I?`L8d-zPnMdo_pALp5(=nR3Mf}Xc%b98CbU-!;$Yj zgq=737N$4$Ln4>)LM#dEKAv(&>I3p##vMSQ!;F@{zJzU8-id}EJ_6%wdlBfeAQonL zFJh9Hl|1KLH{QwT5myyuL8?&-pF|-GVwCxru<vTb;qN@a)weUdf$}x?ag|SUL)woA z_p$LuC!<g(lyrjRQBOAfqCl<<rc56x7JvtG052)ig0c0W-Ntf%QHs<IG0t6n{_t<` z2jxqs?8rbx>s+kqx==VWI##0@tXl%#iYE9%=Ly#F;jirn8;&9x8|WyOyGwX4;15eu z=LP)uGf$wS+=J!&OHkp?6RZRHy*}8wZ3vhn2(xZ@x+1XkS$VI-(;*^QRqIlOlu>;7 z(H{ubnNL@V8-*q<S7R#>WG9x-MiMMUw5%J2J-K{1X-aw%i^lMW$NqvFUjBc$rTv%a z*ZFYXt~})TS0T%HK0fi`<yc~>f-E-n%KkcYF>ZbDBUs#g1q`-1Sb8WAtT={us+;lg z#-HNrj=$q@nF({6^jLP_BCPDaRCp)J`5T6##|@^gWy1Bzc!&mrs8&zB15u{v0UKh3 zy+K4w2EudN;Pnp33oDzi!NQ~2c%}9jzWzY^E*J@_SY-HypUfZ&eF)z0=((7Er2)kK zGTvKwICLHbS0@a;W)upAG92&Ub%GarM}iz9=Oj$mzw!5%?eJP?--I#~2q6&6#j15x z$Pos=+4Jg95VN8``SU8X64M*|;36KX%H)c_ry!q8gpZ>?dHkI9RJD9~6iAV$oJ6C5 zbAOAlM>!WE-kAMw9YDhm9>Lbjf6j+`CV74}mH7jr?<5y?5HBU-DJ;*{K~<EEK(`g` z_rHaMU%L<6uKFdmUwaoXY7!4)Wxj!b-ifCs<xI$RR266QA+?D(Z{B=ZI5P7f0t3m% zI~|b72j}vFA_|2vJ0NW-$&sH-$RaOAb_N_Y4D_@i`T3o{_@(fAREWEFoclkQH;6p$ zrX@=M`}g6_2e;#JYahB=8?k4{4m3#TV||rIIAeC;bUGoG4&TwxXt>U;B>5f9Sd_ud zFMJ2hg?6mnnFpPB30C$l5{`~cI2?l8Z3d(a0t-73ajp@LjGt(z1WIuq!fqC7%|N+O z`3rHf?gUQ0w;z|RNKA|#4?i&8i<(wFHZ*=zungdFxe<0m5iK#pm0gCAIuoISAQZ48 zY~ik#I0m^wA)E~qdGZkrn$S>hM*DjWxM=wW!jXyL_j7+sDIc*MOa%}2y@HySY-Bnz z;g{z@#7s~GuGj<V$wg>h-U{U&FY4<T3P+|Muh$2yRs-=guH>95_V3+`cecKZxAwk= zHxF*c+lMyeox|_r{exTa-oX#?=Alh^x#4X*bK*@r(e^qX>3$WD^u3Bcg^Tyvozs+s zn5Ybu#wA$QUJpZP<h({?(Z)#il9o}SzN-Rdj(N~Ki=lSqAk&_M0#gO%wbWyE$N4Dm z8HBS*4)FlIRxeoM`5R8X_)Bn@8Jt(p2)T4(9TT;T!4XFdblF2iQzlXhmB$a3WssrB zK%u=B62FD3*KGJt%Ny`)>%q#UD}}QMiAJLcg~QNl)PiN|AZ1}eV;OP+WrAf8uib}0 zeLGiQkDhff7S4w(qZzDK0gZlW8|_gT(OPZ8=PIrhEXgGth~TN`p2Tar-h!>mjCu3s z^Y%!D;mT_iDivL=pG-obFjudNp-`k|V*OmjAK>}|+%=ufzQAN5{e8dRFYFCZM#ku4 z0TSZ5<=wGE#s};XNEKq_*Xsq#2t>Q^-1ncwoNs*ut_BG<Y@Ca!uW2dih{fT9L8s#D zGm%KdZ^>e!<KF>7Xx+XI+stwpf^F!L6(M%y1MKtFVB@uOkt3WnTNr69ESJl<I&pCP zl_(*L3&(!)7-X4Rh*K!o5RU{2xAncW3;OwGyoi)cJlr@&uN!aKnHq>^r1`zWkn2-Z zo09t$v3p@yQN!nNPDgw`Kh$a!Hzu0eaIrH15`I0*J%@oFD}3$!@U<A>ZSLbY@|t*B zlDxdGL+x<vY399q``<YT<14$-{q$CJKC%gYFYJW-NC#Md2#OpX?|DlWUL;yi$GG}8 z(R!VPt0WW5vJ5pJE}9P|&22JnOhf)|D(jKi0`D2kU1|~;fP}{<B0Ul7M}myeVzF3S zjnTTEc%S$FPE}-s(TK<52wUAKxgoWY&`u{BiXhNw=AKI<A6otN(B@=rBFK&Rv|8F@ zPFLI>FZBBK>?2UPK7v2M*KeuNbJCg4$2Ri|gOc5DlKkvpiwF6aEEX)2BNz(t_eL(C zSbZO3SUw_9Dj{<!q@7HZvF2D#F_H0aiUr_yy!^m+Sms=h?|=PTTz%0Z6j&OteU}Tx zm(`;n8RPSC2ti088|9ucVHOhUa3`?bF|MOH+#DVE9)^2QqQ!lx1>kQ!{1g1y{XA4b zDV7~9LXKw*=GaD`+$T6~c7)s!uw}h)E9Z|X2qZ!z%L8R+Cd8^3q%s*Y7!`u?P8?WZ z#%C)N%R`(fpZWK9z)E9Sapa0K#4Uzl;O8EI$ozJAqe}!UM@)&3JR9<iW<;$jsJX$w z=?_Mpw-WJ?5xW-l!MdpxYnP2ryz1@j0uy4P<~mi=Edc-X!GEGzZ^61ZFF~}h8<Eg} zcrp>fk`N3hi_y2R1y;8mS1-RzI5PFjW&!wxm!894_CAXH`d`FDy{}+X<RDHc`(RYL z;ZOzOR)o1N#J_f`g7C@1U_~(~gJR@cl&EUgp|n>IX`}#omU)<KUWx_1mB<W_y}*X| z^5ib5WY`LD5M5Y}Qdcgjt;MLcaev2u<|Mtx=|aF2<et4|_yTJMYijY^eNZtD)aUjH zmhmH%#L@05KvcxFAF8uE4BUk&X@J<FghrhSX<UL*=N!bD49LTFY^&+OYX{$hqA7&A z^X3WfnO*_-iT2mA;^=B<&hq>xA)RAtKm4M^7IB1Sxlp+SV51QzRc9t^h{RYl)paJd zUhimb#m`^98~<y50gqZ<$5B-;`qf_S&^6=LL+|0ztd)?;<x~Jp0aO4^4O9RgVAub) z^5SkHguN{0UR@;Y4Uh4)pW*(>@8gGytay7nr~pj)FX<})>rtJ{^%*wp#Bn><@9Js9 zNxKp&KYa@>%}-+ekmh6Ii3-3ie|rV-Xq*>lQ;k?5<Nr?IE4!g8$bz=IP&hJvqQMB) zUu6I5ftJn!aNo<j5H`C~DvXh9K^~;6m34w8y`Tc{AVk=;Y(9eS|7=0qy>Fo7!FTut z0`?szcu#KPCGX}o=fO5O_BC_1R^H}0-VI+%A0IL@V)O7}mq|TL^7Rq{c0lx<SWedQ z2*8x&K<Ywr&tjnnFYNcdu!Fa2stShG73nAd6Ay5yTAB2`<_W<SQ}P^CeD)H-kQ!{< zGf8r2x7_m@Iv;)y)(?;J(K5)t-A`@d?_Wk;F%^Jk08{`@Z_ZT#xc&d$f;>~pi@?c* z^aIGkv1HYe6BU4sukOL|Up$HS2i`%T(*oVR(t!m~DIlE$U{QqOMpk1$2EMUaoNqun z`W&dQO>|In0{4IW9TfcF+qm}BTTGkr^Se)A?T^2Xjb}#h9Un0#c0T$5Ug!j5F>YY7 z;ClSxlV{}?Hf_Q0{=Nln?tx{Hhih;D95x8wBY5<A{LhokAa7vgi}CqyU4?q`=9m?4 z|Ls1!<x)b$-J`-QK96s%&mDNvD73e?L#Z6Tz~tuULMEI13bdDAeGxDDwxPGs2dz(r z<p;9i(d1!G`|2^nyGFUem(yZLToi+EUKhg7ReV877Fx{?$a+eVn>FBl=1_X^;^ohx z(xk^dU-}(yn=E(T|9fnyX~(*)bFtL4L9h;BHTHw4?1C@6TCfa7heMF`79+Rh%z|gq zI?!DDJ`OLjVO?_>es%lLg(E|G>goSrQ}`fExqgI|QN+XpkB^KIV{S(l?*8mAp`5@Z zM>6>0qu)h9$>Ne{FGg@l1A=`Y5ssV*S(zAg2YEqNsUJQYcU<B}mc1Xx&Oe0qYByfH z`d;D4)YHe^+k*VOaSzO!j-SM?BYU8f$#LDrYoW>**&B8$>1=7opSC@K)?z0ZSqwQg zCCa+B+-o&FT<1^8sG$xkpa{z#iEx#9R0Kv8MN~YH>sAsIL(a<hb95m!vO^h=MaP~T zb33`e7i17;xyFH!AueMeV!8LlMTo0{+;~O83!!?$pl9z`xEv0!o;aA&K6oW#M;}NQ zt}ZKTWhb$ys9&&5jMv+i!!OQ4PUY~wSsy+I38O`hwnDIQ4Y(|XeRKE1q3=ddsSjCB z1un4H;hSIj2EyS8Ty78Y^QP9X=jo@O!efRl`0`7)36?=@T{d_Y^@4SuwlHQ4RQerI zo~%Z0QGs9?Aj-8Lk6iNzZg4Kg7jFHcV4WC^4ae}CV-KOX$cIdq0u`M)gylL&nLMcC za`fhz5LR^a&&ZP({}B-;f-J+}ie}2>ak|`)N~ByTb9O~!nQkmrJ9J!oqf|~$UmzKf z{=V5fy7z8T(ddHh!&hwO^Zh1*SSH37-Weqdi{)i}>wEq9>LXvl96R@#>sJ2!uh%2F zetMLd%CQy`DvPuD2>{Z!O_oqF@$v6jgq>(_?SUNzRMnIsJ6To33>gb+wOZ(O<MaL` zAvBx*_#*EyIF%6bHe&Z6_u|Ev^W8HeO$`SLZ%YI)^E(H@cmoKToQOpju2K{8p>4BP z&4F}vHEJ(jCcGoP*!`)yd7(O0A+`y`J1rJsu;!T`3YPT3)Nesfb{1rl2pLNR&!EYH zI1`1;z_tJRGpD~wLNVHQHNv+21gZ2O%}^m8VIk(q#Iy)#XTfsyub8XTlR0li86Tv4 z6R)j^!;74C^HH#I1+v%7;g5NJey|J+gJFy)E>p;sOMePYMSjY?x<-Pu3(N2O8kB{J zE=$!kr-UF(*kT>GNnLz$EC=VyC5lRB4p+!t(Ij*`z@1c%dug8bPQb7c0=-D5;q z>F8rB%Jk81w(@gO6ZJ5iiI9Hqp3nRR>XOvd8)5GAt8d$g{3|BkH+DE(kjv!bTI50^ z{&YS00i;^hK>f*$9m%4h#ebgM{N_klES4cDNk?O>IhIpQWc-_A0oa5We{~<WNSEQ7 zE2^M}7ftVM#!l}deDk;0W1!nGF+AHIz(0N0<HuK$0ci+3@Ba(hH~a**{`W)4-+ms_ zFMWMLd~SLfKYF(iKl%D<=)w)S_g5#d@~1arB{$F|pY>bQCHVbKRp@!`uXsdyEAAwp z8*9hEf4Lc@-?<GN#^}eVLIC!Ae0c1&e`60?V9N6$Dv6`eq{iGvJuF#z)R^X@%suvi z+~@Tm=n5fjaKfc8M>Hbi3r8|L(ju7$hSmtg-1W>7#E-oduVL4DeR!bg`>3g#+^XN+ zu5Ns${nuD_Fc<5ZZxE~l*!nDpX4&CZ4SM2@gq}GDv9SQT<!4rA$Md`3(jCCTWmXiM zG&nC(hw~S%LOd45?&Ak>2<_-9^6=irM6e^C*Lt@O<^l~EWfbLYUYwlgfIK3`J&S&b z(#oM>SX0fFZ+{i_M{_V|^Ex<IH*&4jI^oD5MEtdPySdR+1d*5=DzylPlHoh-I{gQ5 zMznbM`UCjj!UqKF)H7ZI_})kF!`s=%;LV8e!hD>o2g!ZQaw_pL<~)4rrcViHPA#vz z_7a{7eh713fGaz4)VAp1%+AC7rdm`v&lK8{h1cUo*c;|90CydfAw*015z@rL_^S~K zWbpNpNao}kymrXh5ajLU03(A~5r;M-6N(Y)G=k09jks5Zz@k=!T~ibG`4pyRCoaz2 zg(8DbuuKf2ON*`cRmiO#zH=khHbHl&5c1p@beU&10#=n9Co7MjtF-;Uxw2QZ7UEks z{J$v|fWQ0CpP{S3ja%RSoM0KiZnA>SHp4Bi7c7ZmuysMqsE}QF=7O2$Har1EM1qGu z_h-S97{Oo&e|X~0Fmr81Uyhf1J{ja$R4D7tMUHb3=GaPvGY8pQ-Gn3c+fZQ9;E&h+ zj0(U>h6=!`feOF_Bu@Z#;DbBAj*eTO#1}|cl8C`n0H&NqItsvBo_wAgIgCUpF1=+n z_y3b)ri_IrA^`8d?H|0DMm%$;8j|yjgzywAWvKYfCD7Iu@!^Y!m_S5cG6`?r)yRjv zC86WEV!~ag7(bCj1hZ55MPqD~|E!SNiK1&(qvnpQcze1)!apDV>Elx(vWNPeSakOn zkTIuNu%r)@5r8cpHt_jv9s64N>qx|O))(Z<ieY6fN<Ml%GUg8L-%dB~V_oQf`v4!Z znFvW_u>wCgGzH%F<An;q=|h~W(;`kEL|i?``a&=)sY2c@7eH2MNE-n-WO88Nm;RA5 z;UW=8`%!fL8ovFQDCAl@4zwa{asM~WI^*{9g0D=k=IfeK;Tk4#uZljt7K#n?sQ{eD zPysl-ITr<Bvas>wZ=Z$SkQxUn#uMQB8#1hV?AwB6;*3`S_P6)*V+?h9-Wl~MY10a? zT7j}pT_{+RM>+|>DGmXhfr>0UcI-Ti!$(fwa6=n<g6brMzVc!Bt8J*d@jMiSeeg4K z)SR~gi?X;wr+%|(FaFb4hc8`NI1sujCmVrHTi{%}92Jcp;04WEe0JeLM@3O8L;HUk zp}iy<$DTK#{+9DlO1_@RfV|LFyzN?u^UFtH8JnyS&<FSJ!*AaCU;NYb0v@xyg*Jr+ z8lMF9#|+TBvXSAQgDUG{Ea;yDeF9-#Nyul$2}BmQz>6i^O_cKYkra+(O_1=!6p$7i zfykrge(%6_&GJ>iu4%*bk!`r8_G;cX5n^1~yJ_zaQPQVIW%qhy2M5ca!(u~J?uRFv zOj3+2R-Ga&$%9g4M99fPecGgVo<9>_8TZ|N0eCZlI9Sk&x7dAnhdGF5jhXvwpt@a$ zn7ABsTNh$Q^D5Mt7ofhQ5{p`@QEs1yxJL_nZZF=n?7+=)hxfis6{enEJmdWUEBCE| z%m)PJMG!^FGX691n7m*jQG9%56Beo(aL`=EJs(lX)kDj7Jf{Vj$7|8FycNA~9mo1r zLj~ojU_2q9Z+q<f*j~|r%FYaEg2gE7oeO0=3&q`XII=w0li7w>j=YPLyAI>RrR#-v zrIv@Dei*-Mdk9-H8ewoMP}`b|T<>D6X<COly%nS8>76Jp?z#7RoQV3l2DZ8rzM?1s zvMPjQMTmxq`DcZMq2TMKm<q9Q5g4WnA*~M4vSzL^bc5~I!q3_e2ze0lgb@k^5%BsE za0d`@gus^c!c!mz>m9$iI2n9468_nV6-6C_W#VKgBRFKMfht1=@h}SvVi9hzRB;d@ zs|qr?g1cUWb-adEqTE`Jy#6xC!v;th2U^M;c){{Mg4;}3v1GY$X6kvY?G<SJGOj!g zhIu9vk3nY`zA7)D+jqQ3a*so7&4NaEX8AN{G^4BBi(6|FuhW10uV3PifoEaX_z>sH z9m!W+(N&5CE$gt*v;;YU;U^!9ojG75b{wrYA*UgR1@%)~)@D021axYkA)p87+QGFS z35I~quF;Mh0{Ykkxb^=U;X3v@UVP<6y!g%@9B)2^-CKgFy?!aiU%OFe8|fGV`rSvK z#zCVQ#$h)Z=5MUW<d*);oU!nDLqHQ@iU?|{3dJ@SB-|YlbK`7`_l_iaFngcdj^5|D z^RMkYPV#~#pKDa7hE&asfw}L>i@&FdROB@uE|nW&g}ZHVAL~T!hQ!iwOa;UPf_SP; zMPAeaF+7f}Rp}Q7dLkj9y-mG1@vEm{+jg9<9~4~OKtxnNbS_y4!2L_kA>os)TN=36 zeaJa~?1k#7gydrGd3GBb?|uROZ|&zpf6FrmbK_2zx8zvR5YXuZsnbY#ArS+}<UJb* zqyNQS@Ez%Zwz2?<-02sANmx&^xP^otpDxH)H`M1q(RFJC%lPPg{Cynz*%NRb?%)?| z1pDlKd&B3@=GvQto>lV9NF=aqJ`C-$YGjs9CiEsHmCz8->CL$q0=nm!ZSc37Qq~Wb zaD61QP$p~5oJot!;|&3Q^hb~K_2b#Ujr806on~aOo}2XFeGcUS24wm3ap{FiFt4Hn zbC#{e$3FfMoL@BANwHpRf29L`j&0by-;CyMui}NyY^+_Ji~rvC&b@fvT8wL#obr&> zz|*=Do~qT*wts*I*%EB176d?%7=622pt&FeO)ptdan<0UT<h(^CQs?$xg$UZy(3i$ zYPFibDWmb52mXLR8UKUVMEk%ZirQ8^3i`94h!mlycP>gD^{6*3M7bjmYBsS!wB2Mu zEF^+&X*)um!O+M90x&7}$?fPz*ixwc9zG|olKaiFm=xlW9_HLGJlXm>F4QgKSAma* z`yT%nerkJw_Y|AobRp*02UpRCLP3NbQTXN?5e-xc*0Bj^d7-e#5n)15ao_V4L<H9Q z3gD9{!HPM1oaFm7Aj@8a((YQ!F)qQXmN_W!7$A=gtzdGUdPt=PbQZSZ{T(0T+Qn0s zH}{9n{1y&F2&+F>2BxGPk?_y~HXCN(vN2e@G=OaPafn56NMvI4ICRiv4XxxNk|@*; zDePr^Xf1Hy=DMjBwNF8EXK6K>F)OrBJ^L+~^88qMtO(1RFUPX>d8n}DV@_8Is?4=0 z?yZ7^Vd2PhqPf7154V4a>lR)vygRwr%{Kh(xj*19)@RV5?LkSen%fFdYFdsJovTo4 z%>`FQ3uh+}yUPf_%Y&HAk8pK2Jj!{9M2it8LgrcgR{%bXSf~);coBlx3Pg+A5YMzC zrgbAqHiH99fffGzDEIs=0hp=x$P8FLs8cl|R~ry4lf&fAM2M?X724tJRW?!zY4%Al z&M4$EEhOSIuNldCUS`X~+@3{XLu#mkE^Mu8=kwRfnuC}-C-oCP5B0r*lD-U7*aq`{ z+pS#v&z1E+d}6u4$$|i-tOp@`9BS=g`{$PSqOHb(&z2{a2ly+G{16Q_RxCf5i}}qP zP~W};3yjN9>a0v?5mSk)8eO?3&=l{(4fR(DR!TA;9cD4t4>*@rv`z(nZlI`;69IVU z4iriI;PZ_hVPPzfzKD-S-vOtvH#+qnD?xt!2os0dOL5ajugBFJuNl~uOK`M(0e*1z zCox}n?hM8$Ba@B-@cgwaaq&fKap|zzd=x393Fp=wo|!Wi9<Km={LX*#UbJI+IHj0V zc`%8{OY)$Rjl4c3`X=)IxZ;CYGzOVcj<C%GZ;KJx(<}s@;^bXV@p)ONLI5VX8#+<? z$um<pr2`~{ty(oue<XtGwY|JJL+Tk)7KW>Bh(IID;L1vdiuWq<cbIv>oruGlN#vIv zFJyw%_{tu%{QXsQJp3+v?ftxXr^p%*tkYHWJslE-fxFne7ceD#;1>;W&y0jC(~Q%H z{x=W6y6qTW#))UubkX_fCO%|!D#O~31rck5y>=9T^gP~MI#GzwOT-EH(N4(q8or$& zpT#dOnLs#p@_ds}?twNFiZ-qgtdw-a?e;>iqXKZcLKcasltchd|MoIjki{o~ZYy8+ zqy(-#Cs7XEhucx`k;&CfB+4L}pG{;wi{wjp9%vnLu@YGrr>V+^x+1yD%f~AKCmJ;= zy>bA@d}!MAGX8y{6kq$+27VBE?AgC!Yvzsk{N<%*<|i6AB)9O(z^A{1+m{jHDujIx z{{@Fu{sgz5T3CDZY20ye5$^r$;N<tdhwp*?>aSzT;d^jDZpNLLpBfNb@#=3jqwJ@P zu=m>ySoPPNiLdMc2cO1Wwo6VscOb;oaJbyVPnH=Bh7mnMe)i!X(VrJUTogxbiw+@` z4w->6)SWDbAu#dP5N@X#ad{Zdf;d=jxnLcD+(%M45*_G_2TkrCXd0`aCmy*1$I2Sf zI_D6~`9Z8|DaIXF-7Xv%nFn5b2nWlI2q{_2Y05-F-zv;8of$%j{KiOSCtR+P^Zt&6 zD(4_{M{1zT<p%ch<Wy`?t`Z~HbkEWcaA<J{)*R=4>vdlij;59$Z2vu!AqmcZ>0(6c zn&7gp6^;zzvjh+eM{r&FW{6?~`D6*tb#8<vQx0Y3P!TwnX+`tmlQ_D_h6m>-v{oeP zkZ_tg*@jWe>o@oP1`&A-Wt|nc@W3c}1UXvsOmJm4przIVpO(dC9dq!>D?ce5ojAu2 zAH-|69q7$+^C5vrnCH4?JzQLeuB5vbbI&Tj$-wXNa^;ReTpZ>Kpaju;3j&!D1Z|_| zQy_IZ%azy3<hz;<Vq9}&?GE9irEftgO+1XoXf#<0(a0=9M)ByYmJRAdkoD(7F4aS& z9)1#(gjn~=f;d@w6!!d1bd__BvQvReUG=!;vg?GilFw&b?!@|Sl~~YuF~3jhc)uwC zXT1p#*TlUo6j{fSd9)OoJT)Yzm6b<7{us`0FUM!E`>bFYo<F|v4;-(xW6ka=EN&e; zoJz<a027JuULq2?1RA09^WloaXq<Ndx0^1;+6$5&mgQW9EUuACB)sPfC3!fUZfG?s zUi6r$kU@&wZWs23XW7!R^S;0Ql>@LfdxgE>$*If0_5U}HhzT?VH03PPJ_qnA!d5ix z-iz0^GT8X@>oK{Q7BgooOcp|uXjqBhcwskuI~pOLM(?ep5(gNT{cnFe>Ip)B$wFja zGrg1X5w1KNfAa#QIa4?3%vjtg`Nd6qxY%^UW_KV{PZn{HF0hcixRwVuA$Xz}5{(if z#rVchvEV>1WD?dq`{uRKtf&@_B#)>!h)}Z$!IQlR9PR+?^bh3nB|^VuW~QY`<-8ST z@{s-_Stv!sk*F`kKPQOBpgn&MbQdp#oNJq>lFmEs=gPVg;#3J23BuR)S1d;6m5JrJ z40l=4|L4~r;>N#Xb@HB7(vM*rKIDJyVrW(-zs{jd9ui`>Ab+rbMM*F04kvUOBz4}* znm3&crn_GOLn6eaxZl8XbyN1u>yWW-u3#BI9*>vnzsb0<#^~b)*6v03_a1{RKa;Cx zhrgGxFbh>p0djA<RInrmhr_}5WfCQkITk`%<p6Gf`LFoX7AIuoSK-cYt>!0bjy-i3 z?%m5Gd&6z`)%BCfd_)GRPeXp&*RfGpVAlWcSp!+qX8im$1Ma+inXnR?yys=;zi}Hj zv^<R;b+5(mPko121MdG>J2w1wEt-Dw4syPCE6x|ndjDI0#lN+;;d|>x7h}hp1NayB z{Thc#xDL9Cg(NCMRrhkNI97|S(BuYUexDBkzYEdAet2TT4=z*Pkv2HnA&z7q`?Q9v z)nJ4<XBQe4IS>=aQQWUZZMXmxx*~31%%I!Xk6xJr<}5#-yHydEpsHQReNH(RcFsdy z@Jw;WZZ#vSbi<V~adAVV-41zM1#<I~Bg~Pw?j(#X@u+(6+6N(yiSX(*_X<Z-%O&sM zjy1bWQ2F6`a9z3sv9_V!dHroJtgJMnPJBeL3~<0U2d%zx<Wvmx=G5eDg3nxycW!wH zZ>_x-ifQl^8!HF!wXffSC|9QDom`1^&5H%=*fbP$z?QQQ#}_!E_sH=X@x{3Q+B5S@ zkH-Gpd+>PktFSBlux17MJj1munOxZwLF-e)mX(cp?Nz9BO+LSGBpgO46hbt@fQ@jC zhawD-UyZnoK|IHfP`(@CKpo=YL_3e%V1OYLQd56FCxg%8<pxwP+)%SiuuLA8Uye77 z7oeba>;bA;vkS`Obr7ke$j%$Ox|tW&x%yQmmBOrcp|#*J9JxkxR(Sb3ZE;^Ae)pw2 zg|m|fdH!#F{TdYd2STXYtVXc;#u3|q@t_Z!gv6VPoczId|MgW*!Ic@t^B=oMungsq zCmzP*rMs|hdl{DZj2u0{#dWHgND!j1d>~JCt`%G*3W<rkzELR*`B{9u`p9*Upxl^& z|GoVW!aFIGh2{WG4KxSvARc27@85M|L=l*XzZc&*AG#vC2!S$j(pms!eQ0kufK8jW z;IK(pxF%kNJN|s*Bn9A^GZr3C4&Z%X{QH!7q)rtPhJjq@Z%2a>u>KGh{Nhv4FRI{e z(?RoJUgGl@%d@6V045&LS*z<X_Xjr$mh>Qz9KeAd8;<_?QN$wbq{9`mUT(}72=gIo zNv^qqE0#^j6L-L~xF3D5?B)tq44#uch`9Vvm*#TEg1pBd$?rSUp*euln<y^;$N2@N zr-+3au8ebIx<G{Yv?|!R3_0uTp(;wf7`*k0pY!>DPvv5tE=WC3a=@>7>IZ^lWJsRh zgI~Ls_o$X-CU;Cm?oAYNXz~oG|MjN@D<z$vIe^ofb1?_-?oZtf3HN)+g0)maqBW3y z%X!cKXu{scjs$Z6lRlic*~o{|AB~X3h2{6&0cBzG+N*OS2k^@eyai|djri?Xm*dnW z&Re(|dB%O%xj%&RE9PV3!9g@oyLaLP?_8W;q2hWOR&0NH7Zj^DV$rEcrx_}^-h2n` zb*nLl`?}1@_waaM4Q^Ur0!3B~%}*Z$&Z|c;SElV-U&R|4=i%lBCEVw3#*1Aw*ig$g zzHBpIe$s?_*Pn-qF%}|^CkOCX9{K?e*O;;BXf}ds7C}WGKDqfqs1gkAJQ5C%35-R8 z@PcOeLL=v#Aor0Jj$}=coDEyh1x1emGA%do5Pmliwo4rIz=jkkGKJvQhtZO2!$D0a zj;VUlukk_@jicD8Mrm&j@=S}cv|}aa^;hw^Hc!Xr^?<d?;hEz?JTmdsQcR=_@{AUQ z`lL|nCg0g2D<DbUC~{r(mO?buHR8m32XN_%3xzXM!=`uM!4C0BtlD)6Brz`nktGnt z&n$~!PY3}oi;Hu13YI|%)eao8R3jsMXd#bk`(bD*L{EJ?_HEmRjVmq@j!Ye+<p93! z)vrOuNMJC}!{tZM6uW#L5APux3Iw>pBLjuv%-@&qGa$!V1#vKpxX6!p@{i-$lW*bR z*1d>X0uZwzFyRP{J-yg-U^iaf{Wcyu@)92GdKnM)y@L1HgYe11PzR*kvz!HuUyrD) z2)UM8%rh;;g1*Xe<@Y7erPu9-$L>MU<A>kvN5B(6Aml{E83!wOfi+kWR|gO*v%zmr zBd9D!NRr1rS7qGI8ebVE<@NM>X(kY`y0Adqh@2#ae<mp9QJi#?@}c~sW31+9m{LSb zTOqS65%75-SCG8GXI?{+t4hKR;IxbqWzGs@Im;jl$dKa*;Y5WE5B0u+H}`LX^+-2X zE}XnT``}{_pj~Fc1;+-$>XAi25w0Ha<;uAFaq|9g5w2LN%u+Cj{C8qE80*K$`Br?c z;#z+HY<BJ1iN7mf$J{0Z);E7tu$~E@r`qY|t{2zwtLfsuZ%|&2u&5AW6d;t50R>k- zhs|O>1a&~%h4u<JK2>*>@ClU3LURD82ATtS5JiTJ&+RK!h!N)cAR!M6kwnDj{3T(Z zH~eTGe-4E*8PYKXwEgH7JoD_cc=Xv1u)U!J{XP=<ON%9+{4#$0xs@oK^jDlYW8p+Y zK=;3M5VoxYAw*{rL?AiUS0thw343SVaug*bJm@ruS|@(}3}kw4Y&C@<2^k}MPV}Js zmeh`m$IDnbfSvnWaO8Ur@gnVH#a{xMP$lATjHD&n+l-C}-{C_N67OIVvYCX9CXgJ| z(Le|h8#kWqFvE4EgU>@vLOQqo>n(IW@jmRk8xicWfpPf<LOW;c`1+58jh*fg>dAuU z08Vd6`$#xlpeN!hS$sf3q(`}O?QJ!p_t|YQy|IrkuM)10tV}^90P;MMg)QXoPB$cl z0C_HTb4#Es82PvQ+Dth7?fd!ios9ZrI7k`-f2#>u%WK9IKPXcV%>kU=oQpYt+wOaF zpsq-9yG!~hWWkV+Ya{Y5JQcoZd?b<snDnu_pWX`9D0T5E$O1Y|RROfzmMqdK2XM+f zfbaVDoyfW6PTaBL%+4pX|KIrLlTKXw<G~fn6M`G)zWt8}@SZ~oStJDYiktEM8|$DV z;jS8%;oe*70Mi!y?(bXR<{nrEdARoW&tZe`)q_W$$NxOp3=)f)kuS#QzjYPrNq?Js z?%(diTP`J3+&wD1;`8|C`msgEL_B~WdEybgtvm#4P7qm+I^6p1K)BAyLJE`3+XYb+ z@K(9Nx|Rw@h9}h?zyzK65TxBD$SE2A+V$#euw=LpP=+Cji=YlGkm=W=z?V6u5N<V^ z5SrJDpnJVwnFLZVGF?W9JqF|yO{PF{mBncuz=Z8}FX8YK3pQPHKfj+UKKJ-{5SB%8 z*$Y=7T+s-xcW8bkXP+C{aT6{oX%H-D@>2U+Nc4JWvd`>z#wC7ab$fC2ifw4EapTP^ z?-Pzp9V2-FU-SAMkTGJ^HLk&$&cS?Iyhovjdw3;L#ByyA#kdO5%{5?ADR+NF$STMg zQaKv6cC-{7M_k&E&I%uVT88&j=ZYp*KDovzCgMFfNmyr+k6PzZz>+IPM5;wpB*oIE z8dSK)SAKY*E)+&I%sseK?hkR#KwRfR%)mXvSw1ja8y01Ch_hP6f@Q+HlEuvP0FJRy zSlV5<q2fcR<cWvSNQ51Ib6|>Bz)&)J80b@o#0)ZU6w=O8h`4G$J8z(#BW>j=58$&2 zNg_B>(g8`V1*ReodQ1G=J&q#7D@U1AkL8*<_}q=7hg2SpPe1uBu8fOt!}eSGeXFGp zOldcK;q&<YWT4F44pn0%atn(2eV;6ZXRmk?)ut@`_4Z%$wo`1{{4V|we*tCPTC6>C z4Zrv&&b3i)my0j!F%b(<ScbUPhhTLdLaw3V>Q5)r*9@^Y6V9~}yt(>yeAja=uD&+0 z{KJ&dp&r1gfqDQ_C{s&1dH~<gje`%1r6`!Q8kb(N3M=Ie_}$$NST))4@9E5tv2dau zz>U9u4*pIHx$uHzc5u1}@F^k=FSK<fSooW%_sFtsJC5eRzY4`T7BnP_u+5FyZ(fJo z3nu6Po=isb05-pO2*y|U@b@cK3lqqK0M^fq2ZLdLpLiJ)&q0!0cpw)x*`yHhlh1Mf zU$KH4)6NoscPyv}aC$SG2k=OcGEThT<4hEcH^_VHX3Q-{=8|fDvOQz&<f8H&`}q^_ zHua@UNF6VX+t1e<D<1rYU^x@Z2MuWa-E)v<Y54^?so>Pygo67XnTsk>`NhkHBb0Q3 zdH|<4=b8s_$mGQS+x`Jn@f0Om;_9fN$-zg5$yvWZFeXkS9>DuP|2ITKk%Sfpk%c?@ z#g&-%lbZy~<VhzF;FJl#k9_w|99wWBe*J|-r{(({$WQdh@wz00z7pUrB*P7`WkR3O zzUe+Zqq_}vtQzbTGhr68VLiLJ_lCLm4mU@~y@%o6ljvhUmH_<g2mgS*m3>eLBv^U; zQq1k0ObF~joGz{~`Ql)z+u>!FLlhg^lV>UfV3EuXLt6xLy&O7yVxmeS*sOhsW?SJF z|2H$$q!Folp*Mvg4r`E;KfaY7;ZTTQ(JYrMh6unOc@WQB`6Ti!YCQb8Kl8R!aOvjT zv0{HAs&_4ie^C=6=AjD<`db}Xo4F4aIc~vnCWo!nX!KPhw{mD0Xq{vq?8+E6Uvmhr zulOs}8B^P-9!>!M`14;!SP{kiW6Q9nb8x|f-DpKTEJ1i-8~nbNf^`6uVGHCIu7d0- zL0rZnw{+-amR}Y^YmNyHJ@;J60*HxO#H3sm8J9!h*FzW3BFCdco<9@n33%*#TrMyn z1~G;!cTw?x*jr#jNFm{#<GEawG*#Dj<_W+7lMT5{JJytS3YO&2>&(D*$4cZ@pK_Z? z47u(QG;Lhxh>=0Ubvz6?StAO-r_-aeq9d;hnxGk;Og~J80oZcFd^&&&x~uW?FN_^7 z>J;SpU$wUwt2(cS$L&VM$--GH<_6cvU0IzOIspljf$WTdwrhLsOE|R5g3T8XE^ztX zgMY+()y=4G(_{IGjZpS*<t!Y5$ge`&;N;4>8LT>jNIV-+&tOi%(GY9ek=c+1RlXGe zx#elh>CD1k?)Z&xlrkBp0Gt}A08F7wEomzNbN|XTg_yH?H8$2*@yPwnSer}%_!MbA z7EVL}_O=*t<hu_-mBf&1>B4XVFd@|MM9nv@MgAp;r>{u{`@ZluJ{0wIc*!xI0552+ zdi=YBW%?OO0B(Qq9T;EU&4<aJXlO#p<e>s^dNUpY_;li2+Zqc+_(^&4nuq`*u5Kv* z)P*RzZsbUYqp|h!pFvfe!+YPR3&g9x@S0Vq{QPBtWuPAHc<^1=cAS7zt$-wnIe*XQ zbOD$M8>=4sj&OvME>HnDy*bweV8{LzH2wa0$ft0TAPJezdV*N>@9&PgZ#tF$Y<g=y z+V6iG%DkaE$4G?j9;|ra8%bMCbWVhT)&~z`>)wO-prH>QKZ{^*173OdS-jg8LG}gL z;^w(Y5(T-=pyagQdHln}Fl<<fiqqOriFBk6Blq5s`Ul*5#?y@%Ed=zBp8pLF)tRyQ zNG`6~`$^<@CjZ+#ZWla03u5sI{B!#d^sj*^p4c}g_mLEiWKEC^OH7G)`5}l*T4)k< zVIK>J#mfC&7A`{}_xVZeD6u+(4ME7^M9>|ATCD?jb%cY15HB_ow$$(Rb6XHWdjt_5 zgRq-Lz!gL&<cE~&aGoj$il_`?pAAPA_hYf87=^{jZ`dDr>;W`tOjvXHGDy1B@MLKr zV$PgoA)b?NM-=OePjCY$!FU!K5-*N9tDqYu479l90QAS^!Cc*qcaLqs^^30%j!X?_ zhk(BE#R2hm-tl^z*EtwM*Vbo2TpEF^E{ceMXd-zwRDrN0A8geTNV#$pGKvri2cgmo zHWu=j6a_v5Ds3gGwp60VT#Z^2*(y+JFGP_i8<}A(WZZd)67c)svAg+j)KO6YQG)}) zs(yIY-1kxBBEpt%10b$!B(QiRnas2h(EX+;R_7jvMv-{qnJ79{1cz)jTv?EgD;!=l zT!@fH4@uE6h|PMw9uRRwgM@}24SiUHaz_!0ymiof%b;@SL+8_>q?f_Y1wGicbqB6q zGB{8iiI$U%c-^-P7aY8rUy$H&+Yz4M3??{7I3szm*+wYZa}ZN=b){4VMW_&_+*bUj z<qe?8hhH7NAIFL<Sb8`IHT%zpvP;C3_YgvJ+Tk-~A*d)qBvt}8Tt1Fm)m**H7$D0! ziI`Ode?cGmbN#q!{>V9nDTzZvK&J*80-8dZTGBBD^r8)OQ6`VTZtOzS$rIRlpa((3 z5QvJAmzf8>PCBk}=BT9kSU8ao(6(*I;A`sR!`RL?P7eWnIwbtF^}_}fUcHJB**g_@ zj(5ZK`d%nV?%!0-1B?Lqog^2qrZS)R{!0hW4gpQZ`^SFzI2<HLa<*>D?hR#lXb9-^ zhP01_@e1_heiAQs5-OUApi+&J`!K+Dq#d>$Cy}|dnh%wpD3<pd;5^uxGEdrxLOk{z zdz$!A?qo3uSyXcTmro)ZW+3B!^Hhk{<QWpjxUsx11Z{QUtXya{lc6D?)0=ZG1hivs z3*5)LrgY&OsRv`xDF1i!FCCww-&i4_kNxB^NYq?id)6Yq(O|hTpKfk3)WbN8O@wp` z0i7}d*ig3+<^E<IZ0tdMOCt_*|5^fK6rBGleE0e)C{omiD_*+%i~;XdG18#-JKKMc zo?<`dx2(p6$1f7B6UXJT!V@qf>Xjf|*AA~_5u%~-g-e+d0hoj2C6#3#N5~q5MwR%A zi{bDE++eIygcde}4b}?QBtk@Bc^0IZjfgbr;TIbb2zwFo1o>Q_e!mC7ei7JQ6PWTo zgo_;T>$4HmWg(cQ1XIxsiPQzgs04djuFOJjHM(+7V2ASvZmhqOw<VMR>3ImnMlEWO z*Md<v5tfz<M+WiP0?@|nm}}@0EJKk?W9abYgNceESD#tgM@0-&9eEHNEI3wg8!_)^ zsyHP82SY)8?457&w%W!;Sl7Oae@(FUS`jli;L6Je#t@fc-18Ea=7Fs^2AM;N0QdQ^ zXdH?WJov{;AP|7h?S?O6MZoRj8jv{H@-75RU0k`EkFX?<duB@$Ua*lWW|9Ex@3Wvx z+=e-MR>6{dEWQl*Srru8@i!n$q!b~846@vlh_~e;zznnpk^}@G67oLZpNR@<F|s_> z&^R18vcQa8TejosrK5|$KY8_TzHwgDv>HxtKcbQVyrR)l08ErvxB&6WgAg_6aWz~3 zIa02^^e}L8&)xRS7U(>3%sX0v+N~=fk3|ri+X<hv9&DK8r=FabNf6mk+gW(Zo#?Ev z;WJe<1avZ>0z|z9SCnlVE=<=@Lw5`{gtT-HjWkk9x6<9+9g<Q~0@5fc-3=<zHFQaL ze)se4{jL22VAi_pI?p4WKYl^tj^J%9DMsK(vHJ4GS7Nfu8JW{WI}GN5;rWReh~S(5 z{wJ{^;Y!iivw}~lx!xIq<h;v}<6Zl9gIZ*XObLPclctN~BVY@VS(S@JvHK`||7qwJ zCSDB_y}9}KQ?2N7^AM%)(zOlYNNgsuZ_O70{4;YZNa6fh>nkZ6FslLD->9&eml8+F z6fI^)erpRaJhzc0=!T=h{*NOVY@pAnGXfW02z&OzgUs9Pc8q%Sn8%Lt`$RG>KhfuV zFt)dw`6K{m74H8%tJV`A)^-j}12rd!U<Zx_<WV!4_xFXL2NmO)5xgWDQS%AKlMjak zNm(Hv)AZPqzv;W-2-qo9J{NkMqi-hr?yq*?zvE^%M%lUgO0O1J34_W$eMj#|>bw+s zCJ#)XqQC#;C6O2*p5B{sCWFGH*Bu@_H$hB=mJ%E2o$g@@iIF5L<I@37^H2_Gi9Cvf zCN#Z2&h0Tz#$-MunJKorfe@orTSz}X4jhvKF<g!C_LKxNuk<?z?Q4+XVv>(&$gm>b z<F?E*B$}eIo(T&$)ornGa%Nt5`B-o{BT|I<m*4S`uWWPe!u$)+udYTY*I)LZyVhkx zP{(kqLw^&?l~jRLT3|vV(NhNs32oGO6tw{7q4*Z9SdfsS&mcWO2oFn@l7I4J)*I2Y zjbF{$5bGoqaPZ5;=%<!HPZ$lkSZR%11NTi<y_(IG>lp>OntjE?##*&rx{C#o*L zz|p?grwUZdSDT|1Z1q&?j6t<a`|`<^(@%(`j}mzP@zhm<XaYkh%u}(pO_Vf$4Z}C( z4716H3qEs>6Cuu9T!)@T)qg!@Ol#QMc^1zzF3b0e)X}h3bKZ6~jOBhGuc3rDXVwtx zusKR(w>zp7&ZQkQ)7+2TF|7^C{u3D%r)mq(uB5MiN(qUi1qtO3Ee~=KUFw~AFvg(q ztfES9hHxCj=+Z>?*Q_rx3C<w0&NRXH9^y27+bQG>i3wSGE<m4#TSp)?gwmLN4Zg}Q z_E$>Ww60gHzk5W2d3pZd_y$Pb3le236kI&SXHD7>Gr2gUD9nk*xnx`kU#mz0Pi;p+ z$Da4CIP@YllNXxz-<yz(Su53?PAbfwUh$5ATj~TPx-K?@dnfzQsub!ylj<DwjLYXb z%ano3n2wLAPft+1&BaxGkuu>gG`rs^pC%3b_$s3|f&^*>gJS-AXQn5_&EP7lP*jO} z5io0`lQ@)^3DdW>D{-0{M8>{2Xm`|Yr6EvR=s&+BHj%8yUrL*iLS4VyF(eFrSX-P@ zz|Tw6xG!=1;UtOcpla5uX)aF~ob-I@QnR`(Z$zkXNOADJc<-~0l0o?=H6-VQ_p}wM zQxWn4f5-Dcl%!oj+cft1bBVE&*vsdmWhs<oFTn!%mwk@^q8e*yLsp?hzM@F!YC>+K z;M4q}Jf24CC`>|~zE?)rO~V*bBWO{TCPLf-JyHk)3V1O2V_yzO*l?q+Q0hDLPLX4$ z>aevThmb}L?^*f{!vt^dBWG6Q6IWT1AM{(eAQIF1ecrUokEikGnWjp(Ib)xBnt2E> zsxQDdZ1@?tlC1=Z2;1ZqermG^ZTM#W$Ugd$5JTILW%9nF$h*3o$kXo+&kDdVc^=Ov z(gxn5i|P3yT&PiK{Bf0-ZN}Ldh2(erf-xAjy>mq2tsfO$H^5U!Cn;Q1lV!x6Z?mua z{9xL6*<&rxW6j?;J0%`x%}~Ycc({GNl-JHZqP!Bl<;C!J1@VQ7;W3?4C=PVXJC#X$ zGLyEj5L>ne>dNUsZAJg0=YlF6!7+IlD#`1UNj|HG=;V%?3zEV3+Zdv`Xy$a)`*CYj ztAN<^JF#OKNX2q%?fXm%xbkMp;=L+M=VLz}>S6ZdzrrpVD#F5c8>VGkN*G8TvhnT} zl`1uO&c|PLtFr#$D%ALc4Cc-It)h?K8)M+DgR!^W8EtfDZ<68Z#q`U}`mFz@4P*-s zV^uouC)jB+9cd%noN+>#p*2%z(O&u&CMIy1$x<wn9L(yM2a%WN--xaBVN*d1o-crW z(HG--(KN_K@H+7E#+k-8>>D(W^aBt9a0&TSd6zsA)#W1j?j*pd4p;rp3fCzTJ7%Ri zIk{_qB1WJ|oMs)6Y%!Xh3`O0<%*<68USp~dRC}D#el$-XnSQQqz7=tT6jmv1MM@<R zuYOHoD4%?*wTtMV=<@OJMbhdE!@F-r`ac~IbqmNa)_jyxXcL@;unXpJYUeN|Es*WB zE_!v#)0U<<q@c#Jwv58;VP(IOZ;4X$dCaX?=rQ}jb#)lLoU*VZsUCtj{J6muJN(eK z!-%y32Qx~tD3p|Mp5Ae?red3MsFKvlI@FBnulh2i=ucq7)bI<;i*OGi;b!vdn|Cg& zu8?*0#Fv}(r1en@iwQV;3KQUu+Z>H)M=JV$K8$9B7!3>Lm^j4eU|{&v{|nB!evJHO zTu2)uj`dN)W2;C@6IEb2PCPDwyJIE#ianLfx$}IGy~S&3^R{dswf&VaMuJIfD+@^D zgJD!_lNP)xB)99AKc8Xj9sU5z0z7G?J8~4PWlgVT4&$GS*3Y2+rNUCQfE#GxN)^53 z10FTQAD*GhN(oCI4d#yR4v*0j(i@bC=tci3xqpB-9!Yj<9~l0R^Vk>7S@T(9>kzs3 z*GyrW!mr(pd_CcjA-*$AUahY4$}hzf5^D~M`3V}*81_n{8^TY^VI*6Dq<Spx`VGZ* z*DugMac==~z@A`Q>7ZcJs5kSd+!UXG8Ei;DaOZbRdWnU$?Wm+3mCIN<f{;2Za7q<M zMKDiP0vQVWL?V*yD6n>AERuTPucE$D3*JFgLz+PrdaZ$Hie>dt$qR4YEh5Gjf$(Ha zKMT%cXw6XUgFrYOuE#+HmkCc4yfG-EdcF>8-(B!_{rHvOJYG4Cw*|NH>{EiUUD+U^ zkiAQfM+^1f+)dFoZEfLh>%Zu%Tn)>Qcc<kLM=EtCuK#c*nMDO8$!k^Kb{6iNyW>dC za0f@r0zo|?Ud6f19QgbkuY=Eo4N{p{FlK*c(w5Tfgt~DF=jhfKn(qAx;cdk6zr+qV z9hg>*%z6b_O{|eKnkB2vfWs=3Ju_)PMl2Vdd-Hm;zcxQIRW>4PeI8b{KFGP;qAuv9 z5tIep-$LYw<^EN89B3o$D8)gdz)vBkP=I{Z$fvNC#kRP#be2~fBoE*$SG8-=Rl&MK zNP*m9%M%z8VHmxrv&0e5a+o18Ol4PEh$UXpmd5<oh+i=ddTY|wLrGC_28PxO7+NGT zY;S-sjEx`B(o+siY=OkLtRf36;mS`K(cUOWoAcQqrCS-)oYUjcHjfw%A7R1RGW(K0 zP0$8;jqur4Ua#kky(Fy_Ku<dFm;=iKF?UZmdPZw1%|OjzJXz~F*u)mpD~@TyZO+DK zK#*FV%<M=0hsqRf9{!m`wRT!p8=fDLR7W*_#8DaVG*^rJGL>rw+CYLp+7)13+N`a8 z21&c1yc)o@MdC$y%h6z{3?;*?ill2;Rz@M_fGzJ#L_5n#p~#bjF|viOzEb#*L_*(6 z)4HR~!S~nMVS1fU_Pks!-M9txH#$m-lGnkL0KKm+c_dOjqkzrZWe$u&Y2CUTQ5m<f z{JnQ!por|nDV`nKT$6}R6W`;rCkK@O;v|QSA|7=wb{1wNQZJDBBfTV0_3dqnHO<y4 z1N;C-GL2Js?$3Hnm>#I!1Sf@^Fp-xf*CVRlFM)h}g<QX>mSUQB-aU{H5ZPoL8zQ0J zjMp76P~&F1XAL(_)KmUyJYu}A+3)|E`SeCeIUA~$NvRc+c4+xqBWk4IyzxTQDQ$G- z68;rQD=B-Tc&<xMuEOY6x{&ezS#}!m`3zu))Gvv6yd=78QCRWss!2pvPJO79o|nSB zmO&0zM;%Q76)YC=EC7a$Sn5s8)sS$Ez;C%;`r`6OAAj|gtD!T_QJyJ&`bboMdsI67 zjxUPTd@7Oy-<kHGY}$k1^oNk<zZgEuf@=Jtg%!iQEEsaUti4ZaiY-&-9rSe)j_vT> z=PNX@%U!q7)3PwZ@DH}<lT-~LKO{Upszgia_W_Qwt6A&4o>;<#ZrU$;kfGvxR@Fiw z0-ItC=d0px-NB<=#);#le*wGDr9?CGS3vUa(c>P@o`;$2CzDT~%`GT>AZH~_Lh^DK z_b6d?zkkeARF#%k@jE^6JF!9JxS0(0lmL>r0M+AShX!#X^&S7;<j%s>2b!_x8}?8u zppBGtZYaaeBB7^qGsZ*`XZM+;W=-_WQnVfe<naNQWZ9$AG+T%yE&}LFW%?zY5fdK- z$nzN!|JnDRh#vRJaT)h|XtjW7w?(8iYl=rxgIZOokS(z{T+xyh@b$v+r%vgaEx;q^ zp^eq@Y2<;<x%OY*Bji`-av~6JMRrWn)P#!Y;WWNW;OszkGCIfF@wA3JnLNJ=nv`uh zn5c!URrL}-IEDftKYy1&FYPq4R5Idmuc|!_)4qdHvtxWTM4qtPKNu#3E*7SDF4uuR z7wZha8c~oPUw2-3u&v?iNf}7Dsu?Ck%)}?fWS6#p04rR7zYEc2W$OjNkutUqAI>Ty zZ)==FGI$vy6o#C4ZSmi-!T%m;{?3eZ^`mhO6>&73lp4i3P{-_+eN0e-IqUI6-`V1K z#PD1p)fEp#NNL3L>d=nrh!1O_e@T^t<Fb9B^I*_3R?I2YQ<s==i?^E<BS9ytSxkR3 zFwHW5`|14U<C+|_B1hjyWpNbnf)*G3fPFg%{Y@)l5`wDg=I|0Q8yo*IO>j!)>xWY3 z^IPW|Kl}{|J!UidIkv(Ha>Kf`#9V@4oLCFf>Q--fqTbo-i_(@e!UFustk%bi6u&e1 z9!(S-j#w26vAdBjOa(1G;$UrL;$YUaivRo?J+dh4oupnxyLZ&zMUEVckevJXH#<ml zCC*=!$D)zosb?fct;zuo*wkyEiqL3+U--zgV+ndAR1vU2zwl3D?y`^DsPEH}2pV&z za;R|Ry{A4vPE(g1{qG@;aV}GA^`C$%oRb;;viZV<#O1Z>Om7Z6FsuKqkJ(rNJR8pv zY)dyjM+}L<;0+_us*%VlkD6;mXca62b>|{JbAmX%clD4TcJcEEY23xHC34Z<r?j>Z zST!5=zlg5<Zwt*tF?~3e=YQIlgEo}OXHvu^FAPW)gPwOudU(^mqnh5-b5h-{%Cvin zF8n}{{)7y`t+-o;@A&}qSPVoTwyNs1GK53d5-UxyDc)M-f~FaMcSxG<f=uB;RH(sB zrcJyNPx1RXMs-!41X!UXf&U1Lw3I9vglz5LiS_?vHyK#{E5BUuB0e)-(6=<$_ewno zWQ-Hm_>*&p$`2F9!zI=kD1=jrFE^FzPQRKl>qzTZ?WbPHILKE`3Slromm8c`q6)ag zV636qvLwBDkexXmK2cC{3`wIbace}qkz~3tYUokkC_mv;aCGKI_k2qvqd3}K<N_J0 zjS^$tZr0v7a|Yg^PloL-AIOf+q2_^X&uY09gux>v@f}N;)Q0*wa|lKRD67Bdu5!wT z;-ghUz8>h5e^^4-;YX4&Fwft}S=yz~&_Qz9xz&q5^ANwe(FWiDO|g>gp6=_Keg7>5 zJ^?pLBDISGZ3x8;5PQms(@@pTN|loIX^1@=UTF`oBI-NrMean?Oa<Mb+S^w5CUspS z!{f6@o{juY`KB14hqJCmXw-|}?ll&w`?@-B72xqRFw}Kmh5Bze`43d*1|zn>qmt+> z-A`AuzNgCqSGga)>1I;neM$*fF5D%aCGL&q5#?&~No8%YTxzBi`j=lI(Jd9Bz*CGv z3}{{|h=N%+qn2~{P)rj6RZB$IGqyWuGLL(j5%2j-v>?1;8ex_YVG~#Cb761jPsi+Q zRIUHHq?r&~fF#7dK;Jiu7~SC-U4)B^5RT_Rr<$NilHi-5vy;aSDT0H*23acOnSkBm zzOS@oc>9b9fAZfF#BkZlwzPqtdzo)z6X}xY(45{P>D`EV<1L4G&pI(Q-CQJJ+Gh!z zW%4$9Z@g)d=2&l7xO<A|$YfZn8U)7OPiwSz&64*4`rpIK?W*AeCZTf{G9@KJ(LlJZ zZ|`u1XhcUptJY`m`l=|EL~22AS@4*|so&^|PfMK-m{Y=&mD9F8bV*<%^ed>o(}SM# z<2(0AAY`_GV4552Z5vi{Pq{bV&aeJ8?K&`t?i((1Uv$F(Hn!(U{Nd36zcOUG-^PXp z^^)5gTs;;I3{^9xyXht{i@&bzfeU)w(Jj7N-&tU#lPG1cuGZYzErnw>em8wyV`h6^ zY%`ozerl;W2p9*`_B@W`E^`9_atK*FNrAKOJ;C)`*9c|oFzpb5tC>i2b<`L8K(yF8 z6xxLsB}11}wkol=A=j>Wo+tCHxza#t718KX|Jv%*ZgAej#))?dz%`EC0YQjN`pohd zYsS(i-<Q77-A!31-@aMz@Qk7J`}n5t_6_iNyA+Jdp%y0J>%1JN&rba`whnI75b$H@ z#%84Q^AI;H{eXBkA}j-FierGhRY=H-v7h7vX}Ah*kBAr`%-VRE9>EX7)XJmCCtT@W zJqiY#d5l!%@Dq**n_a8Z_D9uQ<6BIKs8h(iw*;f~=dw$~@Gb7^sY4{fZ4pW;3i%k} zi8#|SO!x`jb67J|ZzjzX&ElIC{mw3`Lkg~S2ggjp(@OkU+~^*gzDNjE^AgK1Mh147 zO7*4>e5KRVRi1Y!hR8XzN9^2iDrD%KvmE`_v#0cPlDCk$5KPG?dbjrbQjF^d%GpfD zj4<DZ+gyo$8G8G+t+={|u*yqZ<LeKK?T#n`^gX=1I>MagvWU0;0)Y^@Q-gqLd3{5p zHkmr?FENYGO7OtjWw69YsW!r~`m;D&4q;@E0~M>6WAQz<i^3|e>0*1UrEUMcN1LQ} z`u`J|mz~#=<RzUiZ;ZB&H1gNZ-%5~!mLDUd-^?(O>h?g=Y{5TkDo<Xf7|5u@+ICm$ zw0t;bHgmeR@O6{)ua=ka=-yZSt6WpP5+T*B1MH@|0wJ8BE-A(pjUB7=sgwVlza3K! zq*XSs@Woj_fZtB0*H4w=N81h2HM8*qFcn7gwwE%DlSf8VLo`j>dqNu)5Cm)frxAB! zo9PEHdhW6Cl(&Q~c~Q2Vr~+{*YbL*&0WV#{>Bql3vJeXpr=a)?&Lz=Zow*Z}u9q>r zzLFvjSLnqO{smsa&rr-EjWjsHH9;B2QXD?}W`|%D!;x`BcduuiCu`NLvA3Axv6n1< zLW6&nScVlo2HLu(km*Tt_o7u;b>>6!9M<~+RJBSR$1*~-C~CwmS^#l@Yhb`lql5mr zw3-UOH+xNKcKCzgn`iIO7Q(=ncW_D#You#VmRP1deeGFReJ%BVcz@3wDLuaA_Ya38 zhttNbDD(>EQ-EEM@G-If+<FQn`t;DHUO%(Zq%K#fa5e_XK=M(b)>O%49){kaB7AB` zfLENFDCT~NY8#+s=(?rc!z<+wM?)>=2!zLGQ;V&1P@4!l)7H%2OFqgQ6H-RNguPsj z93u6V%OI1H-rcFEek_4k{6MrNXtsVoXqV1&?OY-Jm7_!n(^YQie=p-gvj4&Ajzqsg z)?to=yW|x;BUWpjpyIS`l$qSCxk(yA4j?`cX9NhEi7sJa#H;-V1+7fk4@^=9hJEVt z!xcDQ!12^K^$DQ;UmEImjO^=6g94~|1a|(QF^aGC6a*}CP!z8`OdVdzf>6yL9XFb2 zk$sI%bq#Ibxg^E<jakq@h5<!u_Y^*8<ASAVCfKdLh!{@7`}kirOuBkXbDR`Zh=!I4 zuk`=;v^t~IwfchRZd)Jt+M%N;5JpO_hDHlNvHXN+KbwphOog@K8=);D%qbXcQLC<O zF1DB<xr?XY`q6p!UvjB#D%rYnuu3QRR|8G>ZewWmM>+m3+7uOrUIJ0F7*Jp{x<pha zySjt~O7+yfw9pR9r==(jBT+-pavd9cPfG?UD4UnC&Z{*Uhi^8c8I&=z>2?QB8U1C< z=Wnp^-J3xcj;}X3So3qVK@6IDO+Re|=HR3BE5Lg_Dv)O7hcc!yOOh8ZNhd!q`Yd9> zhN=9T`^QX(Y+}&GUp>^r@#60@aU4;hQxjYDKjHksfGv@Z^)qs_L_GETCQKVGC?BcC zbWdEzO><YAbuFd}$EefR>-rRntz4d%&vhs8la1M_wb1uLmn`io8PD{GK<eQzU}t;$ z<{RT{i$#fbTQ~m=hMf4ap(NqHZ9CsYz3Z0WD0zxYWI*meZ}VStrbc8K2d<W-O}9_E zYTdXo#|f=8$9N(XD@CJz_7z}N)-VTWOeN(d(sY*r8K;j(N)_^$yPej?D2^|8LDM%| zQ5u<BQeP>Dk#kiPSDjBr&J=W1^<=@!%I-y)^n__Z!pP0>;hx)<PET%2daK~;YtQAI zznqpNIMl<+tx?dF@NsR_G5Na_26Bc@zj?~YrK2G+89@@8W(?i#(Jgp&qSf%^Cs=v~ zK^0LXT!a*cbPU(9J`O&xBzDC@&o3xO&9kjoN$fP2RE{TyUXgqI6%1lpbkahOM{>P* zLFC6w`m=fyv<%)Wj<Qt{?@Wpw?<jS}<NX^5!*bM{(Il*lGneJGq(Mb-?CltzhPZxk zjY>)#8>FUFgO28naUU2?!9f0kjfpl|0{`aT**>aW*E_cQFOqR%2<Np;B!$}R>zY8S zDucMh&j9%kwZrWU;q_zTeKUy5K!fq-OnJ`N*6r^!8Ns1S?nf3SA5~H+aYx5>!XDMv zK*U3e@NbjY9aHKC_CM^Qa{LsV5A<5}X=b`{q_u$$!sGu&2c5d^->eFJ^4?h}JJsmX zT|Pf&q2G^(uz_GabBWW(yydP;-bbL9p4dUyvd7R=gjWe#<`_<eES7`3>v@R=THU*U z(^j^H8k5WI89X&Kd^rjx2-@#N{O@D_&?9ky{S=Bhi}7EYA1ixTon`Azrsg(A8oJi? z1jnO_E({*m?V_Al{&e-pW%shCxqY^H#r&4U=BXJkzlw+S|7_S9Bavzi9~&tw_a=#$ zUY$_O&-I;1TW4+m!<$=6R4M9Gg{{f~wFhqQ!^T|*i=Sc9mIU0mT!soF>x*lnQ9c<^ z%jAQ-EPqJyS@K)uq3nIE>}CQtK?SisSAzof{&1ep;gLD&Zxod|J+oRdXQ|hh(nf1@ z$zwh?xtzYap(BZr`bOOvVA*1)dGkd1#YBrq|7%7*4WZ!IqNg$U0sAJ%STbg|KX=En zJ>#^Qe4iK17V1>C?Az%kt-lJ<5oCKLI$w6)&tYbqSv2b3cB&LH@(#hWhHo>0F(DM& z_vFr;as~}^YmpDVYb&zk6M_o!pOHCmoI;S|atBSC;?KHornb+Mg!vTwZIxfR{(?xb ze9B|WB^Em-GpM5aGon;vIJ?&LXF#>j822E5XDi&T(&?X8!-97BM0%zMZ(h3>N}t78 z8Qd=8hJS2>y8!MlnKMeGEOt97qZ)&{cszBDnC1ym$WKTN&m{}S-Grs7ni40rmywlH zXN|NCejsVwRiqJgG`EerNxGS~6Co^<vsJYYJL&iCV8VOa(1st;hm7&!8vmTP)`M+W zF9$@Ttt`CQ6yGZy%xS}{L`+XEPj8AfBpYr{_S=^|c)SYJhKJP)lnj2qth11YCmQ9C zg&WP;%P5i$6bL+qH%~AIp7xvo!zMszlYqHE+gt_ke`k7JQd`d8rqxI0qEw;d&b^^_ zaUJjIA)Xs5+Zp%YLfP|qoVKq&Lbpah()@pAEqLbtd73$SXK>Vz8sp$$o~g9^6T+{* zjk&ljh?t$&tF6Nm@Cx_#B`u2SZfTy@;?AD$Z_XwLkRQ_AD1prk-IC+z3|F<FP`9#G z<ZN6*?xn+tVP+Q3g+QjUl)*AJ!U~O~K`m0BZd@G}a>>=oJ3_1$-6D~q&lCntAfcX= zC3Ae_+}{0&g3MHH+Ui2gg^*O&ozL4L8*y*TF+?zW$^Cm7Wi3B?puFYbPpH*3SZEb_ zbImwPEa>v?V~j4UoE43$1dsUsj*2xq)&=@bB<p_E#BsQBW{NH@O+|Ne7jcGyM6i40 zt8pseDVtM>b}OI-@s&T0?V^s|){9a->ho$2ezp)sB@QkF^X{!&G%D1Ky3=F(Xbtm+ zAuopLl<xoez;xrXPEX!iymRCxL*u!*_-zIFwA40L!=#E790$hVF{QFaYY5XJB>5Z3 zxuWxmLXz1^xVN@2biYiPhRTTIe-`*tD;L2|+c2L^{rdb)Er;Ab1l5>>xT?>>v3`?5 zZYjBf96z0<*R7gVDlZeW)X}%^I}bJke~bHirr5?~tXDRN5Bq}Ro@owiJjl)Sj6SW5 z;C&gU-;Qx~=ePR5!8oD?a)Hx{<GuDhydn>N%F~lGke?rq67Ra{*gp$^NZRFD+~dKd znJoD-j_OXRi+!($E7fSZ*7a5NjX0yPMa~f2Gp^vLynL&j8!z5VDjPsP>=5evO3j9( z$|0_mux_N9y!1%AghRTA3Bl>h6g-!z)%a$Cuh-4OsCetx2Z*_EIYQCvT!B*QPrl>! zGy#YPnwlx6AH50Ap2Y|DjJuI(x@$T8QM%f4DTBOk)=x})$>W~o#DiIp0Wd+F&cdwJ zbuUZ0@-NH_kTv$h2RgZ)sg)h$T{!XKgFNSq*UhXWR^7-#9VCa^X!NUbfFqii><_rX zH2FumB^Z_IiS8!y27$)fv3KoP(nbD@l5+3YugFndPY5u1bjxguc(>MX1<_*cG`Eqa zS?}8X;-T!+0V;CY68EThrgt1S2*-gRLB0h`le@q2l#=fgF4q`Yj~Gzes`bxrmQpnJ zIP?djB5n|@BRcBI?Gm?%2+3c51B)oD4tf2%W^~Ut*c=8ruVq}Weef@b>IzT40CY4{ zc7kj=(e-b<-^pbFc1iLGYQw)$yd{R0m3xwyTd7lb{S}FgjP5FQ#!xE*|1w<4+5jY1 zJ2XdoCH*F_`UcuU{wZ{2B`RjJkfEc9>M_(eIfU10Tpa(@TJD#lZ&~zo6$5sg+v>Hv zoJR*tx|Z<d8C4Y@nW)O&M|qo;M~y00LI26clz{Rl1N#TTZ5C~qZDGz2jQZJg9Qp(n z9!e}kMRWz9M2%whLZ`DY?zr#x5(=I!IENX`X9TLH4f`Q=w(J|KnY}L?Q{A#H0`x-( z!hW;os}m<qiyZB7uxuaqTf(VAiThU9s>h8Jjl14g`mH<WRqaQDbpdCTZy!rFtfBdv zD{t+A>{=j5pMw6*$uEC_`DlU@NnF=k%{}SWGr@*b-4vq9d-FJ5*0hD_+5rp~tl!9! zXo_tVPMb%=$SS~AqMhzA!1j+uNCn$`d>jl?i^7rDsI(0+e%%IroAED1`XF?O-=olY zD0HipPMGWA^9py9j~T$4IDCF5{gDGw^B03l1h<T@UM;fEI${ov@ZL5=fwyUx_$)$` zu0Vfp_YbR1PIsiuF$@jshC@N$c)jt*g4xNHG$pTD=LQPr@M3~TwkU9u@c<$CQGkSy zhyz9o0jaz?c>5VGhD_=p-Un5;N5tc{^*pe|e9L*yW^;@+RG~ELd!<QT{KVb%KPZV8 zt|7zjZS602d>5Y-u6>!vkB?KQ1S>8m9cE@+Po_n}!*<`0*b_EiXmSl`x)t6`lpjsg zZza;TZQyT+zrh#rMC^~pZTQq2Y|ut~8clD}e|x&61~(!a6;`=Kp?N-+Ne;TvIa+#> zxOV73XJ37yITjUYjd%lGbYFngjF5fp#=!XBJ&54_10E}hM*Hp6E=k^2u!vP5a0ZR! z?Vtd1WqN5MT#w-nJm>GQtjJo%V1u0B5rL0;@S-7K#s}Q)Rmxl!JTNBtXNK=)sy<}^ zO;T!*aO!8Ve3BdJ^d6%7S#&vhD6FOPNXGx`F}2~_r9@k96uYuFLhRzF53?b!{zVL| zMrhDM^{4u`>-s+KZ+8Y!g}ftdw>O6XOc*SM*JUav1v+sK!^*gUejVp{6p%bJQp*wv zn;xoep|j(1tXK&j8^yCQlje%WmLRfj(@fv&jbOrQX>=sqVOQ_A!RcObpw&Y`?plao zvqH}xqC_2=0b|P3f$pc@XSN`y2x&Ew{JOn8*wL|Cfo6L3*nzrN4vDJvW7dw*DPznn zt~7giB@|mVzSfgE&xmGLo|X=ujyk7i3x_q96+Gbe#zGH7Na#txVFvyPg+_uPh1Tol z3X-F9nPn$JhNT)=w@y_dSmC%B+iRcR%6jsv<)&)y(mgFhgEH#*%*@R;b<Tl4T4tKX zd{|)#`Y#QXYjM@&@(Msh``_;8PM@m0E+!-@oA4**yQcoCWUrr468hl-PA@yw_HtN> zO^Q`{xcKP4Vk28ilE=Bnm`W5@H><f?K4#qOD6<pgHSW(gGOpgf{*bB>Nml(qW_428 zj2T>on@c~$pZ8)=-S;j=3%Yx0$VFC|@6OJ?;ia&8_GVZ^>ID61vSMj&<H7o>%=0SN zK-5*;wZ7TErckp`485yf9Y?A8W<&3}c@&5i(ZQ$9tEx}lt*+95kq;n}!z;(oZgb0% zz1DX|y<9$wNE1<_odZtE(9l5thU<X0ql?pP)ZtTGWFBVdXPiyV1}Q88<~lA>rg`tg z^6AuwxM23VK2i5$J0VpQnD>LE8~&;WEE4ZJQSYHQRZRx#$Lh6vNM#tUkNRIFnU7TC z%|DpD^FthRwQpBkEcgeMb4_tBX+nDmw%kb=bZ5h&T~eGa@k@gwHVinQn<I$!bIe0v z<Z&Q3QmooR>lrtYU48j?;(ukI>&x*Tnw$m}W088KfEpYuMn;B192nzQ`Y-=SOTfv< zCL9{zS@*n<eqMc&zVZm++kodn2}-G+Cr1=CN;Iq?@nchEQ;cyis0_VDhHZ36bA@`{ zxFMiyf$eOQYt~A{KF?lWlZZylZ48KQE7_)Rg2;W1pIyQkkl~qSAiqpdgffVeLyS>> z($Zo<qI|=6{^y@@!ug-LllKUZ7eDbo{|TL5wnjO4zn;At$=8K~xJGf@#+>h*AAO6Z zNk+ce9>t(pc(CI(G|8`+W$af*_?3i(7I%4OV}lf7Jngh&lYRrQ&&j3dmvzY0hII82 zV-4F<gib`Y#^_*|@&_L?mKydz+TX#{Uv=@?m%HiKg)Z76jvV!YfWlJS!Nm>v9+AfW z7B%Ipgu)KrY#^@Pf$0jPZKz91fhlPt^&E}k7meI9z)6mc?k=g4K0c|H!{t<0w-)ns ztc)~D>q)tDY!qq?K+S<WGwRe#j%i*tbiF7&oT9;sn|Hk_(y|G2kavIL4J`!x-%*4X z@Vd3O+EX9!VnuYM(C;R@DKKsl_#fK56FWakXxYDLUY|`0#JY*-ilH?WzP8bZrd{@W zlpym`UvYo=-@($BVq?`Ez+XR{#D}`c?ng&$lLk(GNE^4(24}f)66`g_KV%EfC4Wpi z<-f-8<NHsKmxX%Qp<gb6s~sm(SHglANBJ&=lznW_q#WlnckDLvZR=DPZZv6rG^UwD z&mJ71mIH8f5(YOibjYrIm*azXDNw`?@lR`H<^v>(o+;pax2DMSM5C#svCU#*5eUUn zxp~j{Ynb4qzS-^2Are?#6S5wu!vG7LAbhm9x?Bqo0G$?f{Y8A^=bIjKhVPEL{@M?| zU*EHA9=5a>Nk1R6ea~<!`;UZ!jaM3yzib|_qKcxYL)+_7N?Qb-#VT0Avx`Gb6d=6+ zY=r;wRSVQhFe5`{jSY(D#YN4A9GWd0YUR(b#~A??ayj`WeCO$3WsQDd$>N<dvuM40 zJTRX>TM^EI=ImXfug5KXvZl>3ORX@UBe8M4FPj$J*_?}4Ax3!GB6e!c4L$rO!vU3+ z>Vr{@kR;-6?7a;$=%Dq%iNoK_-wY7-1Svq%vWN06BzeAJs3fMi4MP0x^qfzYoHo~9 zZb@kZ_jSY`!`^7flCo+;`EVZ+KjE`)0VfTBaX{1dL%o|?ae>|@KHAhF!Hvr^^9k>i z@@;48eDS$Zm=aM?Gcg1+9Zf-oBfY!6R&GA0b^50mDsWA(AkMzNNKs<aEru>t<JsO4 zji?6yPW{Rx44J?|9W?8`j{(Wi3Z$%7zlAu5#e>NxBc=r0U~WlK$ps-OBZRVBzsX#; z1#TzL5mXh1zlqyL5&G80O?mxfEF;eT%uUn)ZD^#g;;`=<s%%~_%3T@8C#*GPF&w<a zhErNtv<nJ?NvOpg^_QsYnii**jw06%B1ER6MD}^?ITk~5>?Hf|&#m8XGj;|3ssMuW z7bDbX1G}H9jas;!Bptua$Fo=B7S3~bOli#1_Ktiz+Xm!;b(|o907S9bkymK?`j$Jj zQ;;jo{?Pu5<%BPGzOvd*U6gNLN8Py=V6(X`Co_y^_PB>(X43yG#z-y4z!SeuHdQqV zyxA&f-PMg<o^_Q4klzmy>O#CkO661ybtt3BM77lZ>OheeUlK%z<k{&?&&BWWcKbX# z;hWq4DmYx@b3Z}Pv0rVyy#;1AMgWAo{mIc@hVy@KQ$)CuCry^PPq3fKJZ22<*<MT; z13{eGTDCWvEYRMu?!@PUhoCyaOQBUwunJhNBWygwCD;(u!Ete_z)OZ^;dbvsBcDt2 zcI5q?e<vZ638_@m4C4Et8D9~FM=*y5wSuF(`I>m<L&9Ohb<d%xwWaN5Jx0AN2fMPP zEfRr94rYm$BYtY~%IdyUOmB2TsUPa$DUsE^v&R69VSX1fhPRnxeV@Z;U;3<YZf2Ss zOCBWf{P0b4^V0NG0Iidk<fApq3?kv^z$<9_5BTrLyq3lt@zt@L^HDSo=npz}C71~W zUo%C_M)~g2&+tCi7-fsCCH5Oo1j%hhbP99j&6-+CO44zX?)Zj?Na-cHROL)5zWkl< z^_9wuSr`dtXSEXZ7++FI-bUhN#Q<J0=Kh%}cw>W%x80QrrwMcRva~h1vcR{|6vhF$ z8lrdMz)!@EaCjf4FD=>d56j)^tJfty786)8+6Hpn>DI#q+!LDDM|^;)%_EunQ|uA; z|6fn$4m(G9SX7DT;bSP8wUw0t&o|Km4SggWn_=PI9<(PUKuL}py2*P<qg-JU2>8TV zp5*(aCBU{uB*)whETT!Ud}zb}(MD$#rvIMj&Y9F>QLVpNd(P+dS~!`Z(Oho-7G<iz z9`E4w5PrkNGIw_$*!ebv)#aR~cEnQ27(Fy4rn)2tYaSojE|bz~)VPXWf?j_J7&X>q zZXJggkUi$HExr@x)BH?k@jkz+Do%lNou)p(#Qsh=a0-b~*ENDu<`A9qB-@!;wpIhT z21GPCKlJ!yKRBz72rO$fQq$HhM2@lJn(4TR&SrSgu`uv|&3tHubyJ2L<ZvCS<_=nA zBG!|^ln6pFFV0n4Dy-RcZD^#dXD}3Ky-waAIlIrVA9?iIT+tta+=2vsg#VBOi`jc@ z_KVblr0khYv<f@r_U)MU!~LVE$~AP4VeGDnF)p@P`N$+r@K`;=&{wG--FpRhX9^h9 zj!W7Q@MIga>PNB67@^ue4xh|*=clj6IsG?9!<0~#t_ks#DnOF|dqqBkD5CgW1M7-? z;0e%Z^%?VqZ}=fv;ZlXv8=7`TaMY-YD1@kJ>*HimTPMg@h;&Nyy}X=uj1Tv$&~USU zpV%$h=JNz=&z+9~>eyEKxQ8_Rb+3(T0F4UdH!6}#)hiXlQ_(!80cKl$O2dwQ@(e$S zY#4nSbnv*YC0<~D@nQXFFKp^{lQXPc<-3*V?LWe)%+JbH^0cl4PeQfjN&sp_FzRhF zKyRqsPP|<tzxt`6%%_`ge$_Fg^3zhAgR`fo>7je?s6+qzxnCcm8Yj>ceiAncG#IQO z@}I+aeI(B|uE?qJf8M81@5|Pg5XR|5IZI#D7`|_dAoYpS@V45Pwud?m9{lD^v-Y{G z8%s+$`}q_-7!vt<4kKga*SmHEMD_DAPoE}q(oBJM?Z%VM(IV>dw5Bg&fk<pYw2x1? z-YpF<5~O5aO9zHugoe)7t?+{jMykBvD%eGKOM@+@h0&z+mUW3dhhc^XK_Kmi+EkWz z1-O`FC;uAH9fqjn7=k7^xkQ82XZ)jKK9aeE05_PkDVr#aTr++dXdU=4B`GzisNroc zVcz&k>Sh>TpbpNSi$Cf_LYdJnco*`&)5wxw>z<8Ka8367T=W0jWE{quGPjrUDTfdK zR;2ysBf{0wV%=$O3AWrwgOppOk{a>~jNl$0=o158A{u}lAriECBm_!wI3#`;BUTDW zPWlx?OpczCXL`FOTRztaIZ}FZ!V>Z?*Zl!|9pn6Cm-Mv@v2oVTa1Ks*m?n`;{PEwT zqsCw}i}@eNBxJiBGUFe&Q5);a-+HKy1W<Ejc#<kJ=C%Ad&jN%%jQwW~sS<`J9h3#t zK9sbqi;*4%5A|>T$gW};^ywr+?_$w7bg28auB18UXr$7sh<7Qk;YG{hE!<vQv%6H? zEZ+n+zk4`ctParDu0&RT!!?yLU=VoAU(C6d*sCJRo5m;V)$P8qD*}yT(*yZ^Dr<CQ z)hjF!7;2Oz++ZNC+G-kOPdr^+nbZ5%e^pBBYGorxE6`^6J09i^1G{vu?1oniASy(m zHhGFJ%L|G<(MT8y8AMAPqx-1>Y+^F`LGNHu9&|!EFN#wMPlMG-VcrrDe1$#I08)p$ zdd}syPxQ3P5AK{$3ADyx^rR2A;uFJ6(+dswx+Pxo-}-z4G$I+>r>R*5Vf`N?7#Ijs zVpII=X=Gi%`GGs+J<9*vsmA-<s_c5r$p1Ig7nVemt$&6WMRpa_#<L9X?&o4djwOfk zt^Z^1o>1Ce0R^$Zf@Pa5R|P;8d?I+K!t@Ay@Dwpy{o}0DbNiN?lJlm?k>*XYKIwic z%h0vKsF*x9Mg4a+Wz^}ho-?G(Ua&665Gvq7{+;_x;uEzn0i|xH2WCc0p(eL}jvKz3 z4Hf;=IPUimH1hrp$kw3(rvgn5<(V_tLv1Hc{<zpt`2xb9_VnoayyygQVpH!rN?)uu zB7;$#N9&iw*1AryJ9ZK|!-=elnA5w^$`)SL{5b%3;MmG1J#Q13>RHlD_qd?xXUydv z3Ck<RfpI3d7!A#ME9W12QyaIW`(92OvFbCcbMv9o9P^25QxW8+ffG`<$5aDSQ0+!V z>YG|H_4JMhSUxtbM>f3DuNDm+Z$TZfXK0Xdh!&yNXlqNRwwEa_A)iYRjK)cSrJV}$ z)eHo@iC}zX<M-QwKjHOJE}dqQlk9d;-r+PO+XFxGvHPR8w5?%G!xF-Hz?_mKaUE@{ zDoJHP&mpScyK*Mv9{>WX@WK<^z<QBiu14ynGIF8R|9xGB1+$Oq>l}uZqOJHJfnyN3 zoBQD$+Bi;?O7l6{WF{*)X_xN^B=AnRcCuL0vSq(4Vbwzd{)m4ihJf)47JPzjIuYy_ zLe~Or)RrZeAIoDzuZw@4UaY;Ku00hf)NlXreDz-%{+@!D+fZQK$@4J|mFV^|+4qfq zUCR+p&pTYVcT>lMYt04`$RlOELlJhB*QWJf5zV3~Ck6Om0=vmd$ClFJChE1+lh52c zmFlOfwiRED=lY)_XD|p56%YbVs479sB*!=Q(cWI2fn0{xl0gC1Yy^WzNc(%#&E-R3 z8R?nDQ92kTrf*gTPQP89zLUp`sMf73*KDFQZ31QNM{H^7)N}fmn0J{L=7NL_Fh#Jp zdyUV9^na2R^7}pi#X_@MFgDZ-NHaNdm6iiJmaim?RVKkhM@axVs7%@Cw!b#M$4;;2 zXd_xhF+5W%R~0FDj~ftx+(?e)#Ms<lKt7rrv4Jw4*L}7o#zv2MbUz1ID$RA7&E7XZ z+V-TE=G$A>{%=ck^_)95L$D)G8L>5NR(6>B>fYRcEv3+1Zou7}==#J?pr!eB4uj-> zq$PAeSdn8#+{UKi^n=gKa7jlHCL>kT+tT1&%a1!FtZD8HQaf9zb>16>(!6~a2vg38 zS(INS0+Cb_>-^A|+kq8|#Q#F=@tIVzyTEC(|NYbn;z=fjV>iQ8-R(SAU@!FpW?=nf zbWoQ7{$bZ{LXgEtd+o-F*Aqfl*P*;+*9~t|$vls}V<(Q_to_jFxXkdWNAKTVg7c(z zW+$#azczQoKPr`*H~;F3O2BOmdz2^uWiA1;j6m->;h`l1O?7|eyxn!Lo%^Y<a-KYw z^2S6<;iLFC*B<G!?>T*b3Y%u~%hE`=cY*JbbQQejlRyl;yORED>0<HYRdz*ww{#gE z>bH~ugXRIuGG<BztVrJi2P5BC^WF3CJiG7Xr|A8dyDX~2jOyQ@TYUoqQFNcwoM;?I zXW$XoXBrsd9ss<1UZ*oILj*x-ufRbS|3jaDa<xcU=ivwTWH#>lH1N7a+}Y({X5|=` zT`g^TDv9Cy4$=k8oma+&%W)dcw!8fK7BV$DG0#U8aYZ~d*U--?PP%UC{BshSpR#ZI z+l-171&T_=%Rfwps=v7ak`n-QQ0R9M#}Nx5lZ!~J?0?@(Wf4e*^128=pWE7^XZ}{i zlBEw%td1*y)q?y0wNRw@_tf2}R5C~kMon>E@9MaP{Mv`9rp)@{osWhFjAUv<?nqJR za~q>Dv!Ka$xhEgG>3MAe9#Pu#ss@zlXIdTqeSAypC`Bk3EiscE_l9edT8OTuw{-We z?qqLfEBe*c^{KJ3;9OvI<(nq6gl7a2HrzsGe<U9j(V0%;=rFbZBWrE}_I?RUb2WC_ z-noU%t$~f;Ur6c&$Yzx|yv^WyDV9-lmU>P*S_z#8iIc0!<gXI(Cj?KJ!^I;Vj>nFo zJJ1}BbkVlv16evBmdwM%?!~F@PnK|BHs^3YjVvD5`G3hO5i&Y<;T?RKWUla8vi15z ziJunJ;dgdh2W|`!s)b*SIyj?40}XZCxhmoip!-Q>OW_Q!?eKLNGm0U@vhdTWy9vTr zIs)o|dMTNGUKIP8Kdu}k#8(ypv&P{LYAuVZr&(gipj7+7Ko^3MeY-<<K&v^&+_<!E zFL)swKM;tHwLmh%o^<ZhkeLzB<*tm_Mi)DDi>hAMw<VNfF%rzqC(a<0brISg#S6po zHX;u7z(4m>6!P<H27zC)<Lvaq@Up-9ho}7~699lk4eMV0dAAZmf>Oiz|H1GPscO+y zT0+;8<IBy|cpY)xyy9e=XXNaW+R)7bh2E||a-T8#6y69e3Oo1?Tc$idPMSPwd5g1^ zsPrpY?cqSUL}lvK7<$6I_dbwIJeOLTQc6gfz~%Jwxu_W2j|VPh%?*Fbl6Aw7)eqIY zzv;>V;S>~>Z|Vx)VaPEN6gzo=_(?6l>@%^m)qGRv=?2?&7|n07Qj2+^<f^f>pV&tz z3X>V%5N)IoP$nXQKU{s|cKs1ZWZk0qwYiw>Pkd{n@i@=r*~_fewM(rZ4_77Sl|i0< z*596v<DSqe&T+oWFU)_!V^!F(JZLPT=qwo)^@#N0RmO_MTR)qZuuY2z6yJWr8-gF! z)3G@Ae{R49tg8*L5s4JzxqaO03^t>E#G`M?QCeReff)8MdrmNV?C^SRcAvM6(Q|tc zlkAVZ#V8_F<aF~(kK1sgARD2}b4jQ^eIT~LW?E?z{HRO^A`TgsH?N_G!iYLwb?dP4 zTICC+h@RA#0LMmJ6-lh?oKC-Z@hT1bRy<{9`cas!^uXGP)aP4UezXykgRe0J%%N3( zC&XoFj$6+lTR5kyTZ}J1-f|L8Qsf(d$IH9t{V`Wd$kDgCB{6jX>e;~UAvEtEHVE(O zCAEgT^EjjQouUbg-Yli_j*zQ=RIC^Xy$(pWIbeRo_#}`zQ%hKJVM9<7SdjA<BsbHy z(#P!|NJn5vkL5$3;7y-|uEF*}A+L$hqm8qQ!*+Y=n^s3ytqkMFnFp`MvIo=LEcsM~ zC|#7Y;QVAPzq}$<wyp6@oI0`5NamsYLcZ_464kU9{IQuUOvMQX{gKSh7#1;Rch<8X zZPD3Dt*nGXB06WRESn5P%a1BwjXt6tb9V3JA;VSBG%ylBTFc%ep-Z7{Ib=&wj1*UE zj&U$P4oOyOy^lDw!13~Dwzp3Mx(c=rzo#^ftB9MZv8L@0Zk+af(LQtF@;Ocw?u36E zzNC1`SEERb7`YPpSyT7qC(5WyPHTN`Xz`_sI`8+hfR%r;fI3J1V!bGniy}4bSdDGB z8cMi3L#C>7qW%tcjOP<M_&EpP#p6rYSZHs~Y#tAd;?1l16WEPKP9fVQL7)=BdX3zd zq1aH$Ny=*{W<R$!aiDIkkEVF((ut-P&-E_Y>&|(|=6?3BTU2AqDpfw?C4mn|s|)<K zA*tKd0_G4PTw<_jR=dBDlgTc_G4||b$UmF0M>Qrp*j0o<NbSb#X{seU8E^;vUjdMK zW14pxhS^%*P2q=s%`S&j>!%i8<|hq5ogempIV4FWh~jegb`<1sk#%?M(#+RBi)2uB zqTg&<FZ<k8Al4JtK3nsR2DV}Nb*%*J1a*+V31<+x<v(sPa$Rhd@_RUC5B2SOvWvcw zVL@l?l{Iba27O!(+8~PadOF7Tw>3%1#hNHV(+!cJywv2I&7>yfs9?d*=sEh+Lp=+j z(hkNJ--3`(7$Eqz(z5x4lk&`$lXXTD%sS)77y8S}vXwXd)v%4?L{TJGF`*qT`InU` z>5psL{3{WexN`cPxbNQbp@cU+xiZg;MRa#~-=OP{@Lj3OOsTJ%$F=UZ2C=-Fb<!k= z>L^$VX-i4cHL0)^o=&^X?|1lT4bV@G7CK6mrll%aHM2G?@`}uqM&7C<Byp4t8e}9M zB=;UZr^BzK>mfuB6VQvRuwT|v#F(%_dAhM_DYj3iCvIVW*Y=)2Z1mv6zsJ0noad3i zw+tM53xkDKmS5NtWzu<mOk%cdp61A5c#O3~z90A4LCDjJ#Fln`@4hEK6FxEgOMP5B zqDmscHK8f3^5Rc6w^y}4@Vt&t9N+0+bx9h<xKNh~-4w_e#@>i#(lxu*C8pX?N7TGV zpL*^RJc~&rZrS;q1QZY!&~`nEqNHmY-k-2{&;IC!lgIbsUq`}!G_;7_$ZT0+FUL7F z>C~ZoH-l2|DfII#rOed~jx~2A$i4Ep-I;hvYGwg74}k!6Q35kht*^5AJT+$^U9HBr zVUpl)^&d+LQ+Hjqa3nZ!1aAkG9Zz9{P@Ks)Y>;w<!mV`e_Qw6U8^_T_LAF-pzox#2 zq?<3$5fYAFGVSHHuko}QW`7)OB6$_l63#bf>`e?swS`~HGyLNMxb`Qq-%%;bLylo& zag?Q!Kf$B1Vm+zP^Z6^7HyNABdw)Zjgk@FK-mX5854!@bT^?B~Bt#^WxpsjyX5WRs z%pF==NhW&SPe11=&~pJ2*X*IUAKsS{UPYyNY-^DTa1wJATQrNdoMd~;cFFGQGxI*@ zWg|I1P~IMgt&|%lgLoZ2I5`E2x`B0#+re|3_{@>2KsDAJcFV=J6`6jmG+Gmv%uB*j zUE%hJ$8x_{fk6SRK`8ZT%P-$O?Qj3R<A2(r-ts@u&E>Lv`{eXQ{4%>5c~VVlM8tq> zws{i>Vm+e~1^r(C`sYh16CUPGz*qCv=g;|Cm-6h%nmo_Eo8ND}YnKQNNN3ZV0dy|{ z`;_vPoY3C;!Pf*3%dN6i?cmGCpXzPSMk7(vl~Fr8ICnCqH%q6zuh+mF4BCGKF~%sG z=_<`zgj{9z-9EZ38^i=QVVKL(lcXHKNC_tm4h(C$-EgU(A-g~s!QlqvdOQQ*m=q-! z@LaU(6_!`NAuFQb<qo>1c}#SzQ_CnCB;(;25i@Orj#jIkH!5;hm89gi4H=rNfvan2 zylfUF(W0M2vOKwe-I;j{7YAt~w&`JFub&o{Oc&^Nr^Jki=ok30sKszdXBS9504y>} z(rYZB;60R`d^$O)fdcWtzV{_rD2>6B2wN3eeg(O0Yw_h~4t+;2t<xWI=p~M5<WTB7 zdrMad7%N%dm?pnA%2M@&1{ix2y+g57Cc3r9_`sKJlGXX?yX;$*tI)K{(|p@j>;+Ud zC@&hr8?v@)?@jjF!UitFppH$!P>=i8#||!wflkO<2V=a4J5_uGT}%apH$Op*-*l^> zRJpP?{H?cBh1-AxQ98W>W;hPfcE?|u6_6f`JsY-pf#haW<#GygA=p>~fC|vJL{drJ zhW#q7Pc}T8H5pK*2!Q<hwBM%bn}ooWUhkG5<bDODC+Fkk>XXg5eNT?aM40~{UGEqi z*&A&ScWm35*mg4U#FI%Tb|$uM+jcUsZQItww$X9k{@r^&{J*@Xs;j%Y>YS?X2m9G; zue}xnQ+AbjabAynIuGK&9Rl})IcOEYrniu96@mH|6ctDcmF7_#Y)9FXMmNM)uS8De z&tI&IYwwME4#n!J_@~4NcO@RxgaSHcu|d?-x&N$~Xo-}I=s!&8>k-qx9>H_u)rAEj ziiLY1e=DI2o|L4OEjBBF?5Jj^7>C~ZLG@e75yQG0;@<Ft{|yc74b;g^u-x0N*!0Is z`8n%h4N={#$iFp8koXXejmq3%w$QW4(-Zk?m0MtC!>L;Ig~p$*dxZ?*>%G^B8<{nN zct?lkB~-0Ew)yP*tcQzrG%K~;u8=jUCnDyFPxe(?+pWSVaj7>!>Cdu>#Kw6q{(wIo z*=LTSNIo6`Li1gJQv>s30Hq~sq3D)SC8$M$Z^Mka4ChI*q|1U<^cST-bG##5u0ma3 z=tM=#9=r4Pju95gPI-cLYq=RgHRD=9W(C(S1ttG&-I?`9Gm)Cw#n6a_%TD2M{<iz( z{k?Y`^#Y-Qi&nIkc$0$=&HQ`k@26)0!wbPyHa(hN=xUe_$Ens5P*}dEgmpYS7hD6> z%Dd6mUWTA%jZxafyt@dVKP$#NvwJUYR@0nrUQ5(<?qi&8kL9-dU+n~;SiYB^EPUF! zJmh3%=M<&wY(h-8sow>H5K_Q6NutSooiRmf&Ggx8YxG%Pts#ih8=`#*iQsclLYJ#X zw0b@<o8R}Kt=jh-bWyo73Hvop<*VnT<E|%PnNlppV&ua=E6c`Ka$!+~oP@<U*nh5h z2^qea^s=7#V>v<j(K!Cs74lL=Gwa^=WV8beH~b|&X4_D%KO-|kIPyP@>(C~n>Z`uB zMl$n5{}nSqpg)fn@s6N;zwYb4z-)wikbL{|1O<AZbI=i3@uUsmn-(cC1r~`h&95f( z>JSA3TN74xM*#gJ4b$Y;^)Qz=K<i_wwLXJDKfz8d^Pts23F>Gm_-1dLWQ2hKX=igE zUHjt{q!a7qY%X->NMOG$$ZWI#GQ2RX95NN|z;|b}xzW}N+&KsC=CnNNPryXSX@5$4 z8BVWmv3eozxVDH0h$8PeY5tb!TpR_amunLxLldak{5@%)C5;XyOBkEBo1=zn{D=X7 zb1@Shv2fT^r<}#?b|!HBDL0+|%N@R=aOE75Akox}QuU+_31MIAlsMxrKxlt;{Rpg6 zPg#P^wgBmhbz%!_p+3!Yz((=ag%ddULIf6~(0x2pdHnhIP|W~eBmf^Ab&x_SR6IbD zI-I9e%VH+#wMH}7p_FsFqo-!7@zc1rfMFrL6A|4a^H41%;Wwj`j*A1?A!qV#27?Zj zI)}!p&+%iJxuhN|wOT|T^Zw^lEcs4!pT(cuXh~GV(Q?CIreIh4kkfX!CTg$-co6bL zk@ffDvnKLBhD!Mm^=Er=hjl0SrUv$JXm0(8PXBI$hbfD48WT^`&Zl<o#I786hBB)| zANSz<;2|s6=KeBCe$*AhqAHqIizV8`S;~+;(!(`uo=^A>v$)y*O`kfMx$o6R?JcRO zL91Mt5R}RdN=nW%(Zfn>lh^mU52Em!IT#e3vOlnZ0QvcP=U_*Bdzc~J`Mb1Vkcxk- znPpoJn3gBJt%E#zedSFZO81-wNnr0*IO6RrPvPB<-aqGX`v3g-CWH-Yw!+c;0_rqw z-6OYuc7ep2ru4lHPR^mO#o&$6vl2lP#!d`g9$h1L`+V!l>tr)a_rN3V!$qEugq$q= zo{|M^B;z>cd)0@}ztknMb2O)L?jh(InLB5q!x-XYBg^n155+Gt%w*V6e<cA&u<1H9 zpC|79x@V-c=??>c<NyAJ5%LAlB|E4-?emeiqukQ0Nd2DFzCJ2jZifBF2(^DOoI`Js zKT6CmCy=g8<~=1Y?G$rJI1VPj_9PG7#oM%QHCi%<m}Nm&tK~)3xJpanr@F{N{$B^$ zSa2jTL9P!QPJE)G6N7P5jJmjpLVY9jpndp16Bjhf4|)ANxT=#`aB--!oo`)sH3LzQ zHLf6{TTyU6Po4PIexGe4dFE;Vzq?(v5_blshe*!nn*c4yiPceXA+L)rUs2`X(;q1h zC!>k8=4|QzIf@0Typ|z?S)W~ms!b<%>G{ajM#cW1L6%ZP-1T9_O~tjBge((+cGtyh zH$-DgAbpJqxvOH*e%z&U<K#OqwaiC%K#!GZ5AmY{uFo6vPrpp)e>|MRfy0!2?Z1`U zJ>{2@>w%2HN<wiWhsE8>FMnY!BOlQz{kaODVCq|rB^-po5h7JD!4Z1D|0Gwr(uCgb zS7m8NaP#clm73ihlIPTV`Ph9{JTDq$p~;(GCe>}lz<WE|X}cB<^*!{(alSu-H<c&G z2j}Aix#<K#Mk{2so=do8Lg5THjF!?5zVqAqm785`hz(!*_HPsOK~lim-G05%cGr3x zg;W%LOLb(2xfzu^R)2m#o!!couN7*-a0q<L!v1dlpF&uOpMvfGNQM3PVYEvcWE4PV z=A=KiZ4=nc*Y(CvmI(1?+XFKW4k1okPSD9HseQBj^*zhbKI*?4OdMfPtkpiyBM5cI zv6bYY7K*G{yGh4|?7<?cbr{tp|9d>y<>oJjG*=S{?svabmVVGH2JG63dIv-W+eA^b z<R&DNbgN|A9et*mg|Ra*#t_m05_4f+4!&-Dv<AGT2BCJhJCK0DnHlw4R~}H=pDX1` zf&(?6$;fcU>e~-krChxr<6s>S&o}=k<j8m`oM*U*K(M@q@y(j~sPXt9%cDBlenyJ* zc8_Xp6v-Z%_D_0|7Y{Ux>hQv+5L`NDum~KPUre9qqG~Doz4eE2!2>6CkovbFK@{v5 zvZlhX4=)rEJwZm>J-9X!(64VnwXx>#;Y@kS&uC9blA-!(UP;7NK}lHqTQJvHl^}mo zly^8KX~N%C$Bx0g0kxjtc0u%7SEShHim;#)P1>&HsPL?GlsjnROXb1J-s~N%xDf=i zDdWn&Q^tXsQ|fMPK3RQgO=-KGuPNM~6<ff8n=x-OQ#qgH!3lEn#tav>Cm}y%B*6X$ z%ryZwtlo6Ju*80vPkjpA3T6<?X1h%#U8vX<+G4y=qG^a#PXo!cQOj^3&xEI|2~YBY zxgFxz3_#<HkeRnmA+vm7t4CPgoW11s7r)Y@J;Cf=)WMp{fru=)FYN%tjUMtXVQZHH zC6-neUlMgH=v+?T7g}jpl^RCB<o1D|W1%w@gz$DN$;JlsB#8VOG*a}cq@Yw+6pwk+ z8@W`>$Lh2HcUTPC6Yu8f`yl$e@6#0|OLh4c$+PQI`k(r1>BQ#KZrjEp#-Er;L};iu zH#N_YYM9=wH<*~(-v_D3F)%SzzAK?>6ft1I*C!P@>CJOlKN*{_y>)-gv^k0`a7j~- zeaoiWjzv!Qyqa`6+VGqaJpR1mJ?>2^8<h^)y_1~Va5S7oV1T$&ipI{Y7F3qUTRk_e z3E|xd9Vt<OPb-3d&<*Mg9WM>)(Z!6y$83#WjmLd1POH09Fle~b;dsI=SU?sfkdH=0 zw}XxYQS&?Is~J%K*hnLg$1k$5@_l{#HqjA(%xP#TotYr%L>@OFgo6EkNvTJn$6rfT z={Xz5?EQkMq7}uG!qA5${7S4aZUjgx6<(XiMz*r_S0HQ<7Z*?ZaFT=|w7pPGtygYc zhZ?cum+JB%^IUN5Y5c82j<0v|wFXwjvn6#;6>%$<YZ+iU0p@%<*jFVFk<hpOlQdwP zu>%~4of~1zO20M#dq0)_6TznnteWA9_Q>JIprvDXD&TtvqLXjlg?wuk9M8I-#o0}p zCSjVP)nHTvG3TKfm?Ge<JcRf?*~7y0dkFDHfrCU+HB%_e3m9|GXKyH4hR?;WfFdM+ zjb`BTI6f%E!G>b2ZeNOh%lUjq8qxZO9V&$0CtH#e5cIO@3LH{PF_L;ggbd{!A5bsj zYN8;HDVL=UXCt6%cbL_1@q&2teVxeFr$;ax;*X3&v_v~$*Zw(*HQ3oHcsAcl_mCY% z-5B<)EOO=M3?p-CA`T7ed!`{+(<SaK@ze|KUcM#PrPmIy81WFetH|AH))Ne4Y^PKg zS}X7N3#*sc_e4mrx1XMOP}d<RFTn6;8iBmNQgx=~zkC@I&YzhfdPI{vsBQHxj(7GT zA6LDTMA`H!gqW)vy`w(68}QY?p>3!~g6PFN07mC-!Cu)=2lT;y@_dm$mpdx?>JqXu z$?Lxw+ow$=K0kIUl4FJsq#KLqXHFr3L`lVo6k7Z72l?9#EUf}3yuAX!IVFS`nr%cz zaRO(A-gA0m0tIPeT=jo%if|VBhJOb{D`S_*imkAK^g5NG35F4PL$lX+C0sL!vgLJZ ziOfPfcpq1`Y^1~q&ne~%fTG8DJ4J_<JHoCJ8GJ%kALE*Ym6_m<9EkE9A%UAdZn3aJ zCKXr6I8Ukak~bF*En_jZ-T<X4;%26jj0ErUJ3&|}l%7&Tdgp@12Ef((z9+HI<SVuV zxW^*bv^X2JU(m-lO2G>_GIWR|H7B}>kzW=UBI;GjgAU|0m-YRQS-u2wKc7+l`p*5C zT^TPQ?Sr{3^vqy+OQ_k8SfFq_xYrxP7`f7x6CaVbQ{-=K3q69M0*$~$@tk=wX|3n_ zEs0Wvunx10Y5>!4zc#1w!zl4^xA)~4`rPy0+9u!Gy;iI%v=XvA@DC7)UCI!^6X=4o zG%&XuI0&~WRw?Yzy<N*324kY<H#@H_$HxfQ^>0oz2K8qfAM$x8Zceo@QQZ#?OCMXc zOd`otjCB*{eqrAy-uJg+*;`<)UT}ysd)!5HVa3byR(4%!J48uxCzPuq;?`9@lz(9# ztTs&h&m5~hm~+>%vX6V-t{Vc!UFI?s<q(^%#-drM&T+d1C&L60Jy26Pk-Sd9od+rr z86#J++`wxa^?n^nR&<ZH9a@f-8y#|*%&z6RxMu^B&C0B;g58_wtee-WCND<w#8G+K z{={^}ivCJK8x5?xL3qfV0ZJd<=#{><X$4a857c$x9!YuW?T|xiddF1L3NKkqk3lq5 z&rumAil2?X&1}G{6j$zwGNbbj;5)!TnUH>401u>uG)FtN_gzlD`os~MmEQJ-!1cA9 z&6x~eVKWFL=AiKTyI?^GMu&|EhZT|Rjo$VbA+}t7fydmj-GG{W>tz{mF<Csx+A9;; zPM!C^1rxQLCpE*r#*JbpfGP=6<x>5Ofm$0-gdY5WQLH5P5kG}(0<r)EM8_AiG=vQM z<5$@x@{k;-2Rjl|2e$t~wR{bpkh%Qz;|6%s_#IoSQ8ZGK{C0dL(JXYx!S*a!GPE3H z;Nd5<IJ;e-><$#d9i+(Z#zayp_RxO(ce|u~^ZHO!8@9c)>F(v(w3O_2lYh6e<0`dt zF7U)sG4~zvN|m}@Q?!)(YUorsR?w?d7<b^Qz36QIM%>|kPDvY~CVSv}uGt2YFdvFb zZ$l5>Wrjd!r#{xF-DR@dVR3B1izWSTj~8?!Nx7eo1TlDSRtT{Ai{;bp<G9xTj6BFg z;Kb}qk|#;mZebUWRNry8%ly9fnxLe#Y(Fsz!|j98RP2d=D)M=50poineVk@`@Lt_t z9+D}k0v0SkLijI<g`HZ={0iExt@07E^fC8Uf!^Cc#17fm>H!|__tB<{i*-%bzqrpb zs?=9GNybW{Ma0go4%BHP^wmRyi%w@SekB`8F(kkPq`I6(&uK!Ls{6WpTh*i~#$DQ! zCzeb{xK1n?-A60vM9g!`y6<GlQ4K=f3Mw(pok{2S{8Gdw(9qDbe>O@AdT<fHW$df3 zza;jMp+(UNCXGa>)&!bLHMEO!2n~9XeKWK-M%Kp{>Y|Cvx&~R9C?FyNOjrBTCvG+p zBK1+s=d5S8cE0->7NFkz+jKKL!!zS?#-}Sr5~}HMM6tMefB$_$-DuZL*u!7g-jT)f z;^ns2e;=o=URg|BeTgt}GvhcDICsu_CFoxP|H0F7Cd~3|ZO7@-zj7(DN+3E&q7(x6 zxaLJJtW$g4(l>V`kzS01Bmc^svPkF&i_UB17&8+-U3R~ew7bshPAh8U6sl7U)dgmp z+el&->Zr#~7;RnKw7P@+CgT<~Kx9f$!xP@pygIjd;kSJ{P%M3aGkYu?&TY#>vZW{| zb@f+BZTiiCalRiUY=3(L6bj5<T0pb05!2F&9j)+h=~*WbuN?{PDoUJlSlZ>*iBhY^ z;(T6-6%O`ZC@y5!2^v29eFQbl#ds-bcYs;&GC{wXl)o<IiH)eFL^Wm~+3+*E45@B5 z{~Fto8IXXF2~3+!;(p?$-1J$wgiYU1Yq-YbvEJL}4n=ciR9beo0+SoF4phqZLD?D) z02IIx?U8>qeQJJ_xvYIfT*Xr8!C{@dRdMlnr4U7Y{nttPyc2*^cg5IL8ST0g-qRh{ za4e_5h@qqkcXjI5{Y?=R<v^=8c-l~u93Ln4q78b-%anOT3;BC0ze}H<H1udaE3N_u z`lX_^%2DgG8A|T2=n%;*1`MlfC1PrLXg?_t#lt_9!jFloS}b@uN7WcqVap2^1I6>a zJZ?6ERE%=AzdptjG2B4RqJn}1I{|2fEA;KP3zyw+_8HNOq`puAWM4b<WkzA7;U6ch zKuDWA~JVC`%>k*Xd>fDZCAA0e#sy1|`mzHS3V31(09glXkS87IF|T%zvm{PB~K z?+fCfJVmbSgc3J`eKa=MOt>S3*f+x9!X^U$E8<SAE?mW$XP!@o*WYdr2a3Bz<|X-a z+Q_z{NX^9xt$aj=JyX*=%lhC20;g9l`3*xM%_aox1naxkF8wxiy%p3|g9|NQ_*HsB za3LCJt(Ri@V+;CUY6`;;xnGzbPHnI)jr9J9-S>TxM)n-ep79qK!^L<AwQoHu<(5=2 zI=On$)ZI>a8*MLp&yRT@`Zxj_Y|OP&yAmALL;R_gs>&fF-fIjOm08H1YA!M0Kdo-b z+y@^Z!Xs>wcqVI8qdsnym=$dXCquyHoR`SUAIUTwOMy;2NxcF6`6xxd|JZ@*mm-NQ zVcW~WhPUf{k@eRh5Cxx$-iBCrCaPt!5Q#PMHAL#HLUtchH10RbJa1n*F%#1r=rfF; zdK-3<U)=9#bGL`vbjQdCm~5c4BsD%BW(1$N24|!B0FmPN++6k}W!X`8zeDXZ@qOu; zVQAy+;&^s!ja_Rq<<$Dzs}KU;8?nciyo3UxRLn1X_duFsoh~VRzHB$2NF0;>{KSMR z2fD0RZOj%fazc4hZf<_$`k>Vq{Ag&++mpkS6OoEKTxK0FI=bQ#ocH7?6Qn20$e;5^ zqA}aiS*)G(^tcgW<KZ}&7RP-)?jC}=1NSm@ddmJ#{LOuti)7S+VqxFV;mjn8#E-1o zWgG%and=Q~>=BNzxQ$|-`MYW%e!u7*XoYMkFD+aS`p=u&ss1T8mHUP-72bu_R?zR< zIVjP6=EP@wvf1Z$VMFQH-C#jXta+n!!J1mgPfKSljg-YF_-_hoHA64<ONBv9u277e zIJB(-BIhTf=B+L6)S4su1I&P3JNS*~`wg$)4c~{%%vR}uf1WU6=yb&J9{xp!5X5y( zC#0uv2fTI3l(>^i{43lD6;Lg`=3_27W47G|dn=Qh#3vJa2$_F&6Df8*_D?JZ<FZ4i z*OU2;Hjj??$RGxNbKXni8f;`GZT%xDS#$CV$TwpNann8vIBkBv(}j1xBj^~B!REEO zKv#FN<hlztqCds&W9(Py`J>+JGTAc1prVktm{8CuWYMRR+2T8S<w!T^AZ>CMEb~vj z^>Hh=Fn=<J^EwMEaFt|_iD+Z!aQwZ;p&OVLgb_I&D&&;}vgKsd4$A_sQ%ts%;;0n6 z|HI6bYv27E5f2yKFO!oYX9f6^AdISVr&1$qA1a(YlC^!Umin5S>Uz=qeQv(&#>!S= zaPBJ_f$f<eI2>s(+E7Xh!IEUK%bPohb*$qTt9^^7-P($Hx}sv<d46@9PqTgJL*zyC zcw=Pn#_e6t$S~1WE!+tycAKAxn#^@<Ux`Cx+jWUMt}tNUv#@&FWixb{nEh&5sN@nq zGW)YeeozeUTYy1QwX`os?%cfF4Bzup^MHC>hNd~gn7f}?wt|XY>#tK*Ui=MWW_Wn@ zo=H^@V1*s&uD!JX^s%ihv#vD_^e#AOZstt+qd|LL|NIxvVtnp^6H8-7PK|Fx{qJt0 zFCJhz?i0K>p{15En=;YBi9he!Ow4BPcBX>@1s#v)K6uaVEYLLX&l)RJ|6^gEW~9y* zeC5Umb^$(-TuL1rxN9?iU7HPZMtA!!d1=K?Arctx;IuYZ977X!x#3AnuaOS0Q`fiM z*l)MX8L~@B@%Naj`hC-Cc}+*6)O6oR#Q4DUO*4`Dej<Kl`3mXI<RXdvMl71xFV%&{ zoKYxnOL<2~nv^flR|Ri2FIv@MC$hz6{Qj-Le`zi{tq|r(AG}T<cGnFZ+Fq2f3PbuE zexR%sb1r}0!<Msqz)hQ1t!Lq^lPid0J&?-LYr@pqH;;c&o*AMuacpq@LSJ`WkT~o0 zYQ>{iE#amM*O(aMGbs}Y<>z^N_jtoxHTa1#J<?osU+39pb<t{t#ix)SBYrvSi5n|b z{leypoyVUOyT`@f=)$kP%Y&$i^ngdFHNV6#E?z>5;@Ry|p25bJp)ClGgqFm6IZ991 zamnbJ-XREf2#wh=#<DXM%%#UF3Wp$fHuXCrOF=z@%IG@bBW+DJN;>g8>0$0^;R=S; zQjsjBF$=L}A^$-fnc~sbvqL~Vtpj4HP3$_kGF-~8S1WL1k1)@PJ1YBdmA!|;sLvV{ z%tq9q{Ct74g3r?XAFVZqZ~|SY5{<T7v57dyzN^4)HY`92Qo-4MswjQTej0aB8qbQk zlcsu!n&CVpJ?n7?4l)1pdMb};cnbzm2-waA>3V-Y;}k3?Q*TzB0WX?zAAIp7HvsT- zumZ9+aRJjdpi@)NE~MY#9JX0={!{&=wlXf0o)NXn_A9KMaj?wWuG<fB!_iEQz^s34 zoRkg!-Li%baIv--^ncU+eh={#;(0D|;y6{Sg5u3lnI;^R0<yPJmhR4QFy!9?)7MZB zv9?jsW3iKq0Nb%Q%LCM6HXIcex*)&XBf%56lA+7?k|VF~7hQ|&Ilsi&@!YPKqYOAK z-zZwNOY<FAo%xfUi23xL05L6X&m>cuAUk?kJ9kYf93ytkmI^D2;8NZR?PsGpdwI5y zex$PdRdGh{FxC84mSmz;bP{5&d_q1T4(+)5YXs$Y7&pV!g#~2l#JtMTR>@8IKE{D0 zm-4xtzhtgIz0|7-b}%zb-dX^jc|6mnpv)LxPtn*1+qlPVdEvq%VDLP|y7v%*X6L?p z4~6Cw(X)#iuuIcl$J0NDTd<dwGLZKs{(_6`3$ZtNhBb2-w{f@Y5V~Uzvx9Ms#IGIJ zwsG_A;e-W;ERWmB^^$|IvT9&i8QTCjqA*<Tn3{hZct7iN=G3YxC+cQEi|(vyR+)}K z^A`hfC7W0nS{&wI;2%_sv%6#7P`9oUtgZ;Iss$4uNDLXEG&q3f4u$!u)HzW-u^TLX z(NdFJhd)OPWpOq-_EB?`+!f{_SbG#}gEzadA3SvxIBg^LJ*M>XeqbLiY+o*mopEnf zpy2il<;)-wbKqATJSk;@<N>$FY^NP_29SF<>(G?$wD2?(_zo#g$yDhMl6APW9~9JV z9iNccHUBYn$u#c2aOP4`L!2Sth?#l8ZotSw%H-p5dSt%p@{dd9V%F$&(*K%G4hN#Q z(1-!NcV*j?eOZ1jcuZbZZe1@Zj5VaH=yG(8csPrfqDM<u8Ei)v>fSQ>`^|x3Zn?mL zW4dG#9B78ZtYJU(w8P9Pq_Q!qGXMY2V&j?KpQ#9(I5k2SUP8;wA2Yd0J#4GaAkjx2 zhm+HU-wjJilc2o3gXkJidtuOiJzZjVGjooHA{iGpgy#H`oO9akO+=%~C*L-mRt*6Z zjO`U!w-+pCJ9UHcYw{p^f8+mE2>H<V(!>nyH<I56-rGemsLTaN<o=tbgJ{;Jk*wt8 zrh^H5jwgUE4^y_6BchAZ69v6x0HJC#WJJ{qcT84tSy{$*cBd+fR=w4f+o=v#4v$dt z<;P~WVAxQC+oLiGq)dpO-Ak$p`66maQ<DbX9#1xe3Cl<W7+&OX+{IacCcSXL->tl2 z6UiU8{ZYk?wV0+%r5u=`ozJ?;g(Fk;_P7m)T<_^wr!=^a=ixmS%wXVw5K^IB{LP#_ zi8$L+byCdHopaA8ugn7df;Sk}LY_su5SGgM$M|?&DeCipVMoVi*ekdh7xuu3{Q${A ze-Y<^39Y!S2xSJnMXl`_=7p{R48Kq#CO@;F#_8{M`!lD;s%*IJc{<4BR1qy>FhrvT zKYR;7+C1rW;;l4x>x$g4);*4XXf^h<UCgRl|16LP)sbiCRzh5YEkK_%pjJ32OgJoV zBGQhh`~i0vdH+f^bOA;u7K6D`7h!u%d1-Odye`RbZrOSk=O~hm>HaWNEGv@Gg*l;* zg==YMBvUObri(-nu0yyHQU{sc<a!&*Ry0ui98C~vpOx^YOz=<gp?2DU;EhU4Vc~de z?2Hh1vQUK2o)_%BE2fdix<l&_SJ>oMq*<%3#xV}kF<vpW%8_UC;jY<T5=F-1fEhy& z<=J3P(c7-Y#V{|uB=Lp`z|ASj1@K(+^^A~{)(A!2qu~#J{cG|>lXN`C&?$vGe{MS> z5+@7f2>Z-N1far~|KBO>@_Fcd*l)j`!{^{RYR#_n|7^kB(0&cucHq6g$T5F=^k6dz z3Mu<scAhfJ_)VyIczM!MiJ}E{Y^FKgTKwN0Ji-pR5S|Ub-1Y@1wUBs3N|$L(_!Yhr zBvVLpIT!BhUDJp)ozF-0T)f^6u#9po(74Fq$&T;?7$`BM52iALvZRYF#&_#!0hQt6 z`(+JJqzB=GWIiea#3TOA#FM#7Mb-Jb(b3V**TOCNEXOASBD#<l9js2fq~$0!BL&5- z2D6cYY8C#%bH{)rE%8omdZBT%*#d@{q&Yu30c@$%0hGv@Kx(yY|4}J6Zn@&8T`sRb z=N?H4Xm6u5gn>+{H1B_AUn}oXKlPh3_}Cs@yMo&;-+rnn!8W)*_bKBAErs{1Z;^2= zTW#U~Go<S5dNl<7@RZJ%Z&cKRG@y9Qo8u`_sAQiNw!a?+UZs^J0W)>z=5Ux^s(}&c zgX_N?E`!agRM&$)G8QhZH46VjAP4pBJpYc5ShjyFNxb$qijEYbu|2g+q0fOtz-V-E z9K~mfJD)@m6b&p78haRg@v7)Qam3;RaP>;w0{!2@7e5M!=QeGm6lOz@75I<@Q>T8Z z{n<L$?he$*hGA(d{wa^gtI={|<=l%Ky_4)dVbD|Xl@rN3!?D|`wj!pb8t^W3A0K%} zOuXUARet7}ob&O^-vmLkg8%l_eCm0noIkm7?(R}}JIK39LHp)?sMh5SwV84^CmHoP zKy+}x-iiC|c5duE-YBZ<8n<0d^QeHJiKco=D*uzHv$<SEy64!ah0a&1|M2O1&B6d3 zkaZk4vr9^4!_k1t_|ppy9@=h=e|zY``z5UdD5vs<jP&tvQSJ&_&s1J+Gl*XM?0Ra- zp;F7h4(rXcUth%vcrOC`;_0>CwlKxZnDjWN2l6uM@R+q9{2cX+8+XaV{XgB^1Vwb+ zmK7EOrK_QTyw~+7FQ(d(c<Zy>51R1DiSOEzC6mq(SxicT%z)#LhqoDYQP8P!RD-NZ zxXUzuSZ#l%L%7~{>HSt7R;XQysnTmj7_Rp^d29TLMX2+A13K#uC31ha9DVoM`nT*a z%~`2uQc-Qg9}xks&k<!U$TYheuCwinT3ioa6h-voS5lw7FRXJeKL%yVl$!FJ=@(Fy z@cpuZ<u`U2EwxZl4C$Hez^Qm5?Hn=5_>4w`bZTyOG2ygH0~xmBmxq4)ih9Vxr!jy> z>`x((z}HGlWO|N2M>mV-Z5=n{$z;*xYQV~r!hu30=uz_9P)MZ3W0<A$opxdYbWjy9 zALcQxxR{3<5@uW3rr~jt*iwiSRp-O5GOC_NLG@IW9pJgQCPz$SF?ybY2@s|7bIW__ za8Nc<G33LDg5<NlX3Tdrz!=8$yJ=Hit(n_E)_A`Zw!las*xG{Oh%Ca4gyPbPb{u1E z(_UxGXUs-f;$l)AM&wWusl+uB6x;08#go2kpGv3G@K~v)(<FIR4Cw@RdW}Pl(Kw+y zF!kb;Y~uZMRKK=FS>?n^Q{0;GTj$;%YlZ&21ybU<-aoT>YzN|HGybPKo=Su#B1*Hg z(?}-9JT7aniLNb_MoN&J(CT8kgbby9#md-ZoyDRbYLYYMxq`9pL8baJ`Uk<fF-$m4 z`KV7xibKsaxlw<vdXp{OM>fv*b^E`WQb)w3`5vDZE9WyZ1hkPpX3I3L@1Wr9>E*f; ze-VAsq%h7*W|d0a@ol!2U#eDmHGoe%WuufdK+H*7+r23MT881zE)C<}`<wAGerMOj zi*Q?PN*O@~GOxa)B&4}BT-hMr=y@MGqYs(czm0$|ByMXP$dt-@JwCDT%gAJ>6RhZY z&L!zZT{3)nQm)OjuFvooeLajVD5UfARNbyDMpP7C1YRPCwlDU)+#i+BgvEd&dqbcV zLW?dCwNYyFoDqT>gV(JP(obW1T2rc-owj52POaHHW(4WU$x5cC`0qt)|7BwIA3@2j zETP$^@V?RBU#&wiZN4wXtGz~A-?@B%V1pZACnNe6eNN(+#B#6yUPg7?oYpP>{^M^~ z()nG@q|{uu?)md)oN)1D>ujG5$HxhpnLPB#U95LD)zJgl6-wbz#WDHULAu4++>_3| zXOY~j#>mLcOhP3E4cN}Uy3`Ly3Q%Y?9GB1isf>JV$CGrOUhPy<#{%#o9Fx3u_O+2R z5O#EWa#bti#uRDcj@o!ZQ<<LatDgzK18V*RVa!D~Fq3t#{T>kD!-Tj)_d#aK-|~W4 z(In(5aE^EW{)i=~eYy|2pxam%W5W-o$xgC{uA3SR5tF09-`a!B!9XZrtO4gYnF_j3 zNH0`Xa<oR-Z9w_N;vIsj#nxO}jE9Q|9A7XP8ODp8d(n#o#q)7bC+2CdH(}Ux#?dLx z0=KYjK|1`n@xIhy|5WMbFzCx1NUoeGdro7gNw@3mw50|;m7l{dG$&yfU<^;@=@mk$ zC}g1sUuC)xL?jx1^|YPqPU_+>y~J?oC6InY?rfVKRz~sPyr#d2=6nOc#6LmQ92BTW z&zJZ`xPq^P?vB>;%jG!qohIxxiFm+X$;DHq(vGnr$lb+ce6^9ot?r4Lxi=v!%pni; zm1Bmi?<b~^A|Sjqi<uuT@)=2|t;o%DiY;h{=SiOO?#iLd&FjJwNT#bDuqQNN*S*CD zL6qTvx%_vWQf!yM_NsgsA=d{?f{ES7b;(-BquBeEdHee)?!ZfpHhh=ZyQC*>b$UTH zCN0*R2zb@odu!>@Hq9hb#WhcsRg`B(_Ky(J@p>+97^p+3?P$D+vl}3UX@Q9eSX44I z=}FU0FhcYGHc6|R_s|Wmj?G~~$wie+`hvA1sqhSZ18;+%AG7-f{&S_$<QhLoiLFji zF|m!F5=_p=O`>{R3AFIXJj&u?e&V6Jnp!Go1)QBtM)iww{QBS1icQZI^SDG@_kjdn zn>7{PS`6NK4n>mC8ZrKfi8tS;VZnRyis~u^5C0fJ?qS<6VgHF|E2@@gfsmxRhY+&G zxxjJ+)q>CRhQ*{gSC9bt?8^l|3pR<xHHoc32h1Ufdf2I+pq~6#)=`Sf;!?eQaur{P z6%7+zkbP8jW_vSnZwvuRz_gWLKIw-gN8NYzQ#kF<K8ez={xvD2+l@P*+T8o>1WRn; zR!;)M_MZ=Ho`DV1UwLaG%DG+AI(*u$t}PA?2JQ1*!)#ek%*P<0#OVldg%v(DcM9Lf z!5MMpUN-)#aupod3E!B-hT%D_9kp43Q($f)$XB8u>ZyZd^&9JimR3aENZisLtKBgY zHP&*M^Qhl=I&i^E;=57Yt*#p)?@dG~MF}7q%Gj*BvTrN6q4sjWV$Nw<0MZ1Dc3TRh z1%*F(=)vK9NU?1oc1qgSK7Z))27+~WtIQGRUS23IERH0OQuJpUPk%Ir#2?pEn7H<y z!bN%uL6$HhwopUXOvLKP5|G`-`35SWyffwgKp`cC-zY_FjNN-5`MBkDHN3x2MrGjf zm~?k{4MmiZNFL`!#e|Ao{n$YiM@0)RqDTFm^VZMWDu%`ri1rYc7%KLzYf6LIAu9ls z9HfZqTDFjLX(na5E1Dp|sX;s4o7<1=C5r0Dh#|+7_bz7eRff791=ek~HE4DS{BY}Z zGub_@)e|CkF}8w@fjkBfL!E&ZTc+hf?T|?Y^fEeQM6uaVbp%XB7@4DQ@xm1T0eI4B zCg=It#{MPK5wj6&&nqfqt?lr2!9%mDF}E94lvIRSx(NG1D0lxd=FpbN$sp49)KNy! z@X1-9y~*4znB2tEchL_E7}z<PsJ)~iG#w{ud&-fov)aJrbG0MfygeLWM+KrI3%;3# zBD68VR0fBhk<R@9zwC2R-y&%ffsxfh4p?F|t90FuI8C=L75tb~cB}qFplX#BF?O}K zw?Rth?92!&rA(m=$wySWbTJae)=}^EU-3_`0}0uZRyWt14>?6qD!&T}^_Zw@2t509 z_Ooz>vTj5JBD-|Z^6rCXlMiWq<wA6eA8j&y<tuLObdL~}>v9|cd=3v&Zz~T489ZAs zORhd^GdvpieiHZJ2Ciz@j+B@n`);R>5FP`HF`z)x6S<!8!rO<S@n~R%*1=QmU<V2G zrA-zDPlrA7&IQ#>?7TSBe#55SF(Bn&`H|#g@UQI3Uv$ZI&vg-w_dEME8H6Nvg@%w( zSY8ynDJcb247N~5fN4t|=n1oiYDeAJoTj*uA<JW8&|c2i6ahqnX0Q7m#;p$L4|Mi~ zr?a1nTC2V*?BjOxg{)C8w@FBj0NGo8NT_?N0d4ySn$|1`H$r?FlpT{$5_?BP<rzMc z<=BWid}{A2Yvz0|vylU|2i!}cSukaWC>)$B|NRJei!)sF`+?P+fHG=;H%~bvZvn4l z(0HXxFEMt!FWQ`$Q;5OoP2S%r{~^YXc+8rG<1{5zp;tj;Eg~|GbNHK$4HViwQw_kl z5htzGemok6^k;lW1{YkFbT!8M7Ad^UZJgj})9|+kces-&KLr6qlW4cm2myV}nQanu z*?0Lt|3hGBl!GU?C^Uu>TYhRgzoo>kNqA^}e6oC<E`4{@dswR^^=HI2=`lGgZj!<D zRGZ0x`V`C9j^x=T{EJfG4@6L#mA@TfaUMyB>{;DuR$?4`Zr75eoE5<nV7fE^2_O`2 z&dvYx?Vpfbb5kMBL5Kq3ih~6hZ?cFS;kta$lATHlsR=8o+tz4E@pZNyogOClvF{gf z_P5$VMrQlm@KN2)OEqU{mc1F%Vt+5wYPq($?{rGkLz3XURL(oR%q3M;&#i+vg@M~( zTYuARNRTVG;Vv#;CiwG@DcO&wPo!5-SeLb%>8vIm9zjlRr<#Q=H%zP?WXvUEp)6oe z=J9}WR#0a<q|B4l&0=a!e`PWH`+J4(FFjU)3orpb$wHXgjpGQ<C?4}S-qKrSA!n(U zq39mH8x{qN(%~vSnRDWq!de)X>>vN_@wG5^U!eDnIv6__Bb1$sVdCC3d7I4m2SCaS z7dRsq!`H`(e!7#BX`<Y^+tB6s>_W)C;PrL9xR3|1dB5@4*GXk&Jl+s=Czq<AAB6(R z*4w}9IM_(Oebm4nsfbYWaeK@&C%Rm$N64g|8X?zr#E%VnEe}8_5%Hq;JBQrsDy_vh z+1esR8rTk^G&f-56t15j%5a$SbQ>(r+z1PR%n5C`oV(ojXWmf8{FU0it|Gr<lc_^y zz<hdSxQQN>SMW_S!ofx}dvu{+>S=G0(Uy!Bo~(k#4k0ED!Sb&md0=~&$lxXz<?8E- zF;1_VGOV`<czY+LTw58Sx{s#i_JqTPUUxNA!}jufa0dZewi2AjgrDsXTTz>D04ZJ8 zcKY!-&LVV!3Oo_}de&=aa!q|n`vxT5J{Bt1mVR^uGT}Wna7or^Wa+Se{&YVf+9##k z6?3NVmIhEyOqs!}rwp!al2-K4e(bj7P<|u7Yys*eD+ZVHU*ZM=ei724N8q39TMO}e zI)Fla%w|p9&}H3d93~&QB^Uau&R11RK@6YQ(Dj>Z+Bds@^c8irj$l?Xs)MrayM4cm zyR?#Q+m!gHGn(Z!uILG22iDC;2-p>+MkYU{8d@*L*7KQZ(T`)qTt6=_7zSc)$T1a} zR0Gy+v+Cf`jKER6&yXLn1_l5(C!<b)=hyA+AXn-ZqtoI;o;KDlrJuU#A2j$T?KcnZ zt`)Rm0|3yB0fEHGhyY+L2!9?1fF{iU!OfERpv~)M#NCm~pfC0-H-nAu>HVF=uDiqb z$G}!h5y%alcl(VeB1K+JnJo}>FBdl0jyOk*yk0kgQVGg1VhmSO)KK#G+ALYO&hZq0 z-6Vmyf({<`#;XSBA?kmCoD+C&wLau#H7ZpuPAKj1Gbh_H9SHH*a^8ue)(zmrlMqp2 zx`Rox(|*!!iX4~dCsFZuA8(7`u2PSh)o?zeuh#?p?S<Gv&OP1)6;C~(ag@MX{UN)t zUfT|Ozn?5vq>PB}byr_Hu-otPzE5_in+BLxr{D9MQKh@Ep-oY@f<uA2vY~y%L6Tl6 zROI+*l0*p-RN|4!Z!#*=U<~wC=9p$lRQ8=9+{Yae5F67Cw$twnWB)qAT-f>fbI8nl z0o-jeF-|YOoFy}2>-%&!-GycY+&cE^ZGU=?@;v+Q0J9eur?slrL9}AHHIbO2CrW37 zPS}en`3_>QEpT_Aor|WB=m9HwaL3|&c>S?rj?Xk06m=3axe8oH2xu}KPJnA&CY`fQ z_=gL!@oJ64FbLkO&7&c{+3cM=x}eNQN~##5*Yy3(qH;q$1`9^GyqGX^bFWX2`JG<% zx@;)6+bJ+3LkQaMRJ<QFuKIvhU(i|cKkSMCqIWGCG-N5^06Wn}17qf$WEAg;?Ol2H zzRxq{i_aI~r2O0l=dgc^0=)<<9~C&IwCR1IM-W5;ZkBDgQLb~A?YV`o)kjkRwTO)q zy1ka4FW#JoaaQnv1}aWTL&MNG#|rEzS;Cc_YxM;t`o34@?8vUe#Q2kwL4R!OjDI?2 z=cv9NT|Bd=BC-uc(;rng!RV_T#!R2~OU7;=Aqa}4JGQ<VJ*|YXUU%rv`r;MO(GDcI z)yRsd(L_Q);>4$M-$hnKahUjlt<b(j>i%#s3&o^7O{n?*-VLC9i_FTP;?09$XK1l~ z5w=i1(a|0c`8?sHZfQEZ^D@9ya696o2=Q%rL5&i_N&#a;CEqh=caFBV3aataE507A z)5Q2x8fVl}JU6n#)ST)1^k`2I^`$?X3j=x*ItF4Gn`B{-K4okbbZP10?4LJ7kMtO) z5XdrSDahzz(2UTBtbfC5&ew>WK=h16YWe!|cfY({@27y6P$IdCI%0t@4|?7HqN!;A z)Gcz)C(IiLwhRNi#*n2-<mKgQk&cY6rpc|+7t4xKLF5`Vv?4{z#ett`kzG9=qVi*v zb}a}UK6l*4tH)M3xhknfqQD8M*c(pb=O^d9^P#B|uO}TI?n@~)`*%tDY%`aShwGaQ z8-p7lB=s>v?ArjW^Ft}^S~%DaD|x8TvL*&iW(AcT>A!Nqiw+Y`;;vIixAVV6t6Hj& z*s^eZa=~T{Wqp~lDZ67eIw3f|5B80^-stG;lD=0PbqVU40gv3ia_>ic4;;>U9SddE z(y`oxL)}cwo!z)j8Q&hl=)rcJ)qEhY5?3+&!>DpX>S<wlP@qsJ90kLIP@r@Y{l|VR zau&8~#ik=cxf#hMwwpZXxF6A8WM2#>0O{-<7DT@#6c}K#kg6w@OSbU~vQYSGU?Cf~ zF?&`Zy~!5m+wYoNjPLv_VfsGk=;qh><3ATe$#1Gbd)~_Vk(RZOo501UwMzuwcP=J3 zVCE`|IY~z3hMV!cPZscr`!@NlyP`Yb)e-p7BcYXya_=J7FI~mHxQOYQ*_p@z(piMK zVcd#TSK_v+(t(9S*~Dc9_NL!xP_$mQv{cZ7s1Km)lpXu9ujvJ#ea%@;o8IvFDlC$l zn)rC1coyXWvZrx2_cHU{lkfCCBwBsVezU*%#IfJ$KCU}{8oBuwEG)X;b)M8_Wd1Ok zNk>?79Z*!SE}xH)P8DgZe>w!X!L0?2SI}OX!!WDVI`IlRuScse8$`68MsU@(5QbKd zGxSsB@wBW786T~q(WYr(m!2@xO!g}nBn4a}*V^m?yW61%v=7krZPoxq1S46Dgi;C9 zuF&}xqwaH`@y!gAlAg3UN8N=c2f_(^l#II%YJ3n_xVpA{d?9=*Y6Kr^l{+JPLGk^^ z_Eu#zUXC6?PT$2QRNBQ4D0?Eosvk*fsE@@SCL!Kvax-f+0@o93a}cRyC|asnCB=m6 z!y1-DFvMqb42S)JcBBzO)JyBDXY4zlD<n!erWJvn!2Nou?vP~%=4p>={NPWGv^Gml z_`rsne4O%G^~-+BSnuE7oJ6O&iCY7mL=x)r9>p06hUZo|kF+FNzn5U$)JXTVp<2vy zR;9sjcu|%Z%e{t5Z6`;xj467+0>b{gsEDIgA;d(GT{bh;nvb8Zh5dfS;tU&BWF&NP zcx0phb?L0rlIOK|ci}RcVg3vU)j`V{GG*@bTQY`RMgz%az?I#}E^4j@GjpHutO%Mx zt%#HVT#uDcygmifDk6rai>T0}*+6@+1`T#7Kd#8>S!sqS#_yX>TDPl_+(G^Y9MZG9 z0uX<v{olpSt5_Iy!c9<t+2!s2_e_jx1#YKZ*t-HB<JRNpak}^Ov&g!)`-(VBEEe*D za5^tlqQ-E@4eu)%#$_Ps0dkq_cX3<rPHZ`QHT|s^!%o(Q-#YA0ByS@tNq&|R$KaTu zcX4q8<WQ40iVpPWn?%HzXPaqS?$0cpx}h)zN-;g{XXu=ig7$%M3YKtLRht&@ESx<M zVG{Y_M`Rjn!M{47tdrOHqwY|yzi5TF-`Q~~G#8sD2N;fnpP%nb3?wGxyu3Ee)2;+i z68Va0#hh|KHgIk;VSFmO6aP*ScR{}pQ&6>J)BOtyoqlPKK_0AF-=!;8Q%1JOmWO&b zX(bHx$e*#~frv>!{e0dbSxVY3eA%sX14hQ~CpTOv6W_JTQGhsQAm%PrsF^}g;p&sH z?=NLZlQ$INAj@;Gv<WFw-QBr7PJ-FCD;*FI-3bIw)V^f)gx+65pHyKRhh0;Q<+nOx z7owwX-A9hp-b<lS3~%+~HS8Dn<lTcr_El18xYbajvHlw@MN)W23GgA#wQZuTVM$;z z!tG#H?$E@9TH)BQ5Pdb2_cvtGg$;07MU=){NGr-Bvov7>0-+}Zfv1KUgJYa!QkLv{ za8xwuw+SkW13pYZ6^j$uoGI~6scE-anIsx=FiXQBB8Z9gyeeF+v{HP{?)yUZ&I>|x zen$=wm2Gt+OVPpNG*l_2h;_qa<6J4_0O3J&APG)4@2gOdrFnVT>AJEYiTzYQou<1k zAm6N<7>&N6u#z8Vx?sQ)&NPzb<5Ts$<SLKt{i!lU*>1V`_gRJ*A{^1Q=Y3$>VjUus z4Fz$<TF?mbS7^J7;oDwr4}_vFrYx6y|3i)iCsu8f6NQpRAhmr$hC}15G(Q@W6l!FN z$Ec$7@4R|;7-kDD`UxY()hnfdu4rH<lx-kUxS6Q?=rNmobz%)~@s4igThG@pmYQx1 zeYYpSF2Uyq;s5M|9O6fJ;H_NT1-Gs<<+Ng{VZ%4m$G%8F&*<9GtypT*uA#q>Rf^*N z*aTZ`uL@2AAJ`5SGWs+;PmgeG`x?lK7ylYbuDy1@#NVpqb;JhHoZoJrOnu7^N+hfx zpnc6NPi<OnVT4t3QRvc>V!vr`k&ep_1MRv7cAfZc-?5n|hsAKLF8yHx>L81~XuzVW zJdv;dke7LpVDqF?dIp<I<P>*QfmjH0?&BO6LXNC1Ui!4n-fnm<*-eOV!mWVIisC-b z9`xPv3le1G#xbuY^CA0WiIG{iyn~7u?&3d59yyC=ZnnRv#srcZ>j{7I4p2>6n0>dg z@2=mtqV*G03Cc2TVoh8W)2qi|ZNbLUR7wdc<Wb*_d9>{eip0)N_LyQF&FPsAKvlJ= z><frt&y%%4iwQzImG0FdkYdvWw=IEL7Dq`@ArhLbE?fw=UP72-bqvp7<LJu*pIpDk zmm3O}nKNCauB|A+_GkcWPr$~tDq7{QLCQ_{$bXH=NBCb>l_aO}LHB(q$)4~+rrmtU zVQFn{l)Fyi66Z(eypBJUBl?gHNlw)k*yXwcLq^LF9e%bM;T~kyZYSZ-`Zsfd=1X3I zwnKM;uPa%%`)uwx_dKjasG`~<({7=26v$+OrSsVC!dLa`mwVvs&;8!B6;S4KZpHa~ zk($3fzR_cMI=!&?ff+vsQnnQaO^=Xwd9g0<cBIaEPDIX@Npv%*uSJU4ee&hQ$GObc z-F;-bqiWkj748Z7+a7FhLl?r*@r<hS>w>@YiZ9r?iB7DZ*Ks6IPczyVFuAAS=`%eo zV{;*R4)^qb=w1a$rZR6-fsYc+z6;dr{aeQ^B9=n$S>!EJ+xI|m3{!9ZilTqlW%Yse zilRw9=}arMj{isZigRFO#2B=v2@DXl<)#tiPOZ!;s{Ww}l+~(x`R5rIHmG0CbIaFa z_Q)4>Nvz^!R>t<nfvQzd7ZWqe=A-^?TPxY!hf-Nuh(m0RM0}3E#hn~eWMW%4xU3W4 z&W^m9mn9u;MtD^PqyILeU~Hp(-0`;a&jGNtSq01COz)lxE0LVD`thhujP$<@vcX55 zb^7(X7Xl!i@lp8Vx#0TaGFqd?PpLRZlNIe&^jCq%j0EzUG8zilV%ls=hIzYxJ)mR! zXgt_X9!+PKKdy{gsDAU={6AcyV6dGz)<wXLfzzJg2Jwd>sPGM9&covZh_OC&w8Ipq z1}+}8P_fKS8qkW`e5_8>{ofPBqU-)P5fm4*9bg{i{nLVYCNxVkAK2H*9pJE~Ew>c^ zho<Xw>;C@mxV?uwLNWSCP-|z2RlK%BPMz|qUDAM2CtU?AflV`!qky7{X)Do9S->Fs zq=1vvPH3g!-krhb1Q&>wO!TG?3u7?ON^7p8OAjth{~*DN*>t?yn}uzq0mK$+yn`5j z%^wG)t6IL#U|VIjBWK)ntSy%ea(7APmcIm;aKMFx3*uqrj>Oi>wsa}+y(CydI&S-u zpfRr@O{NI{K?QeLe|!M#LNC#*SgyXS69g_*gp!pfhf;f`uxLgIx~+Qvd|f{XP%b^8 z6|FtMuDf2i5T|*H<+3wg<Ir#w0MF^BIsK38UnrN9x|Qx#=*d?L`qAZYSyM6M-{58z zza`9pQ(CjkvGGT7Q8aomB0@uqL;PN4HAcYCMA-=_B2m@j<Z+*j<^V8+x?Oqr)U<Q3 z;6W~DJkX@+<rfp?dkTJDkF7nwer5T?D?z4>V;dT5QMA@Pe#;U}yhH<0T(npK8^Zrj zVz#VD5M3`T3DFyXNIae&xT%Fwa2^cQFQW@Ep5L|Z?tX{qnEf1-ym)yrs_khf3h5-o zjZ~ObU@9P#2{H-`=Q9{9@3YaB-!_Bs1Ac5T@I05`7%<WQ47nv{$}U|$b(E{^YQ$1X zB^jbODYulA+(IM;7JB!%LnD2?De`QuAzbHh{YQY@UM+i1cEs-Bx|W*JLMpIsgOf+; z3~Ki6&S#i#9&;nqEEHK|X1D$erjIwc<+xsG=4(;7dVQ}iS9r`>1i};Yn|wLr%><m6 ziGu{{sL`bdhy0!Ms$lL-Rmv=5T5t;t1QE+D3n3yRME~iZU5jpzUgKPcM*wjbhm6dv ztOoz9h2?WTUN^dn&Zdt1wErd72Ad|IyzIu1!}kOC?`Wwy2bBWi-i{FzPhr6v4dR`s z2R4xywSZEw)X@J&*IPif)vevaxF<kyFHVb7TwAn2vEmM;NO5<!;Iw#gX_4aY1d2Pf zxVyVUkbn1;bH4lE@9r@&SjiySE6cK;`OG=jqa_XjJ&;m$iWXMd1DQC<tVw<eV@&W2 zQ}lE1_8K3ZHi;-eB&gjSI1{@r4a}**Px>j5SQdB!CKfhjVv}#(n%pHD)BYWNwFei4 zOYzQ_wMG_{dxs>x#owixFgv8xP>XYwOT%KP7oM*5BM#@}b_H8t<?A;YVKqdrc)YC? zGODjvNmWaXq#PY}w(b{=4R*7Pl0NOhfJ8SKTgs-hS<81tAz>=%#o+uLbA=cucH?hu zrdg`)o=C&MVvZ;o_C{50F*V>!WzP%)H7=PYoZDYi(4bI3Mn&K1XOTK1A)F3{)(+0I zpRsm6b4!0_auwfa!c9~{teGN?7*vHS%o$Du_^9$4jj|9u4*HU^J<SwM^6WNlwe43r zMIZ{mi)<Y+N$T3bn{iL%4e7!~&s1>glfRKb5_M8OKooh~9*|ncp~HA<%ns^IBO)kA zWO+m380dN9Xp-_<J?6a){DLgIBC~EpT;O{(HMOvZBCK0P?x|)Q@W!^bv6Yp~VNECE zz^XDasM#&Av6KWgopt7mg|t%<*&BH*mthSx=R_m9Z8^1DRf$|=MEDyaniIBpM$&mi z><8~}fjhiA<ZI2E`GG%<r+^dWnRQO?A-)AHRFN#bO-)S`-yhXAv3;-W$(KATmY_bH z!z|rZ%fP|q6ge;lJug+H<;P*uod=@%tQ!Q;UuRM+50k5x6$PiJuqE)j(ia+RBUmu$ zA>WU7u=?@?OU!!1YoZ-YY_WnqOguaRI8xk0;=F*%Lu>kZ2~QR1P`(w9^V@d@Gs)AX z?mqO){bYI@jqgX(^s3?(c+ZI6mwk^$AUO#`{rR?Qtq=E5-9&}WS%3i$yhxH$BF%dx zSmluhfl-R3wB2$eZv*9&Ce{6($^f?NFzaDq@vwKS&a8wN?3zlsE})1GJC(L94=efp z(72gwYFAy$eLu1DD12XREWECFS#9fQV!)?p15BSX`a?y_Gp{~@qD@gIsDyAzD(RrI zg?eZMuR0<17AV_d7Hji-lXmTj+Rn4qF)vr~iiVdY@iqqa_pGUeg9>#`W$-hk<6~IA zpYG8)mp$wCsZlY-hytwJBr8f;JlQJrG^~l!H1toJjy0aI5li#6_4Ow7iF8N?%Vv3T zF<Og-M?_OF#wGguH1JczXbs2TaOiM<@dZWFCD$RM@#dnQ{pT93-ObL7zpigByltd* zOFlV~!Gm^*I@HsCKCfXn2jz7OqorOjbGZoi`6djHx1@R9R(PMQGY;;@NYgVzvCA>! zfq)AT-24OXO&3b0qrndu9K?1!M1YnEAdM=|2b=-q6<A-!IhEmNTFOYMmQJ`{7HGU2 zwBuHisy7xaQHO*ka1pkJFJ#24X!sN^RTY_0344J0@V55=65}@pbf>qG!sYN8iKIv% z`jkm7iulE%YzOwVJd*J}@Xb%=L{pe3nFbLwa_$4R9-!89rNWRfxIbUOG^(D+ff<qC z-g>HSQE3av#<G!j^=3ZQS{Ny)ymZMZBX3#L3W)o($@JD*dVhnrr7j=&>9M_aPbhTj zw15)Az9FH%Nmruh!1XSF6=do%n`am#Bo>%RR?Go?C=u;grn6?{eGa6vS}|3oKk!gg z5LyQrYrc?lq4+)tIrC8Z#60)Xu(u9bM6245%lF=JS#uie00MmiGG7bdF3$8Dz9Tst zvar}>f}L1eS72pTgg3*~H;(fOtMT!vNZwP#;CGiVIzD=syq-vT{f_ZU!huxIBAz3} z1uacdUv47179DiK$@<|gxJr>oNhFz%STf4t(hzt4RoNT8WVIy|@NVLR;)tK#d|V__ zNd=0Tk?cJ8<>U(>>I*gpe6ayhK^%iVS~u*0BbnG({g|e8|Cd)qP0q7=-%SQn500;B za8So7pInE`rNuIxAi@HXYXO(tw+eum^C=oJs}*lqREoTJao=H{N34Uk;4u*1=>5A1 zID6i@5E3|1(tl9Pe?jVKSGWdEE<#ODM+Q@NJ`#PL-V*5obp)rgV|SYJ>%LoO&HLLl zTFL>wE$p1u0@QBf@uZ>ZcCB=`A^(qpI+VBA;QL1b;tlhpZ~21*H2|LdQ8++*xe4ie z!H~LYtilz!-7SpSzNHZrU@h&i<VW3`$sR!}jb5VU9mKCPadW+9jX@*{OZ;?Jw4k)< zmORMy#$=4S(`%57!1H?g8x}|8rA~37SR|m8IRHh}`8QUft+FN0^ma#nY<=UMBwZ)g z1<fmQa__E+s@+L2I?)2DivT1;VO)gv)zXH2nP~pQtw;_->+O|%$mxT={AuJM3@kAg z(Cd4_bdd!`AsQ%e-gfZ5zI}N=%ESDnyv2_($?q6Xi;c7?Q}ei6Nob_KG<h;_v&eAG z_kp0{lze<lfqSiks!ZmPul(nnNy1?(yivS%5StRxcdMTjN#CY`n{xsiMnPc1h@AV* z9FG4SC{Z9H`+t{*K6s^AzWmHgij8`J>^|L$B&CXty+G)BEl$NETvA`O!Qr)1cY`|~ zStV2^;md?d5b&sUWe`iYnn|ozo%c5QC2oaB_POi&!kq$I<ngriIjz`ECz+LI9_)Z* zz$@3472HDi=u?cl37969`QCJkWj`m8R#7OjP_jDN4y{&xC4%jNTu!rB^b^YFLy|nF zvGQXO6(f`45PIAUKX2KbX!OxenTw!d;`y>w*GElUwloJqjZzZfotsEh-)I+gA>|#e z4_in5T1$==OqN;Gk=fIbn+-zuT;_=h+8uEwv;)YL21>ETiH_s@FOfaDU}vJ%a5u$0 zX@^*ndiDo~y?h0gsmsd@nC;K}CRh6GCJT8^g#3PflU^>1cv>M;h>ME*JNN@;I~jv9 z8d1Kmib~b=ps29@ytkI?YhEF%gOlRyd#ZxtCzu<-M(C+h1_K70vNQ709H=H}TRX~T zkw4Q4f;gWMmH(zLBS^eRk!_g{6!yclO#l9H<W{FKu2pCk%;#7IM1&r9^~M%y!M`HZ zG9H%CsX97dPzrl==ntG3RWZY{@bj*<>G}qW-7^NZsIqmAjvjYx*JX9UalByo2*)o! z{}T(2>JRe&?;QF=Y6Y?`!sC677MMgz-3w%*Mfq3?*LSQIJ_j!DZuLyl{HDihvw7+$ z!J8*D0xqU~X*{yRmdX|O5nOh5f*U78(SGg*yU{PjRL%Q`xRnsUJjEQ~vO<q**s~z_ z0=0Xoo&83YHl4gRB3GejWVH;4Sjdf^uCFN6*NDILhCh3rbm=S5TXEy-*xYwa6d5TP zSlCISA@jLh^NDG<M3kHl3r;B*&YmXJ2&ZD>2V+(K_Q_(hmBYUI-GaxjN=iOq^<c7c zgCAgh)6RG?4{~HfJkU%l_ImTMmHobyC4Aycgjd^gSgn|TkcgNE`1`bez~tTze#YqI zGi;smpDqPpQKdI_a5Ib;XcFr91oGIIcSdmTAArYSp_Am-5jguTU|tBKV8UfN4+>5M zdn&7_Mkq@{pj5($Jg=~!ayBdrVGA~@D*0dn5%Rhb-e7Zb$@XSx?YiTA4X-Cphp|;i z|9(hmA-<jOzMiQMyQU0z=CE`^DoY<p4@I^M=-BACBbw=J0)~NDr62ug!@#xKW9s@G z!gBo+()&ggVaMG{NPw?Lr+mZWHQHJe5}_U2n?X8~3hvkJFTWvY^2?Ds@JEVUWZ)}@ zCMAjJn!awBF@K}NLc|aaZ!9^Ucb=Em1u3Ks2+@uFyqy8*K7{NUmQfZk86uz!TT71l z{74n#C|<Q8zEXN)&`4nB^b*aw-hE8@5=Do>EvK5xe{V!@=p>1n7&EVNCc5klj^&NW z*1@xRLG6e})<I?RJ-@eGl?==`RleURv7l^5R>zVpKM*O0`w-H*VGA7Li=ymMI0Ffs zYyCS$y%uxovfH0Ld(3yqK<23@)IUaM1=M$I57zIp7k*G*Fm5Ce6I@5ikci^46bHhg z8rC7WlktCaFaIX46l}?WUw?MW2&T^aGW{o20NQl)oqvFO5ppUi?|ype^&m}?8#A_@ zn$A^I`8NK1Zoo>OQ=qG|KP-Lj{f1(ZzZ4MDEDjh{R}mGb$MVx5mayBXdm0a}hMJBp zSK>R|B^(>S9F&<vW;SN2c^R-|6bZ`IXO&aZ5wNQ`02LE0A4@(XIP}zRtBm=a0W6R@ zFip44XtA4|J8$K+g`D))dTZ@$>;fZ1v)nqwQF|zg{HJR{Pl3k6w_5L)%=4(cF=OW} z?N`^9D8(924!kHb?-DC`+k#6&yS0QAY_cOpt-PGr9hC+<GyUc`Hd}7rf|$^?S{2;K zRlRm<5$y#tZnlNky13ERvPf0Veq4_?Q{UxNjLIyd7s7N$drv4&gPFW1=Kb#edg7o` z`Gx3L_j~C6E%6BLq)|L$#N+fiX9=Y68)F>$_n~7q5voqlKH)?^*q~+>sL;wr%C=2P z?f%)!@egtFV#o*+snnj;hYr$|i0kJT^6ek$D6bx!DPt7L;u=L8;xQ99KQAIW)u75V zW4Gf)(CmO|*q^my8a|<|<x4!3d8d?+B6XrS2;wnukb!lRM`(?zJ{-028NVFTOcL=G z@lF-r!Gs@t#IT4UpW*3^NnG;W*xGzS#0B%Zf6NhEslR2<ZlXo?SSm{S-GCtPHrkzz zsLEOIfs*e!^4hi#-1L@&dzB_Zz&WM07t2Hv6Dok>RYtW)$i5Kz+*yUGlW>}MI{dn< zdn5D5HMD$iJjgwkGC&H1c2VLh=2IcVZvfIiH0#3NitMQ<Oxe4vkc>{5cm)dhMiQS! z3uJouwe|vE@qNQe6ZX=?brh-QsNba<+Dm^?2dP{h6{W-`VR<IBU$KvHRR#vs)b@L3 zk%DFX*O%w1&Qy1MHvS^0Tj_el>z*}4$I?!U*ds+^t;-MJu5rU9SFYS8P3T2e@)~x= zl=mOPZ)xn?=JVM=w!i4lRXD_W3{O?!yiUd#{i3#Nw?EWb`?~Y3Edj*X{wEi<{7kKB z9E$|!>l<ad5qPfG&qwigx&43BgK0Ol1|qKP47KM7uS(`IqGkQG1n34O8l~>?6~4*5 zt25DB9+`vu@}=q!NQ)i!>ASKlf1EmZ@TMAYkvdJbSw8S#n)Yt+;kx_Wp({{O1|1Ga z4~d7_$Wz?TJT<%UyB|BrY3CEUXb5mu-m`2n5UKbbK3G>)Cgg<GN(;!8zG>l=Y0)US zsJY~uUzr`Jqa3zxt2gv;cjcuWn_%5DC^D$!>YIaUn_P~isxGU78(AQ|M+Q)mvf=1~ z>%>l$!Irr5qk-kgWmxqs_55WC?Z@N1`k2q4Lo5$G$x{d+^^3VTKCwRa^LYzcbIX?r zkCeSlJ-@8@50~*=!7B+kyidE+?sa=|dQYpJ5ZA6c$mym6rh{gdP|kAOu_FHn6eW^x zIo#_6ec|TOYlC8ZU*)<r#C@Mkw{k5xs~z@g(#DgRZ+>*=d~O0rNZ*!wr7E|)M6>E& z)qcrtmS4%*6NOdSf7~hb;*~sQ3Bawgo|{?sKhS3N8h5gY9*Se6MfCb%mLRM-aHf7m zmp%0568md?mA6P-KCQpf!(BPY)MR`1@73YDuRFXGwDtEVn3p>8L~^M-Yp<gE*hPEz zlYHEzLRgRB_0ENG97f;&@>bv-8k`N^HC^Q2L2D<+_%K-2ZP8?3SJftFQy$^zuNk{{ zpCeN;;<M|Eugl%nKPpvSjh5yu|5;Nv@RtSR84Hj5Rt;mKhozs_-SD0e*lit8th3A$ zb<2)jT=rTvWtmr7HkrUoy9BI86t=f!zu~^pkN*5)6irTeC(f&OQZ5t6(ec(I5k;bB zwur)vhvI#{ajB_oExCL~OQNV#XXGlp6nlpx(+m~GKak6Wp<HTi;X>G|<OCcM+q<8M z3O+ule@&MAoag-QD1kgf@UwN_V6!shJ<(^kkh?_#)4q+F2?qWk`AHsHP2cBmCfW!B z8qDB?gZAp}mVjd^w$9vmlqG5OQ?i4r4zJ^p455?9CFTr+_Modrbt%_Hh!ynIZKham z#X-xVd(=08d8^`@{rkH<%J$>IvO4>HKC0ll=Q8?oUrb9Cg;e&XWWQgBy%_~)P;!}m zn`p0!=JA>4nTVF98b#w&)GaRh1YLhb_te?(%IT+t6_kBfk)}66m$YV^c(|4^(ic`& z5J}xGZQZ&5EiY%MpD|fdBLRCaEwsTztu0?f8+H~weN!F(UIu5Lf$W3sY7FH&=tDJ! z2DxwP_^SPL(O}B<T|JX83?n#WKSs8XdnhQZP7%i|L@YlCt#cdzh=7QHzq#aOf&@R( zMOQm@whSu7|IY?o6QP|PK`;+^b%)p*&EnH#Y^J0Km;c$o2W|j9@v;}i$R}S+8&kdk zpDksg`#9i-(C^Vb1f6&aqY2TT0J80wS{+$T*Jh&&x0O!S`i@m>TBGad`FtxOX>6)f z*mQ&nxwjsyXU+@>;XTXBLkm|JgwBVrWnIw4RVW^PpYsgW@?48?T&VpN?GN|MdZZ-e z(^%Qoq{2X0erUHlRof-pyv=3ZEIbe(iC=^u)H@m`Zdx*g4(;yNQLp~)tS^Db-In8V z*>OC7L|iW;>0|DP%yPA?7JoceFaI4aDS6-0aAU*iAiTF>r(WEQ{)Hi^*HN2F;(f{Q zKGVDsvKQTXzk>9@9rXKhD3L0%e8dU^AfMi*FdDLmGi!P%q$366BbBGpvoH2YtM+K) z_G;cTm3CHL(DhL%01?QMYaIQ4Rr{|0l}&I?5%_yzQ4`E(`w5;WLu0{b(q_QXZ=;R# z7j^b`qh?egyT@z)H%27)8D8tC#zl&Dw#8w52qsxM+2P;9$>B!o>Np_&qNEW25!)(} zF6!)RqCdqmv-cil8#rULvbJPkcoEN!<qOL7WRZRz1Q=TdwiQRFem0D$PgU?aF_flk z27twgUko`yY8B?6_mU%cS^(=-9mC}9+H;UWktJJ_b8D!%(EG@7{qohz`w%S}A>`q- z$R;;~&iRfB!QNuB@dkHlvm7jW!nu<*s7(<;B}&UpD8V4nx9P!bqf$d3X&?`0VthUE z382Te3JsQu-q7x;@6$@^Gj`N_6Mdir9gJx3)cSE{lho1UXw|?TR?1tPrZ?q`9Qs8~ z8+-8rIB`3Lwkd^-3ivFPD>zUzp9h*Oxwr*SrE33SwL4W)gNvsipM}E0WYx}EP=+d5 zdu8o3*A_`KZYDYnd;OjsVf;^D+1JNY6;c*HO8Kmdm+-rH7<VkctEzWncH{9(q9o_O zAv;4_tuAPTy}n;k#mQ_YGDyRwOx9IUh;8mI8p_xgAeI(h4j8*{wI4puHur@0%qPq< zmy_L7F$~Tmx!w6{>Tgws-G+R4!af@`c7PIGz)hpE6#mwLajns|>)jemdI9b3;l$Nz z)$**PZ5k04Kslphy*3Wgtp;5&G{>1O9|!)P0&Tw$^Y(?Dc}@qw9l#cfWP)z=Db`+f z+Dc*y;bb-vJvOr14{-tK#$}zZly86{ZPtd>Z7n_%T9>y6G)<h3Gaxosx!FBTGA9_7 zPi3qB$zR9s{{jd>SdY&r;-YPg|Kzg6U!%Bew$!JUx1#Doox}t)eNesp9_W@D4m1uP zCgZ~{+`I1r;e((+r|otfz*{c^SXmWihKGihryXyxR-~Pe1HIgay{2yRTG^mE0>zpO zbPle*JBn$*VRbUATj#PEwO%A5jndNJno}k^(tfjc+qjJ-`F}uZI5ykWWnI%sA|KwC z`t~(vmRl{yh(C-I70e(4@eTIMcn4*B-s^<j1Nd~0KMd2DNkn1=QN6n~h>CElqPF*k zWsHp}jW;HHVmKwWyzP6Z63cp!Zvt30zp{r!m><#UYH**I$W+as`_(F;lU|?I0w&>e zQNJ1ZvWF>N|JsBwLvhOewuLVVdZ#X=pLr(Z@^}P|es}+d`Q*arSYo!3Ot@0J^{IlS zfEv04`ROC5rdaewIIg{!hduj~CDDB&r&|4Xd$MNWWSWw=cJ}LsgwBvoW79&zM(7oV zqkuj<l*$*1*DuU2{GE{QUUh;8a_$PKy}PofyHv8>Iw_c6a~H`Nw1GVcxSlz1JFWVC zKED#TIQz*|MBW14suIG94oS#t{qhwR7j5(nSL!3jvP6FLo4o6=fegcgcyL!9{fBMb z+hh(J623QTmD%#C-7kuj^j|Z!WeJCL$Bcff^$4w3ZS^l!6<L2eu39!89uZt{9==Z8 zq&5A4$}z5$@>%4s_yst&`CkTZH1G<U7r?-~f}<ouAHRw8KzJ+pkxT`()uMWTXt%a> z_P>-}bZ#VPKXUe^Xy~S42zxodb6jMo2}jDE&~A5^V5?RE+ZFv19>K9}J(C$+${&;l zU??J}<iGldt7wAaAM)Gp1RI~^851*md(T$Cy)J`<rbbcOVHWQ-TvRA8vA%{yt|uKx zrR(egEf39MRQOW;MWr%6#W1y=UNhxw653Urhcf=zO5><L(o;z%vqw;PXgvQ-4cPS7 z3}5FLb`!v<5*RuomvoTAy_%1xn1!N+s1AHFpFztjct*c=Ehl2eE}Z{d4)L}^8ivg) zc<6fSB`NaU0zL}&biL~_6@Rn%=0R3p{Dq~9G%J=F-VkARVdq5>o@}rLb#NfHWW4C< z8xJ(r*--y&HJ69+gJ`e?9&X9Y#X#8*h-U_`48!{HR#BK`RAKs*Uxh{%lwt}j;O3Zl z12)^(+J0f0Os*sF41=}|9iAF7PJ>EZRS?H9Be(7RMsApZ4j}92H@!yahP70vS$s)L zhL|Tm5L{K^Jf4m}sZLYHz#9pEdAC8G4RaFyUl9`^BK7?*l@jkG+D-sB?>?v9$SPyQ zBU#HYIFP|7G>`$X5D5CZPAf<-C8P#Hxu%iJ8F}a~jGjFf$&HN#b=#9|MMC9qPSPFK zPBbxQ77F<eRRf{ChG;$R_^8GEq4@WSSPY;Gb<PVURSCg@;zmn`+*T<2Xyj`;0%5T3 z`dffZ7Z)kvk+Q~i%y&bEg_J-YXEKh4aDLUvJB;9E!|9n%@RRd5*0mnrJtp|MuoXKE zG6fonzJ0B`ss!cWYUH)&YNZzruPe3AG}g5Ewtt7A2V>27Hi%w_jIHQt)VRa2Xk;c> zexEN}YZ_5EPqoow?Jei1cY~ujEkEpet{}GX;o9=?Ma1%|bWJwi>K)?AkVCsVQ=Lx| zl3#T)VI@g4n&*4i;@%-qZUIx2pXgm-)jP>`)cK!(aYiI3tQL^{ll_ifmK^#?z5yDG z6tbW$#Uax<a6W**p6fsnwd+<vq1J~N7&XmSE||^Iynt&FkIv1SR>XomIWd{Lmzd_y z%73EQAmga16v9UMhK^jSOaLXar~E=LhF_Cbdqrc5g)*wS096!%UD;o1uc1zRU;v`S zR)^<7$6I^+G(g_wyG+^;jk&^7uD&(mGQ^c_9zz)ORlr{HhjL(*!6_-p>tiKu;(+Se za2*@kk@PA89h)b_;l2x}{FWe62~2C$^e~w_=qs)b9YoFXiHZBxc=^?m6uP!$Z~7bZ z>k)NBsou58$U}lDW<CV`U?-I|j`bJIWP3U^r0g?<fL$SQ<r=KB!My{T1W*2Cq2E5O zq+)h~V;8TGvyOU7<OZj17h^_5E3O_yXoAH$0_Pl#q{A@T;UH%@vbxHopWy!+SAv67 zQ#Gcv(fMUyrvF*X!WgoCU#T$mgnNR3|9W_ym^_%z#OhdlqDabq3En*qvc;{K%s|Z; zHWb9*aj@E%8-V6H=bd6Y7n10rV7a%VSh^TZ(z95>H@N)e@r0k7+tFad-G&ShG*z`& zSe4Gis*Z;L#jpztv$!{gO99G??n3!Pk{sz6fr?dgtM=ch^eEmI&If#OHc>SF0kPr6 zk<zX}^<vk_Wx>|;Eh20@r`^H2nM^RT`hgE+kGoUYBqR!S&x%5ZgL}G=m3*UiG-2O{ zJB=Y(K=oQFQuQm9CWg$<3OCn|$s@_!KJ@E!9P!M=fu?a9FFFW?;?U@9j1&)rrKP9h z51Ve5bK5R`kq0oUf=r|oU<FSrF~DwT+%{wE92VTEHRgfBy;geIoB{N}KNqPIG29>D zNIZk-7ph;#<dD@E3B0lsMLy&e`R~~GQOElnF)997C}6op>N2N~b9Z}uA6#4_H}`g8 zXqx&fnS>_CWbw5YdF?A)dALe@ECLPo^i>HH?>liuTHaRNBSk*zD+c#@pSK&HD{1qf zdQ;xDFLIZOc@ijoUOEdYxi;-T_Y`?tERqx~rDO|nGxOO{0#H|vF0#F{5EoHf?Ooi* znnqFGszIW#O0RQ<dzZHf^DBq4!~OS9zUNOy6AnpA|9p|$(=d1=pe%SgJOmLbK&xXT z!kR<ca{~XIU&<>ZPxXHVS3nycAIX>ZoRL8CivOXaOPv2^b!b>fw^j`?mK~#6jK}ZP z=$R*C`r0+JH$47ks7mf(ApNfTG{VvjhP$~mZps0!-@5(g@arx<C4C{!pG!F8tdv8; zi!MW%&@XS_^^Bp`032$bCWZ#Es{{WI?F_hKY-vSYv5?G(Rp@m0EBi|P@26-bB|mT^ zD*#Mm6%Zg@X)t^a`+@oQiXbV;ZOi%BTv!k6O#s%p9AQJf-vJif;r_G_#!2*U5L7l@ zqyhqHKtFv^X55Go(wr9gaj915+si($r>m+BE9Dm9P)bdtrLoF9$LPswTxCdO886Ua z{N6W9T7{&>PY9B<o0bDRXR|F8E|L}8c9?|f*HrhQo4^VRYlxK{-u<x6$&nD6t_fL_ zO_4xUEZsJ^jPD4N*B_G2T$rEF{<1C+D_C%Tp(1khb8My=pWdOZd$SLkhxpqt8;{(R zS?`@CbD8iuvV!E+nCb#+L6)(ycs`+oMtjd)X?kd2S%+6buZYu^q3ibXMAE7Be<o5` z4<m{{Fsm%|W*m<kHz4542z{Ljinh#)z_6;`qUwHd>2mn_55qS;Me_QzLH3kg&Ym^C z+~*>e@(jD5q1=IBbF{LR6$y=bhS4`Mqaj+tF?-&O)@`e(d7H|=9Cyq}I6(Zr$U>Wv zhnSTjK`Qk`R4-xJW}0)8JdkQCYnhrnOre2-^&ED+7#Z7UjHjDN^9qwK@0|roXTG{T zD7ZtOv3B)a;IO?5D(-=EVG*J%^6erk*!i$FuV4}}uO4%f-(Mg<BgJkm!t=LiLJne2 zHU)edCPlQU^U-0zc{at7D-nXoBkm<XWi)m1rmF+bJgy^h#g83*Z;Lat2}009XT5ZZ zuEk<Z_HRfbd6_p(K#|DXzSFM%tTYS1By&$q3~J>V;@{H6h4rrUk4**LzWOo2Gf`Yw zi920uCzG>Om`|QNy`M8~G4Xw!oidVnS*F<JUM&8@II%O05|7T76$9WxLEObj=c>aK zBK<C5_mAg9R62|^{v*!YOvX!;O6dC@x;6Cgl)1n6ClWeXVY`C(U-1g-^XoeObj)*J zX$0lI_znb*3x||9J`O|Nd`u$Bu(-}w&FRK1ZX<I1nv*|@zE1l*2Tx1WMqq%Ls#7N? z@=84~k5kg^X`v->qlboYP6|99wh&wnD?OS&`g4T6o<Is>Kmf&YSgxORJ9RCGy`M@Q z613>L1?1e=mFb@^U#sXx$!wl<aGn~_ZfUu$e@hJ-`xILyJpkvTl!mCN4>l0?;KAA^ z7eTEo8jdu*hDOEtV<!Fb#@As5xS_&t5SI?qV_YjASn`8sP;1`47JlZ>MuNM&NO#YP zDd+4up);qfIhKbNn*nxf4`a|o59Fk^9_MOUld9MOXv^Kcv?J@|4Pa@N@4q9K8H?p* zZ(}2}Qh#7aHmQkkMS5EpPe|cJ6|_O6$Rz2E@n@Nq8JDuhBdRo76gLbFijaHsw3Wzj zya~^ca4&CG(TO?uG`CJyVe6IF7GE)St1zTo=q=@*BARrwL0hY|Ze^0YQJr>lpZ_az zw!9=6Kdt@S*&<S~L6N*XHLA98=8=MQmhg}08PL-Ic=19<C367qXpxc>>aj5L5z_6y z`fM=4UcIXWqM+H2ucCZ8hzoectMDal%RL|g(#)^_us2jRA|xd#0;V^rG`Jb|&~m*^ zD5!<{&ww0*KH&r|LaFOtQn|918mjM`nwxR)U@HHLlmW};pKu(1QbYe|x>N!LM<iT@ z%4jnm_$eM^5`SeA@zv^t+Vg^3V!-!BHeN)ul~+tWfIkAH$ilo7es-Vdg-K<-8$er+ zbL@L<4fquE3q7lVmdOBeioKuYJeT$auzF+Db*n;rf;JI-`kAdXNbV;YRIOM#2rBsE z#z-3O{QNq*tj|F!MBvL<I{`<|to}D79-mXa;AduGX0d02)x1&O8Dvrn$HuLBvG`|@ zm8!;y0wWBAMrROUvD&t{ckGMjq;uVoVkNHq=_%mARtfBhAb{5OE-dmm|2Lh{xQwz9 zICp~7UnT47Qps?PH?a$6@CRvbk3khnSL&p(7f<<E327H9DP5lNqM=3;{t?1d??M~2 z6hSXC@XIk|*4|oP<g$!)Yz<1W6hTBZ0!Ln>)5y{WqGLv-qFu~207<|C-dj-;<(Tf; z5BkKHTZk#DOURK9?yYf8m3|<jy4(u06*9yjsdDP=43CB#TR_s(^(TY*zet*Zm#_X+ zuo1(V!QC_%{cizM4vK$#CeujMAyr?7UO?4Gj)x0hy}EIMA1~Hi!!P((IQ7{h3!wtY z(S<BKerOxuG*sz&vZuSXZnP<)qM|(J71`W%7f_Ugh$UtW<=X7QZikG)od|~6JC&p? zTVeG6mS-CYAo7EA=Z2L_%L<yO#cPNaWi&?h=%&QPQTtu$<jh^iY)Z}Z;p_(!5I-zB z8E1%D(sXlX<J{s4Na{O{XuE(sidXBK)@QF{dYr>vbZk!OwKev<ofAnxr4?d+@>rf_ zdNEF2uyJDa&afM%ujTT9FeJ2oB9Qlr0?xPLR-nb&iwyPtaNulW!7|;Tjy$VSNM4(P z6dlN_0W_M2|2ENIo^pS3YgGTCSDcK^K}%!-?a_EcPCuW~{>1FgC$W~~y|q-Zx8;^R zf9t(Xy2cflE$b0MRJq=#eZ@|XJ9Yh$gO^2@#|%(QF&YMaG7rbFQ9+6pF+hq*N#jj0 zjB5S<+Zg+MQZm7DU}uq3UmnB7>fu$6R~ChUF&SH-hevy(=c|tC^*6cmp?(v7#9r)* zVmHi<?3v+=qoPD+<zy6xy{YHdT?}5&wsmt0`>>7PR}!hpEu^qe#z(Y}pg+o0ft)SJ zrIGaDyM97!1NndX7L(k%JX~{+247`^485;=8wyUTEKYc7Q^Q_h-|FS}yPWathPAi| zmATs&G|}DKuiVa`9x}7{5*Rj9Vz<rb<>UP&z{17-(nZ-$LUZi-Xg1G_zZcWyB~w0F zMcMvAD1tqKz)d!a@vICyTdY%T+-CNkn^560SM%<z3IkpD)5lmBL@&CJ3F(aLN#!kr zyw#yjZXQ*y!kmtLG4g*2C!|5#G(D|~@nD1dV<(B~mIE{UGl~9w$+#gJh;mNvB{by; zP~D8Q*U1Wt1~ikdU6ViLnIp?#(uVD`qQUaQf}izG-c~Z%q3+4WP*!Ol6<6sEcUe5A zgrK6wV*91}@_LT2$b4En5&eCy!#Hv~eIEApU15nJrx!x+2S*EY0n)ym^lDFXgDymB zi;vpzVbs)n<eVNc+$1VGz7{i1pCkqm_-hmU%6G-s^%srxJkD9vl!S!Q(s8nEszH<~ z8KOR^ch*K+?aV=~L0`XWLYCQJZA}x{(*`A8clGWk*g_TCak?j0n#oN4OPAB@P~MWR z(K0b2o&X>BwWO@}$646v_gk6RE{XL&un6E9XA9es7^C3y2{>ph$Y^ivO!yK7B&~D? z&<p^Ssy5-vzGUMFfZ@N$sT*fFW5S+&XG^9H`7-^BNl1z|H>RDkU0dE@J$aLCA8l5) zF_XSdfp-G;=b=c-d2zAP58->ys6|^Mm#-IIa0O1$QlyIiSK{`MuHbA!ZEq8h6{Oc= z=KBWv&1x7Y67YnFXqnN+p?@hT${~N@^zlT7&>}&A3yy`|vWhc9`;7c6{wACqLO;vb z@~UTC-<*=vV<{oHs@I2rP4Y8>yo>tqQ6lk*Rx&SnMw4lt;>n5iPMw!8Yor_+bOMxV z*nBm9z*AKa8s7yBk0laz;zVZi^ZCp9pn4mOE29T1=h$?v<z{HTucSgw%JV{zufqDw zSn_2G+4Dg3rc?IbbjriNludeeV%5)}WGcdEGM@9344$|xm18NP%W$~FRAquNg?1e! z19><QK)r70jaJr+bhRQe5OD(4W!0g9;`4vz)qc>iqFo4J8fr8<JuX<{6M$mQTdRHv zqc`dkxLXTMfAU-{3h84Vd$HVnO$|{!yS*O}<r>SYsgZn0c<eXM$~yK`)hA7%T?>`9 z>beiywH&||7qL&(Sg$r`I4PLwZ}60Qi8g(5_rN5)?xm%h7sOV8^~$ksEpv<HrZ2sd zOrQJF1;}V7*s1JspUn(@goPe1U-Z{iTsM}ErGVM366#b?>ifePD3cF~r!cE0VcXUN za%XoDT{a0B?`@W;>&7jH`fO+i`7?v6H1%qZ8-1#XfKi}eOZ9@RPDQVVAX2ObjWkl_ z)}R$TYq@-3XzG8a5Dfv30Gtu{cYtq#vpx7Pg5d}Ni~f0v<pN%5vpn`C!_>XXt}5d$ zRbg+^vEe{{aQC>vaDD(tdY3nmq45@en)ipla+rF9<Mi<IZDH8?jo{q2kM1IrGJ@t2 z>BrOC;Oru!XPN2VFQYI6eh7Tsh0hMB=eX7+<LI11+DK6`@VL=?`QX6c5{4oX#!x%f zo6Et_)kuqrhnjy=szb3xO7U#1o}t?=Y5P+&(^HfgA_it`rnv`Q3=isw@=cI2(!9-@ zn`C7{2_>vRDd2jIWN$7n<4Fwrbu1Fb4|(JFgV$_s;3>CqaTNv1C;`YZ^GF#^+x@vz zVTOY{WuK$<Hz&A)->~{=PA_<QUEk=<^2|G5>=oL>#^d+G@1JD6pDu=Ml?Hr)c}T$z zxC;Oe*L8A0MGV+h@BwYLxKX$IKcDiW{`nCBFfug6_AWq+4iIYa!?-fwSB6af*zwW9 zu_&~jD-0dZpgOf7(|5~UuSA5I<P^*!>oZ}=C{h~;UE_BpCl^^&N4CBIPiu%c%d%R= z8S7P@mNTVtl5AN#wR(GbbgJC^<dppB0QlYQtgqcDg*j=^zzZ;zj;7?XSUG#{bQ}6) z5_sm_xM87uVHhw_QBCMX=%wN80^Ip%9V8{^{PN3sgr+|A8Ir!)j2be$@AtTa)3Ec# z6sT0*TX=a5ze&#g=ul}GT$781=T~YhCd|MLPMT=fP(d)X!Oe{?9n;vPDI!{4W?Nb& z2(J7cn{LY<aj2Nk=%u_`-O1!zKKeN*Y#hyr4l85!IVmnFout%ax`{@C>sT1qtFrs% z0gCiBTq2MweIN6gGV{6ghQ2s+TUHA(!^^d%j|iTp9(@EJR$MeP-boz^2D3)>&A6wK zu44=%`Ah4A$DQ|d1^Lsk6GSn0{199!M`rmiCbE;VhIVP;SMID4dthjuGdG|PZ#^?L zZCiFGuJIIho73O>1&4wTC)3q5DxwUT_jSen;Q?~|2^h=@{r?0A|M0_L|1~{)1?R2D z0rw8TnZbaNE`aGbU}r20lzQJHw!c4>5UO<DPebc_U0|U0><dnaJo)%H6l7y|(n!ln zk4^F5(GvCLFN=b%y{lGZChF%JEc<f-Ezg*vJO%C}qr@WCbvLx1Ka=5)!`b#$8OD3! z>+S4|MjF>v4qc4gY8NR*>-Z%c+^HjJt&);<&$M@8lv<2D<&G0VV>ncabAe8BgFpNI z)pMhPkXQ9cHZ;(s($s+LzRq8p4*2TPM;`3kMTksRhth4Ms7~C0!8bX!kpykR%nOMj zXlzW#d!e)EzX=0BH6z{*gWuVFlG3vC=Xs_axRIoV)bx>j?%ND%&YQOrr#}a1d8n#s z%h7xzk&}$juBx7Wxs}DgY*-;!9(=y1<Dy?0_UV!d+@$hMZn%>i|2TT@;_ZM@<&1$& zqg|wM680&+^r_u|(N#?Ut1;gr>v0TzZ_#0AmS$6my#<QCqL)%-^q2ZiZO7ciC5?UM z!uW3~vEhyAZ(x`!LQohfVY)kQs45BvTsj3Y+|hscYX9+Tc=#_(3gF7J0iYJpIj8_* z8p!bg^N<oK(c-}yy&#FmI|*+sw5abOj&9G2k34E}yiv?RQWziKKEeygqmTJbx#TZb zu1~Ud!;#yU@J7msB^Svr?d4v@dhRCLRKeEUMA(nQmz3&6_Ac}*Vuex_@<EbXh^;Cu z3@;2`7&3^yV!JxmLgSd0QEAmG;j6GdlGivfrQPUj@-OtH9>)!?oWzco5cZ}f7GJCV zAQaY$hpys*y_vR}W(XeYQ)>N$v~~Xh(QKadi>`Y91(K^ftJZWQO7~<V$5wvePsV$k zF^bsrx#NTOUf)}h5?Q+@a#G0u!@=VJgy|)$o~jt60HA3-v!^5owq8Ac60KVKFmJxn zuBjCI^z#mn_l7xf0{EV~RKcw!SdEq0h4=KvijqdrD9SijIZPW;RxY5-hx=~#?KCP+ zu^XY3pBSu`8VJAH-+dTBcEB%>qA@on&Yv|-*$!5*e~<J^j8bONku?Y^@%-43L$L~u zF`34D>7%=-C}zy)ki_*iVMH&@jMyuxwphi{w<nr&m&;a_%_WqNU^4MlCb_e~!qHEm zqerTSoK1@Cx@Dq5bvZ3pJ{0?>>&Go&*!NK#hW{dekHEwPzL+z@yyGzH$<$Y?F;z9J zdLAm+Ucve#rf8Uq9gN-jb`KRNMde$YqXpoakHK8aR6d>K2#^+kyD$~4>OV4KkRaJI zeML}e204P))p<Z~rSnc)Jtj4yGL<GWzF!=O5Z;pB&gJZOG#TC_DRCck#fsg%VG+~z zxnK6$^ZchOG#~x~gm;k4VeM+xk8`M}J#HhNN2BH9qkDKJq9&yJLT#7u<V=MD)wR0X zqOJE7HzF#MLAa(K+>89zm_aB3n}WBgH7s{JS*ux*4p)ayJ@a;yr~Ds$|3I66uH5jb zzhF+9TnEcRH?d$~C)0%Onk_8m?>YF!y;;VOX~$CjD_8yT<Yh8bhe41=`5$9qhUxz4 zzx@<&aa909=bz>R%t~4>Tb)+9w{nV$FpC)uC4`?zaO0~_$FK=>RbyR=0t@DVgecWr z#ps{^{&R8TW5N9u6+}Cc`MPt8kL4AACo4#3i?ra+DWm?of;73Oq5sqn-Dt6%J%E8U zh}s@*d;0+|H52Rt`X{Ksjb_Ax!=@weoWY8c`RlXqxRIrWPiJF)lApXGbIh&KDhEDt zFX*p?@*jX2n8Gr0GZjaEc0JR6XV8XKSpBMI`P>A-UsYqQ+c-cFc741Zlcu!*yHBdo z&;<PZRtG>eL0cny{(LF*oc?}q%Wi!tja{lAGu9+XUSIal+noa=%SwC7eeXXJ=T8;^ z?grMFe@8!mYDv`-dR>>aI)t6{ov`+j-zgE>&hGqs(m&W-4@F(b-^Ct06n|_Ji30st z^7tkuH4Qat9IPMmf7K3{b)bLqKI`Ftbe?f?8}a|f^45nH7tW(gwN3|>j4D$D6|mRM zU?dHvoqHK$;wC0}cTs=vQi~5o<>i`LxD<x-*RDpE3Y^Y@6g4^^Zqn!`vyTjluzL3Y zR7mGvtI!(`Ug6vLZDr3z0<F@e+lAW$_S5XkmwIkLcoI3_W^veF2uyI~cM6d6ZjCY- zwZ(w9_IHfh%qY*DojD9;P|sbQqg>hs{9@sa1_*)G^x6E+b&}DJ+30!3W(U|uKHIE= zUYmr)2A?XjdL`GvuB$Hq=lX=$(qk{V!O#!cR#?WmK?{+x#RJh2>On9?_=BC;-}>V0 z!%TaMSYFVrpkPWl@F&${==XSsHwDGe^f>3#?ZDyN*<fJD^g-ACevNm)c0u&t`-$1+ z9rPT=|GA$oL*1yds0sg};Rw8U@Z5P>dxXB*`aU0Ua`oeh1HY*AI44=lKV87c^`C~p zM)8mR#@ac%+=2_eTjD#-qw*s&6id`U_`4@9(oJ;#BzAztUI4ak&lyCp_Ei5K-nr=i z8CHKr<Ub$%Zzl);KfCsy|1h!pTZR>?SP-O993Y*iG986A5AY!VTnP$)d|k2so$CMG z&;gKv2(sm_rraH%RTAf-UkXsroH73prATpC(DL!5+p<dan2GH;vu(%{Fu*#4p$hlF zJ}g~XRkV<OYRkWTbg*1GS48Uw7#L|oSzwg%6g;L%MtM9OH)1M1A8CheNRCWDes{YE z$Jv!&;^0Pm(TZwfX)Wha%ZIg)YfPqXpN;dK37b*4I)hT6yOf$i38uS=@!W+SXVdI+ z0ek20r#fRhq)dL@r+8TT)lZSv)tMYuzXs;t)PeTVw5iGVQ32jhXm2yY^}RLXUYjdu zgpFwM3e>Qf1hU^wQ{7jIacVo$E@4$C@^T5AevoS!`9K(=Bf!?8(;_{aAwF|IR_^e` z2KerWrFSQ<0VG=dQAWbie2BwuC-s}DODmNZc!5HvD>kv~B0I)7<^ezcqJ6Nc8q90{ zqqh3$5k^Y1I$$;0=}mv0QvMcLL#D6SP3zs+bTGab7>4YRsGW061uz~rkY$)JMimo% zivyF;9dDRNT#EpzEE>f!M4My>tBPf@70xB1^E$(z4gKw6>@`028rmOt&8PNfJf(+R ziXg|OPu_dJ_hxxSD5ZsA1mzv|Z43iTw06;CAG63C4k(^b?hiW%g<Y>7X0kV?pKoWp zx#|y3FOQ(z*Q#Q(hy9*myMh)LVzS)cOxt${F5OL0mtc-b0*tFqxq?-T#0yvaws-T2 z?b%c{Wqd1*A9CyMxJu5uy&QU(6;BWP1kJEmN0%aXo2J*wMu!s7A*+qv7t#E&+7JqT z#LaT1Jl^}qH^TETJ83zy@38c|E(fS<7a5$>_*GE`mopUrD-IWnV18`SjM-{!_BfCI zqu`P!v6x?pN{NxkYJ*T!EBm=?_WqPlMGS{@OcX&B|5oDcT}Q<nlnc(gM27#FJc`8l zw^1rNTPq8qL*vxGgYt#=`;lR6y5pPO^7v8LqHxq{dgysC#h$p~__qfmFRpB1JEh0< zEknkvoy8E>r&qTbhS0Chkp*AssowgYW9v>#d&ILp-I@2Jx-w(pNyeU~L7$po@q7;# zn;#*mO)xt#*+(-Ij#?Z$MbZ0CuQ+zE4JW&*#zw|O%DK4v;p@j#Z{|b~+4==hzxi@u z^EQU~VWmgAvjTQ6Rm{_)4Lq?{=Gzhl;pJQBGa<fvK@Uh@{IZQg=}}sF&}PQ{P}<{U zhECYAlBj*q4BN=Wnk2``G1U#RlIL9fFql5kW4^rO>}Ysn)bGaG(N9mC_G;0O0XnO@ zl%H50<bBn^*JnpG3VrhRrH_X_-9r1bpm8smY3jB<RyTcU_i_AO<+eb!QGe{WVcwr5 zTCk<bmEwKqCMGLdp(?6WF3OQ}uFl^SL!i*+VrNH*5y0BN>~s8OgZH*&R3Op?KDF@U zyjq=#sjhmRHq2@eyxnodZd(307oF>S)`}WeUbOAM;X6LED^i~CQI@iEm=<NJpM3ww z{&?=I-@I-JReZcaZUkM?QMIj__op2Z-Y)4TZ#Q~dgI>jA*MhYE44LinxU44&#qXH& zk7w}`(++nv;SiV7z+b%fj{_!KWfHe!?waJ-Pw!qraLSRRzrvqSpPQY<kO7UZ#G19i zbALUm`Y96v5UTT_DG@{SzU8_FjW${`ec6v6kZGelOq+Z;0IySse?B#{z~##Y&rSj@ z-BO0Pl#I%sA%)ARu}~#VH<(q!c1&+&2V17wL6;YSmA&FIg>?0lQLQ=i(m6s}eVxfL z3Fc(8YKc~JKKR(?lUfo(C{0Q0*8I)*B=dU_KlO17EYC<!NI1U^sK~s%%%x#7=5mHD z>v$%7D}KtT(R2IlV`^gIatnc1m(v!rvd5NsgKIQTZ8od;r|=muytUi(bNl__>6P{B zy9LMJjE1NqLS#$xI-#$`3YVh4m-c=w_ICG5W3LrH0s#xJKu^7x2cNA+-5Xi6r7M%q zEfPy|de=ec%FA9oyd*|w1)=J4%!a72gHzkj%WdZJm`P#xzfik92Fb(^rIy=W<g{12 z?!=ZNQ4Ql;c~NE=QD1oVw!-^0gFs%aT2&3#w(75v*h@RFL|8vfqcS+RVaQBZUTEF- z+eJ7fHd)|qb4t9s@-ou*53>;8D_>-SB>blB{l>O;v4=-p>eMZ1xBOA7TgQ7}9#G4% zB_-ffr+zj+`g(ISyxbE_G%BfR37iFDVx#4;P+-$8c(6lC=XY*Oal5ADf-LD#AoGr5 z;7U2o-F^BclYY=fxf|*dZ)ukR0Vh(cC{nHb=_Zw7ur|5@CaFB))^P{o%qtse!s1Ol z2KFw;CwS}R&he^Dj{LjGA}sgCRF~k&X~PnuV9s*uLMmYIPBK1g9JD<wb6N})iT1L6 zdeH_OQ$*T~_*L8Rm~8lSwo12U5e3rk%@h{SrKTQ&gB5YXhUs`mu3Pw0LXqPe+dI8; z_D3Yg%r-nXk&D9jg*EBExKmr<1m|qs`p^da^X}h@U&;jpd_vX7HFCH#H;Vb+yfTCj z>V7H1uOjIt63r-*`->Jbvoi|271UaqmCU?uY^V8a|E6|zgKqTs&%)1kwK-_lqn|vA z{|Ok(pg6ZyVWFw@M+jxtF!rqd2(o<QG%)c$0TZ8R#qK(%ni&P=cht=a^&`c8I-R~b zda`|8aF$Aw?z1@D@Hj+s>b>IQxKibIPj*wEZ2SwKofP{j(_B24^A}dE?u6&>AJ|w| zvv|qWnj48aCT`YRkotV~ZsErECOBN&7iMdCS+CanvqYHoBCyx5pw25p@{@2MQ@10+ z7u)(dT1jqOL1S8ok5@HB%u7&#BQ)XU*rL@<hpf5)cd`A7Jd4lw3a7@fSFvl4UPoOI zi&hS&vPJ79c4_YXfA_AP7ph=xZVPO-wc)A1_w<}<F%(uGzAjT^><-w|V#PsgYYS~` z-{fJvl%o);Z0vn8x4R~=kKViJKV7OV(}zJD>0v7K;1BE;!v?SogPLt0P|9EnjP{Gz zZPAzyv1<V~dvc_8Bbw7DUXSleM14S0Y(;^v&FS}U7UN?s*6oLWpjA9Wm2FK1!4BZO zWKKUogvIXkp|eN&J1PfA6X#79UPd?3YLAs@KympkZyS#}^O47Rep$h4s^7R&g|H8q zZQHiWg3J8kOj$9?g^wrAcm;C3NK_V71aI2c(JU(z85dW<9i5C<__u%Ht-Qn5E^|f0 zNF<S=t&#qzyXuy_H+ev;{@2v?*f8n7s9$$qQ=mo+ayE9Wm#sdYAI47!(5YuohMKPA zZ_YL~S9*>!qIZ@ld$*SSX(<dttn4>`NZ;>4uGcbQN8KPlr`#&FnsE|>0k#&~jKm&^ zp-6~eG{URB0{j<lZ~ImY$rUK}GH^|qR3NMhl{tBxSdmiSG?8$3y!M0K?Xul=*3paM zM@2P*<=dPI38&SJr{?4xx^q!ls-(cks}k~&<e6&tk~D^h0iteKbb@2MM7J1Q`>;SE zhtWl$$_@D}e(nufzopUXj<d3N8y;8zq5(G}W#A1h6xhn?$G-=6rl-)@FWTQJ^$(;h z$7-QJzG}5OzhHY@lTm(}(w(8k``tgyE$X#EdeK5VFe(f2H0j$eIw!=bt<RR~9}Kwe z4;WlJwHsTu{Tj(^_ekZvA8)=sQ0|AQIW(SEC%)w*co|^geI*G*f^NcrY9<x+S0jH0 zvTQT&O8;8*=_Te?Ze2d5xq&aP26szHz3Bt}uv2%J?Q&Eo4zHNK*pjY0eVGAApFr~V z87I$zurJi+`O>K10TZx9sa_POSK|z7W`vX*YD7*iy#p~X)GG*=SP>-Tn{3z_5LkMh z|9&6Rq&I7&CcIsn&B>+p7{RqS#J;Mz4C59(tyEg?vC_=tRtc4@+NRk@pUr{pPXX$i zoSdGPsyyFzI1W=uz)+%#l|3JWo-Tc+jWJC7_n`fZ`;NL(<+ptv;rTC~+RoEf?jnBF zNgMEyL>lY0m%6H`(e6)?<vb?+xt^OYc$$<L7*YY_-`OIhKbpItc?z&eE_=Xj4NENw zm9N^=R=Bk^;YLRii7OKIc@uQWUOdk8W*^A|D_^V0DfTy|HgLL|saDlkDA7~t6@C=Y zN;hALR@>{0a${f%agFU5b)9h?;xQll<_zpb+3U9qFsWPJMm_aUEL0OiYR+6LMk$X` zM2?3f*RPhAIH|3dCc^IS1W#Lz$=ibt;)4=e*O#6{eMXo4BIXT29dYI%-A0|&<w%ux zbOIj@JTC*#Gs+Qn0Xu`E-FAU#DhE8geFRBKI)bg7Z-cIdWyJWSKAszWt2Dm^HQ!xa zDddNHZc0FVs@4BL_TDopil%ED6;Y6z3?h;xqa?{0M506i$zjMj=R7C~Lk2-|5+vuG zGYlC-CFh(O@{sv@(EItG_x*L&I{(f(wUCjX?yBmly{q=V_ElBt%a_>LuTPv<(*}CR z%Z}<4XZ&0yN4cbL`?;Y*BLyb0i(7|CmVPV$&MG~<3*yK%12t(h8=WT;hL(JQU6Bpg z6^*#H;5&z1wuI<(GfC@TR_=D=$|mLp<M<VAZ~l%q;T|SpH*fHRp)r6%LL2jr2H0$6 z#evj(r~R6!CnWQSc-e)bTjp_!m2xN1W9@^<Ik=Jk0ER40z$Y&C<!N-w$d+VLy{_vq zlsX9s<`736_fxZSwW(C)@b6b{!Th9#y_(*@0r6XdY|SK80=r#8wUhQsr}<0dpTb9$ zc;L<GjiRhFksps~>-LmpPonbP2NNi*Q0aK=c_frJK`t6@zL7Xgm@4&^lM{b(?lW0{ z_H2Ala}x&Brxi7zsIy8lQ?N_VzwGpw3pZQ4O~YBc)>L2K$tBNn5J|&0`;S8xb>wiB z1z)}$5`B)b{5(>tZ273LOW1pr%*TJ#N^8a>g?P8=cmlNYk<3q2u;+WF{oWWO_G!s3 zSFbQ1aG=U9=J||2@Q*BVxfnto$nYM#q*~|sIYsf}2_Oadd>}8f0wcci{WH+=yGDWw z*oG+I@I^>3t4EJ31;P=j`a?ny%l#!<IQAxMG|x&ha1Ye)>oXHh$tisLz_)VLBdlTh z1uzTExAc7niWz8KK$A5{uU}l(4bbkcS08Og;t|mfBoX1@sh&k0S<ABP@)$%|8Vq0) zk`;wf=s_@d;H0opVef*~W#0lWs4zp6j2dirH)Pd+I-vjK(^xvuG8cMU5WD{3^`7@> zWZg==ZQF~8^d^(@{`ldyJ|b)QJ7`8(c5-VP6UAQT5wBgbLjUztfXzBE9;ldp?hfvv zVKnv+4VfV7+KIr!hfRdQEA1|qe)g*$CY8C%r@y;c=<z0NX+L5ay78GM1{BOa4Bp+e zH__%nh>!VF2^5v}0K+uOTi&WMOa+)^1A0q=A0v|izpL-k6nVR3aFmNZ3-V6?Js3U@ zNXjtE+ep_BN(m6$u;TS}?N&GJh~<F^xg~mo4Va6q+OESt;{*`(SmH&VYdyxa{eCfE zC5pIAux0U8(Z4BTW`mc+cm{C08h*~gr5X`K(jN>>{>fi74tEqFpPsS8&|dt7;Qo9N z)8A7&|LfZS^DF$Kk7tO>nwh_|C{5$Bi5qPnXF*&HPPd-e&-}p=M-QPjTdxlrkZ^Is zksC;CuiLT5HvDDi)~M0qc_2q$1i8LKAkdIp3&t95$tZQz2l)I4zf1Q!7hFjkj<#R+ zDuH4=ggI@e!+oN4em^B1tHS?K_IT&^K;Rk%(rarV)ay7VnTg`CqRe5)t-`4h@#OJ6 z3Nkwk(|`}|$iE&+iM+gCnx+35Y{L<8lnUT9_bWnU?LMj%eg;4l^2ClaR^V)Ai2Iy= zfcu;u1XA%E5P)4IIRNT2_r6e?y=${F{}5MT-|n&iWi8PR#WV4!;#}!Q?P|SptZeXJ zo<9DK?Cuze6v}#s=j*(unLuFMwm?4jBvNC;;}mK3Gzs%UmbKf(aPC^1n*3@gYf`YN z(UYUx2CCTd2RR&_Ty*h|UUESOh~0{uz&23%!W2<W$9&P!ROFE>Z?v)3NAi>bP<B)U z*i%zs_PR)3_f_ZT3Gl=7&Qy0ivqk96hT$>uvWFo{oHp@+t1L#~bfx`Tf3y2}x#Z~k za>EO=)<8zWgpV2KO|yruxt)6+6qA1~6#i(Z%a0$|vi0`1=olCPPA9K94(sP+@ZPiB z6<0G%o?`g=E=W4}$8Q%WfS10WALX|-RJHi?obnHLPhU$~PZB;n+CDsF^?7D>m7=BG zN~qC9hwY_jCH~&3yC@W~poSUWYE8EmzN|NT5E4QwHYTIvGUSn9Hx=yF0#rjW9oXm* zFbNMZt~VRR!t?VMf?f>vj*NxUQs`YT%p7REZ80UPS@0ma9WQ?hx;>OgC?zQM*%9@= zDaF*du1|J2{q*|^X$A*ubT=b;(F`JJt<||{2^IO+D9k~)Iq#Qjr-_~7#HZGD^#cfI zPnF}gHe3<y9)?uGk9Ak2T4!u&UwMQQ0y63JU*PEU;OPU?t9XE@E#73?wu91S`$nA8 z7c=)}z%UK6`RIN9*()73Md9RNM%$Ku%wk{l1?@OcZJa1BE_;}YYo~Q0H<~Vn45(+S zaca%`YH>DoEnv2SaY(K?9M<41;S@fObhD*GkgJ7YgAtX;kia(tEVuJ_i-Ze;$l1Ci z2de@eyen3j2Gk;6^AdHZR#xwWB?T`wvGljH&qs$A+Iq~+%Wld~CyTt1mJ$|3$L5~& z{A4`LVl5FZZp8(AF}X<XEm=>lXhE!6;hHI$Yn^F5>Fp?^kau$~JKaqKtsk{p-CnMH za4(kx)w~MlmoP%bplEoiuGh3%CSZYCuQm!fHte*s>vXfb1_`be^n4~PeOkIDDI4T2 zs)s8xR+;&&M#jU=Ov-MhKGGR%1UkbKm&m#p*nmh@s;@2$<X@Fvse3!O&=Vgn+KE%M zfSF4gFJ|iY{XM7_D7uVnj<D8lYsfZm{|qxp(tx&2VBV|TB_bHIDXz~>-SZ-SGdjjB z@LTK8MrW3BmegPl^>N0~O0|;q>ukod)f-6=#~I|zpOPnVZ=|?0#><f52l9-Bp{jhf z#=@su-{S}5lTV6ncH<q^{yBuLCUv(mJokk875DU%-f*&%?6<`z3I|PmjSjTl+-=@D z0a{VD0oY6gFFae*On;B5yb0#VFAgk5(D0g9{v9{I^68i9!%ViyjSTZ#t{RASubB0S z=k-cGO4llUddyE~yRqzmNv9s*_DyJy1+-u=<QMl8y;LoOpsWI0EK%i^ywh%IEiXOF zuCpJL8@xME?oB7bz?%QT|7u~6+580g!jqm`upWzK*K@TqSg0`t*;e3h>%94|)&Yol z5c5x?@neR!q@#HWl|bzGNLj~2hU;iT=c9IM^V5yN0e`a|lv!|(&G}fN@F|%Ount~b zq=^LH!a|I5DqHUA5Zz!HmKgHB^g1w6!*^Vw?&#=io{IV@YhcC%YUD!L#YXVLb$y}z zDKUp2tf(h&-|=_9!S^PGN&I|uDQ*5m95AEo+7G3vEPdr|o{{ugym;?RWt|r_>eDk3 z_wYA_0rOsCfC$O55u!IA8I<=na@b5(4xvO&FFsG=A1pxqgeuVW%X9FRgJGx2PMJX~ z_RO6xz3*kbWNeDZ%a&VBz9!rsqm*_dT+}^Ow^bBv>sX__=CC)F=e0YEd=zG^BmOrT zD!vClrg)aidBlw@BF&}ywC#6tsa!zTpyex}N;Kv(Nx@SYe~psvqL<6rr`3L~n%>yu zf>}=@CdQX2O1Y?`JbyiS>%KGM_#{`r;f{pae6|v4hx>DvX4w<k$7)QPOu`&>Xo^Bp zPo!q(X0N`V!<n4Q-x3L1Rpog!Zg52tYM1Z#O<ZCiyNI{Mk7o)Kn)8{9^@Vx<284cB zWv9HJp1Zf^ewSV#{uXdA-V>M*NZv#la@&&qBbHiJS1D|3*B%JPkrQx2vSIme;}f9C z>NIl!C7ut&gwI71YX9z{EH~U+BQz-gh6KM8=d|3{h>ZIrQRl8oyPn=ateQ#bbSKZV zvg6i-qIe;r<MGj2Yiwl@Tdn;^GcJ7gn3&kCs6EK?bn<x8=KSb1vQtTLP{2cW$G~Gn zZ}wnj;>Q>5$VgBF-o<u-Sjg*v3f$Ve)kyd9&JayPP(ZcEo7*J7Ycv#c6+Dq0ecec? zSVLd{AW!e}TMEI;wc!1Kii!`G_gWW1XTXi*Gu5Yc2aNHC_3ymb^V6a66S3c%Iw>4F z_acqF>M*QCn2%(NNQ6%7R%6hkDQw$A!C+FD=Jr=fjegt<mX;mbbZvjnE0kH^6`Q)H zZ^cjKG((-%6HupI|AaX>qYoO8N=blUxVWbXUi<e;?>BwMc){X$hwh}P)1I%hN*<Ay zs_Q0e0_UE8ethv`=c^_LFvoKSh&|aJEF;zY-R}%JG2+q(=r8h~48PDZgE4n(>+uzr zF=pLwrjbZMsUr))$?1<-nr?|xbW7;SUjnOym|*tg%NK8(Sd(xTnp<mw7hu7Vk=SF} z{C87yuQrg_PdU<68W3`C4tROB$SPJL-+panj9JFk7}JqX-KL6&MO;V&G0^t=+T-9V zjXzhoa?=p<^Z(XP|BV7|YVr&<P_SAU7Z>xh`ID2G+vu$=&PRb`kb&35zMJ8^@0v;% zBvWX7!M@LUmTCO6(<M`4_y0;275_6mo=u8U62KlnGX(Nv3<=v#8l|M3$foVy#slUv zdZkvy`V~$Qw<t*s7+G~aGN%?IJk?=@2p44@1|#GVg2iJh<R_JEZI6#TdT!iXjWrL6 z@r31>Q2*6t^SAf*NM`gvzlyQu*0-&{@vc9*4uLQTHA#j1`^WvKc}njlY1>BH9orhL zHO*K2Vy00rYJP$&SO~FR14!ntQ;`3J${=)~fK$FXh#w3;y<g06|K<O*P=oybtoi!a zBqgTjeZ=|4m`$~x{7*g)v5@5dhzkD0e?x4Xxb{!X|9$TMGZQUL?9E?W3oYnB^`TnC z`SDlIDF5&RfWPpO{&&$yZU3)|X4-pDh9^J#gKX?iS>n`m(|L*87c+$a3FfH=0xbQv z*o!pK%nJeHaJN5|Ck87Q_ZSUZasnsM-qQZLCe!nNrq$xH-$Lt^=Ji6x1ZCQL^{|J{ zdZogD#vCesEe2B6=^Pn!cZi!P#X$5P=)&%A9Gwii)8;OR;DG|2N;>0O!YQQd_h+T3 zWFw{Ye{UGtLw{+b@~%eJB$-Pk!uqZs9D3h#kWKgJwpTnulo>Jk*uTy7*+_6A=y1Z` zY@wy(3lw9b02I5UuYV(x;!<DSgx&a>4j?FH*2(?cmIa5&?N1JH5f$kJ#|VA=D-1<p zRX2se8Lbx0>@S7608^JZv~vX$0_OcB%@kZG{r7c<ZTO2(B>hoLvv4mw`1)UKXhp~M zQD)HAfd6E(=}v5M<tB}!|Bl;_dNjfOK@e_uSClkeETpnR%jYkHKuHn;Ew*0h4^qqe z+@2RSWsHYu;A2TnvqqbU{Xo|G4p15(jVAQzU-1B)6>GUkhYM`QtqA?v>Zn=uQH3mG z3bF7FNSSh9F!e8_k}Ws4owYVO`PWKy6e{-wjQ?*Q{rdOS$%iYL^O43KG1Jh>M+gm} z{Pn|36t?$Gr*1!xWGv^Cp2^4*lL-CiQH?(1E4N<Ypy+(4?W^g*K)@+Mq;R^aNqH`z z7%|)Ph^PNb45u3V=q;J+J%i*=58eN+2vxiJmqqYb*ywrx)v$l>g96O<KaKbQUjh71 zG<h0g#e$I_n#5SUUvILud=wBZmzatBwdex@W4<7TY|+MjKVXd?A{uN{gI&pt)bT-G z?I(si1_J2(W2KMAYtg_r=Ellj8$L+LdgP4Pe;;j;?)4-G9vt1&CyPWp7(pB30;PQH zr{dB<lR3&Oc>AL1_I20QR~JNiv6_<pSRVCss~cMSI3p5cdp9zmobogT-G!4|qX(f4 znMOnwscd@>>|tadqs=jNU9pSmO`ifsdnIGY@FQwyqQ*e_d4}-Q2*q0oi<Z3et*<9? za6AJcMCs<cWfAw>nI86^{3}rp+bZTa?JqKZStu!e$-U>}WRCt`JD?(0%=rGJV0Md# zm=INS1W_!pcF_tV_tA2U+TlFaJFJ4*4u)HF(2Js3W?+s3wq+0-Z#E{A8}E+gPrOzP zEW59?v-f|>o8>psLekG$k)g*!GfBh2A`nBol&kTaEO4roE|W_Ph|VkM?^A;R>#2*6 z>}e_WS2zfHHP7?hG0N}PGyS)O@_*mxH^0%e*qZ_0ZsKub#GMf&*(`M1#);(Q2X~cj zg!Za>cBE}!XY_}!CMQ46&CRiS@w?n@&Cceao7qBhN_6aOS$5}l_xHyW_5EX=d+yRY z>z~Nv!#kP}uJ(`j)7(E^&7+^&r@>P?c@b^N0Dld=?~o!vyTODZ+Kk8e^IV9ghz<Q> z#73ro`L6xXeYO96-`=CLvth*AZK{6S5r_DNWBn9Fhs1_Hzi(b8wKNV>?+BuMn#gy? z;%Qg0=D=c=2)p&0@18)RkX{Y8zoo8<51QkV#7CJlXc))h<HtlrMLmF_iXJe*O78EJ zCKO++b(@e>^%~D65k6p8$Kc@LNJvU*>k{V~c*H)8sQXU}s5?-)cr1ZWS6B*wbpZdB zEm^3e#ikS#d@GC?5u+W9{?M&7N8JW{A0b9CB^Df{c~lB{C9eDTK>xSuj#Q?`4-r#4 zLy(1kQ)x@W=YQ4)`nkQl1Q2GBxHvSJ4WSCSCbYSRl63c@kK=n!(X^;3VG6|e#*_ca ziTyu089!Y`JQICr__rE;TM0_6Xp4O+!lLDPCO?IQ)AEwJibv90qxLsiJx`PPso_9g zi#v8Lozvj){AY-!h8=aZ>A!#fzTq(b?BMuV-K-_$TWPrWx>GBgW!SclgBN@+KHUmB zIHHOUlr=ig7uvVr00Yw)<=|!aZ!xtDgb71OUH7J`MK;7JD8|09MId@Bx@AprFfaa* z{aiyeoBbOR4-IYY&i;P(Y`s!FU6=8JL+mF}kweKm-GyQ(*`x{z3Kc>;0H1O>*<)de z0wn^d<@acH6qMqkqO{<x?cW}F@Tp0Sftq-NMT!UXQazk(ddKTS9}p8jv_UUWqq;iY z-N_b}IDCBka*qKu&2f64u&ll5W+`6<3JTDFY9+j;c3-Q~G@*mBc&F8+MSW~-?Z#No z^m!qN{p-ihuqi^50aZzV$)ST87`Q(&G$JC}?fKd69JWf1Z-|VlWrqBW0<Y6q%C6$j zf(Red^?1T)0>4S7;QC<NW1d>73ZZKG<l0P=oihgYZ?C4^c0*2g(u=a0A2z*8;~FiJ zAYsyOQTDxoL%|GJT@&XEBzBo^vn}(K3dvipEz>Z3KikfEWqC^|GU?Vl9JBl_vDbiE zZo1c6QEGbwJ#tvMMLr@mfTuN^Pkck~k1PEY7@ll&_{F3G0PP@0X=*l&`O5Re&#NlE z@P3`{kBQZsZchni4?&?Ql^fn4ld5+H>Z}f;ChV`y$UM?q&-S|Bsj%4}{D{4M;GzPu z^i6tRWZ3-=d(G8>zeB>!g&YQv;QLcy%H{Ge*#j$$Fz^?)ILM)R@;SdDT~LN@;(Awb zOhuZOX}O-|ObsmRFqv1X%RX&vr}w#<G>i{oMa*@{{GmvPOto+<j&V_3QZgqcee`%> zo9pQ4C~j~0IOJT#yXg6Mz;4S-iB>mi`h7;h3t5c9ao+ohMn^|CS?5r>{mxW+#au~A z$^FhB?dUkTl+Srmnr{UTR7Q@k)hkl?+~pQlkurL&?+FqqOgM@MfnyFcp3m$~)(b>X zfVszMfXuJ^-Wx(m7mzhD^fAc=<ygk9TkpdCDl0j1MiM@MWmGTZ@N?Q29uPP|Ie`^= zIM|vGd*PRtmlyCPawA<Ij~tt1)`gfcd-MC`Sj+B>GqZ+y?2siSCmPIp8R0&A7FZ68 z=<!FzF7p`RuI~yh4rkWlv<tLry%P2LP%*(Jx^qU>_}cx4U+nE;^jqa-Dg(0cwIuyo z?TO(hSXj^iw3yYO5Lok7%`U5U4Tf;q!ACjkR<><sL0{6aclaW2`|xO2-{^Y=+W@-C z?z^<qhb-aYqW_v21U_c=z1~v)Q1Ag)ED)osT`Y)gb!^FExlXlkJj3wwk@<X4KB;js zr$^GZ2i5D>IFyv%@+LA~7EBZcnGVe?9<4DJ@Dv9GJSx&_R>M-}wmbB$45$n_eMA8Q zHJy$(x}Wr7r}DdW0bOq*m^l4w#Na!fD*<wtP=lL93>0(i!D^Rt2?sS%vn0N7T3BaL zq~Tz4){lA>RA9p;HeXPbH#IGwoLeAY$RW4wG!+HN%Z9;ntmtD*u0rPoJldM0%7HUF zK)2kHYr4iJ+zlELUA_S{#%PrV{_hf9*)$Hr^Vgh~lP`W9w&%9`szs0sL};cDiH>IO zC**nfQc_YrXFI~|>gvklKn$uP9I**(J@{=@8c*HuTDVMO>GCdd(0#8oYgo0*WrY73 z9)5f{bDe&$MX$297zfLe;`LcC+LovL#H2V<z@XRkeBI}m_LX``zyg=(S|DcfZ=yb# zudjkvdaw+qeoC0nHPH6Qy`)CnvYkKacsAsO+j48kVmfsF6kkN4#Ym~n_Xb75;?t~N zv*%)rMa<!i+d^As7N=T?R@Shz-ai|;6s?^b=|gYe`*S$OXeYMj^FJv4QTuN3x<idt zArm+RWVEQlz=3V=n>5(Gk537CJ(gd;QZ0Icur4y27+Yz;pR)(l`ZYViHx~lJsoal! zu1-F|_hxuqcLVs5%_mxl!w!8Xy8J$)6Lv(THvSqkBtZ6+C+7L|Is5Uv&jxdt7mRGS z-iy#^-d8>KuxJ5%N3`H`#r2PHkJ^i=;M0J)MtJ{oyWb?#Y#YakNmf_#&@YO=gi*mF z;0uprZksUHb>E1K)?1ntCALm+0d!tQpx3Da6=Y;Sg@#tHUo^3-^nFCWDvSg~G&3cZ z&`6NYwc2Teg-<ifx4B{S$G23{tLX|ZkL$r7(9teT|4;tS&2qdHLQS9&8I@dD<`2cn znrElGQ)FD0A4Z(L54K9v38D`On3$QBsx1kwD_yhKU1lBvGxQX~@cjNYnawDI-p|L| zd)0^t8~Z{-4!>7ZTQj=~y_*Xup4<GDZbPj1Q?K@HI+Ulic5@8CX*ilpxD1eNOJ#hw z6ZuqrvQrXni47JZhuaQ`skDUHCtxsG^O-EhKLoD?(P*l@0*W+`BXyP~NayEk9q!tZ zr%Kkgx2Y(|(4R&{Dvo@3)}_CCqL@oXCHRn9kAgNH;sKw7uaQ#&b&GWie!vQJm&7OK z!C3;Httkz9*kdMBr6>6-O+T|``lWDpf)f<IQ(ST6%W7&mzI1~PX5ug%r#7(X!Pv!G zHEl!5wFd`FVlHZ0hHAaX{kBDL=O&Hcb7YVI;q7&_oZiT|X`0qhXG&%0U*<=C_|QaK zjomZcR4$#aH6j6xDNfgW?W^cP!y4_#{h%5H464YeSbg6MvW$?tPfu&>z6G}igeQNA z|HxhCP^FZmC)J5^df1Z=9xzFmsoH~(3Ap9YSFw{&c<b5BR7u|X`Zz3iy*%+Tc|pu$ z5p36bgOroQCp;(vyebM)tr3(oQm!i_(f(M?5yu0*FXYja-e^fE?#F=A5MVW7*DE#g zw&X}jmWi6faFyZwf)+L1aO=6~Apg6)(pEx)otnLbZ)l`;I*qXvpwt%+wU&lc8;=g> zo4t;de(clER$H0vPS(65CmAnpub>4FobCQViC$V-GJoGn?z%r2rYlg}8*mz()l=r) zr#z(y)X8I@?bc`2m*SMTCQfR*S>)&yIyyRE27Iwytezs-{st<4k-$Iv3+ifErnrmE zWU9c+&)?78J}{#Vb<dCIe#QZkpc;dYt5q8h_V>#OTN;U#J3Dz+knC8bCv)q0U!Sve zbaa^T2~i=jKuk~`vl*#(HBV2_3yiQfPvpJ%_>f_7(XTu7F1AuzIFX;I7B<_SQV;5L z0t{~HyCcuAT=|m_hk6+J{2iUs=NNj$0UI_98&Y|umqJER`a1i4np>UM6z`Jmaukx& z*mD?Y@`WHABox18QZa^^OwlngzHoe2Bozn%Pn5*Yq(hp+BMWa3Pyqw@@8Fjz;1&>* zMeTd(AuOEAhE5Hlpq)X@)oM|A<cW-m*)0IfQ$PzVeU~<<_m;H;Y_7=`qg+9|{@;p+ z`(CgCO}u6=_y;qZU&Aylo_qXp%%I@vyP_Bu(b;nPrn{v_-WOwng9#V)Md-`6(CKPR z!-*21bGJE^BE8CI%~x1$ks@~m!@UVlAVWh#4womI-iK>EM_ApqLY`S>FyG16JN^FK zZi&!mUn3EkQX57Ae|EM%4T<SF>lKSV_7_D)?KO$$ZfV7*m+uxysvF31w&1k<{U#Mj zp2_(Q)SW62->aqC=9dheRCP!LVfW|I_|+E6mHE3TrC4O&g7b$nbQ`&+w3erSRfc$z za$VXGQ3yBlhpD+5HODb057g~%fP6cF{s8JdT0rl!HV`k??CIig-m7$R3L-fanQe3f zv51&kStS>5#LrvNd}&+09=GAQT%H1(9I5}HOZn4{j$!n+g7}qY5>1ZBPy)fa4cf?B zXjmLimCbz9*dlcF_R<P>+HLcbX8Oq26<-I3L&qx&T<wNBni0-tJOimhZo>C7T1%AX zpzGxzQ59@b^%p$ude)FsW$>0@B_3!S@jyI|b@zvpV`BsZaV+9rUwtiiA5{(r2waY_ zQ29{`0=b~9raBp7hwllvetY}*=zMP?eKkYfvJ}&cAckK4jk|aqoxS!qw2F}4#v9A= zypmyQ?vt^0N9-EF*fHu3QGL#vk-m+j=aG#)PkIxmQ}X@<n}lEdl;%q8_@e8Je`q{Y zXTg}&Xub-n_stsVSTdVKe1=Bm2oy?;PRJa6RmFUMqn=oGtBfzL_v=xzp@=}a4hAsU zE9o8Hp1U2MX=km4O^jLHiXNNFmpk_@Sl~4Q6INh)tH^L_tDosm(qg41#|K6B@<%8A zYoVi6?~JTx>xj&T(>fPLkq>TaL4_RbJZF1s=9$25!6kYzl^iJ8{qB*oyIr1!7waAi z-Y?A_-krh?>&<kwi3YYb4}hI^r?c^EY!-r5=oMGSDE$vEinc0P0lmzc*sQ$}$#TdF z1PsN8*NwK#DfR3(zbq;cUh1X^MBK{UJb{keGOZ#t=mnkQ6A~U9wmJvSa@6wY8~E=w zV6At!Tm|Z#3t;9bpXK`CwQnOSLZK@?ipKoH=O+`9k&z>uraQCw)Hw=?5L9f6{zsM4 z<abBM*e|vH5RrYNhcT0N0W_+^QFXsq+1Sv4bl{8gXyoWJ(E^8z%)erdF{^=2f2P5s z*>a4+LDx_+P$NQdcFFy6P}3PUDuwd7QY1Zp_}-QDiMKs(%SxzBY1X&2q>2|G|C|zX ze6~NsYK9W&x#A+1%>S+$*jLo=!O+g5r>|BEfRm?`S~BTnVi_WQyj=4*oI4Xh2*1{o z4f+&d^MoVJCbuT(!GskKKg>RnyAr7^I_B5Qr%D=C?k|NeXgpj2T^(Ab23`Yq&CU%r zFP7>VK3X}!c<!}iSpO(v(ym0p2^xyf<yQOq?0Vf-cOKaTp48!C0QS<_TDYQcYA|5W z58hg%@VlCq4kH*9nb$?eeR$_Sw<BfM)$L)4=}U{>I-m0`bIDzwQ@fxb#OSuoffMn{ zD+cdn?=fkm%P=3bd%W87nUNsktNwrD{}=k*-;?kcfkb?dNd46R-@B3;92wb@X#xzT zVWn(Wg&_NZtCDTRTCvO&eFWg~y`M-%M&_-dAs&zxZ(ck}TbonG1{&3b=472RV4fe6 zkRecEyQcHc<9;r&j}`JJshb`;4NJ(8hQqHpIQ;YT(d6XhK8y?)AfT5M?=xZk(B>91 z2iE0pMe${fA!@eOKMJIoHR(OcJ-DDVEmto(I;nr;N=eOvH$y?bpihY^Jys>rPD+Sr z3DR_2x~}1WzQ|EwyZ?ctCR=ZmR0vTZAa%WY0B$bbgJ+aw55BS%<i94#^uGJ`n|h71 zKN9PP?y8F~Fz+q{x6Wlidkm0Nh%_TRCGoG}yWoq7@nug4VdiocaBHUgNwAY6o)HiG zhumQu@!T_J<9c@`o-lI0CuqUJ6@a-I8&BK)nQF03|4}+|gvQ;Fbl7op5Cdwur~Fy4 zQ-#&?>Xy4r*K^f@G{C^7W@6HbgU!|l*$Sajz3PA6L-h`PFG4@dgS=Y4_RWVuT7tY< zRqvr4b`&QT=0Df?;^Xp(m2PQGDd@X;R(cOP&QFeE2V-uneN(TVt*>um+*|Pnz2=F# zLy%2$!ONTi6Ba?Lhw&^R32|eZ{W-4;t;!nf3(J7d^z?pnbD{dPjdmSSsO0#|JADLb zBpuXMF`^X7u5fD^OjE|o11K1p`CN&>L%;&OIfz~{pZw0;p}JUOb7dQLr(eOeQ0IZ3 z)e{!K0pH?zii$-Zy+_4RG*RY~WHdMf8)#u|xg;-CyJH;a_>#?dxCd`7AhtjJNpP~s zCG9&*;d?D#onr@2yGh{nKoLGSf5q>-?H@@m-}CjZ#GtoKsp59$T4XGdNMvGh5yyFJ z92=dGH6Bze1#3S_dd%j2a`owR1;~3g)}j)JELE_Qj*U%8xRra)E2A<8)WotZSPa+- z{iW2x`&p49qWbTntO-Xnn|zI+zJ4`io;g(_@`HJr<=Ye|+X9bL8(Kc$(~kgnm2a$@ zJQnd0|Nd+<{m8&BAAr6o&A*8AX(9h|DyJR&=-Gzp`enHG^TW#-_Lk?|h8VUBEsXJ7 zHVgT+Ojb9|_C%&5sq_OgZo87LNS&kQP838F-+BBy2d=m!TiBj4Yq_<zwU@4(BsvwK z*17JX=#6!m4QaW?9Zs87wn7_DiLmAHn!_e1dCwm|vS=yAXLIS8*&J&|_SG$u`$b-e z7})xgfj%ln(*D3~M1{sQJQVVL#&Rn2WrN#ZfX$3s{<tyKnb%FY+m9wyGd=~{Wg&yf z10GzQ=QI@E8rsIjPleNu!*`QkAF32-Hj2gpjvUL<SESOu3Tf_8wqgL+`DT{hK3U-s zWKF-)eIK23-n^}unv!{d&MomO%e^PKATCa0KHH-by7{|2Rc_dJx{B;qjxWQEsqY!! z*cL!qd>V|l>C>}3t6#Zi+bLeVo$}9n*gdSV(W8cs;~}$tl9mh+g~EH9KSRSwdLP+u zDbxOdebuV~(fA=hq*!TolN|VNcHwC9dTRxai-$*r+&7gjFLt_}b~KwCY+UgC&Gj*y zLa8W0K(dvKJclml{P2pE3(1t`5|cEd_3oOeJEXzvT&MdBCmBDyJ+_Y`Lpwf#*Y*0T z@P#vXjh$}jfO^TYz|bkU!~;tqA|gT1h0b#PpsNkZ$E58N))0Qa6;!&~LP#Zjdejg; zM55mJLW3r`HJL|BZv@zOZy6e{od7@GIN}>gIi+2{yJUW)#*kaGs<~X9r(1|3><uoG zlLqq|yjmYli+DDatbYpw11JP8V5U?4+Q^fgMMO&(;Bo?YQZ;yOZy!rx59YBckEL<x zA4rG!8r6{w&NP{zk_(=+layk|^y#sQtyF*WU-dA|Z>@!1W19@b#vwb&65a%Md`35z zcWce?ZJstCuZj*+7f$CR$mT59P515Oq!7Rn@^WtPQ!z6UtarQsF)Zpw&X;0z?@;SW zz-?2@?CvP#esyV4gn_lwWa}AOq3+98ts+}agQ<L!CgU(35vDh-^SbYKj(Pm!;?*xX zwu?u`)euq7PJ^v!q7r6y&>pS+U4}KS-{)QLmRhJU1b|fl7o7vzbSkV?d@ftl)7|~8 zl<tg#&HG7%^L|F~&9MGu7~f=>Eg#@)uhD#70AG<y%+0NKg|^%T8BCnHOjg1lj9*tG ziQFB0y%t=sT$T|K(7sm04-IkPVw2Sf^S?M3?9hgJL+UF<B*tESU9FF8tV0EWa3z*d zw1#3C%}@_-sjq2cbxO#5D<61$E{o_gNSIK84cvYM&c0%tnL;`Ze!((}5L+F!seeFV zVBd?j{(fb0k=qjzV(ucwnARyuyv~{$Zu^Nx!QsSXJ<PDM^xXGDmH)`Zv!DUqoqT-( zY1C6tt-dI|yL;@<N8v3;O<`$;FNA9f>GTPRx${EL1Ka^Djmdv-!T09baMH*c3>NwD zF{!;evj??lN0_G(Z%PX3(x%3P&Oof#y^aVnuOEd{>K{rzAg!;jyRV#1*st$;h)ebK z-cDCr87EzjZBN%X%jHbu>*BwA7lmB>mTagev|Wp`asvV6dvwkntb*Z{D|+JV8(NxL z{a?k!nSM*)TR~P!f7Hm!?#{$Fy$wrGZ*7_?axLC=jFO`H-Neb3C|4Bj#Yjsx%I|dc zkd$<Hw9rhQ2sOmLsGKeWzfuygn*%;2xxb_FlF<3ppz8D?Us=hACVD>@COXo;=5V<y zp$XnRescRUa%p>;X-CVXKlUB`hY-hU0vbNLYOfr(uwq2tuv4G^gt1}vKZ#3M-$DEh zIo3CWL|=Bj!Ry`We7EH9h8oO|>Qqga#lG|$RZt7l9IG02_4|GM_RaCSf2><mp$R7j zC!jKEQZcOYbaS3)ecjajcNzJ!+nYkV@queD^PhF!QeKjh;!*8TsaY?SRFkp_xNqi# zaY#JWoYk4s##i4LvDZkGf(D}@NEWkH>>)%rA7}D^ZQ$VHy^81G|6R^JoWe&R?^;t) z@rA?kGk~LK-L)+NTe$Hufy&jeaOlj3C-5qDS#_Gi%I(6HnKdgbt5s=xGguXv$LRLS zf3wgt$0>|g)YPbD8|RoRcA&GJ$0S5ud$W6$jE}2X?CaojJT_ceHpbIna+)lJ6*{gT z&w0Vub|}v;P&15!mn)deLHMF4#X8LjEHMlbC39Ep;>REeo&7WM4oz7ma9%Tu!*@M$ zfz#^XTzVAwm;uES#y`D&p-4xW#%Xj(qtoK8YRkxWHWz6REZ}33j@~7Bw;<(v>sVH- zI$+AKhsV<}lq9Ulf;Lw!3mQx~mFSmu*1Aoa%oRV(EQpiqdY7qtmK~B9@Y?}DTDa+F zA;&PN?vn+_hCzFW>T~`w!Xhqkk05rka@Oi@-XQOXn}?<~svnqr9$VgEsG#$xdc|!r zzTYd98^Hy2e%rQIl})cV!k>)0dO!BlY)|+-d186tYYE?yL0)&?#4VQrtAey}tAexa zf^0=W?8bjoRGEGFJmtq)UME=L!CcfPj6oLDvaO0oq(g9f(^VHM-)HR9zHwb6A56Qr zOl~-M1O}`e^KMPXC%${6vkOS6PkawupzfPEsg5Xl$I~i>vE`O^E<6%eM;Fx30ptxm z@Di_@iQ6u=Z202wqt0kUPp8?H+N5dw$C8ob6-T?s3!Ltd?C_hArz|uv?TNVf+)3OP z5M<N{U}v&jvR#u39$sDzx{qFG<SFcNHP+q40YU^$UmTY35EK>u?}fA(pWDS2-ZXME zA8OzSgnPM#&8pX`4&*qtDgFjLpS2f@AtvBnQNd_u>$w_%@ghAM7>4&rui`%ZaIV(r z*PtuGKx5rA$Mz`EzN@v6wdii&7t^#E-Rz%C20VLPa^pjNlcZmV`h8$RqlT;%kdf}b zc04bv5VjoT4fD2nQlM6xvgE|Y!y&5~A}RYQX?eK&YvHY^yZa+Lh3E7!C86;}T{o!M zCc3+cJf<g4IP*a2zJnLX@vklVG3p<kjI`b&kyu$_fNjclW%sr=QlfvCes=fKs@(}R z3W|Yi<9co19POlQ9L&JaZ+k9?gr&+sA<5df^|4ixB$~4(FHP4WaQmsFF~FA^<LuiU zv`2VMnX_2~v>DZ1t<Avv+C6U5xOz0)k;1zE&6)^%cMdF#uZS6481<b_P5EN?xKv3R ze;^zTD~@%ief|20$n^=yGuD*S;YT^XM??kcg&~<qlRG1=@r#iQzVC(l2GDA(X1>V2 zA$b6GA8DwuqU!%mhJv2EN=t8at@vX6j#rN9pSG@9tY9VK3TxFo(SAN9Y&S~bbCHDU zXHv?BOkXl@jHe3T`y|W|h#6WxH+&c|;3UVS%Si$A>w?T2ktS!_7+7xkokLKMtY{_Y zgMwWQhC&nkz9+m%5Ks;LYULm6l*t8Re97{39m^yNhwQ~bT+#+rOB@cNx9YJ+M4?l^ z<?vy=$xh~8+D?*kIx@!&Qz=@G>B|lC(swD;ddWp4FNa*{#mzkBbGY}VLn7VRle+ju zvE9~ptk|nvCEg~)Gih6;*{My$UN%%u7@LBKgB=Vs^;DFtpF};U9>FaQj~eANIlCdE zaAo{uCF3(UZH3o$JhYwkQ>S)aT{{k@QUF)>lFtXPA02ZwZ^Cf*xPo_nz+q6%jn(o6 z>imV)qD@D%O(56`9VCiTDlkUVBwX>MI@7)zQBD!|!4kQtcz}L)DRG9jO!t=1%q;QI zusNE!sI^m#=MaA^E9lwTnd|ej?Kky4Iz22moEz84CU}XCJ)vD;a)^CJw%%_JiMc!T zdOTodlt^m&_;4+$V7gNLMXl+c=U@`sV<0}z<|c9-0-msGpPl+;{F6gakW{x3JE}cR znw<qyk0Nq+gj!>*#T<X(bzBTzbL{t5%~XGcN;Sn`+GIOfM<C)2|D1x$zVH9ee6%TF z55LVYu{|u!N)1iFT<V~QqM1IX00!ojzzPUiEs(AXS+i(K;Lt@8Nta7!#xJ5mVlw?1 zcBRkWa%q@DDn3{4RO_9#;LD*6gs;QWRsw?4`&Sz$V@#^on`ay04}5PAt!9rJnBi~q zPT;KUydzjxSbz_(mew3OY<OE;T@Y8VL~xa%8m(0jr{?Y5G&|z9zq^(9zJ&=0J_UX) zyz9pMS|{Xk$<*?zOGPx&zuaNYaj5kor_S|;&eZ3O*7y7teR5`MfTn!1P<|FCFI-(F zj)?{S(QB`|eRHbWsh_-!G47TU_002({1f&(Md)~O($Z#m%(?qd=Y1sW7tdFOc7Erp z<Z1I<&>{BFHp27R00Ay%i6!7tToGE!W{b?bZVU^Bd4ch6H7hl$X-GKEkTJ;E-`L92 z8H%tSe$$?aa3tZwWepMXlaQl?PR)BByVTo25~`v+XPJeCk><QGV}s{G;57P}u8yug zj#Z}R(JaXqRO}Daz4=K>v9{9L4?j6h0KKpCFbUx@ioaDK+#L$xrLSBqeQ{=kKTrx; zd+-?7o|dL7OKoVip}AQ|`0{|P>1JPQbKWTfW3fxPs-ymx(BW+M@iqIMBB#x$68Q}Z z9co6ZK(&8iA$o%|h2IotQ0%Iv*rZX|<V=Ty<75oUaU{l-%*!B&?!bC_wa?cL8TCY> zJ7jIN5QgJn!>ldnAvExn(f<$@-VS(#KMHJkigZp{^(gp0oNlX`jwsP2N*t-XXDebS zUK0QzHo&x3GZnZ%FtqArX`H(U=Zhdt>@v6iAZ)RXJ*5O*i-uWcqq)GL1olIpF~+!$ zC6snQtVWxH<!=93Hmz76A~tHvh~AveYSnqSx}~bnCGWf7eZ$9XGandd(_lDXD>A_Q z{XM1n<mBYyPO0e<?6d4>ri*%_v+b{j`b%k`>!^MbME@AF47L?biBURIpKS1nPr*2< z$_Xbi+^I=@S-v1ibmbCLzCh??*1MYV+Vo^6fdj5Rcz0u`sVGn5D`7Y})=f)$!6=u; zOa4(ba<qPiaZ|+pQU6zFfBtHY2&=vW2L#tGI)WA|xwMpK$D#9&77aMG#uqy$j=SKq z^V0B~m-|L@<eD<C2d-HV03)uI^ahv@)>m<MW@kto!Hq`R%eX4lm|0D`?x2Rd+IO<` zTZ$B2@{Zxe<5Ka%d#dPb&wvqm83Qo0*i8*BcG{@3{uH81e3#S|#-+slrv^wk!Mf$O zr5ekcG+y$|)~ok;3fslik?a06BfJLRYpELw?bmEcQ>{$%L2yV<sKgD`clQ|M+@JO7 zS;^~?W-?8SFAUItyZc!UlhMl4?ZUUy3QpI8A1(IsK>J6SzkK4co$olr&sEBHl(+}? zaJNFFcSD+ey75M+rmQTN=&+TZ*<?ocf%?(@aBL)BQc>Z7d~cd(QF#IAV82noWBH09 zAv`-f{BLX60HJSc*S)Do#a#Ix7kX9zz|pQKa)$-jS@w>O9e{_nvI|0Pe-6|9%i0<L zcSyt~LIQ@3HOyr*1$7aCjA?ug=xr#%9Hq*v0+615BXUW}ffY+>-j0=JRV#K1`Nq=q zgoUb{BGfc(dWv*YLjz1%DL0L044Hgs*4qHQ1pVyqFAk-x6t_!=#O~mdT0FIaV4|xR z_ZfC};yB-+^t<hSI?`?E5SS$_+~1Xad6l^xW=!sCTgIWNXF1+EFO&`NilCp_<}3eT zl`@FhP`kW(@f@2f%S0JoltYS37>P=0fr`vgQEMi9;cm3_{vv_EDTu1fnE?YIokWB1 z6?*d9HT^whVmi}j2&{=+oRpM?-TGZ{@M|_M)oNp5v*GVF9zI-alV$G($A4F5LGL8L zo!#9A$+piYxGZ6hC9#n>S;dw)Nm+RDJgsnzLo=ic42ED(7&V!({oy@5!*3Huk-okb zyy;}XYgxwWo8L)VF%h(T$g7)UTIR^|*eV*sl^H#`gFxQ^mdXFC6$Jo9G!fNcQl$4b z>gZf@y5#&+0*ddOm&#QvNFzLM0)}}=mp!c%_6LVo{wOS8l_vS^_G1*pjf?(gpRnU7 z(#w^84lpWJ^i#lVQT+ApAurkb*=K!F0dNRZ&{wO(*c}7*1OUsnw~u0g3z4Bd)@%Jp zlFZWqKiRS#*&SlhrS(f!n;j-oO_9lJktJ~F_sCNNs>d95nAZJ=v!%s~$C0C(6$WvB zdkySo=LK%|Bx!ad?c8k6EmKw*cR5l|U+%oY_4YvDw@Ji;alzVQU3lV`As>gBU_=P? zwO<7fA8$1aAjkZKPd3?=6_~kMKWE3eV+Q3Mua*kWMIN+Aph}r2dz?pWCU|+gJUdU5 zF!rwE_eVgRKnf!R8kqIntZE^_Gs4JIHy<`**h`Fg2RS+)3Mt|{eXX~vMgiBg$k;Vt zl+v$8hnt%qh`T%5yWl?mZ}}i(g#0$WaPh`p+6a<mj5C^xYJd4|0TP6*T6!1~5(%AZ zyE~XUt#;q(Qa?YO);gCU8NoyV_lOiL46x1gqFmp<AAE#+b<qO8#%r(>4zL^1>GqGZ zpNc1>g$7WbT<QtVUOaA^2<u#7oQaObp}$Iag5dia#q&dt`J7O+=e_Z*Je@oH>{_@B z5?e-OY5cK;_(R<kygyrP+(lJ>LXZxD)XNJ<)XAr+;02|i;U%#l2PplU(^U#IrS>Ph z^-t%mdZ;&-O=-G&^2mYAsaC$kCNNN_(EHYg`yxjwq{r@E^qu0sRyEsuT024nyl`>w zGw=C)h@79l26BE_H6R$;Rm!%vQ^*=T`t6BerSd)jsk^Tbo)Psu4j%0QC`EVpQ1X$> zGTh=LTf2sv8PB}%Ep2?zUp75E6<|J@P@YymsSanndddY`2aiMXf)_HJtd^?Rg&w77 z`k!yEPC#VE03$1!5}(6Y{d6}%?3akiG7qw$$0nMYZ!E_r>Xj^2MnvcYNJLaX6IW#D zZfho=`;hh_l@`%-OROArW7ge_xIY3d6lx_t$+_-<?<=GTod{#u>{B8JMWAaMtJvPv zMPj@9oPSF>Bso0zW}Oul?DMHRY_V2>h80sj7L;1|{1Zj|TxXY^9S}rS)cZ%Z`A@gj zN22Lx$^~?Huf%YuuPKknT?>muKRFY*B;2>VBXwB%$ex%M8{5=MuA~PT%#<<B>LcEP zQGUN5k>~(E9?CsRKioHXL}2y4Rf&Yt`scIoq#CJ6J^wQAHLWpp?}O0~)^lgp?3yVQ zq$y46MujzV#zLp%W-!sI_|YE4g!7l%T-K@U<}=@Nw`r-^ZKu0RA@7E-Zw1D&iNd_D z5_`S%UIs6&fD9sGnqxAtaEWf3eq&h0P)nT$51!I!@Da{hcCxo|I<FKcjzv4Xtc;#f z>6C)=VXdu4VUn#*vnScr8HF;J{<;~+aA|q1;LK221Sa6N|4n=3BP1l`i;M2j@o^7H z)`HltXJihSgM4F`MZZS?;@l1Ut&iTamCM#V=sv6f!N3MIFaLYmw}#bmpOYB}4AlM0 zf@hveqmd;`!wqOp8k$s(sR|`C1$R3RE!d}$aS;t81E6*Gy9X?!!ey8sA3l89xRQS7 z*Nj3tce*ShL1*T31&Q157R)$u7k}K^yzD_;>B;o0E+~kH^wtG;mAck%9Jh{0SwA=1 zZN>mhWxI(9!|4l=MT%7KV_bFbZ%x+i)2R1F!GqRGeKc#lxjH&d8wp0ota!@q+6+{Y zJ>lDdYRnvw6qR2tJsKMc+w0sG0>Z<?i*|mNujJ}mSbS`5<j!<0CN4PDy!6Jpmx#bm zfIxic%2=$9vd5V6`UkAj6uepIJ|@P>_>%J3$K1G;`SC;u<~u#ZXykxc!eH7o4p|`F z;8f8_T9u#wvOk_N6pFdCba|RwpeiyrG-P06@)8^1<nZb9IG*E-$%-2{8Fq9!=tD7B z;6<ntVoismi4<jr?Mg(7_-hh|QybceG~jA+JdKp(L=ql5gOh|qL<r;Zgd_wRHL};W zJY?V(V=1TI7*TYz(sS->`A3gX`g(qIII`+BP^pLDrtbt%fK`ft;P#^&xnx@PfUPOB z*EQC2-#ovVVU=n&BHf^1Nc9Z+9Q1%fO|FJ@y@0^R@miL-qO^3|I<tWNuEx>)<}$4I zF_=X%NWo`y;%Yw#w8qzw){OcCyl8)MiU<U_?a#;TDT>%RFsF}PNS>kT&DXA7{EZIe z&h9ekg|f_D_C?}OOk~0JNrfGC&ayWaPLmIZYFig)FG4rRjUK!e>ty#%P4~spn%=>m zV2OZ%QzFmLS9fIP<YG_n!yxM(WFm<!O$fq@cx*B-@5`)B6xOnN!d?nK8HMSNU1-X+ zG0#rDCPLb{9lIt-z3$;u(pz?NOMYd-+_(|)k&DgGWw*=XL*9FZ>h1LB3{0v8j{xwY zNDG)pLeESq+je-$TuvBfr>jKA-@&^oe#qL{C~%M@FtGCZvRyHv7`yCeb~BFVxH#=j z)gx7!%0&+_S>fjsX`WB8+PgT!MRw|u<!OCX$sIAg_$gB6JZd*t<3q@zQ%5tiF_=nk zR%g3V@od3|16Abik{)mrUtI-QB`t(9#@&4EM&B@6D*&5N)#%Qsxx-SGjvSKLSGtD$ z@{6@wiBPf0+rLO0ms{M()L);3e7)<<jwNK#ig+0#5?g6EQ)3(bC2V-p1O4#u@Hza} z*m-Xj>B`lvqYe&T1Y$+eXhx!f`~rksm*9AV<p75Swqx|Ev@|`ThVLHx5fP8mu3Vl? zxGlCBTzHn~)xV#2uQyyHo~rd6@=Ou*K(SJ1?Kq-EFLR==#Rj4^0A>z-^m@%|T2!=r z0+uJXQFgR#qe(9$E7!JieAhqK=71ZiO06`HL++^iwc;7>mkaCpQ<9oqA$JmPi=YX~ zFaRXFd7tYlKZXvVc(h5Ju&8xuY3W9o4y;A&=ZTqHD;FE@8N9>fIp4MF*jsU9?=I`# zfKg&Flbm&Me#tc^a~bQ&{aQ)y=s>CJmC4>W(E7%3iZEK~$=*w|t&jxSE;B>ucIYNt zhIMj!x@~R_^AGqf0EvibZx@p~ysfJR^!%mW=FVKDRZ@1Umotn>J@Ms-UIH#F_l^O^ zoz~CY){9DaUh=+S=Sfb&cTaWsYvY4WqZf<UH6M$LMjF`IgnPmF3nTj{6DtW9fl!2A z(gvL`Xrfe|B%Eua%xyt*%Fe)mGN$uwCyEPhI!<Iyq`!%fKpNa-zQTomp8QzkDbprR z`ox_iR;OMy#nD-+*K5z;Pk8zG`&W;wI#K#u5zR~`2H03@R?CrtDoE{)r3*VrQvI52 zALr{D9sZ&6F6rfu56HQuL$ipfR>VhkqL18b-zVEy`#I=hnL9UNJspefaNc&t8I-6w z4W7Q!tfY|#yTbWrT8wyRXaC8P#8gDItxlD!Fh$bsOZerG!Sqy{h@N(6@A-3JUWqh5 zG#knpnptz~%bF(*T4VfTHcdxkN=|6nH9W5N^+imxLwPyC!i{^k2n++cOlD08P%&G^ zcUnWc@XO@Gu7?f|4h3;+cYbf4GOB$TdlA#7pb)`c|0+)E*#IZ_{E)Mr(9X4_XF=ov zZHUN)H$c|yHfhwiv7x;)ov8TH)mc8}o~AW&#eZedvcGCksjSDrM#4u$<uL_oYkTq3 z#Vy99eXbbt>W`H`N9XqvgfA-$cHblgUPU69&A`3RiRBZEO*D9x15FM(JYW9lehhfV zu;{n1D`2p+Oi&=v+C^>#4xVZSj?s&UgyF|~b7KUNr*>BzqvH^z-z3I}2Nsw|)1&<= zGIlMIW*ELX5&$Ts8;(<^Y_enG$LTLjo1nRuGas0uNlB7#0&?S|`==)-4SxlyI`SDi z>`u2};R|~zrXCvpQ{S*Se}h^O*A!neS*T9q9huG>P9RH&Km*%;sH5g_u+s}K++10} zja$87E!JSo#O7fl2NxIU1ZYssV+aWH71rm7C|?3$v}lv1Z;_?{{oc!8Mo{QpqJ7L& zUtF4}h>FE2Bn}RX@#>2xh^^k>1Jt@@vJv(s0)rdi^eeL1#e3FU0J4?a96Y!&NW^ks z{bpb4e6biG)Gi=+vA{aND+MhOt$qkRJUA>bBNLFApD%X;?dmcd{XziwpzS<4HPtWH zWk_{#)dQ0Kc|ABeIlVYtO;W+iVTiUaiMg}flkOYC@e$eo)4`bl#qxQu%63>-ux4|u z-NM%aIuUs$xZoWy=5K7U;uldUUVb>%fz!KLrsXE+lHlQmNGQ%#C5#@Is1`Qpe7OK7 zh&7iqAeb%{k@jZbnfw?LD@)}BA9X5!RVVB5U91;&=vroL8XW)jcN6Mc09{$DMhyz+ zwbE%a&bFpU=n(q{xd$pO$FJFm?a29iXwxy@cg@8B8boZ^Ne(RHSXoi4AS2b?$<u;% z@h#8p;>iwIo6kQ5tP*ABsb5(WZvX?18}R-d3b&B}Vjz$F!glq}+Tu()n00LzM8*DU z*-gFQw>4{F5$FU-CN!bL!N|!KFYY175^?BvUoNRw<_qKcwJ?ZNS3OaAZEcSfMT>v! z!p)O8FD!9y?}sVq{P}wN%RMV>utw02YA^6p=4}*4#{a|DTZdKktzE-PBS=b@G)fB6 z^;7AR?oOpkI;90cx<No{)7{-INOyO4^UaOtdCocC`(E$*$LkW<d#$<VJ?DLodyFw9 z$6!MU?s(Sw?{H5%A2ccq&5zrUkWgJh2IsWjJ$O^f7Mc@`=4wfv8Rx705wS(E$!03* zCTPq(+Z~1<NMP^$p_lQyLew+rwc7hic2iaKFFWJ!4;qW~HxD`G3e<7VFnd^C=Jh+v z;?n9qpKz3$|G_zFyd$tP@qB%O@1dsFFu1k#r;ccQG}n*(!@`e=hr={<m(xv#o9pu} zCacgF6I^bq8k_0oG>>^dt0GN_4YJz&S4IRyEbPeDMy7f6uZwE)T4xvpv1a5;PRwD9 zSM~p{!13bLr_)vI!gTl4MJm9r_*NrnatS;qf10f%sZMdF(a?e#r#6|3o1U-rmtAA~ zz@wmmolGr<zaghh9?_)Vv{0Y^UzIqyGxA9NPxlfLe7VYC2qr;nPMi>f>dlj?eVPN5 zo44V&?hPN`OsXDfySif@Nkpy>(daC=g5wBIcM41&Aq1IZ9>3ZzI}k$XdeQ(mHcb+A zL)f0DNX~Dr5>)`)w0wU)6?j~irVbJXvE6agYxdV;DGZSf#<*z57)HZ1*KxPh6eGz7 zCyhM{Jqz{bT|ODmdQpjzm5HK+*FhE;b$gj6vLT%b?O&kH9Tu#17utc#I?MR#qZl*1 z(Hene?ens$$}F;8NYBEvLYd++7ExzW*XkMdjJS2{VwMloqKIEa)V(*xbLG{8Y~%u; zyZctNkhS|=<|wb|>Kg~8TL^j;Xi$TdJ<8L@0ZHy!==h>(txs{_a0-X<8E;j(b)hV& zG8&q7QF6V7m!03U(v)b?;jpD5?%Q;qB|PkqnI(b1kg(1(4685xHlNgg;m#LMih^|i z7<APC!eRjK(WZjRsE+HwwiM@b8}W|T6J=Vx?a!Dvjq6Sgk8#}UnBOFeV7s5ddq6W6 zy1oyfOYIX2WSzbHAuTnqG;hqe`^^P{3+QY?00G)UsrDe5Ss=D3zPZQEm&Ft|o$z<v ztF<mt;MEYH$6LeMr;H70#9IFDnqaFNvefAW6A4soAx9%^{{kSOCFdfj)NB?Diy#F4 z`l08WvLnw6=Cow{$3-gEL*o_$keXZx(>Tw`1!i?B2yrjQS9EpKV;}$>6a+Yn#Lj%^ z4c&ZU+^rhRFRJ3q4^N{$&BIV@lnZUeFe;kuW0d)YA>IbSh7B%z`05qHwNbHPt~ve( z0s?Lz6KmZag=T%dm|0F@{5d%?gYh+ot)p;NgzyNc1O*LhuPG0Vgb|%6ch7C&QE^?5 z2qV(>1eT?ADSE58xWrD|qf-l;O~TNhvA+s2UEzC9fvovUhCga&gdkJBT#Buavue3M zyQz?ap#H7}(<hOjA9@ca|8Wlhg-MW3KJUqfB)yWJrSs`pET((mFuYJ|Bvkt{qG=;5 zw9V&vp6-%5K74PKD7~YKlc>mwj94`f{l;7RE&r;#=r*T;;a`Dlngs=lA4_NPU-Spo zSiFljWK_+B?cdeJAleRYk4y3#7{T_qc(MC!!nfvohABBkui)+1R=HxY?=;<P(GQlJ z#|vnk&W5Pv$OCnW2g`$kj)oem4M@I%f&VP|`{`|Le@p*PxO6_4EEf1Z{O+2@_{If| z$lv&nfcHkTw{@;8=DM;!<_7=Wm%RZC*^+;d?O)B~Q}51Vydbh<30kbfYzz!Q=1b<2 zpEm)s=PQ8zNCn&i(}jco9k(E3;<mxWtbS|qO#e2*<OU&`FTzZG5d>4**w5G6M|A%B zmE({xuDG~_mgx>yc={@zXfuNLfGbJ?R<X17h<3Od#R~T=7nZVeh3`T_SFnhPuLS?l zz?1Etv{YP8D4Lbdiouv+H!+&H`(u44>?3IEjH+g^)IchjHO){9pGFD}Yi$UeU3k4_ z8pxSc@GL#Vr%tXRMo5S`_`i_K-iu`lN^8q)*TBQZT=j@Gdn{Ed58BZ}U%6<BzNtpw z%o9lMv0p$ux2T#1pPwX2zJ8=d)#;l%pA`Fo%;)fCgZ;n=J1#*3q73k>Fdv_&)Sv^$ zChYs_+X3pai19z-i|<oXs%xAhx5tvRv72&DZU_zXO{^@xBNQtAt{1bo6cO~dQuNyH zA<2e)kQ9f(#$3bukNE`MG#%G=)R1cIWyVH&gQ%>x+l%6k=sk?>?+`U*B=2S!N#>SR zOV}B9QOJ2V(IS@96+aG2gna*JG{emL@Dr8EYw(GP#KgaD8BQIzm<AiX+N*`ej`K&w zbpz6c^)~iHU2iV(=3`Y>Jgt5uUJjt__7@D7TYpisWq7`@xu~f{cr6jgY5F4H>ubz8 z#4nJ}${^%|;jg-LL?5g^)D{tEO=Rn>55Gyr5*o83n<?C#xfj<EtMeKA-;|pYNEND) z56BPI-nI?=P_2vlYT!3677`{R<rQl>pr+5g4al6V$PBAv6A6K0!C?G!I)vMoRuO42 z$S{l4Sz~yRchyVMWg?p4=@o_Q4m|#uI^R3d1h|)90ZMOXT5>^>HSa8<95Ud;Z?w~P zuBdPF$N#8gVOJ>tL>9b%!@p1fJ!?C>`%{pW{liMY(WfDfv4|QXLH8iUy2|e3#Au0? zx#?)bD`6bF@~-+U_CwRDWeBy^pL>1GK4!c^LAVYcsjoH3dFFke!_fyRH(L>s@4cad zTK~0HJiOBA+p%N8-GJZ|rk<w<&$Q~&ddfd44xQw^)qUy0r1os$8P1QWxTD>b`m72K z3pcq9i*n`|w+t<rI<h4nK`RU8M?NM<lU%f=o<t4@pL_b|3HB>h?20GD?W>LK`9(J0 zy^ijTQ2rv<1I$ljuwe5lEpg-^T^rVP6Pgw4`2g9&Bv~vlh1TPEf%qS)D|(4qVpBXT zVq&FXW^fq4g*NqzW*i;l;WoFw^}O4nit!_rt3>HkB7h^3=FTQ@vB!0F8~Pi*VCUsW zr}669yt}y5q!RMYRX=Z#!m9pRz~1;tU19L$IGXIb=yZG)&API+*)h*EGqkwX+m!mo z01sjL_il|I%mB(f#=<m!)A9(Jq1SQC3&N9ziQStf;g4bPaZTn4VSGzit=tlJCio9v zsf|V0;ksJ*J60EgjUfDu3o-H63tEp**np2YP;D<oFEU`Zg-ka{n=O^O`#|^wOK6MZ zeJF{0Ro)xXB63ZsuT+2Z+LqRVSE0R~El23(n(X0;C6n=Z-hO~+7CTb>N}d4oLWc4c zxy;$Nj=mHZ)=IjbN`0*xsoTeHS@W42B*FVH*n)S?*nJNT!PpyLS1(LzM$WfU(|1?y zt)o)gMLU;9+Z7T#7j!JA);8v_RjGo^9uA(zX{h(|7<@B~$y&Fy(k;Qb82gc=;Vit1 z=*G0kQ~SaSKpCiQ;TGs6+g*98cXvwWI@{m-Xkph5g0a>vt?2lh>Pc7b8VsK&5tGRN zDyEl+X2?=!>-uY2GO|qC9987&ywF-8=%HAF=V;b)0oD_SD^WRRHt$5U>kZC=AQ)U+ zh+lbmlXZK<Z|b@MU%!mH{Eeu^2Zxuv2mUe-sx&TgOW{XOR@2=5clUxxP52>)hfdCc z$4LkGk?zY$A(z(z?*ZodBhIv`2M}hT@A00+H06~yf+QaIwg%sEfA&LF3M?iLPNqv~ zk>i4j&BOf%B}$53t?31ujSYc?#-{k5eJR~xrt4P*uo4xWQQMTZ_Trn(ak}}{Wh!HV zwklCHDL2;*k*b$h!kU8EzGBd3+wFx3o)N%ACmJueUGvrNiXcYck4V57JhX$&W0^1c z_#{9=0)6YRpm9)p-A1hB;_Mx8z9^3?t{Z-veE-!IeuESD+)uW&&wdg&FHPO{J(e5V zZAkkiz7lOOf24^{#3RZ1l^s*=blL3ex1T>Fu?w}Da@D>5m<*CLFc52NS1F-bi~Lb; zvSh6OH}D83=&Ul!B86bTZ2S#<I0j_%E!ih?F$|G=xmKlVCP5Qje{xZ?l*!^V#23Ff zI<zsXS1n)*F)wfQm>8CFQl=klyqc{`5JN&k?r4PV8g8)T1^FMixB**?Rf6;WmH>19 zBNk=t4>hc6oy-xbdjVPn6tfo=Y-ln{ym`2Hl7UFR3nd*8ktklk!^JBNuf`D9wXqc0 zo+x6BY<v$;LqHu4U`n3JF8A~I2h}GtL@_s{yPM8UV1sC?(RIi}Dv1v^5}Z69Yk7A# z9r)(6u@_(oDGcITPG#}pGHR!(M>;z%bb`KSy6_GYKG)@0R8fWDR&Uf0GwL6=L93<* zr7<r7z~&swo4_Q-`ihJ#C>*<g2knFJ&UlI?zY~h$x52bI)Xq(%_WvUZ6TE5_pyT0Z z2GZAL`XED@o9n0&$F9-sqK!{jmtAINYm||vQ5xV`CWZu*D?%Q5-)s|Bv#o&>3nJNt zMcvig=X-Uf{AH@Isi+h?zfi~!J+v4SkeM3cE?}<3;N)@sG`To0E|r*9Cg<1I*4`q8 z4(I7Hz#B8L_b!moISQ58=Ombs8A3VfqeUnX6ScjNpN)}tQ4ENfdCbJ+aojzt`|>0_ ztT2yIZg8fyOEkJPW~%0FXRIZjZNunlwk9}7ZVvl95qop)pC0ikHj^PhE7)e?<cvHU z4JGCG4yV-aSpS|UTcmAqd)fyx#e_iAbbAd|6fx{h){3A}T?-sXy8qv?zIXrVVi0m# zdPQarcAvDiwJoiz1pmp7G^yenadQ(Af_tsZ5VnBF;G3wSXRh7^qvQStg^0s)?H_(j z{#YW<h2K*nLtD-~$b3#0q<~itck3=-v^!_}7TJxzou~`;e&t4}W_@$~2EYUPgURPx z8~E4TPt51a3Gc4Y+e5W_`jSj=CHLlCpQL(nJfGnx3b&cr9@{Z=H9jJdvqVNi^P5OU zRIP9T`tf_x8#Lm!n7te8&COR|L`qqWdVjfsmOb!EFo1~*8T14j5%rxD4>ALq9LJjO zA3JPV`{fFH*{nCS8jY0d5xl1ii>s_xIV23^z2a5%txB)Vw5!+zsw|V$tF4#5%_6E& zxf6{R`M7)$*05b(K8-(Ntw~JTX6KWul=`e@xlo8xUms!eXK%Iw4^WHW`5hbSef}K8 zd4EADB^~*}o6Ur|xuu1)!Nnn3u<ndKwazBv8sI1|d!3?0+}yrO-F`b!uQK(Ii&N!p zc3NH~csr6rD<I+~BiQr=4uL;L=OM93U<Hh-k;d<}u0!Y0#828$Vlb`+BSQ=;w<1*D z*-D-y9ecA%!5)mY6$If&!yx|3_UDk4GV|w}Fta4K%0}9y^uNUB<j~c$_CdMRd+{Ce zBr{d-<QzF`)<m;IE!KJ&-#?D4jqPhMW@DLVTI_{z(Z2QP!!;iy-&!cDfXD&Yb!XGF z?i@HRo`4^c*9!{bRNiD}jWI8=7^QUUxsIlTCVn>xNmq}O{8T@qr}>St<T!NbPU#aH zmGf+iPPX}J$G4gucdrifnd`HiUbup~IwH&YWBHqhtkfr!B_6CqeXX-FB{nvt#Q6BW z>H8lMwmNLhnFisFrXHa^ylA6>2<mTr4)3UZKBn*lN74>;PxSyiB0~IA_!C_B)*VX! z-XYH2ieY?K^B3p4{B$!xfvk~Hac*tn-<xcW;nD4Q?Os?!vmy80Hn+_`|6>-?A5LYh zD+LY`;)0k8`O}M^=okYTuWw&$QLj;ASK|$)<-C;kF-Mtd@A$Y#bOd%sNr_sQBXlG{ zRSbHlB5Q-a!jNA&!s?E`b`|PU_uGLS$h(>q*u=dceY?i7cF6=Ml=l`%Mn(3<l&ECp z8+|E^TdMPXAcxCUd-+wYaeu}UDa_}CY-N6cc^{LC3Lc-+rQyfg%}snb1o$3Xa%r9I zhOd5g7yEq<<lIl)M40#)jcJ1kda`9wqrV&_!2uq)+u^-x&FLxP%0egY9}`N|6F%_N z2KuI7!8SlK%X>plS5FsyTfpAHVg5`|{MYZkGPbHt4V;_RZrk5G`@4VBmbE0THIy9r z@lu{DMW74QAG9|I?S<!rqaCzt>)q#gL7A+<2Z5-<8|RF}_2S3U8IME223^LEtPjqK z<L~Grnxea8ei+esei2QCV;cDcXFkiXly~3mcm49T^Ic;=)Cy0<;5}+i{vb}!7!LhI zc$r0o(DIk9<BA-GI)Bri#!3ry{X2g~+oI~pN!6O6B;^A2F-$^2f6oc47{NS!<Ad-F z+TW$Z3R5~omKMZ<HiWpixV8?q$vo^O23^`UACof-j$dv7Qr6*66OqwWOAEQPr~W+> zmK;s{?!sj^ms8Q`Lqz}ds?}3;#Ig6aRK_r2g}*h%PIrnl64xlLPBW55a};cpdex0L zzTLUHo{><jyaBB>E?l}F$DDn^?eg&*|M}`y2A|zMkehADF;QPh*3u$yNJ&Y{m5<dG zTT9iA<_o~3X!-o|<w8S~58a-GAVFMta{mH@{iFFyL6^S1Q3RFmD@>Js#NnrAG1j%u zTOWkE60OXq`tCO%r2Q<z{)Wd)b0egGkys?y1<>=~rP3<>H|Hk($#<`w;n+ss=})!T z8u81YlT=ofedv+zP>AuwZNLALSh@R8;yFLd=VN=f)0R4GTYE*&j2^-&^E@!G4Z93< z8cP?9UMOr`ElOdxF8G>~Ih~VmuZe!tdF1urltH$Z(@7GX{=OhTRvC9A(2h;t(lXR> zp42ZWGAqL(AVsxGk1b2eP3OEpj3HXh?a`@8=qgs+T=RWQJZ3nbxRliU_wQlq$>3Vb z_%8a>lh!J1G4~5ECC5(N`Ujsr-(dFf(>jl}%EZIN!}N7c6cm21SZq3s9y@)vq*(y) zwzZJMZYQY}cVs-K%!fP1h^AEOQ{RSy8dLJ#>AxmH3??Y?2??7FN5H(uHaO{y2q%ov zigef&rrK!&D~GH8>#c{$i0s3<w)VF&?}yw34@K+K<`6<krSEhmLVjH<AlI?J+cg-A zh!MLFy~^h>-z8X)JAc#ZI-aYLEn8)&1yV%7<iHCe;D(Ju)z%b(BQ@8|FuhhdTOp9T zUw?_hYf{Zl2L9vZ@)(yJC610ZsD0m&%}l?QH`=IlLPi1av2))Aptv>19@mF$&-0>& zfCj*f1AiS-MSuRzhWCz!u$@77A6|-En#y6}p-{kI8Ui3aj&tMxMF0&P_m@3E8KTJk zQd6PP=_&!#7NEK)0a7&7l!u@XVu45s?00nIdpKOK<E!#5++y|Pm6p+qvX$|s!(r`l zEWUFBX{JKlk_<--5e<nk9MJ)ECKbDEm~tL3jUS#}aLz@CT~nOj72L~xQOZctH?jQw z2FJBc<Rs@~A{ntVb$SvtlwdY*RaFlC%gSB0@Ha@v&)XUh1upl7)~c<qAPm6<8!0}j z#nzcTkM?^Z@Yv0U#pOM!?nC!DH}^67f{HQR^i&(4baq<L5Zgv2nr@EZb>^ta;uis+ z&Wfske$eJF;F5Yre|}f-$~?mqtdf&;86Qe$AX#Lfd*Y{4?oovekRfI?QtqHBojZ!Y z;Mk%_Bb(h>JA7(MYaP&h0OS{d-Vrc_zeKIIN5h=w1iPRjA+NWj;0A?)q{a(~pwQSU zCCwskI&8z=uYEE-)ih7vu$s|W%|YkedWLd@P^|g(ry_l|DmR!dyUzB?(NI;gVxcNp zLv2;;8m{zCUnv*D>E1WFHpK47ei|z&$nD>`mOIwH-V*J3+P1VPCo@LJ8Tf7r#)z<` z@)P@5I$6sD^$-j|wbN%alkbT3@CwxJKQi2Dt`+o__7gSRP9);E=0$jzds^d%A6?&Q zgMxv%*Ck4*PFecR`GDT$d`$sEaE5c_b#Yadg2}(%y)tw{?oDNx)|wb^cwuRs@xByR zlT>?Fdn6VYr`B64;Oj~z>}FpLoi1{g$AU{m64y(^3$VBjxDb(=9+3Yh;O1tU*5cui zeK&aekPBey1_lixGX~Xam{gB@5`nATnW#rbR0s<6b?#fJ=Sf`4Ijm}EUQc<cPI9+p zItVKLY53Ld<U2oE^jNMFMPNl>;9&-Mtz1h!nQ@)WFpQ0Wi1_O|O)(X}sR9YEhO0@{ zYiK%l?RsQ7Tm1!(V|={0s!W_4OmINQ0GuHN$<#8>f@gIZpYqVapZ>!Dzo%G3lCR09 z7`zX!Gn0A66EutR1IWW-<Kp6*O;W=kTcTG>IEq22e#tfLF0rN=;F0jLLspXDrG$QA zx1=rM|E@xz2_f@CUvRaB=7QlidLVQn9*)`qkNzwy>UNZn<zf*$D|u1QrD@ufu5Qmv zPJw}W!EDX^4|r*st7R&lZ&D(&v6J#}71>#-=Dx9i4UUe4Y|XD>Fe`!Ih#k%{GMKG9 z@Codu6Y6UJMGV~%4K=}*=<~vQ`N!+6lnfI_Xlydb!cxs+*QKYhIMU^VuK~a<_<Nug zj*5!&1=f7Nt~*vAJ;FSUz{ALOElVqkU&z=2a%#BD?xZ`?4SI5OK%|MmZC7gMe}0AA zR{iTO?W02XRZ0_YLef3gWyQ=$x6y!ZSCb!6-9|qF)yp;^C>Qzm<EGVt&fc!hJ&{$l zI$myORfAUt>+kucMu-T25+QPT37^KR_t_AkQ;Yx)fF_HeY#*699@0Npc>nyA^TbO# z1;6|Y!^8_3Tn>nE6+j;_)*O>LpFlaWFD>MiQ~LzXOdgIGoZ6_*VfJOEJWDP!F|43N zgf~|6A<rHi8l|jEAYxjM=IXSf?TehMR;QW_tJ{AT<ksL2_pz%m*5cV5<bxT4DHkdR zvrMskT6tU65WsKpKp3PfYzm3k-@D+?0EQ>0=`<e4Mz#NOvv9-{3-?Wy1w1)e1Uy~6 z7VHlbL#Qc-b=itRVoh%6=3P^{D>l|*j?RIlL-Qnn{X_SF(I+U_NStq4yD{IZC187A zv_!A3g&`e0D$hPw3(%~<%TG#_BdGy5Zr0b4Y?FfaO?*OX<(O;m7U~=_-0JLBPr6{x zK8+NI0#+5c3|hbf6!ob|)J5aIMr<dvxVTJ(oz*@DO;}eWzZSBk>{czP|A7=j7{1>j zD%5RwP(il-a&<%v;8HywqKe!FHGl}3wal%9aMWrP;dx@YaO674VXxOjvH4Yk`H-dA zSS3+w>qzQn&XY3c66+&dD2720#w@5zJx8xt^y78-n@!F9lGj`FU<(INeX&frVpMw3 zL{|9na>PKI50Ckg-TN`=gJJ*m>1LcQP&A>`ezEj%y>Rw{g+@jRG%_T9uh~I<xpvgq zUYQuIsOBbv>E%0wqeL;%5L}ksZD=JTw9vr~)JX<&bzfRb+Ukl;HbkW2FQ0N)O#zdy zYpt`!>iLG+G@!5oGYgRK&Z3?;SjZxA_*p1^M@_B{&kbqd)yz*)47N;0H4G8Gw)5C` ze$-;~3SN8#owjVSxMvb?Fy9A6W-~8_dwTun@Pc|n{7K>zcXLZd5+uB&m|2@7OBoH4 zr7?V81)jMIl<4GB)$zhq-W@elfE5))$j)~(ppvRLAh-j70E(G5N(vUEX$J0@sz(hk z``+=$4LY$1;41yD{P6L%QCF{bcWFSwVR5n~vea#5oiT)zjkI9qi)gCH2b;x3%qda; z>p^)(@vZzW&)<j5`vB7tn<-JDQ@#kS$kW;ftF-H+J2R>BKKluq0tT`#d!~V4&D)Am zUFN=&AD5g8rm|M<duM{<$#`iH?ncaY`Nhd~p@sKA`X7KJ=-whmVq#;{4-IVEL6sK3 zJ4ePY#QDa3DwfCLMXKl6wx?0uZ_nRIK$r~WSj8T#ka;pH^G7Y*mg;`)ee(czWQR7C z=iB45kJLj$2IxA3wePMFZQs{Zmi0Qkq{QaW(4ua6lSku3pxcGHLS{byP|f>r3O^_- zJW0?~d>rtK5Q&J0pqc-F5sTp7U{o8QQRHjh(>m-<2N%pZKvHM{7!$-%kZ3S8_&ko? z;F~5X4>|d}b9gv}mZ=&bJE}v)rVVKeiPr%eX8K0j!JQ|{n4J<eD~%FKUrvOjaEE9c z?<~OB?7K{?&Z~a(Y|rK|7hzF<(>v)@`t<uJ(>>=lztpu8A4j1^=w=+gv{aR7O6>3k zFbShW+gqRs)0&w`17bmZ)|}MaxEdWdM4&<?;JvR|6lKt^eHMU57U|wv0Vr7dQ)H_3 z)&k0f*LZ+Nri8vmOR2qgvx<NHOu+5H6YLR*|1rx#ssoXPHW{Dx*C~CEE^x(B8Y#Fe z78MKmuE<U?=bS>M)@mjAAZ4$jq5?Fs<UBkGi0C98H!JgBGc&1yQJMEVe_gLylR9Mp zx_3h|iDptV`<hA#5ZH~KwD)gC9!e~v*op+=zYPK0$W$E+SYIt(#~;AwY@0%WoHUGV zhdJnjkaKL~`NNlE`>%e<evt4tpBL2$IM&C(JogyBp0P?eE@CO#Ez<un_6;$<Lq~rt z*1<xf$2NJIUA8a@5kQ5_v$MqP5ZC3UwpAix)@x$@n1)N@<;$b&h2O<`T5m09|LF{M zJ>T!TyofLmwJ}&ilFp~A#2?-sZeWv=mX^!;K@arazOi`N%~x{C>SGv0cmWC~%*&(k zD2GR9AJxR6`*mOlB1jBaTI;QHBKEnJj9?~Jr?x*M6k}{<Hyv=Nn>%}Zzwmr`!o`F8 zkF|9%are%YYq4h5N8&!^{C9DLd@sIL<0w;iPrcL^>G=Z`A))uM0S2RqE5&Xd>T&V$ z<m7MU&eb$Ddfo{!qS)DS<AX)N9*q0d+%HKPrDUa`5II0sR~lhFKpK3KH5;uI7^`+m z9Z^PsoM&Wu{)=I`wB2|0g!P8Fb)WynW@*_d#N8|^KX`I@a1f}K&P$o1Uds`C<eocJ zlz7-G_}A3HFXLaVNhVeubVT6n*sL`r9;=q>C!0fy0E(4Xu(XFP_TedtSLHvevwP5? zG~U`oV<Vzf`op*nD|M~=K-^@NvY`ouG7E1Vl?LyD_yLeW896Le{VX)bdp|Ioa4uzB zj81ul4N)pd%%cyEQCp+~KSV6Mrdu?qC?<yKT6&P`&G4<L&@Z!Lk739!4fwomT9Kbg zazny`^bNDfL%^_5XlhM%U3~d1eSC6W@FjiT(9vc%S>x@tYk!=8+Wc=?E^hA60!%PV zOCODC7it{I{4{Gq1Tw!I9*UQn&SRXc%Y=&tXPwz>cE187QE+tvA}TAs=6yV?zQQP2 zB!X|AL<&?_?B__R_3hUT4-W%(O}<V~76KoH(`YuYf`WqK96ha?^3*tFV*D4rRSQ=b zT_5=flCE|RGHri+?7!7I+@L78imgxjsVpx~(Jv*9Nro2&AT0*<QkZZ#Ik54HIi5y8 zaVS)mjqb(=rROh++BLF)y=P(<vl=)6sxopYWLhrj7YI;AbAehUEYLT|)+&lmF&iI$ z6SB^W88|$vPsJc2qK|C%yla31a;Qi$(O)<PS1GU5(sPoOm_mV(U+-Nul;i$4rKA*~ zxN&{Wk1jft01@i!9h4R<`=IZ0kcSn%UtP8Tqzj_<Fpmi>a?A8ql%e!$3*OUqJRSEi zfrdJR(;Q3)UL2ndAB+!ahFz5kpr;~5(Q6=lF`bZa_$T1JV7ThR%+|S?qd?=QLRUGg z+xhwV<z}1X;dvcdnkyhchkf+2dpDRxNip}Vp&%9i<L-lG=fL1B0+VvBC)t7MWe&KF z7ZzJ71q-~(!~i3=$MiHh=LQ<{z_Jad#n>y)^v}Ve$D7$f@P1)}F6dzW-uzvIE>eHb z;g7l<03=p@0u%+kIDEx!XCk*H*3`J4LZi(%GvuA`7#?ldgsu&sUNmb6zT)EIVp38O zju9?`NMB8^LLTDCUF2?DGLz%8=%24M2T1F2Dp4A|Aj5$<P%==Gj+K(#9~0EmH_h4> zs>LM3^ak7i%hhA<$BJ1wkqc$r!2#k&XZAl%NI`n0rV+-T>#?99KV{rJHb_c#5E--2 zwtrxyn+by*>9hpAYHR*>jWK%@hrOyLy`iS3VpSkXu>lDHoQ{NNOpVz6c$_TkdgVIw zbNz8w{CxfOhD$4|UHGpY?J}PkvDnL6ap7A3I7_nL9(^@hpaFIcaBT|fiwuH&4%8lL z617Gfa-ls7e9g0+nCtMTM*=kDOuId#xk9xRSalV~10O)b-)C49U(f)}=7I!hJcH<( zoZQp|`;OEnj}Qv$e|}RS0P9NAW*0>07cWJLhKUP_BB_QjjqAYMFE)XtpRnDf?juUD zgXT*zblR%yvKMEVvrK#2Y~MaA<%DXXd7v-L0TGDcaX;?hS4G9Pufcuc;F0D?2-VU) zmea<PoV_2S%GA%}1L=x_!U7ej=SrCeS~lx{7L&$!;nq{SDS-^G9jeQkv9&>Q4zxAy z6Tl`2z6%I7Xjz~)oN8@_=~J<29>%WK>3A&DPJzaN6$#k|$gG@w;^6bqZI<pK&=9}I z@ZgmK+ke)N3Xmewn-`?Q9VQ<JKz}w>a<TZPX2D9;>6hu#_ie4kPFFm{0uJgGkA>1B z#_)b}PXhvUDQxc4UTwFE(5MOtlGY<qqA*zB`jzUQM7f-y1pc?wV8PpfP9y8Zyt7@L z=~Iy~V$?ACj$+pXy}zv^=t9%`hoQR5I3=wJ&iZ}1hcVQWH2gmBj^ovV^>7>O?{KJ6 zmOr*D?1OBY=I6M0{wae6u#c_vIlp`<CU|oNJrE)M9$F-llT&DdCn~g81GiyfnUU%} z5AB*kmuh#Ui}8<1qRs)qU*0K=B;Q~PJU-~s?yhg%ge3(Ku~j?)#$VMAb3}viXLcVr zz=vXP0o{QiMr(r=1WWF^-{Kj_OsF5lR=-q^FpnNb(H|mQhH%(;@+8&$itT=?_&Kmt zj9lZrAng`_el4`FQAjaXn3Rt0o{ZnJKwybQ1gkqM$2zFDg`D4gcJ15+D#`+-GXKr_ zOhy2Xm6#o1?#?^HrRgng4p$`a{_jS!_F3x52Fd#Pb^OyLe7rmf@SY4Ab|rapu-4!! z*DIlSQ<G~jvmVE`LMfGa8OpaV^?*S#{Z`>`#wP_bK8g%L-9l5K=T-WX+<r;-cVOt| znxrbxXg_9~lX3XvkD5Tqj9RRmoB<koD$aqd&~YH3{g*?Uth_uHAm06#Q@8!f?&uL< zZzzx1w?T?|2?untOr!8-wK*|u$k7$0Is46%<<_IEl)eXcTmOq@_kWmJ)mU^SHvo}O z6=Z90ciO7Wr%w_)+#B#P@)fdW(k$m{p~WaDN3qy!0dq}lT=Sm$;0?P1Jjuu~*#!7O zRO8OZ*U~S(0qqp5d`udmn&w}xcZCQ5Y4k4yDHKHp)RY8p?)^_HPv`<%7H$i{JvtS5 zpd*LBgJcoK3}(tfW>7)p#HyJ@^1jnl@njG3i&slDO1NsGH<ys04p106fjUe{+EMv! zY9<(bfBK?MC!3=q)kaf(OK`L$o1Go%{sKv+B(6$B{0dao3HWPVuyJ(JNC$~vf04Oe zdQBx1&?~CNT2`t;O);n8d4VZH!V@5qhz{yJ&QH@|!CISbBFK^eqlb2(L`|_`ZSaT2 z-1$VB!QziPX}X)ii80unThFx+7nheqK4UMmY=*-gZKRLA@|^h_1klNe@Nsq^0-p?a z-~nJGAS$jgaPgQU8x~To&?jKOVix2Ee~8-nJS~*UMax^?aPU*RZJf(?wHi>Y(z~ta zy5NL_)PkO)0}<c{Hi3?eKeU)cxUbRW1m50ZPtgvNQ#6;^!k2%N*NM?eLV<?OW5+V; z5$a!7tuXGDD}69~C3+TrA|D#_urJL`cOVuRBuhO9m^7zLJ}a@0jpUC>kt7;b2k#3K z#@NH^fM<EQ%`3UVchP|(cm!kx5<>nK8;E3oYsx}&K}GiT+XDOCWdL<Wu7N)KPpK^| z+n)Degh8PTMcS<M)nSF}&ZWgSmG7L4CEi!{M0|gRJf~i^o=I$kfGWwI#BY{GoJ(!k z(7{d+)Q*40aQ3Mej%6!O^_&|BuVc7GlM)F^gr-4C_{Of&Fp1ysDR8rfAeOp=cH8A* zM(*bCTy%kyw<Id}b^hdSB0bkdCl87yFu>&Ep9k-@VIY_eaWi0m`d-aU6f7{9;AIN{ zih=4mXhz3uRDPO+hed1(1>O0c^MBiML6us+p$e7tT|<gqU+JePN)!@+uS=!`CZYb6 zNfTT4|JYRZe^0Dpap`v|x11nxzkc4~?IUV<1GNRaO0qIkgF0z7-rcJx!Cu$1zECOB z7PS^bqLI3Np;db!TGaRfYFYFJn6e(#y5rTIlfDD?M2dab|J`l&c^OfSBhnZh@tbk< z=Vyuc6RrE=Uzun)p6X%R99J)q_?N|gT`SS8;q<uN$>LK#n~t^BtS3wthzQYgEBA<z zXDgFvq>L9+shl0Sf9asSp<QiFK$<C;Cbsk*SS(?llZlZMkMejdzko^-%%_WC?w>x5 zyVx7LT&W^^EW&OMMEIOGDI+_9d1V)Hp3*-k{QhaC7T)9Xi1X@buGXPF?QUkZWb;4= znCE=mPpG`SbOJhk)>?;sFZVi5sLl7`a98Tfmn>o)Nlx|-Oo3+)Ol|GRD=Y@<c))p` zE*nlDM3FlEv<ASqSmBnMLln7*>dG5T>h)L_>YQCE$wVw>oiF-tcO|K)sD9e4AaFTv zhw#w?y9EJ(MyMxJzM#u$Kwuz}V!kSA6SFE3XI1NDxd%M3M!@ATpY3f>>Unu}4X@kt z?F^q?U$@tMakMw;RpkC-W0d)E6D}!1{)|;C`A`!X@tcHY`xBU`1`j@)0;|P*^Z1^G zE7>WLT_Z}4heQ51$6|X~oEJL1^r!5wagzT;7S28Ixj#tGADja!pIV2lmwgm!^xL^g zC;4ATG9`iG?R$m7Z`Wt0z`Y*}9)+i+EhCEfZFxyaOKX!y_yz(zzkfvq`~IBdz1FK3 zUF;1MilCdN8{mm;O`gQ<3iQ_Jn`32pvpyPb^PS(f4GtQ8jx~?YglttmP5xwB#$G<U zv>7i;2@15Q{v9icuz>@r&8v&Hj@q99!RTDsnNWOX*1}nsCx2}$lj^H-eJ<oA<O}-N zMCU+gCEJ<tc6kNc5S4bh1Q;@q)6n1omvb(gV^qS=%Wp6f)zsBnhnSyuCrlQa$B$gD z=M5S1hLa-?yM1K7bfpI6@Ap<eO~pr<=GqH@Kfctg!Hl6vtJ5uUlz4v;t`owubLA)8 zo`*$HHz_L!GBl>A(*6?tt#dW5WCBSn(o6$<=9h`U#Y{0lC{JuX%h>dMHTBCIq`0^r zbci@j4>paOkgK+^?4F_UA8|Q@66bU1P3g94&wxO%CjaIJ@h5QoDa}_Z6|$LP0_P)S z5*U`VaEh91CIJXNb8Xo;1Eg@9H->6%tlA#KVM^jvyZwLeiNvEKOB22W&bvbme^(A? zm)_O4isLLk3pX0eT3+XE2ALE-pX;;jpO$m#z~#JN>PfJ`(i!H5rDaWwHf<uts)xxk z;8MX)@uAdsP8kkCp!2}9s6C2cp}Vb1MbL8sy4(*#<>)@TCT4{awO{U8O@6K64BvBt zQnWw*2@MJTR@~h|ZvP;^%kHlvE!X48lqiGlMUTk}*Hj)#SE1I6%b-81CHCH$q%NCz zuz-o{6A)_JIF9Vb;*oFq6sjY+zYBoG?Cv+*fbiLLuBqOl#dJK#@=!)ht51t^pC*bA z)K+eJh6C6T@ySG(m_ao<9syR3_r$<rjm88v@zebu37IFUE%9X=a#b>nz)A=4u`L~r zfxEkFe$;YfP~yDuS{n6&Lm*y&sHJ80J`lC9JME6eYX3!l0*AnwwIhAL3pBA5TYx^O z?ED8oajwB993Ink8yUByO{>!<i>SII6@%1skM~dqoE6`Go|PSoV7C~-&|u#il0LDc zGL8;hgT({XAVP01uYcUmuT<h#w_v$Xgc#n?#gODf)gS(cQ1!>q&o~>~)lKAb^I=7I zDokK%;K-&J-5uvFdI^cJ1nI+;x?iEz<2NNnMIo^%D71HV^i7&U#VR2#rRnb)YAe<T zT=o0D1LHE;Y#Hg@$r6?QtDQ~Ne^S`MY`;A@5K(imw)U#2A&_7D0e!H*K=5E~a=!wj zUvGuR;$1x9aL|vRnv~U{>e$jN5>#MQovedA3%5L_Q-j0NOa|Klv=nrqpv<t|Jd}G2 zyxA(Kux~Es8|)t9r)uIqj~1tB@&Hko${YL3#2n><Z`)7r)vCnzNjSmg@n{ud*BD2O zNZgSh1$?{Gj1mi>Yj7Ozjlzr9?WuSaE*>~$Jb4k8T7ucBnLe*fl2NPEK^d;Mkp&7A zxkfXU7lm3XW2`iTm>b6J^Yf%+?w4ru9v77M2i&srZX241;=90_*$4EUp@br}s(*K$ z=bhZ~IV`5Ufwoe2WOeIu)5mdXbu;Jj^#%q$fu@)JoY}(|Wu8!7hrriIU5CI;Sz=)6 zPQ&L7>-+TmSo0LlJQ`Ew0Um-6{`9BgdVD-@b;j(wYGt{jHu<ChR!GahL`p{7<h%29 z>p=#)XQ7DDskP<3VBdnmy#1CxI^Q?$zNzV~ESk^fdL9HJCt?c}ZObf$eWXf%NHkp8 z`e#Oz$GTOYbFm0V+was}ll=^2e?WLS_w6})*C4up*FwAkgqkt@*5YDJrMP_K^@k{T zKx6*$Sa2quJ;#G4s8=0;gbOMygbee~hyWfei>*d4E(tzw$_4f3o;v6p+DlS7K*^04 zrYbT2w&x&=Y!ZsnndDL3G$LE<K7<2}XpnSf0PhC?xr3@wAh6vMoSGs0=W}YWVUb^h z&;Qn_&`H#d0WE&o<_AGJw{vSm<MmxD>VXlgggfs+cH{w6!osDFX0cg7upJWPPD9b# zC!r`dUnM_LXhZNR!+}Cpuul-LYDEI!myUjrN%$^FsYwx^5O<kT>fcFfO9%z@dYv~- z5Q+t2qO+1Bqc8#{f6f5dQMnMbrNAwm<~niTwL5R1U4f=q27SwQ&=G_y`qB3gnh4)Q z34-|SdD|w&f!3jAUf-2{!#s@r<J9rIGC$}&cGTTp%gG4Z%wNGaf@GP7Re<n<(oIpi zN@4~FWWtZvzdi-c0hdleMDT{1gbx(XhUTr7B%sq#%A_a*`m$iHkcb)^Le(unXEAw7 zlnrfRM(iuCJ?7)kd~pN1l*pHvf+Db>jh2h$QwT5}!1TEI1=)i7X8<l^SH~Pe<+(9< zIH@N@**j^DU`aRcFOX{o@`i{AMd<lD;F7zC_LZX&Qe;3mA4b}-xQILm!@-d@-{sW~ z2hcyW{RaS~MP+<bu~wv>TW0;;`Ptr&^s;i^>6B?qxnffUpp;ji^z|vo{*H0rF(ugB zes0>4Ew**t0T@$wa{w0d%YcTacnNy*>BLH9xgO{98{m`r2097uBr4iVjoA*>3f|-_ zY090|SHG62?|~X-LoYDebl0<pPB)}FzM}1Ugaoyd7-a2mPPqw|YFyD?xiy)O{a=}! zDIq~*U2kOFl~VgibmQA)zk&c5%tJ0T;0;Q*Bl`lIU#Fv$CUx-+@?p_<2Qug(aHjP- z4|qEL9Ri>lB_MX+gJmG*!Tc#<xcT04SA}{G7`uS`O`<)2$^>%yF?&e{SP~q}Y*WK- zigYaN^ClKJzjnQwv?u)u2Yyy~a~&;zT9Cp*0;1#Lw`&wz>v<0JJ=%is)f%(27+pE{ zLB5t2Sm99_1-0-17b^|4Q#d>!(A)|jz%>JT`3N_vHp=<LU%!NW=lOEf?S~o-N&a?F zM|14nfG{XQJuMt_1YM@7a69tAk77i@3m*7>6})76(_vMIHwiafCptNNUBe@2d$T*s z2b~IQ9xchioB`rAo~`3HQ1#v43yc#h*T8mN^ZdN#nElRh2EO^s4hlHTXzabeNIfSe zIupek8mWrSnA}W8^SQiZnkQf!G=B~m{k+Jp9Z+jVkZRWsN?7?F-vwDy3kqspOT<(y zpGQ<^YVuptaI;yFp%NOUIY%K?&*G1Hd-Ec7%mrl(MW7D&M^jWlu@vY<=Lsl_sPd85 z%#|A|4xdb(MlKE07?GvwFq56N6e_v{waI^cFE`3$anR(>@|m@-Z%1BCzi=1L@}(cf zZTT&!z+Ddt(fM^-mmq?%ZIiB}6ePx>Nw?(Ij=<INY#6$jf$NG0ME;L!dq$9AWGBN* z`HP~W`p*e?Z+ay{gfECCCO{zzg6jFTI)eHlgKk??eTOJkB`K;R;}GpzK2q0EM7@2o zs-uq7DkE+xKBfVW7M*c|yup8&cmT(Zp2fhHv}4k9I{9{qicL}wvRQx?b(yMOXj)4f z>*<ge!TJY!AeU{5K&<yV_y#J=V_uIpaE(vjK-N0=PlLnDyalg@Q3Q*s!>|*Ei11S3 zH0<u~_Wt>M|Ayw!aL>?6sIr>CI+i2jLYZWN*P8eD4a7?%h#-OQ<ybi$0>4@#mD=|Y zN)o)ell}@;$!AY5fJ2<kV5(M5snC3dpI`kS6d?r6096oJsu*QnZIoqIhy+_YZua=3 zI-?8oO<xVx9#I&f8gcMx!D`J!56ANCtZ;Rq@Z@|HAP|adq(!f~6K@}1WME(fsMZns z8j9HoIY)J{RQqA>&ER8^-oAL6twQli0Kh3jodwSrNWSGjrGjf&3DRd(sO813V-}i% zc)v`e65F*KTtl}G8j348J{0t#CH>EliBZqMOZ%O$<&1kMNOZ6?yQZs@F^h$5kEq9q z4oUFfQeJ+X(ID3}&X`aGVU((~D*wAiEb)g{<rU^K_GxUAg=QZz?)vQVzpQWQ*r99F z7xOiL3gLW3X~Ff|!_?PumO$m=H{_klPJQ<rH9?KHwb9UG5EX<e>&rQ9dcX?7+$8!A z<iyjGmYB#>Z6-c81HA7mHdDH<IEr?Dnf`r4LcgGkNd3gMYfgkmcjix(d1dzASJQ_1 z2Q62DC`aK&?l$?82sAWF1b}I7C}yjVdzd9?wRAtg0$52e@QDMy)ZKwQf<#w|KIOH0 z+)1Fp$_SlWYwZk+!t|Ca>wDJVmtRJdgca@n14OwHVL_>WhDF8rK0%O26Qo<Vo${in zVo1JI7?hBbXw`WT5YnvoP|^9UTm=f8zn7dTSa#%oUohzE&5qg~o`os1k5qknNJQeQ zkQzJf%9WsvQ=7_zz9|AMJjc5sm_|o7RL`NBU#7o4PUC1-l00j_6TSYPCy7__?kb}4 z-$P3R{oL*#wdSRzqd2||EYMX_LfMjZQ=}u^y+RX{P-MK;)3<~-8De_G!2IcGjh~Xu z9X}DR<X2=LK@1PXp|97h#|%vtg#mQR(>+;L0o&k5P6b$epw>_#l;AI1o&8n<1T7(i zTM791QW8?WL-SVQ_$2A-XW6=AOe_`Uk4NFL3UR+dQH9DGXuQwFsPr55h4q$d0}|ps zvTuunDMqK=o>1obua$_k8N9gfNpbKD<_jyqP0I5zMra_7bx;m`=*a{m0e7~F;&<<c zWd7bZ1Gov4yr!nFH=J#X-|c&tS#X;Q8pAH(?mCBz6Pk4ED($0<2LJbXSTZn$73T@H zwWCkXhQG~BR!*p(y?%pydtBbL&}_GZVf!W9bc{LMbWYc_@PP;cMAYV5AlJKon8GPO zXK@|(Z>S%eCk}{3e_BPEl4~KM1-FIu9gy=VH_f;r4Q%tYbqvctwg{jrDnEDw!x%n3 z1BBJ-BJ8wd={j~zO$iZjS$NsfKW|8{P~L4kfO*JP_C=(nmqZbICIf0b$2cKJ3pU6T zO7lNbAm8@=DhvD;sK9v2`o3-D=;Zk3SZ=|*VBmLSNf~sfg<f(ZU@gi>BOy*YbfBJ` zN$e|a{2bYQiWPyiRCXZhJ2#m}cv>X363)V2CCd(`Fe^eO8m`k?@@q(z&V{A_(oP92 zCe9$$VILA|PzPu5els&2$Rkv4XqZ@$p)xJ#6%XaJhq~&Sq3-CYkDZ2zFXF}GlLaHz zHNAVB_Yb?0oZvVw)5mwx`97a;q}o7EF(;XgjZYZMNxtcB7W)z&tg0>bevX*LkcarD zudd|uZ}TGA=)uX=j~4+i2ras^J$1gWn6J1(SWW|3HxQAjaD1Ymn;8?z?Iq!{bWb3h zqEqxNhCX`uo8iLqV|6q}c*ai`aN{)I6&fnC>-&5mofne(6nw1jDmG*NYLNXe7@n~y zSPneuNkHNR4U1tc$#)ySQtzvSxCJ$ZoeT&2ZCQ*du3Q~=7~0`x<rx9jZ173k4A-Nm zi+~c(k;tAzPJ8V1BbB#FDA!lY8PY7Ov@Nyi)k~!Vq}%FUrgQ^rBYs+Y3pFG^Ed~Hc z|Gi3~U0lYJ7pK3Ua#y`5FLn=Oo=<i`<FpFs<1q>PHPqgU5EI&=$Y?TI?V7He$?<pa z@}(E#xhr3kj|1>R|001GBC<8*{1&OHpf;@fGAY=*FQ!~LxE7wk5n8H)iysk;Th8+X z9YUkt$vpMJ!{cXN)wvtihe9t+g80_<zmFZ()O~1V+8+>Y>u>zAJ<QBzCR$zPBnh3T zbj;m7nLjI(-92%dq_~4~4!P8Z4#Y+G&H_GZhaJSucdg&ui}km}womuI_)u5Z!@6P# zT?N4kunV0L^IcXdUmF@~Ab>u5=)`v%EMH-GW#c95I`BsBDVM7?Up>=<?~X~6(5J)a zAc06zoFGV8V0F1?r;HdVnD>s4PBwiBKCAv^cI?-29>-?%$m_kXnhzdV0$X&24<iHC z{aV(D^B0s#W`kQ3B}l}3jE_+Q9o|ylmB;F`#=m<chjxHCTb)LK?ZC){{gadF7Ine3 z?quyrTD9>*K!COWg&7huALUF$#Ip6)BvpGSUyS(FGD&u~gS$=PJPL=Oorg6v6FB|I z0IyZ%tf$n+t*^7FLz6rZTjs5xv$0JQR;)V!D<ndj6O*KnR|;#Y!t@htGN3@9q&WI> zD;&pRN4h?EgX3}U4FNbF$SGjJB6HgQQmG7~(8<h@8w9?7<q;UFD5Hk}#G__2oWhip zS0>H=0W=3hv9X9q6%)9mE<_t)GOcUa3O_JX>uF1s&C3OuNN<GfEMd%_Mf*r5KZS@^ z<)f&0*?r%4zCHtq4$!Je9|{H8qCsNH%FDbd+m??920WH|hIG)|`gIztFQtlsOw_Va z8UOby=PeTu_cDo<`y!bVYb*xaT=rX&gnh~uvi#8Kj>3knAlW1#*lf1E<>L?k42K}6 zSQnT5qiK#`n3R;IPHxhiGR+Ia$#fs$d%3!nb|SAY@5*RXLSF5#&zm&Oh80Fe5^k}D z?CSRJM7=+D?W+=ROQq^_wQ(FfUX>RXcPZ<4oCBP&rQo}+!V7%hJ3UpHyDnhIA+j^V z0s*(yZBtsQpZYcI@jUNNFuYrO*i!8h8j=Y_E|?MMCGT@3gbe$cv>W}`rMLS^P1c^- zidb8V<>dcuO&k@O_A1Pi%Hcw%f5QIRzA{V&U-Igc#VvOOMYGr9$WGQ~IH3o`Fbj3O zN*Ok^Un2o;iVS<jdgI!bc;*d~K-I%oxnaYL#ScYFPWDz|QonI6?wI<`pFvV$4NN}= z>+jJv_e*#lu!C452wkPJz9aq!6I?WLP_SAcBBS}N#nrBGsmtiX`sUs7fJZ087dJwD ztYBdx(&a=yN9zr}eXYbyKjh9+=BKm+H`0IsLI3(Sp|Y}aF$<>(<3HpRUy)FIlcTAj z69-zVz4};ERo0x3oMIbqtc4x4wsa@iTPo%&B03+rcG}*7u(TaN1m?8$Y||M!)-t7H z*c%!jX+x}w$-$PxMpm*Q<ZuYTYhg;V&mo2t2)PWy3$=up*xQPXE543tk0*UF!irGt z-P#ulkJoaIPyA)_3)O8!c04kdmMwiItA!XAZpA9+>WPA^@D@4_%=TB(bVAJ8M3xa6 z5X1C~4M8z?zx<s|rB(6cKEZef-8bAy*6l>vpg=L_YsZ?b!H&Ae2nc;-=6h-d3pW4% z58y@p^ZdZN{>^<9WUJF^VN}=|-Brk6usKM}N6G9wE@A4YY)K`Zau)A1uIN~c<&EAS zu}q<%r#=S_$%hSPN(zld>N+9O-+8wyw4d0a@CsdPclhk3=A~>H!BoW@G^Z(&DA58< zKhxZyx9A%aK<sH59ThTp@MjsMG)ViN9BS_F<yXd>t9)l;Q`WhWr`~<My%%PExm#8i z)BaH}KBDrIVjCv=>*==E*~5b(>CyVoSSu0Thn_ChcniAgFZLf+5<<a39p$+?hHo{J zt)q7xbs=y3bSUYDvNM$M8ErO9DU*}4b1tZ5!;k0`vO->Ct|b9dfBcgyK2csSr*nCh ztbqn!uw0IvEc>I;eFra?^3|KCq7JA+n&qk6&K_v+dFh|F_N|%6-c|5957scgA;-NJ z5M8u0LHT<3F-LjM`n4h%d!ie8-_woprK2lPL+%?X*<V`^vOmogpbxJDqItxv$+S^L z`t8C4$Q6AvG)Qxl@9^)`7)MQ&@t=mRu$Xy_!;W$&XXR*BJV_X}k0?`pF70H<H>mZz zYaaVuO=XjnNWuhT;JgJ2$UWumBfwdLYd};eOO97=F$EV}sM$5hAHcG|hY8^{nEqw0 zjjxF}5it<~S5LFjY{rTP{-G@V*xffF=D!2T5&$`O4!!Ke^V|$<aA}H{JIRdoDU=%k zb+8{1o~Lu}Y7xfyC2L`F@^@Gw-lf}N@oHBCoYoJPx!QLl`x}Ysn&wDu`}^&gD}QW0 zNuu96ZYw0ydcq%ZTWQ^Yh|w}V^{UV?QX5R6?a?`!2Lms6PZiPBYDVUoZfti)jY>c2 z5YwZB>3CcFdc{IeKKPq>`x0Br<?_%>++7{#_`%uG7(u1})OKH0>(POw;8jwVn<t8y z10QSks+HCdjMP0A=GwbCo{APv6)WB2)1u<fGnS~k6Tln>1d^N#z>0gz+Aj*k5Q5i! zc75c%c+ci7e0*6|w9^V|u&=O8OS5TJN@4r)8eD0*OSN>H>Ix^Sh#U^rH{2*FHGW+l z*_ey2aB1Efie`g3>YljIa4f!oTGgq5AfSCyb}TZyyhlfC?zuGBaiDGAg6>1E{Oh9x zZDSbE#?qSEn+w4rq<)<<TMfOy_?GCt>CYaOyK)jI+*(NtiedZC3t_Eiyy4Xjyja>7 zs3y)EA#d|EFx-LHOS)U=8*O#x&ki^y7X*mfkv5C$1OfF<(z!u9*vH505lb%Nap6!B zg*FHB?eL_;=IFHaNduXW4^b+^R1$W3V-0-pTdx8wxw!b5=Gj*5lTUfv`RqoBs;Ysd zBn%8BqoP#IOAIMy`tQr#qD0+Zq*J<K61qKKYEi)?w3kax=54ntoLk15<c<>3Ts4^0 zzAM}oM+bPNkUG@TB(*TsXP+mXp|(L;=7cTW_oAu3Q2k3+>nt4D(h}O_1#pM?c(~)* zI;UTpbF3GaO4W%eI4SbAQet5eVurCeQ+0P)4;U2FRDKT}9i?2s#5zBoNm>0BDy_vH z0Q5eXYdXMyrB~8tM`|Dc|Mu{ptxShKln7@z`x_@!`Y>wYVFS(6_lH*`BtSc+LJN7+ z>?~2rT$HamFrcnckv})Sx!@`h*hX{%{Y^LmB4QA0rP&!t{{0dJ(!d2!V7E$$-}*3& znZxzc@2xIKfW7eT`u4=|^Y5lSG@KZiYj*$d+kw6dSKw$NH~;@p_m*K%bz#^j2I30} z79dg<AT83ZQYs)I(j^^3isUd1iiqS$hlF$tT|*5aB`qDp4Bav05W_Iv#<zUGxX$@; zu5+%levo_bS$oB^p8I~PNeTr%OUrKJ2b(N-(0L-|A9p-dy!07d>9x(TQ^CFRM;iyD zxlU*E_giu(Nb|46Jm!ihtry0lvML&2r@~rwIaa7|(@gB~&^h&Pqhy)tn$Eex&s%`p zPGY?<wA0Z5WKy`Y86SMEuw4ZB2CJ;A^$v+d095c{;Ip$ql90~GVlD3FubZysBBMID zd01~~y;M@mPgi^bqnySKWjw0V%iegbP0hiE;${d+HACnWaq4Oe#{t=e?xyB_HZz%h z+4AtD#`7%=HYi&aMHeDY6E$Bya`;1`pV68BdhON=r@@?8KEemU?mBJDbk`dqv6ZwH ztq6XbZ!#d^p>R#CJ&S7Rz;jBaMA3s#8x}|<WRL3nVPe=hpB5DKlK(chTFL4qZLu4g zmZy8vT)Fw$U01kvC*5p4ocQ$lQ~_YlY+H4^>x6$2$rWL*tru_7)o?9)&iFKX>Q_^v zoI1@UkdJX^mL__r4>doc#2w78R+o6~AH8rlIFyTi4(;M%P({ppjgGSPRsv2AyG~G% z)I95mp!ZU02j<&46E$?1o{vTVs<0MU%yvycnj-nwonkud4`2mp_I=m9*$2O=PoaC7 ztKmfmUy>8xE7!cR4xV0z&|-t>cu*($W#kyQjiw0+4|p-9-PaUjq;5$b3&vk?wPNOb z20VU$<Oam^ovcgmixO-Pk_>Pkl}t@bQ_GcAyTmw19~~WC!pkOWT~|<f0oKpbR|QcV zbKU9?wC&TkeERenIqlolZ{BEQkm)^q*`u1w&Ck!c^h++lj?WrfI`~e@81{KF(T>Gj zpP890_8Rh>06dW_;`hxRi5m|LN-EcOcBq=E>Uo-r8X>9$O6+5q=Gh~WV<SBq`@O{x zmV(%2DUXoLt0PZiDD%z0_6AxOM*By~4tbMh6n%VrMdj2G$dvxi=eUgMKA?z#)QY-= zk?@k!!xP;Snv@h{AP;4KtqNpHyWt5g!@>%yZZUG>=8e2Vbn!J~<FM`c3wzic53UFJ zSNvuB0Y658WLH)~F5_&;xo<NCq+_=I`9-+M{S1-_EOKVSxtd%)te5qR_luZKOt15~ z*|=2RD3g_4GSHXe(@B&&GFzQGLyKrFD)SWjcJFQ~+h56mj?X<m?@@h=;l^pgt`tI5 zkxS8Iw2=Mhfo3<1)>AV*`DWW`x4OJ@6pH+7Vz)ka3g3H8g}k)&vMd)HY%Cf09fNC$ zo(C)<R5xeo{3nTq?++?PfHFeg-oM!15Outira&nV)T{ehqI2#BZ*+ngZM(^n+8E0o zgAXzL2DQYV)odLk`3c|&5wbE8{d_jrTkr1mNMwy$0~$}|gx@&#`oZEIY0XIhhzS}& zujb|DP0eYO%y&jd3sZRE2aMdIYtPE;hR&ayd2)aq=LSN`cu*s|y`StJXyl5>Q?8Q% zYRaQHwrkR&=BuF)pcG>Z(U9IJH>z`bgtSutt1P3B^af(;*FOS*Jh`tqcq0N^9hKuC z>s@uwW-z;V)_F32ljRPVgC(Sz)j&zH(nd|?jgLXjjruhO!F2@(277I(QSOBDlcmuL z(L65hBCQ>M&6><C04nRNTVT_G@&PU_$~5`$!ry)B46bGz5`dV7_QmSV3D0C;DLQx` zoRh36SE$D%s;Tajmc3n<7qHh>@J_f>uOi#J@uxm<m^4c3K2~`7;jF2O&dV1{4TovW zIfngN;nbf}4dI7HysEkvJWs(*wF$`K{fQ}OJLJ8=M4rZKX?5rW-rkVlmkrV9&g4cJ zenKY3>2FsltUi5<JtgE#_ggpLTTNzG<+4yvQoc7m1J(eWw8V#KBdq<olEs|Q*y##} z`Ih&PGOCro_0X^MqTXjZXlTR;nY-GcqLm$2c0F`@{&01zIM_t0#V@KqDSs@b00g=L zv05F~v3K+v?i?PKI4gf|=^Xi4`y8T!8}zd01M`X8e?9E{;UJ!SaD+C1HEfRB|Cvcp z9Li5Wxhp~76R2YeWQij<zjEd_f)as#NJ0Mqk}-s15zh+BZ7~<wRE)krs$2}{AO>MS zVjtSS>j<8l`dJi-tzR?zbHsODWCTX8&{0P{swvk=tF}Vr=5Wy;;UoJ5TUuJ~NJyNG z;=J#We9vC&;^mMpBQ{Ie>uTQ3=O2cB@=YX-7j%r8hcDL2*+v#}MR;qYXcX3IcGNOE zE)H#*wNe5CGpyh<eOGf`gZhbW@Ni+e!HUY2fN3E}1S-bX>^O^pT5@f3>qq|sqmJlP zv96ZHauRYqAl8V6t7UH4sM9Pp_K@`yk*>cEXWMjoK%VtNEbzRUs%nwME8bZf2M7P` zrBQtXLOr<i9k!o_fAQq=AL@%U9YcCR2wBovXOEzqoLnbe3r&v27WG^12Qfk2>=}yG zKv<9CZZlmZyTMbQs5mB>`pm96SBpt$K?zRN131JSKid|+qn~K5a|O`bV9x8+B|9#K z`bhSAA|<n^-%A_o?KS;~FMarI2RaDar{Q*GwTS7~HuRz0u*7k>yC6C3OR3&3lWcd= zKqW;*UjhzyXo(TRS_z_1x+jfy9-XoMdkp*3UqnMmDM=$@W^SXMdC?Smy1b9yf1nn9 za9T6WGXvSkwP*XLc&>D~C}3!A7J$O~Rn$D4yC%&_lNh_8sqT{&V~4L2&syjKhW|m~ zwOP6}`Af~80gs0D+2YSISPb|)MJOr5tMD&RSHfWT)zpbnx5i_iX={rN+l>c2XRZwx z8<3U_v5DHmwyu}(){LZAWzQ*aD@zK0KU0WuJ9P}*T}8{4OLRUcV2>zF?AwQBWRQ|) zyr|7U^4y6j<P0j*%XlRU6rwxBI4;<Rg>uK(jn%P+ltJQkDUmwM5cH31EB(rMm-ki% z8?quWa$_YKTl?XH+YG+YWP$z~R^#1O-(unvpV!f$+vawiZ{WOQC-Qs9G~{lF1AtXL z0~2MQ589tdc4&ghGA)FFO&OgL#hEL3bd4HRN5St5Qm(ywmgZy@D7hdjD$~qlBD84< z*ZpR(^L_h@OJCiab%+ZM^79Jy+Sb-8la=Af^SUJ9N{>YC>atgUB}Jv>@1vqvbOr24 zc(LFTm9F!{8@#Kpib#%+&zb97ym&EOF-c5Zyir;MtnGe2FF*gSOM>j$omI75k%_<? z;Ugot9y0oe*v!*)HkLh~Hajz`K{Z9Yd9&wbyW-Kp4WoJzE}ddS)+Kc|_hgHuU&r7I zUIEW%0v#S4VZpi3C~_?hy8trJRA61I&O4XdN)^G%baar>mDjrx|1lV&0O?#>r71!v z6sOv6yZIf&*<dP}YJu33nkH3XeU*+p61gwSlOtu?A+o#N_XmvG|9c;wN3w^Eck(aJ zAfVsa#3b;VIXKPxYyI?6Dluel=TVp}uZGk46@<b0n7ezgE|C7Ie<h@cNH*I}30n?@ z`#a9rUVPdVK^DDqc!S&4&+S4?^Jh%Ul<v=`6@0D0S$O<-?Y*dJ57k>btLZgA&Ig)h zpScRPOP}|oQ?Cr>enR6ZJ7rCO&c4yFdwXsuS5r2?WF!9I-ZcdFKAW%WhG@E|u$xwz zvy*H1^C=)!Rj2;lycjw4i=^(DM9{TdjeH-#x4g6Dz#S~ECn8D>_}Jf$PBGZre{qR^ zP`vwE`!e4@Qg3G0!)vGMtWJSJ8pq27G2VI=ITL&Ad(&(^aSzgjiR#lsCsvL~;H5B6 zRXxBSamUHtP;|H4kADPa-eXTI)T@pCI&n_>>nP2L+Mwj&{>PMhl{jQHXvD7VP_Z3w zwiI(E2dX$mcD@EABH+>+*WAit<`Go!Z1g@WYrDApue1WSH7j>In2(v6K8-|8UZM6@ zC|A`~QsSE0=%dKsr2Lqn@ni__-sN5Czs&4@!#NsC^7S-ZK>K$Q*mjM9f^QLC`*x~H zxTTNk@S0us&wTSc0@*#{djX#ffuhE<)A%3E@L~IK{>k&j`|}lvWLN0u{7RTu9-2K? zsgllo_B1{&zGE!wZe#J$BzL9ba;_H)zq|DL)(}T0tRAv2MZ;_4zqDipWJ|r&bYU;7 zG{Cw4eG{A6hs@G((B9x!&}gk)p%;dSl#$Qu4X-E&kOB9v%|f|#@;&m;0@<JaM?Te_ zMSL#+6vpFXD{3BtI!YkJ$I#i@z1Az0HPq4OeY_T+7*6#LKtPn93J4Wj+k{%7^;fkD zp~;Ov0-QPQNBPS(DWJCD5=jJ57q1XZ$Q0Hjl#&7|lut9TH2Lp_O+v?znO(qQ?^s*K zRN+ZDhwWvxwbN_cO_shH#<zXAK$J3fRhEjM^i$M2hI^b$J95qhj$GJ}l6L-dB6XFn z<`;{?+VSDwgjQPV`SXCys!~Qo;Yq-#6?90k_mNCkIOCL>^@BV+v{?{N@IDX++}8Ny zE+HUPxuGBp<}~&Cipgx{M<;o%F6E!O@A=1t-hIj-rHue;{*bo10|G-fG79*JLoIPN znxx~~ao?<#Us{dYvv<5^Jm*Z&<L(|G1g03YBam^7oIGQN%Ju8N*^U8F)1|ob47%zH z$Cg3i&Tk&*4-bV+*TUmtw@}47oSD{!$vxfrzxLiKZT`sdxHIv=*RtnI>imk4T$5BG zVtJ3NY_JXE6_vKVz2udHvg<VUDZ0MPrwR<ln(H$%^3*IA?B|;=>WSbEW>1s+S@h|i z(vHUi^}Hg%3vMn=Sq@QhZJ9idMf_U7*j(U6%zjj!7wA*GR;qu_3pRa{Uw-iqk$l}9 zDGE;78$1|FdzSI<jX4Rnv#$Uf^-<IOnSi&3&)gY8jV=x`+_`uuR6XD0>od=h?4{e5 zPr)}z)qsS6>Ab<G2=Vg+PQX|G(HW(5nXAV8TT2+8?qWuu#BZ)3_Mu?udpOwn274c6 zj~~d|yFIqumDM}o^DdCuQC5`KCD?h#K3(a2ay?Bn+x{OF#gF77#!36qqwkT^6n(Kf zeflnpkLeVAnfn1{aL(QFw)k+4w_$&S=e)#(@5h6SL#Dx9Q?IirCtj4vq@*evTlB=; z1}yCFfyaNk!zy&uvf$(NsNa<dkDD2~ArYe}tuEXimji4&!&V(9WibOpU@txd#M&tc z1+c7{&$a8bR~Q`6yobHKz0~I<>o=(>`Kf!j@ti%2c*?V_;i3{JjeLeH_kmh503Y#4 zqS4r?(p&r$(};kAwruSRpFY4tJet;1c8+|9Fws|#jqJQRdv$qaXcZwKt(Tdt+9Ol0 zYCj8pD!Y3OoN<rN_Jw(0+?5P`T(5qSs_xUMmGn>qSV@Wd4OW|uGy&eA=6vIU_8-F) zej2<HbwCt=&*^Jn)!uLwcqq&qC0?AUy&kYMDmxS*4lG~QiS7#-LAq)0_&|~U01a}V z7YkVGKE3B-3bRiXfdE0B{@-L!3nfy{8?d6HhfQs>laVfvu?lBteFO4T`}LLF7Lh}; z3&uOclJO&3XD{qyLs>7cb^s~GDBnyr&VCLImDhoV-yAF?1A<A-CU^Q<z~Nek4uI7M zphbRMWNB8oBe5%V_bKwqRae@pcMLy=?)EY457AYdTG*3;P$mx%?|l3+Oy2_W2#|o! zKZ=|jzx+5jf1#~W__n-r^NiY?OH8EAjcm7ddBGRO#bVP`pD5mNt%xl3k_^;K`}no| zG&WYI<tFSI!utfmXYYY-8YyLK2h{odO2FV&2pysOZ6`kJWWR1amnKiC$=$k42uN<4 zn7Cv-eU<%>sR1B#6eb5(LzmtKs*3q>qi~I5V}C?QGo&3N&2yz5;oof~%|(rT{rsQ? zn3@h#lhos@bklH;U72_3g-SGR_nWPl(!yeIQ>ul2SYUgd<X~cQK@nuBtu5BrU*eOw zmdYNaU^zNk)t(Np1$1nuRizkVGP6^(H-f$txt|AxesSEo>Aw_fNffwV>dr0b@8_4} zC7=b<jLvlAwmPH5rdUZ^GMEr^GWGR<Ze$2c0f-<;Km=8n8h7&8qC6fC`4mbef99<x zFluVjyscQ?9h+<__Bq!j=rj<b=cbB?W(%yYG48}cfXIcGAI1dL5_h$aK<wIB?j8`1 zO+T!uY1Nl^Mywq4zVg65Prvl)B9GzFu@_6KZWQ^=iFz{c1D8AY9+MG9r9i!Or6b2+ zrFi>^YO=;VDe*#Xwf!~MUqn5KUSGW3bG;Xp@$EW~5i+2kCFoRXo^P|^HbR~ugC#s& zq~tDkBl6vFENc1k9yx!>V5^ka`)OUoX{9zZ;q+)qfld~D4hU#zR;iOsI}&u-s}|<a z;tINR&NH|7;(^q5Oz$Dyh$i#C&m!S~Z9{UiF=3gxvA_SRvq4R<OPhBv6nt7YEnn(d zFT^fq^12#rZ(G`Hr(=f$l4g)0XiEZBW*aOeDK;}R4>~Y!d=>n&_5z-Y5xTG^^=>!P z3)}R)X&xk4WaL&!?b-qzu{T*iTHoBE8mx^b{aK^H_8P9M=FJ6|kv+m<w+<cp?xZr9 znyX(VHGntf%hE>{h}<-X*M*V~O+K9;$EMjJ+p~uqFW3kXu9e4obHn15>mEmmF{gQz z_dU>Xx-y~Y;sXcY1GaEj`!BDqb3jI6QjX{f8G=oy)#NB?VHxiS;vRcwvF{5B=im)3 zhI^j0O>p!?Uws7qQ5kozJvT)=3~o?G@}gSLGWIlU-6>mkoNAp`{Xo6?eqJ6IpZD6c zxLQa&KoAV(s!;>=@>=_@FAD65FAZm0KI{8ozU2G&RC8j(r1ZtY`eEjLJ)zcC?i#0! z@6-tZXa&w`IlPcE9}v)ZAVuDwqm1{dL%%ULe*f0|#mvwX<P}<4e<WFGYM#O$wR~PA zADGBYr^Cy)ohimVD8Hk@QV+CxPBD4g-dBt^!pVQ-Ip_WRHwr1&0)AxQ1i(;8?HRL0 zOp}P$nt<Uln$Gvgr744GZ&hF#b98a)I0IxgQvHW6!YW-iyo#EAw=2((=yUr9``X=z z`VY^l$-U3K-{vb{Gt{#YMs9q2o3U**mRDEX;|nA*P1=<tcuw|vb(_P5zCA(S+LjCo z3Tk7f&}w&%DVVq&v4;)yxD4oStvubUcUJ)O;+bF0mZzsV(98yb?DUPp!{n!Khgz9~ z`~gZp$%UeXq46SbZXJj5bSYgSI}$bpvX6d1VTwAfLhzjjBIgyO3!9Q43UeNJ+qHkR z=wS5S4wkGZDwq9w81+)T6K_4x98}&BR0bU6eXq4ln7@5H`laqct~G7IERZn(*oK|G z_ssSf$^xrtpTI3Hn;SWGFH*Pn?d~p(GYP`Ao*>1B%t}OWZ}I_ZR_MXO`QuV=pd2mg zQ|!`BkITAyHd-=@-HS9v_c*uy_~UVZ#8>ii&~WV6ukilsW5|lZKpQFfgn=purw(oV zOqT?~)2RBbas{*4TXIFMJpbN%(m=FJkX5%JE&CeR_0bv}gKXH{f}28>D%F#KC$Yy> zDC+cx{j;ehS*FT2DZjq<lZ%DU>g;`%z`X(U{7;rFO2<>m9$iT%cdFl6vBZee>>as) zrcQhnVu@{!su;Ehn2sdA=T%8he{X?5a$41_{Ic>qdfr*J`B9H<PN&KMF5qKfbS&xG zoc8VcTNW?rVF-oG>d0j_2mV(;!d9k3(%V8pFNd+xn_)0NimD%NgC$_QoVa)vwwRXi z%%>~dO5uZTX0C)8ujzcX!VVDOU4PBC<aPc=g^-(EF(nm14T4YuvaK;%X~6xBAS-ZH z%`lB{`EOU1(`hdQ7_qrv6cEyt8@+hX0vpY%cMDi9U-&Ixgk&V1Bslj4{^3dT?v)o8 zQ&=8brTE^EyCEz0`bOR;*a4q!w;qTrKpW6kYrbA1RlP#0_676AlkKtR9m%*Z;r*U~ zhvfI~%!Oa-?S%+0N&<yj+P}PI!zMT*_jjgdd2Pl5hmPzP^pRV*Hk{3HN*h&KP^6BI zC~iH^WVV7a2giY)8+-?*U)n%lTc@tDUZ%tGX_!+Rw5cSdVxh-!D7W-%^)^?F=E+4P z<)7HnMbO4^299HClr>~1AEW>l++6brTH}t+|Gs!2c>~UAI^|P`b3Vd>O$C>;j0DZc zl~~FGN(;;N)K?US)ZMjXX?P5cp_J0fd-=^g2>cEj31X}1)y+o=TKf|mb|;0UiV&XO zxOqg`o4!YvmnQWX#qVc-EBa|jcZtOR1A&J`k2V;=bJ2ltR#928&{ccioM3XEgruRd z#e<22<nh!r1C{?-ke(@=L<|?S)q(1bN{TV^s(=s-S9KdSi+4WGhHBPhB;T3UV^$JI zHH~0jZDD&GV{(?kB7Jq;Ic&^X#~+?TW*Jv88EKyH6bGpyGOdrP)CVh+YavP+I&gpW zSN*%Qz4c>q*1!Y7{Uzqkki+i#f>u{DIC&YZX){%%rf)KeXV<e5CD0g}Ds>a_M81iA zZMaRtRmfTu!d`Ls<MaA#2x_?csAQ+xs=KJdTFPP0ks1tb^%6f}bJ>_PI>rzVE0Vra z^w6aMxjR6+HOUWvTGKH`kNQvCGxsOi?(r)W)}GXbM-03JIzDE^Oyc;!kvgWb6R|$Y z38MEac?3faegBkxiKW6iH6c;Fd+EKoow6}<fSiPcq=rkc)L?zabd9!ncvyR0&qRZE z(F6E98P%dY32rVd%TUeTJ$s{yAV09<mh5$s=#7UY8M+^N%YF3!YDn_<_IR>$D?#^+ ztzDF=`t~-35(RMc%UKC35|Z1Sj-g~EB#u^Nh13n*^!{;S=X!H>+k^u6=zwduUglFy zpI+7=nlO8Ygyh@Z(0r1D=fi(+0RKEo^{W%_{hcL9NPNb#hN*pkOa5+n|FaMO{+Wd2 z{O5C?m#5Sf5<t*n@8d;W+qRcO2MiC-ELQ&2nP^`KM5Jd2xPellXAAhI8b%Pcl=wzE zb<QtB9{3$fhKKo*k^lO{nqOV^30&(Hy{Gs1+&`c4*yjy}70?wipY=09Pk{cp1OLnZ zagxAaLMi~PG{y#;5Ac^idrb1$4BA6CcJ|gk_miAG|LbvsXKbl1l8{_+7ij|?{#XtN z)Y9hu20U?vq=F1sxECe<8VtO1&kvfP|K5M)_UP&3zEQpbKL+~I&?E}1ZxW6Y>BV5% zGnavvA}f--{nv^I{y{yK>?~j5a{>7A&*np-wtTk8Kw0WB_UY*c@!m`dV$9aKQ}b)8 zrf$zL08>Fioiih9O07u(o@-_|RZiGE3k>hsNu5`ITG&ylghimA3|xP8P(#u(@Aa93 zUbjo0MA}=|xZL9Cvd`SPb0?D&c#fj6S^$L7B6-=Px1Dm1c}%1icv}&Xzh8Pkq4*W) z8(5oLeYEG*I~+WM&o6(@rM1=t?kqA}q#0j)L$jLDB_=Gc`oG>7uJ;)1<+k*e{n6dm zaxCT?526Vhjpz0@nt%ROl@qqHf&y>ngJtw?Ulxy=mFz3+Y0Puxn|zKrbG&-{*A$2T z{i++HY4FNsgPePk7%(^IPImfWenR3QCLl;de`O0OkozM-9q3Rb1?5|IuEC7YMGpmS zReLcTEI&|g-%hNwLU(|*NwWCYf_V+R(+ko!K`#B;`ma_bt+`%3cgpFIFn4VkV-jiu zE_@^Hp)!-VBf|MX{|WhJ3N=~x%KeYL&2C64{FigYwkH(7Iw$z?bj6dro+C-(*Q_S7 z-AZKeoO6w7Uw1+==TCq!OL~r7&+h^Ayr<3PIqZLX@(%-`wCD$eMw|j?;hd;plv5J0 z<y3wD8fnTYQPU{$2Ab9TeI|ByOr^<#(3b0x-M^mTqRr-vOP=lMU8+?EfoX#r@)jI_ z-OT^@&2zxb6b<uUY@h3F_}PMhevw7Hyjm1**?xU^zOee&++@@JwI0Z35}fKO8i0%b zYChvEKfHj+_`3}{-S}-2-+ru0^84z1a^~jW7XsVsJn5T%f4g|)|3^0#%v?jXmBE{5 zi|rS}J2f!>z9Y#RZ%ikP(ejWfVqf8OTw4rReGQJeRm>z~IndPqzAwYJSBs(1nPK}l z-MoxkzbOuCAIP2o?8S?Tj&{zUJ+*f|92{W3iNjnrE&=V!so#ela=c2xRCE<|MQJRb zbv#_b%!I#U$(qmhe*N**5KL9pJsW+x$^UBEN#9einJ?0C2p6h$o6>V|2{dmMuDv+@ zxcpm=o|*+8e0>LNpP%CgguvfT<V*N~TgN(&dc@KZj(n<v#>cm-4Z^{D;z=~BI+Vk8 z82x=Eutd9yY)UlkhI9MI%XL?Y94W6zr?w#(ogE!(pp%ky{(r9=WPr1RhAi@L<vVO) z&*+~43MXo)6zD6T9ut2x$J>!3)|^%6-pwO?Du$_RvPu4x%|51UxANP@FI2y68Gm7a z#?Gnu>~Cv{VGiD>`S!3-D+3sE_-`wagfi#*ijg89?f!if32tOdMrGjYxlhM*ytN0p zbfS*>Ei4=4oh_PjRe4?a5UBji&*@geM*D;R?iuhM=dW=k>1aSCf`Bd&L=Evs$u~AO zl9Rt}pU-vYwEF$|J~EnxI+p${!os~L?zh!`+W{nG+J2Mb8=ehjg1*0>Zi7YJGgxP$ zw|?R^@lnX$s=^pE)qV1Ye?MK-7x((Od~Nlp9v^ge^GSsr-W5rxchk_;HuL=Vs`G(J zYAEPM2(1t7mV4fe9%95v=H(+vDRpR^#lG~>Xhdd~&Gz>8?lA@LYG+{`pCnnYbyl(+ zlxNs9_lJtKaxpVP<Dk5Drw8x3G~HI)+s5m<@$rs~{}{|oWosQlwqS6<q4scia-&9} z(A#2nO1BEg#q03UvPXa5-_d1oe@P4F7Sts%EOQSKa=@4%M)Gt(v>3|B+FCE<=1eD0 z-SmbJd?qV7YZL~?;}~7N=c!{>8rAlfD`!K?o!JsRJ02RiH+tOSJH8?AfUb^8)C>D5 z)VKjJ7#tM9&sHsl4ravYo3(jhT<wZ%hHUVYZ+a#?G69MBR}(3sl;0Y8l}0+{G|M{5 ztPm_RbRuGZypL;>)G2ZdLxxw#v=xXgj9x#f%`X=$Dfir+*x~7)*D^GP>A@|ju+TEe zlg=`usL?8a^od8u>qPf$N0(=7nP(KJhW$;YD)4Svy1Kb+NnY~)d!kPht9J*LiIsA3 zIy&QJ)}@lYWA}Q66<*Nlzso^eQWp;N_rD`tCaNwLAP13_Mrdd3xsG1of{pDtbz+5s zY5s0`aPp>aV*POkbo~KTDJz+;-;f1=LdHA>|7NK~L~kGh7qXH)LJ1yB9)o<h2*cv8 z(8C?av3V_&v)kIB0ox@>ImH;B(l+=^C={K$m;}*v!#LF>?yrnB_2e%>w0R+0^CP(W z_j@;A-V*xm9m+iqufQT5B6(^L9^v`)z1pd9&O2H;IXUFy<o%_+5;-+M1xc9X(HDcI z6HZv0X<_TS+kx6j|N6s3x04p#3fn5lXaEY*+aF~Supk0n8b1+)J5Gk+{3H$gM!J%c zI6*^LWujXUWI|m<8s1q@Ic~4D0Qz@+M!(MbCt2u{6&`?c`p#~C#6nfg%+i^_b9emw z{H%{ZO49n|pPgZeF-!^hDzv)rm6*~RLXVQGZV8Hsy_a6Kv%q;zQYOa8J7-3>VlW?Z zoQ$4KkpZKf_ZeOLu+^)?l&&r=v>VpIW}v#<;J7sKV-5JkFJ^aq)ghO7)qgdYlXrmX zI@@vu3j6x$Q+gLHrdtX#-Z55PU*)_2<Uy8YVRaoG8o^l6I`@h|K3-swY~Z<q0TX=} z3^^@-etW-yLcq{<D;=FKgFg4xT}4+tDi3Bl*Q5GJ48c(*CMLT-(qO9tD$>FV&-g8l z3mDtm#f*PA=~mf9ZB#FM^Opp<NNa}GN|cm3cRg$ICb(4FSM3?O;;UB+)Ly&@ET~-( z%QvX$*(lW}9qqv^ns4&?uFY&h!0t@1kg&!hgf#Jk;$WJkQO(rJ(8y$bpMew|J`Q;c zF=8y~LU7Rnog9-^Pq#wHO;x0K5)1~{=O8Ef&}0M#N8Q97br?T3;5<~V3`&Fy)8P*O zkO@U-NowK#GdUCblk+B27QhZNC7tpMq5*YA=Zm>+^4SdM<%CMv_!!9Q7r)IAHHrCS zQU$=3R1<Cp`>I3Y?gBkgLq{i0ia5jR{Bf|-sjTj3dq7$%%<%1&9IkCRUw3&0UZDEK z_<dkd5cQ3@SMURO<mPd`pj*!+m*t6ese%awW~hyW$MFJv;Yv~sc2M+QZ;94wW>yyH zASWvu2!X68N-36F_Np1Hg`p9id$+p33LE)eV9bX~@1#TY;J2*}c->UsKKq$QlTs^~ zd^lQar)$7v)EK#TI!iw~dWNO#t1;fT4lwWT4Wj#+B~C#xVwBZ$ADXt__A^(b_T<3S z4;kp{7GvmL9=asG+3UWt4E-7w2-$7d#?S8bO6AvJk773Ju}pXm!Zy5c-Mce@?eb6) zRY<=crp=OoIuSXv#CxC&VtNv(nVGXpVT^fMNnT1oKK7##MmG?u!fxSWa%qp4OsO|< z$dKKZIx_$1#-|Yab<ujFaq8XgO@g=xs`a6E%cSB}gX@1ySE`5eWl7`Dl9+npw3zv5 zd(_GJzls6J>>D7hrl2MX&t#}Due3_<G2VlkntJT8(g|urS_{Fu;%C?0ChQ#0;Df7m z>xl?fmN8~-Zrw6fE354GlLDuAWlSFKk=t)~-VF(kpipd+Z#(PPfMC7Sz(~!M5e_B9 z%O$Jcy)CxnaneCMRK==5sm5Mmy)ptQGJH@ZT{wcOGZ4q8b<UTdh6x!N87K(Z)59HA zDHSiTS_2YpEJDD#zd32LzsOhytO!D&>=x&lJeJNNi5n<r>wJuni982xad8~8e>0DI zINC0AvR^1Agun-2-+bNjl4O4%okC6FgbWp?Fh^RYpIewH6Jy|6Y+t=4R`cTs-!%q| z=ich@DzLd<Ve1rDW)urw_w*#X0<}Ne>OvhmE2C|IEsM6yZ{^X7D~#)q(2DNxTL1f` zAR&n|ruL@N{I%BKfbf=`4)xtXIf)(`{FaC0(Aiczc~ebkU`;e0TcaoalEI;E#K=1r zzfsk>F&XnnC63TnZMYUjfc>0fW?RZ?pG0wjp~<3(%%O)rm=M~eW+1(JAv;~7XzziO z5z1MIRE0Ay3O-Nzh7&uU5i~QSJB~0y*dqyj95)LM5IuGEqob^@Mpc`ej`%be17pRv zZ%YVUg%aj<ZAftt2o&Tp8QuLAF$({p7k|>Rf`u@BqGa5*)J~Q$kAfC}x)-Z0_HN-# z^%W=U!_1?WOUbM0h3!ia`}Tv`VQ8(MYFBKc8yfV8y;Fi&O>J)tHqYce=ag@V)V*Fy zq~s)O+ZN{WSW?@mzj~#is%^EXIbEmkQfJYjBWIy#Ug!GHnE61eHjl$_E<UiE51T{L zg@3H2-(n}}H7ZioL$F#;^(nAhcOFTnIV59r>%D!WiN)U;mYrAFn+A)F&ms3u{EwCw z**y=Vh%0$vi>oo#^>O!33e?g*k^k{W-|->E9@f&RNDGwY+Bzn-v$iA94HgIOWwY?` z^qgpKeAwwNr+_<zdh11aC*;8k>Us>{+n{4wbhkiu8g_QH5+(=iuE{Q!r|yM{=<|Cg zTN^R&2f_==gTjV|diGWhX$62P#THPng}B8v&}`k&IK8W3k8YGrae)KKE&^GjuELnP zBp*_X@KC3cKB_FJTkxzQ&QSCsq{XIFDZHG~Cxtb;qq8c@#cWBM?(VVjNswr?L!8jM z7TT+Gy?QgXGcqhJC54TFh!`5ve2>yKfV5|5jqmR*ED^_0yn1>cusjb$o@Hd%<3X)Q zZe;~f{T>cOy%E#8nGVoyb42gx2n7#9+K7KXFF%EQxI8$2_1ga=oly>W|I8DCm(G^T z#Z5NMmjqvtFiB$&-8<}A3ARu!OKK7BSz-zKXm#jXEY+m?jz#xw2WR299QrHKBch<{ zh}^k(Qis!y88Y`oPF{YAK-HT4JmMyjFiam98aiCsTlY-^uDM#F=P-P@m}qK0?L3l* zSpBqGI8<f?9RL-=G;1pC_NMXJ<EpoA&5n~oKWPA^bUtr&OP6#yAc7TynK-Q84G-F# zn}x;p8N3v;&X%;WGnWA27zneO^j)8O=VJ^~&nVzVKKN0ruU^9P2nlI2LwD;=%Z5@( zA3Pfq74=+dBzuQ3lJbuiaNoR*JnrAY;d-!j`*ncsk>W<&j}~*CC|4pRgjsU32x<h; z@b>YwT^hU&leUp}EiM=XZ^_Y)%57j<m5U}XZ7z@UK{;dfM#L&A;=GRovdYXmX&9l$ z#TK%j2PtIlW&+qodlBV#QJ#jK>%AjNqN|>R!}MqJdZ%lgW`}A$N?$;JlNRNP0GF_i zB6DNT<A&w0zs+CGvVYd03Tw`_dmh~Ia4*Ih7l>IctdXY*t?u0&W>_8n{#D5Cr1ao; zQp;%MR2vdD#KT^S+f}0fRfo>!nu99f242BPcm4-ISYkTO<#7eA1!pe39zpJh^cS}! zTRCJ9a74@y5#yl(u;w_|iGMaD$w}zhhc?_G3M&vE1fp1GOZJq{pP3b0XmT4$m2-Cf z7n=K^i%&VMzuVx`UyRb<zxqG?9d2B?{opse^w{Sc$)&$9Bq2HT;dkhA{^I}FHv-tr z9%hY=YMWKliBqsg=+TEi8oXDWLHF+=smf!y{%$@GuPjIRbxlHo^~q&%rnFCt7sf<Y zsXK>lwQ3G-l9G{e{yqAk)D8c<?qk6yYFew+^sGn;(=u|`yv>C}SMBw=<+OFa)!6Z+ zMfQPr`$(#<ms}WsV>7<RS15b5^WZi@1S{-T6j{&NQ2zekTX}ZL6yf`Z$K00G@AFOf zExHtvSK70T9>%>p)DT|I(84^76WE^UEPFd|gWt|~KoaUcCA%hlN^q}ca^G~KrlA>c z3-(-k&dB%Xvk{km*_;F1i>O`vBF4bUx7{n|BV?}F^4;2(>FR@$)iQhZ1^yDtMWc67 zt=_n(9<jx1iH~OVyW?se_ttM3CAu#7RWF5u%B**2@Re(843Y<%>S9i#I=6(583<T3 z)w*|(3$u~eVPa3s)QijFF6on_@#U-Unv@Z!<jEg9JOhRG1bg{(4fya<e`nzcUt3}> zcmph}BC%Incj)>7fC@2|(#&mClFF8$wc3-!%`~(_@dCYF5&}@N(7Q*aNm*bl^tIcB z;0w0oNBwos)vD@d@#Lubtp)LgXVx8qMlL6QTI0?J5VSP$IDtxtya8`aINAdT$MIX* z01A~x=(fYNf2QlTk*DVo6X+41p1NHJ|J53(+}Fkg*$s;qcX*HP>FyrFt&0mlPo9da zynLx<XqY7C_Ogs3+<B3E5RXNsFhdV?Sy(cTj=pZ;oTx&o6$i4E%VrD29n=a9(L-=c zH?XbILYG1IbXHP2dqn*}u`k(5ON<f!Jut$0fCh3nMplhYLb>eZyDe`v;jJcwlb1$K z$(E7((iO0{;{jgs%a{8O9HfV`nCsI3`81e21X}0o1ilL3W{Jy9`;&uN$&ma#dZBRw zv)4h+%bSlzy(M7MM?Os88J#M$JvA-us*zhr|Bq6YkuvsKm;QjZRsR6#F+{Nhp(`jb zICwA}TR+&zE0I5faaZUhYFo=^L`J@b;P=|9_oC^Qdndb-_*IB*1Le1fXzh&5jOCUX z!vUf^vCTWd$XoK|b5sTV+qcJ9=w<*@CGn*)!8+j+rKl$H5NqX3MMg_HMfh0<!At&0 zcRf~q#?IA$lz12_pHkD+MF}vnfj7(#0;8kUBCFcVP6{A36&&0yD8ETkiWsXOTW#6d z*`tPXp!_1qmF$zZRr-L69wcAC6>f4%fm2XW<1)R_&js%1ILOD3A0J3aD2azh?DYW2 zh?ko7*+oP~nNTrFlGRI_-jFrZUJ=6U{K=yIHXZxs95(g`51s*a)1uq%G4d}xsHr!a zY+AdUGoKKa1sWu{c`_q_Lpf(j{&Kl_rzW_|q}v>=Fpyci0@0?*H^PE>n89;WiQt|% zT%aL*6GW}={MOZ`Z^qn)0sYJ}iEp9~{biO6u7~Ks*^wg`yX%#cbD(G(6#!M)@1&IX zNI8GZB#1Qu>YktJs_6J>A9L(bLcmP#Rn)+VS0U?*WySl4_9HxvC;P>LUD$ejH6(A( zLrzxq0AfgX{V%!Tqq`Aw8MXK0m^!reDAB{>RNZ1w)y#Ou94eJtkE{j?I+CtHWu?Iy z>&K))>t~AuE!w-f`VICa(o1`)1`X+IJUTU_qSign83A5tOgHx`k%59z1sThyKxri| z`UVq?MPXUFwjL+8E^*f~8EUt$mzQcO3I`&A%^-F$9bNR~F}y~`4lr9gPC~Ml0%!fV z25hnXhAT6W&<<yc2u^<3jxjtuqIVczGp#S97_i5#zfvC<tajnV$G)9UHr3ROHaIV} zvlg_@osoawzD7G9S31Ci_~`d3K6)+xTc>nk3R|+ZCqbH5QFXH*+~#o6f;z+K5c^fE zNqi)6Um{zlba4z;DD?YT^IFDTNpB!mTdwVhyn&+Z&9n(vZX=9<x5ga%x!O~I<YxBX z%}Gt8U;fJ5wp31(Dtw|qRfO+E9*v`HhBz>o{;GuX`10CHn_|sD%}?e1t{Zvm&{mJd zBy)L=#mQcx!eXd|dNK^6!Yi>an?c_1;c@7>Fc!1-eMe;_jY=K`e{X;xat$vgCWM<H zIy2JK$C&erfBf(Sc8_KJ0U^fqSYVkR4H?PD9T+L!+#gF$j&ae~&?cQ6b6x<gDL9}| zGDfb;q!!JH{=;qr24DKq&H&}!7HU>Q80f7Z)l(Yzo#lHhD6r-pY^{-(Y6=snEi}+B zBd-`9?g50|FiiR-py+fDW~<3KN0xsXHKn~NY==p8XoEO-Lj2m9q472jzP5JEc784i z<r!NuES+WMis2B*G#@j>!d`7BZ>VJ0^Z>HJQ1iv$DlAK>@yQboZuW9!BAIFHIHx~1 z1i#xq%#B(DQRwZ9Lt44;J0;qjZm?U9l_&TWD=>?rr~IcA?1DI`U}}XRT%<|R)q4ls zoYo#-x0?*hbDCx2kd`(A&>zlbq5CTYCDVHVsaWVXGDCpK|8wx&a%D*Wv0$KF;Wo@S zT#SEJXqYbMYL#^_bP2S$mzFxnnlhA(lGe)`%Cxp#K0Ss!H>a}%tiEB#YogsIi9ax` zhmltoZGILvt;GTsS%Be*;;EftFz_A<!Bjr?g^7+&q*|ce_U(=iB6`=kqwDGSo08$N z`x)MZqw3Y3uX|9$M8-gG?aw-%5*MUtkjpxHF&V6n^^op0^$#wh$Y;AmXA9HWsNZ+Z zurym49OxN3q&1v}@SxxTr(m($APSgtakvS~Wak{7nr->LwXyEtA9eCO2M%rH$^I_L z>B9P(X(t3o^1e*k20%B|w%U)}&~gd@Ptu2g4>3=sis4It>L+i(e#|x&JqbA<Kc0`! zFjP|Nn#5iEQ!9M1KOPtopO|3($Z`T*6HzTE{19NtAEU#Qk_u8&??z}C>FXz0uN&w% zy$AG+=cMhNN-$~o0ln~?CaIrXJQKqMDv5*2&|5zz9XeMZ7u$BbmIuL#3*R%`m@6rK zqJQ;wvU<pj+EwpzjaE0Ap(QQY_vyX$qH)b+LHRAjQ64E=%V=Mv+o<S=VRN;`x)``~ z&E7!#>%9i@sdJzI(viNg7M&9D-)tyb$=`@9j!b%wzzdNXjP2~Fc*12wpXm!6%ANNc z34Y~g-YPBhsJ$j$$oxYcVI|Fn`(%Un{{=1buKS=bm89)Ber`*9T5Y)JtRSerVOK2e zTajwo<e|3pa@>oxpTiqp1>I8Q>%}$IGrP;$#9eGH_p$Z^(f5&Jp24gthsvwMwy$9J zi8mM+Y+>u`xGF$otfEx}PW;*9OWzcunW;5;nczQ$^Yy418SP3sf`WrzKYRAM{Z9?$ zxLdjN)homI?FoF@a@PezE(-se*jK8mw*hLTWCirIVQl=SDi6y&tq^*74&Y$piRlat z96X|K0A$k<mL7{reqEK9$DkZ9q3ky4ie#$Zx5(E;z6A1oKTN9$ro-p+ND<#j3az-d zw6<Dp57N03>+GdZ{F8?=6cr@JR)j7y3&}D=af!bhiOLP~c}eu(4%pf~PZ#WX`|h(H zT3&VHy`xdn;f}G{3(Of+J2A0p$=uITL*Kk5%k2g_di<G$Rd*J6D^U1qLjOEm!2J!h z2Hhl{Reo25Q@pL`U)}qg5Ikapq^GOvRq5!>h<1BRGmH2xwtG!rr2$sfh?No8t0%_A z$dEmZy^*SFHo$%OZB#Hi4~(|H*vHWRqt~g1F@9wOP47ajk3RCR0ZWv5DpE{`q{6*R zA*c|jbX|~dg2k-u;h@M6VdARh`07aEr0G9*PyL+ZX)g^za?(nqtd$*daJN@9+2}!r zV_lTwLYY0|FI-}<X`7l=8R|x$)9g@gqo&Ehgr_hd3>%F>D-&yFq*dZ95c#G@%r4)P z_l;WstqH$4nq*%ODA#}z{$p~cw6|VJ;Tum*1nE?3NLo~Qy!|6~L2em7OdYzVS(eiU zVd}cDz<&4cMBT$}j6E|QUCQ@Vlc>n(_^a$J-0FONC;RB%HLInlasxJVTUUWwv}*#T zfN|y9qv$U0pT7`=1*1CMq&FeJqWj?abvCb@mjR5sKTLaL!#YF>W$J6Fm^KCGIsilF zLGTsvQg$=RTb;eLLwP#tfUrU{ysu7wN#lcO8a8lV;I1g_1yO^tPQ(=b_t&lEA-Zjd z*DvksgzU#{79+BhwCbfK*Kl?Alciub<*!mGdtSV0`Ke3KyLU3;e~K!Rn=?WMuVN<I zL~rfwa2h6_nh}{nL5mP-Xs1rwYjg1>&%L`Y7*R``y_NFJEDLRl2Te^)C&9ppmD?P; zK2)yA=6rr7yt?YFNOjmtvFV$VgkyTogMgSJ(c(m5FHT3x9-Zvo`lL4q-m0lf5D^?b zn)t){g<|i|=Q5$i_<a}rz)R65oSFs24*cFTz15qw1MA-Z%=;Ua3Wp>sU21qoag~S1 z(Kg&^WiTggImZKFV35Sdw(6r?MP;g$Tc-jVOWPrs2U?Df@|bh2DZj%mBY90PK5Trk z1`B<qA7mj`>%coOn*e|gjyrc6&7x?pUrz%PCP)0a&=2&8?ck0C=}xnrrFXOSgWqIQ zPj~!E^zQCEfQ^mIJDQo9Q3CaPe|3zXV2P^vDF4jr-sM{-`Ryk?)fXJ?Hww_&fSy*+ z##CkG#H}|vvI#kqb^ttpMuU&E<xp(Q%o*1|G1Rn~o}PAuN#&7o)74>z!E^g)WBC*N ziH`yCB3qWoqhw2ED$2~i=mY|m4v_a+Ag%P51tgf5$VdT~<+X{L+y9J?Pset4R~gR! z*e%%xP>iZ)wu5e<(gOf%wXn&-!|nr@aHHMT5mfNe@!n1Nfgatk$<KXEsV33T#8Z`) zRR7FDJSR_4A?nl@=+7pS;+@uE#EIZ&OM+s?;<xa&cH^}zyMuYmY$_VI1FS2k#fJmF z)eokA6ZSq4x)pR5zow{I*+gJ%?)vU@tctQqH#;Zi0DuhB;e~VK_5VfJw{*)y)j~}0 zakkhz(>zo7On6YwK=X5Hc0s}J-Pdb1PSREl4JpvpWlIAq`4cHuECDu7P|rNc%|6;2 zB7UxO4Xy6f(4WD@;tV|`n4+RQ)*$u5_sz2vOceP)pt}=9)q!dWtnk{{*qeZw9((E6 z+ciucsbJI>j&)d_<+JFC85bC@DFZ-A6_K8m1<%g@VKxNR^|aIlFY>exgpW~ih}+dI zZviE*z>J64A7s*$s8oypni#<34%k1!JLezW1GkaC`D^fiJszL;-9GtAFI;gFzi#5- zXmw6JuVTnDMD5t``tN)U+2`_>QjIl9ivtnyJXZ|_<U;O$tjYh6Q8NEWW~FiCcf}<b zh-Xmx6BAB3912T6uw3Ve5WTmRS4xda3Hjgb0}99C|H*{@HZBaAM&d>Cx3OGsI#ET) zYWVn4gy?K2y*OOm|A-Zkg4ucxNbaewsho-Z<xch2$o=2<GMOTY6A{!wNL4<|G7v*k z!X=ku%=pO%C4O6vj|Jx!V#ZrAmWDbhEsCX)U-fMKhrEyGsd2F`U3C)Mqx`pawk%mA zws$B5-b@7?9S1om!6G9{)qP>o8bBDU81@5axntio{#PC<5{C(Ogbi@Sou9wE!Q?KV zWJQm)V35VFOzxg>?YKPRRYKXDMw5u=K{VW9o%8**he1lPQ?#?UGG<_GY<yG?)rU7X zQ(BzgO!<w8d+l4kz7uW^<~@ln4Qv=L1`pIjjktAdUz?koyGl7U-c97+z66dcY@02{ zFYs#H(gt!D)}2&@yQM$i<kV|Y5m&)N4+45bR_`V{;aX0&EB<C@M-<#;d46OycJ}2E zz+0q-_Zx7A<>MiED6`dQ&OMj!&n=hYp!H^jJSV0t93c|eN6wME9ni@^bFGOtkCp?n zl12bCo;`w%a8Qc8HC7F)+3<KqN=oV~O-cEmVLpCnUh|8d5ef)UE45HAx7pJbPnL#; zeM<EDfIdB0YaALs$cfDW`xWd4_^1X3Sm!y^pJ?x6AYYjvYm=@}HMQasm&J2p*xFJ7 zyAJoncQaQAyE_5^#<wGtG+!w^noA>EA0tf5?}^G4)*prfx#iPqCnQ+1(#P9$VCMKx zv-OEM&?3P8{OnWM(~_6ZJRwYQdd_EL?GX-o0rr=_p1RYK=|2c&IDem7j?<LT3SmY8 zq6En97xvcKdHn-i<FjN5R2n>A=t7u`929Wb80=#}Dgs6}`2^1rG?U}pG>1zJ2S0zO zp(F!JKr`zybLO{iw;7=md@EyVa7cpr_kHOVfM|a;+uC=^<k%rFK;9%h6{?$#9~XKI z-t_8ezfQl?MpeO#s#q6a)96?qXE{9}4CQLQn^^I)_4auGP`Y=KRc;KIes`tvclW9h z$7&q9_|S6cr`ACzOM@y1>t{Eg;sM?s$hmg)sy(e;r{AZK*_G+C%fK*$#=h?!m09&o zl$p^+>Xirb#|?5wy%ZG{<>2Nn*E-PUgyg4UrwN{@mSv9}8k?SkLtJbIV8HP_4#V$Z z@XVmmP3PUESz-l6tBCvWa%<>k3Iy^C&Fhk4%saJ4IUVvpzzYjwqIvX;9h&qpS~|tW zeB%|}P?R-@)S(LS?JRRy6^eMS7g3S(#P`Cfg(!?bh0tQ^{aXLio*7dJGx(S!?vN5H zwerlFVtFYIHn=2g*gs+vs`lhb3dVc!9RCO3_kXd~kGb7sN20`_OSTGW*6*t07y;=) zEo%Nu@mG(A;j}p)h4KaMk7dEDmOysnkA)83Vc^;mY~8L@UQB;~KN68><$#jdi<vAl zY2o1KxGcSQA1Qid0Ipg3@Zl_ScYF1gcgJ&FY<cxaHS1P7b5^zkz(Yw0a49)2^dN#7 zX;wV}wb2_{Qc_BNl_@1V+tZsfZ2BM3Q?Bp3X(p$@<s$I(Ls0h`(7Eg%MYCVwS}?1K z^+cCo7hAc!?+$58@`13)a>3G}k#A2T$II-^Jvo3f4obZzc)k!ue8#6Fugpv(f^ySK zCL8$-{@$Afv!RUX6g<_s_j)QHcgMG^WYQN3rIjE&?FTXeRZecKs{E`dU$L)2c7^uH zJtnWNz6IPO3MFmi(b9suNl6K4Mkv#9u(?6e@19ZGhi-ffFaC1|5PB~E630H0W0f<K z8Em~mar|O?bq1GHum3N{Bm#b=M!4!Qqnemwu3N&@xqDC=Sg$gHlG$7Ji)H#|@*hfG zBWGynclemw&aqZ^tnGk?IBV66($9D2{{>iSU;a)VG29-e>D4TDJ67-o;6l_Gj!s1N z_r>@>8#TWFM<MuK1@vWO5j05ll6vWJNE#S?apdrxKW9M(ar;nNFI>Q44Z*?1_4?_m zh=FV07&!$+X`4m2K{i&)ddrD~#1i&g_Q`8Df(g|A+|hpDW&{j@ci!3FJBj6uOb9Q? zJ_@Ve4tE~Pv%U__yUTCc1NftSXy@Q%Wt9iaJf{a9&^8fHE#Ma_Np_Dc->pDuZppI6 z0}9DE`{ybuT54)x?YX>wZM2NUlKW2=ZvdC(eY+i_&l5v8RwGt(79iqg-_3k^UyTi= zcWM26eQ$cSu=@v(=GOkqj>?E>n!T2-Er4qijrT3h=dJqZtgG=At9w6@!l0oQ7hR;Q zucXlbC3;NcCK0w(H99;bb*=gK?Prf0g2lS#)?i^4P}Y9z@aOCW8_aQwEBIu_9H20n z<>ZFc0Yn^1?-jYwV`Kv3HmG~yONK+BB&%IZw9Z?aFOG`-dh=gj1}$7FNW50s+g5`d zSXYwBi1}`9UMc*alC9*u9z7~KuCOmKl72zz*Mf`gjf=w+kKW|JTay?TXH6Rp01fI} z*G%NoGmAr_HjupkJ~VrM#rDhr0an*fx)}UZ9X62z=aE#vh8*_uY;0^49s;cAR(ZGg z*#90)0)Ypw*(e(P<m7M_T}ZG?Det?9e8p6Ba$J-hw5%C`dxC;!v;Lk52|3x1s5W7H zhWto$>#KoBH)huXI$#syR{Y|HV)y6B@aUY_C98<TGKWd;-+evF)DyW#{QW+S!`~uo z(H4v1ulgfnk_(U2)LiYY8G+QCRfpr1*6>7?q(=sXqunADgle@wbcY5tvR@vEREH*J zG~sb&ZJw^!bn5|2)Vet|4~Maolv5BcbN+1*x=+0xk=O`ZZ9Vpw|H2+$Hft0mfVM!? zH*T;?a4FsEWytIaSQm8ed1|LTp6T3st03enVx>u?PR-aj<=S}j>UNMkhlE5|9RXWP zyM%vUKbnkEQ^56oHkMgCnV673iZ3{?KYOX}dqJ&w23Yc`I(d?sVgDyIh598z@R*{F z$d^b5Enln20uaz~7iCY+>cGH2B~1tr)165w0vd~7x1nWD1H#8iNvqb-p`5QQDH^~# z=zVGS0y%z03?gRP>2K4U>YqyV>&v8X&Ic0MNk-bazwNWIuqYddxe5I1g*uh6uFh&b zosHT<v1Fl)%MmJszTmThyZ~tVAI!aHTvJ>3E*elQASzo04NYZ>6al3dDHcGb_YTrS z=v_KCR1gs9y-6>j_ofI)C$s>8gd&|#1VSfg26gZK?*DtvJ->Uu-1T9Xu*zI>%{k^X zo@b0PQM&fey~gza>cR%rG&eVwLe~MG1a*ATYlm4}Nh2KLKF#XUA)J1lsCgYbakkWW z?y>oNS2bgQx|~kOT%8v;o<fu`nZvwpKGtGkuAd23kG_`@m+bxlGa4*p65Shq>W3Ti zO3dhT0DK-6o9%xJZho&G$@#7d@23;?@grPnOQAiY(;3Ynz{btpyKz<VH-IL#G5hlf zUdUtTI#@r{j+X`H<<4}*EG)2d4V-Q{Hz=LxX|n~qpW1`2H&B;_M<V)wiYLl7#t~q{ zoiXp01L2tp73b*zLeDNGE$(oCpHy^bgTe(}6<m(-HXx$M2l99Ih(x28NPB&vhh%E4 z@u2k^N^$9}(XF@OE8*_+Q~m>p_`-+on5F=Bo2?~6WbNw|pKTqYVZXEd!p0(kXQNe` z&^Mb{hZx8~!Iv@&hKwT~iy!7Xb_=QWrN2k9Hu^1eRWqo9@UknLI{<ff#+NIu)NlED z?@w5UhlMRH+BU~3&6fl6k)2C4b*1i5b9i_dDp0o&eC6c1e{^gT)IlJZFVBD4M%P?B z9L3A8E+iy^!*7Yp4id(k@}L4i>Mu`hI^zZ+Ef-4W@ln%n8}}A6Nu*f-6E_?05%K+_ zdakaulvkDOVhYl>=F{aBuTSae*@lfLMrv-fzJK!I4TCw>RZ}x6NYv%SU>^3Vlp@VP zv47m;T9njuT6%gG08B0-P3StnLJ&qtck?|Ml=b!NM9nOaDbW@?Ut3yOMMWD;#G<3M z@HimaDvRR37LU}J=3v<-zP&xw_woLN`>M*yM%?{m|Ix>p*!<L=eY|Jbs{lv~+iH4R z+SFOXK@_t$A&oxNdt2F9d^b_lY`9KzxWw~gnT<*)^Bv~(oz+x@OY~!pu%1FvfKYRz z6n6K$a_{OtmsHgU0oNL3bBxr4T!4fT2y%q>Vrn9F9qPth+gfifYzknNwB7wc6zRr( z0AeBDwFP`dG79R&Evz}3-Cg|3-s`&z!2pAsNVyj7<{5+iZ+{9HRHuF|%x5jo>lCH8 zMbXG_Y-W4JV1=t6O6VMlpw3%)J!3SBmlucp<Ln1tLvPz<a@J0supTN<B6uu{`}5^I zlTPr8+1*@BmEo+G&p0El0hD3X74tZFu$DaS9a#e}9{rZHSCSc-4?p>cAH4Mv9Us#J z%FGiP8PvPRuHKjD#MAB_3+84zbsWU@yS}Ck+{2KJhoEtTexJ^4KC-fqDyaTXV>O0X zK4QQGXG1q1$5gnzX^pejE5}2Jatm91{jGJ^5_&_;V>_<IyJPcDAm9D;UVHoc)KpbX ziWWcuDNkN4duMxJZw7-R{)c3GOQPC7&TVCi{GC6ys6<ml-f+Il+8y$<XVn!y$N&=f z{SrO<-*N4}@FMbRF9si_c!FE7-V$ZL2KH=@^`@5bMW2b||8TP!NQ|u4tS)&3@4({d z)vLMDV#iMYQ;~uE|1Iref{oMOL)HTUMoL@TU5Dzikq=?X4DUEPPq_#<&dA>Q^UtW< zA0Wu~A{CV(H_^xm^2!c<Olf`G%Iw(unqGVHTRIX9W)rWE%r+Q9ZgX8!UhN5fODpaq zBU+7`jE(QZMEV9(o^#LXzcF+<Mpzs9!L8VGOGlUd_;L$XgGO{o|Gae!`u`9ql61ex z)bt6*Ef-#wh_IMp63BBlM2TuNCUHkzD&(g!PE4xV+{#_d;Odp3P=`5dN08T6UQ*wa zv`yqw53hF{^B^!)O=ic+o=|0A_dR!&sL+rO^mfpRmOl#wVnKmvZvLX~CN>A+TQyZo ziRLxza{<)I0w<2QLh^m9=M1NQt~kH-ooJpD___2T96I()=H;XDl0EY@7g9!E1E8>h zaFf(x=e=5>z5zD0YIV{Q^kP0ug4g{#ot9WCIyl~(>^4Z4dkZ|KpDRiKs}W@gMtH*V zhC;jvw|PnJHXGl3AJ#;o>1f8P2KJFXwM3g+PTQJ}j+EzgWj+;|v%kO$xL#-YAg?;m z$gA)%?YP6-Tq`}#`BLG<4k}`71$?L=Il<F2Oy6^dFHfslU$5G!S4H9Or`6TlrslJA zJ2VCZ9a@e{lM|J*amhJ|n*O@PiH!IR;8j;!p9VqdOIV*G%NGQX*#{uBy_}7eW8m?F zU=(neKY>CM2YZzo7l9!Ia2koHZv`^Hg6NN)TD)$i&_5%d1Usr`Y)#c>hTdK98GJi& zb1_UszW;lo_nlfXVm_?@_~|LJz7T1Kd`m>V+SZt3b+gy{JMsYkjVT3{rKw!>Yd=5O zetF_TUoaNwI>ihhSSZC00nZb;VQ~S}ADCOK+L&C&9op9K=@Xep1+m@4EtkKT2vM^g zTsd~LO{ROU8X>L@Dz!BS0z|7A`_IK9+Yk&wV9ZmDK@vfDXlQCzyf9nY<M)$OI`ADv z_Rh?uOXcdw!Cu6Q;lfa1ymkpJ|HFs#^n*EX%B<0b!GQg73alc?Yf(t?aC2W8%S<k_ z8V2@|-92xw(7#q4ONlbD*izU&l(yYt;{zb|+qXv^n}`7oo4KB;{-k?ed-xUxkw<`M zzRi>=#{xMS#T_htj#G0UR#W0xd*nY8Cm;keur~EQ3&ghv1`z{7=XuO5CO;*1W+I+h zS`X`TvOLp3k1$kp=$Vy+00Zzj_cA!^mV^&x2as)CEQ(suwjCeCO60f*vy`IvZwD@G zLq^sVW7z}wxO|<amf@zk-d<&q;8EPi^kfV=EG#VQOzCN|afq>AkDH2oLTaI#d7Z7p z&xU{qln08jQk!x)MwjSC?0Xe<CyTfETn8_Tc<;E?q#yo>9knSJN^v+m*!OW-{>HPp zGqknw#ZiF;1WH^9c_K6BKK}WpQ>R)aF@hsq0}e$$)G4>&i7w%@h=uaPR?VMZc76~@ z?MuU<mt0e$*YsJ})?@yM+Al|q*oK8V74#BYzPgD)i_J)hWM!q>6IkEK#v>4K3*a9+ zfGs^6stIbmW$FiqVva%7w{)%C(~!3H8M~XQP9tv(+1S|18oBrTLpNsTT*ITI)BZ^< zTwVwQfwtzeOZ^d_aL|LUo7zR5X=7zZKf<sZ_`SrK)a({*_~GXkgZbyui71us+I(kP z{@4&be(T4*7rN`xm4a)N%CWRLlSWt?gz4buYxmVP8eHM&(DWF6gOJ?;0r8%f!L^y@ z`koJ1v*ykI4jtUC<{9DD$Qb&?L@Z(UZ_nSPyu14-6vsU`N3v@;X%ec|<>iV^2a=iY z0|hY^Y~bF#gWx&yEgi{;E%9vtUAy~|jNcqwX=Tk4UVlF!ygYQTYCI)}Z}L4wf38+8 zi06jyxnznE8D~q>xYv(ociGt2hYZdiY326V6{E}I_WpH=8v(8R&alKJpM&CE1C~(l z!%pv$U%&DASuB>(yC3A;akuKnd3z-2G+_g}#r3SwCA|VuKB|C(^Go!sDJpW<_+hy+ z7o?M4%w>1bwzy*PmGveu(;d3kB*<qy^6(~|M@dAUW?o`?M^7GMRd2iMUtW1NE?S@7 zW~F{Wt&cjY<BHt%FPmR{8NdKIo{|<V<ZiPD4HYte+ODaGs$8hoAb~iynA4V7d`ZWF z)oDO)#KJD%a_Ej#{A%gP40-Noo)LY|@R*Hx+yAT8$wiOG)fTKU3qix}CBhs(S(9%1 z;RlrWhvlz?m;aRcmF(;aUcAu7J14L!GqL+tyDVoG%WrX<6n9bdskUw+`n#JN`cJns zP&H&S&TWrdMsVu(98Qe`G_AGh*ExBYS|}G##R2sZ$t{%qH9K`_xZhf^%uJqWd+=|s zT}wyx9K)Ok6(-g4riY}Rx=T>}PriaU4Lpo4xnSw~b;|L;J@a6y0ZScvp}3jl@XKis zue{uoA1Hs<`@^Bz)U@zHf3gdjJf{GTyQ!}?=x^d`k>#COD{)SIrzpY0tweA0>==%; z|H>6D_+ZJqR+DZWMNKR0;7ECm)>6K!rw;CYhb-_>f$KTC;vLh>uU@rPM{L2-;p?xd zb62Mc!{~Res1d#bR;qD{oDfpGO7{>d_q~Hcmk@mNP5oV-?3{{<hpZM?9ID%S7nTWq zS{aGC0+_?06=*$m&QP^Lb@29xS>=X-?NleB>Z3a``tZzCgQ8l4a(OA6$63*TJCASt zp&XF<t%+IfVw>XYOxIP#*?x3q<`)zwiuloagw_5ie^7lK_~D9A;8j9js`710lwI3h ze=5Tm8W3^}t2b8pO%x~^DgmOv^-`B|FZZPjI3MIP9*17J<6GOx$~w?l&t=b>pg*}( zD*Tt$ZnruBBN1f3sX-IRqw9+Gu3fHi)=-U9`-(&IDXr6Rmabc)L&IYdvWCXWoNfN! z5ee!CneP5UHn3R`pO$_9E`#KWO1i)Tuchz5TQS4~IK%2M_4(TWgH`aN1(O&gAo5Ly zUARbBlQT$1#bBeoXFJ=a>$N{z{$wN~<wMW;c#lEWg^|isGobwDJx15;*jbO(s4;r~ z2n6(6cn`CFqyew;#DpOpm$!J*M|_@U&rxLIn;7(t)!VVe*w_rf4}qjB3wY1fCmOmA z`P#ezr7djS{|1jm3-%u3i8<*Ux1)Jt9==+9O;R^0jPKAU+3&s*AUE@VkaA&u&09}y zCR9x~MaSE~2*4jpRfBU|^H$stXSJ?{aBZHd-AMaPf98Xe**LdIz{;I}D~2wRyTETP z5Y3an+$;Z5XUToNz;y8yBK)rN$JRI`eln;3lY&nu$)$fY9CHteN^(EP{QNvv1Gu#| zfs_<YJ)ZkJG@RW*@k6^li%tj_Us%uCRJJi|k!c4+?RKUG{WGi=A#_rkw{ODBBbs4W z5z>t5k07-(vwvsfTmgyex&0&<k23<Tz63mB3ArSQEXn0?X<{mN68x8*Rv}gHE0*nJ zOYgWb;S&%8sD*USco8De`1?3Z&sRk%C}N&rv18d^@Z5roS{-6+t4Y#9{xm7ynCvzr zi8*&~+T0psR`@q}fjY<$q8_mwZ!R@tQfQKZ`zZ3|LZp&1opmIzPUx5E(+Fnp#3=X! zlA+!5-R(tN*_rpdAymRsVJfnIqRGT2W-bDdDKTD%xx4UbT(m|)E@nI4Wq+fV*pDyR z`@gS}Fs~;=-dFh&34VzI#iQGkjdRt<!?b^3FnMGFhl(Ij$7yZHJPOmGOiSK?xv9Or zQq}rr*D1?CauT-0Y`B+BlRYJGkkBLc25GxStjipl@-eX0)A1L0KbhvXda^)NAX3vu ztV$-7yy4Wv(tOCiMt!$_l0il{Qj}2jdE+=WdBYI6`$ga9RUjB2Lj3<jkU6J9ZQ1vc z54aoM*WTnjK$HQu#BNOepybA|NmoUo$4N>U)SA2B?8tr3_9MMbDw)R);nJ&XE6J@g ziT2*HF5|bsWYZ*iH*A~$w=7k7zGaC|<cZ^W)+{Nxfb>2jQ}`bbMKJ$qG1*TK-hhv$ z9MoV1jy>oiuOrn^k?*1}6$?HCLT4g2_!fmdoa=tJ9<NXW?{uek+cq@2nCUiX+WDld zjV{p0_V;H8jtdd-AgVUv^z4T#tJViCoj(g@L@PmjZ=_~EB<vW!OkKm7nQKJ+b%lBV z9Cgni4ou&q8|~^NZU_cFXH|k&xOR*#O(B^RF4SOQFtCp%y5kB*>h;YHsxjG_M(Q3b zl3FgEb#f=ds5lr3Hg^J@ToFOc$Jm-deT?&(L!>brN$ue3vCq^!hs?}r@Pn?qV5v2^ zFT)pnh8RSZi36oGo~I1z9ecL_m37y<my3K&?5a%G79J$v#Yvr^HJW|yM{0+AKoUi2 zr>Cex-Vj?GJXN?;C$J=#CLJnOmFD3Qx^uCrz2uGn)}#w|9-6Z4esXTs&Nl^e=Bba? zEMqI#$z?KEQ{I468nVwaTj7yd6>dIjj~L+iE9ayypuW~ONE2@^MBd<hlI*Du7l#4j zbyhU^7RNBCvUeC%MRhy=Friq#Q84TA!>IeDPWGUR#>qd3bfmThq|T$C2k<ot@m{;F zE{9D5;;@NH@r{jC*I~D$WQgvbA|$_-Z2bO8_l28oK7X>^t&no9#oCspL26h949<1w z0B3i@>W@4TcbD7I-Bs~!?)?+@omSEVozVv5$W<`F8xD)qVO~XM#h}49Z`7g4s>)W+ zQ4EUr2FB6!=Iaw=tlL~%ZMp5YJxjfFz&Z9twDA|Q{U%};eOV!qsSsUPMF?I4bX+%w zPDYvqh=XWmdgH2w-rXdM|M=e{80&yCu9~aqbNW6}i`(BCrq~$PxUkX7le%;l$l&>2 za2XQ8@`{J&y0zfIq`p6cs^4`Mh*q#>g=A1uF9vCKi80bc2T(7)gky7Eh=!no6Od5- zqJ#c_W@uoVmkp<_!MO^+>F*vSI=q~7L)sZ7a5HX3;%sPFf=H<iM3An6xO5CEq6tjD zS-(v+ao(>#-{TlWk?Qv}ZbloIICP%aTpD~l_@_)HLLDWKb86&^+&+8i_C;UIMBw{! zcD4^pzF7uq#C4rgrE;AWo@hWP5=iabl?wHB3Xx!-ppz0faXoE6Z;t|O-1Dm03~P&B zyG>@`q37b)gp1`ULU$A)=@8u@(t`RCT?nT`<Z#YRya{e_9d&7amDs1mi8PaD0x;#4 zn?buXzAhaN5=w|nj_NC58k!D4gPg?sDY``-(2tZ3zG(6p(h+yH!Zt4-^BrjzIpJVE zC@*d7X!HEYFUyQ?&Q~bHK3X)nCELwUj2{DRaE=s3Lm-sDN8GmJK9}gaum0{9i|w96 z^||NW?&aI)903uifz_yu*%7TZ-oV1*j7BerTuD2NXaL_AOhR}r<gWFuRqX*rvzi_X z1wQSbJkRW{0>|TGi%ko*Xl44q;j^~))MzBBY34^aDd+=}V<Tb#9|9Jd<sCbm`q=*7 zAJWE>gYrBg#1^UQ1zz;cIOLZ}^cK1&;<zps99Ha@A-4aWwlj(ky8*G&wu{!`AVkf= zc*lwcL4v_4)xkEFMfVwbDHnjchypG+8kXUoAp7lz;{If>7dCZk4t-2xcAHJ`$n%KA zjIP_F;FMFxDYzYomW3|xbZqF6xJtazMT3cKyraX)d0|K$)N)9d1_HB^QH#ry_HW$8 z%Ou9N;zCk0B6IC|({Gv+i5=o$mq~<>va%)W`Pw-}McjQ%ATa}kVBWhENE;2x_4sD* zFj;2U#Fq9Wx1AdKc|_pVYQvSgEB=&0&AI*c^_6#0D;~0QJJ1Dwe{mml#PJ~DIY8@g z3?o501^3ank{DA1Ei(=lQl{m<G5^lopicuU*B^<MFG0ta{W)w5TCtb<O)vfZ7b9uo zo2ZyPo{zNzeaBE9x<I+g9gsVaS=Tk31&yigM8Xm@JY3I5#mA&|qpY2IKmKrgK-6h{ zs}NG;4K0j35*g<hvq9R=0q>^k!G8Nn+i-Lak5n8im_***?JoS%l2yqD&`lwN#&Yrr zhwCjTq^HWipWzYlt-AFoS9XFkGIc^!O^QsLIZve=%Pa^Dbl9#0nIND>QZo=#^H)tP zj?%JPFwJfcl>OZQEaPR`Q`gqso#pc}Hcr7ULQWR_C$9=Z>eN)UqGzzqp^xLJ2LDtS zUGW+<5tGEU*pDG705CJ#yuM*YSY}>l<zVTVR4NDS2o=ZFz=1)k?2JlGWKPq$F<=TG zmcdhP$}#5Li67&vINUaDLWNTGV_g>9iwSXAubZlj2bIAu4^nsC3lz*lca~UK`p1eR ziF_^Cu=L=~jhGH|9q(j@xIMt@B1`R$9ppq3rV7*xMFnkNgI|gtmn_c>e>M?YbeWk) z?y!S)VdQ8R%a(C+exj{fwaS)K^?)$roflWM?yd$ll%lVAv$wYt*vvcja?WxY{LDFe zx#3C(4g+&;^E1{1x{)qcrre2FSfil_E$+Z@d>`a+QhM_&bZ0*%E}4)w^px8?E~ZC$ zwmg6*y1So#-raUu3Y@l4W)B@6B+woF7n36^`()szB|mtnSr=FuRbFgthxwO?i(X|m z0l>W9YZ(+yQ{xHU@vIioGRVUMYpfeLBz3b%fG@hcEU^xSe}@HqYu#1*P%fCxggf!i z72}}^uUqnPkI$Fj4iyC2(E6AUpWdms0;7e}9hE=W`x@0@-d@2K#*MoPI(N(z#g?ui z9=cOgZx)dLXBs$-u!Ow#rbQ~YV-`0MyNNDS1#d8@Pd{+8nb512s$tXnp56j<5j_TV zMzj_|@A7Zyd#~Q60CS$7t7%x^?C7i)2WogP#>VyMLfe)9)F;W7<_7<J?&xmE_CX7a z5iHkfrBDW;nw*hAggIb~3j~hfwXj{}_S=AUpM!IPrvD)zes`$s_=XwyMn3MK{<&PT zSW|?wcAEmS$8KhxZbK(9f)w<^pF2v;A&zj!`A6^)VmMZ|xv@<hgH}ZwavR=<)sU!s z`C!^&s2|Bfxo$A66dM-i76#@5>|)i}U)K(a0MqWPed8O`>yyBLQXIep{4o<*RVpTe zLKc)s6LRR6)1mQ>>8Ze2AfjvWS|3C5fD<wad#_jDd6a5(?{SXP^6u*(sn8m_BXg=) zJut`TdyY@{*fW}Zk`6xKcHBI)LCb9iDrz3m8b`&bra-f`V$Gd6;y^k6<vULMaES@5 zj*VSoVb5@#%<IDmp*b2fLLNB1#a*q)tz|fOVhu-Z$&cv_fXC(%y!t18EXt`@?}wDC zbl=hj(Fk<IDer6+dIwF!*wts}nvj;@yypkT&O2_0^%|D#e7>_8rt@ImQ+<!E?QOHF z>xHGQo)sr<qad6ue7*+*GnNqBJ=3DHLqUv5*CRpePce*qLndOn$D%ADkyFry!-^AN zvGk-IFI@>%$M=#ESMt~ur33!M+16s_gQV}cuETvne80-2u@1r(9R#v1l27tpqEH+K zR$5{tA?7t{fe(qGI6^bAeqxEy0m^5Q$%9xW9Mb{#fyUenHh<x;X+B21UQgQj@Qn{u z!qMqIX!X2jp9vn7>3j==GW3WaglI{UAx7d_Jk0{kE@a78foz-=t;}Z@SWMQ<13=`) zaR3O~-e-;c=rjkd+<zMaw_j2W-2qP6DxNj81h<DEI+W%HnP$?AmVe<Cy!6q1eLUD8 z2(K+?Q!WE55aLmZBav*{O9bHnzV)e)_Y(I=wy&l_YAA@PW3{VB`C##yxxx7|i%V%I zZGck;Ko9UW;xV&Rv8?rAhH<3O&VQW>I)W)6Q}X^VKzaz8j#eQNbe=(s>`%9VR*q(w zXHS1HWL|$9ZF9+2oG+SZiFDQeCa`-g2yx5xR|xPmU-fk<t>0?_ZXB^|o^^8ZgmYtu zN#$d&wd6P_+7NvU0MF<E)+N5f-q$@r>P~Q@sxhZG3pY!Cwc#HKrt-vXP#XzmA5tYp z_>6p{IlV;71lFR7HI^Zq3vaGap{>tF=D7phF}lD!8-m(GL<0TbVoh=}ec)i{$Ic}Q z`r<c<u6*<fMch2Z2oc}5B*=Rx)dJ^$xc7bovcjoZ;O7L(tZ_G-htn1IBTt>{<oOAI z?4Bw?&faFI45P<lk>C7r7G~I{=>x8cif_nMY@GbX#<oAy6c}IYO~Yi#MaPc7M(2+r zCo7wxRHR`>v5q}yL-Hp=gj$u!j%|=stgQM&BC2Y5exF3JK{NJxpqy|Aa{C(KhpIDc zbLCDs2E3#XeDQ~O2Sg&t@?(5@qxVi1@~wgcexwByZhY7FSfj%)JV*+r^b@c_6!lci z0OE*8-WAtr3`Ge;Yn98R0ODv!&Q!C{caOLv3{`591h3~j{60)pZs;S?o>ZFg190O@ zx(X67ocgTK!KEFt=Z8%}vWn>1k|lI=h{AoZwvdgue}|SM%>rPCOIny^W8m&-qo5Si z?#xN|)k2!hjsRPh8+>E$L5IHNVB?aepOJUwjaeBoF_X5Y`5tm#ms6VQn=uYCfiSg6 z4?;&T!Ad#}Wvn2Xta=)pJ!@E>IoQ`9j`XKaiQC8_%@@lAp*v|dM3W>KHUKJ=dEa{u zo|(d*Hz^xjf5*6fplLQSzeQ>+e`x5kT!L`pa(7!B@>45FqqasZCVSWpW)CcuxZ_TE zSnZz^&yk4?a(C*thKgCe6+2uKsRm^)VZ(76NpGlg9eBmC72r!f^>LEZ&Wmjhbm0xs z=IidLld+O2a+lldLc%`N{;Ih4w11v-mBxUOWcVOht@fK;UvWF&(BX>p2_t0|rn3)% zX*1o{Y0$WxfXFP6vw887ngXbDC~UK;6eHsEPSh-qh2d^N{n#?HG_>}2g3Ftr>*YGM zjt=yJ^gFMQ23}l+mEYmc0f8<M&#Jpigo_em8XzF0{ZKP+^5Y+y%LAPo+wI@JDHo^9 z$ubWh7}H*SF#<6dre^LfM#dd)mt3UG(t&<{iGd-%=fioP>wJOMxlSuy$wB1g>PqeT zK~ndoT+D#j0auq*%HO4#6@;F4i=!cd#7(fSe^XPRfq?<(gc*HTwKjVvNb2Pz>95bw zN?M?ndEoLzL71>Kk#63dOH7s#o3yN6B5Yy4Z574|xJD;C9OcX;;aNE;20hRxg`gL> z_o9bMl6J2@@eiM4bLQ1vJ^50YiJwXGnD>4^5>{^U8enj{q&$k1EFrmWXI|wOlK@-$ z9^=TYP)sHx3-Ws-Pz?G20rGSXji;g0Gt=|033aK`3GR}M0@eeTsVy+e-h9iO&HouO z)jsWz1&GkJCH{z*h~&$*1|}x2fbgcKtPPj?6jmbJ^iVpsBLk>Y9%mrHF#GpMY$pdv zDK|EDEM?+iW=!V7>{o=LUJ*x*52>2Eri^Kca2s;yN|EIo<|Qq-QW$l*lnB(#)TjQ_ zAE138mM#aR?Ra~+FJ!%0>3^_=v69|<rCv2(CDYGVmnL){j<cTqK4UqRp64Rw*5<T= zb05Ohy>Ujxxdq@B_q3ZZS@m7LfO{J;7Oi&eEt#(q_tZhlqJpzC=H~od&O-Q-C8&1A zvf@(<1>b}-NH~|sVe=QwJaMoy`qd4<(hQU*SEPwyM;eij*sRTd`d<*ftS<dy&uob# z=>I<bg;rXi_wh}-)Qm)*2j|4siiB^Lt^3)MtypBlC0$}i4V7OJUo+QxhM9K}SEQe~ zc0<@}l}*~%F0m90*M{fRs@l14J9wp;J{9y3F~!m0L35sLJl3{KnIlG^6rE);50C5_ zu_?Fidy=|*C@#9zLtnMrMgc~s*_KX_SP_E3XVqn8sGiqo=zJeLaY*}M>|T4tdf3z} zal|ymP(3Ap$gF{)31Bq)cH29aKo;Ybi#do?PFG$tKL*RAw^gL6V1j<TF!lv$$Nu%G zCH*!e<)nhYW3zlmK$U$q>p={X1%BDwtOaAjs;!d3<-|+<@GN)nin8{M@)P~2)WTvD z6zwZO1wEcO%qvYBI2|PA&N6!sLfReng^5pi5xnVxz$5QZ0yp#D;|!`ntr)%kh-)S0 zHmHjN$)_*SRmdZ-VTgIc{f9nwGrZmH$&-?Cj89C*l?l1noR+cRsE(WU4S71!96;Hb z)EVr1Kv)1Es`sNVM}mDuYn&3|-is=Hb&HAE&d$!R%|0){$o5n2!aENM1L5=0H>$SY z-2e--QT2(cb;fw$%tS?`Oe=`PfE)2>DOVnaHRDdSZhh^(Jv<Fg+v*B*YA-#sjTkP< zlL)R|CHg5jIwglI*=GTktGHpHP*?K>WFz$z7uwW}y0ZE@SUNatZog0i_<Ew#XrtQ( zCp%N@I0nu0K5zrs8tGU_Y4s|3=uWO?d@7AJ;wY3Lcwl;VhoIl7Uny&eJwyN$>Cyl6 z#`~IYATX(&#`2&V0h|lDlMD!c^LShC#Eu&jC5JDOpmoKcZ)R@J&w@lhW2CP2<#loJ zt~+Ztvj$d+0i!PF6T$EOBLTE{|CLJrLKu(iOenv(AHNqq8T>Z+iOPSgdHLf^2dXUb zCU8MIPtxb`x18aPR%qKGf~*YifFqDK`i^-$8jt3ei>U#es^I4LGAM7s>sC+isE@gb z`Xi6V$>l|m$cT@5>S=LYPx*yVfNEsAt#gcntc<u5ZExDFi$xH%xp112KBYG;;0szD z2#gdC53fYJjODA#7B|YxtnXCh#oXl}vbKjs_ytsZeP-$Ko@ngC0mfN*kKIObilJ6+ zrg$wNI9zN|1H9-IA0b0ZZ{GlGvwA4oy?SWnsyw+_o@tmuSZoVq$EdjfumWePte^+3 z3L^n}uHV%b(Gc>eOL(@H=5I5F#^sGJt{_lf(umw)t$?diHNO(`y4IrT%?^A5t#DSm z^*I){y6{%T*u3p-o;GaBXYiec7A#)#`SV|a(2e{f%4n*yR8a80#1iQ><UvjpxF%n_ z@x%%rpBmEhby!?cwMm|@f9NecePGtLn?&9wQ_FJfv^)EOiL|lXJ3_2GJT#@~eA@II zVd!hq&%lJ7qN|_Y!c49B#=Pfs#p$`l(p*Bkh!~>}>`k&e@<|HJFZwDmjgk*`q|XXN zDWH@9jD@Q(M;1{$pbM<&RV0!lPhF!^Ug`x&g|%3lo%C&(uqSK11f79tI;PmbuEeG? zokP#VzVeP*fIlwmaUkA^4SAQOhUlqdj||1q6|mD;Aj^95$9|Z!v7E~T?01MAgtz@M z$`l&}0D;cj5;YDo{>n7L#y$1BgS7Fx7N0%!e*L_dT&0!4L}6$lrEAo1p~k&y4zK-0 zpntH>5SQ(7i6h3G?ILtR-IF3R6<Oe&740RE|8#|LpPuquh71Ta=vXm5*pzBD4QgiI zF~A<-i~_=T05h;Zip6s#fr9%S(y6v@w9N2&WzQxz{PxJDjjMuwk#PVS;Eog+0J^lV zUeA>cq$LxA8eU`I5y3NyWCs>a?TJ8lIP=%YkG}|u5*tA-Jd;j+Xh=jEb}Wvd>^Hk- z>FnAwRrQ(wQ;ZssO9@8rtvF5TQGkK}({vFQb1y}P%@daOh?d0`n%uIyY{Q*6IjLN` zh5?*9Q@VW=r%*Yf)$@srL+P{Tcre>)Pn=#DSz!p>3A<=+*}8J*#mA9tDB?Wt5!ve5 z%Om5oazO^%|Emk7)2z5~bjMAhyho`C`ewkLd2E%5Mks)(yWGXwnXncOOvr`4`(C?= zz!Bv@lWz=B_yDh^wn}o5kiYPhJ26n_$L9BTc5pW_QQF1kc64mVP1n8?dAr>_pdC{f z>CA&08i2=`t>V)@*j)#|$<~v=T_-LvueY}9X7L3WfV=}EowLgKXMu`WY-L)e@8*#Q zBB-`A)2mEOXhU~QI1O?)SL#4+Jum&Unvpo?x>ir_$Xh+T=KwEprSn8$(qKlSC3m7^ zfXe1{B+;oQk#4crE&%$CRv|24X1}H%Ot;bU+7ooxCKEB4SzBZ-Yu6#&qT8!2dpZ=D z(0yj<^Ql|{z;c<#LlG)|C4i9c?o-*E7l;LZU<i};{P(FCz=5<_u<ja-QUea8hGzse zcSjD`n#M2_uY&`1@LNjp1*cJDA+T`ePXnnr$3SnMxyyZTtZLf_YlFTmj~4K<1q3?I z;dM-jV6TbT<HdSpP$$vA{bjj|oyRnuV7P(hf?*G|`D_*lyQWJ5I=rUt{(u1LyF}k# zEnU*;$?v^Z*lT_O2q34L8A5+%0AUfmC63O#;@At?i_Ty9l>d$rEEfODL)feLcC+j_ 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 diff --git a/public/assets/app3.png b/public/assets/app3.png deleted file mode 100644 index 0a81ca7a347bd8c0e701c0a303994caf460a82aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172155 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 Yl-S)nPvR`7%qy#<uBV3n{qEC$1F4;_*#H0l diff --git a/public/assets/app4.png b/public/assets/app4.png deleted file mode 100644 index 4c2b4057990aacb0793625d3ee06802e1ec72c58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 218225 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 hdp+`nXH3jmi1x?J9-G4j{ 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ⅅ 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%0DJ#p1^T%2O2BW(c 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%0AYv{|NFHBar5Bk z7XQrI2O1AjAFKr0D5Bmi86S@%sQ-To`|fzE`~LrfL^4`-xJpJt$lf9(n-B_-UFMNJ z8g{s3C-azDC3}}pD$a3?R7m#Tqhp-!>!a(s?)(1!e&5G^{;ETrb3W(&dB0z;=j%Br zd`hXC{&ao|Yf|*L5mi2kcVr5ZRph<JC;{lMtIO)p>#hkfb`f*jYpAvUQjC8Ije5P` zBRgkhYfT%&zjB0n@)JA3VgQS+1{%U>yB2tcz^rqk(@gUaB^BO2gn0i!aFEKoZkeuI zOQV@Bv~b3AR<YN5omb+|02%Gz)q1`T2);%F4hN37j3r()OOfz7%cg8^oNJp$wg-k) zFiZ!-NmHPP$UUV(NBt|<NM;S3KGXZ1Fs>jU0l2ZF<6K_};E)jeZocTLPC2!)v3Y;P z@%6;T9I~h<QO7w?WxUB#REvPZ|1_bDWtN63Q>k?{&&sG~Q^;*03@MXNuG0GL=IX~4 z$m{DIoH*#0u(m>%S0UL@1Pv-Ms6WsGC;J#6VX@%F31;l}v!?GQ{pP%ek9w^<L<-o! zTzd?xwA)LrdV$ca03Iz~QTpz`0Ea*Yi~s~G>bvdKdmTfNRW&v?CL|a_|1>$o%pk0= z4@(1a3TF8QT(To=FvD*2>F<Ms85lr$n(2%!$j9bGuL60+ZF;3*#Aioa-mD?v$|x9r zv^Fqb4p{@KNt&eF<vjdgSAe)*F4y3x-Si62WO_CpHaAddLrDDG^t7O~3P6MQ4zqS& zfKLLUL9YR3rF8;OFp@tdzaEs#VEcvs1{g53iHAruRHnv$sy2Wj#&jpdRTdm2W$~2+ zzKXi?eKMg@C^Z3ZMX0GndgOq^M0~9Ae72>}B|vfV=I)wd7v8?&I=z*-BOHxPpyD$h zg29pHiJ^P;kKZ~f-7dKqYck(szEb~vO4v$O;>6zU-REBoPm8(50MbW=7^MRQvbj*j zHht0>E)95|*?s|-BP-{(;2V#_oqYicX0YtZ|1m;;<S2dB#H86%y>6v`XlW-V8ADfK zt?rAiSr}M741~czv~^I=KLV2(xG+6E0QT^Ik&34Br&eFsian;a>*%RLxYxC7?dh|u zreMakV>PRZQ$W&rc6UO8C$}aS6th0^|FL%EN)fNq?!A7<nF14pn@x6gwx|5J{h|Tg z18$kU{S36u8h0y#0}smULxc7Hn1GW4rxr-~B6lWU$#HI9)CX4vA>gM|lM9<#SRdAi z&8Vv4U-Z9i-^MmT66l3U9<*;1<wwGW5>#`byx98wM80~|kDkV3X4>*T-bBCCQXU)~ zogeUYoRDJ#4qn_HR{*JOFhV#gU_>y~uqlt`Gqo1+9S@IDa|Zt<U+CHe)dPB?K(s7T z*TH{R_ykr|<#|Ir83?e48ysF&x=>QF@4dX%KqPc3WWRXPw)7|E?Z+U6huDDg>@gM) zhciD`j+?x?MgDlEH18F-#qR8o))rSpVr);HeEvNs@Z#CCImdSZ&<1m=DYMLe<YqkX zdb;xta;W|6G@YJ)BlcA=qp&cAd-i1mV-OWm4d3PUy{S;+yKW5ySKY7Z-}o@kQ0Je6 z&Gjy;k{5rM=w_%OxvcYZf`_W<_w_T<JQ5G}wy*iVQFERfuo3E(__UG^SY*RIy;O#j z6bPF9rj~}KG7zdASi(6C(V8W?0sB!aO`45)T@1Xv;6rVK*BftQygm}`tRwm^JFqdG zxD=hbZG|^OAdhdz1jY7Lz7FNodP?PRZ7Mel9EtFirmI=Gr^*71K`5I$XdMiKOn|+i zS=-VlK@pE&W@)E6>mDTQ0P|Ycgm{{BlKp`24^m_X$AL?J7&dx!Zu%qa8c?$@^~$5B zemyi(5dUm(+jM9bacSe<b}6ntSe`jwQQ_8SM&^y{?j4Y58ICOx`v~EtmBd$xj_$Al zz&@}#aj#Grv%EnQ6)tNf{sbU2)2fkvh&Enu{Jg!%1~gruA|9O&21gt8)rdr+oNVhE zG$3JAkRxjMg@?)(;~)>-aJ~(Jn(Ehb+khL^LrFrjcgUWYS9B0ekRbA~`EF>agw1|| zxE3%~S9$*&FxA5u%!3~*r6q((y6vsdw3PZ%G(U$Tl@vOems{51Zxak|_xRK_5NA7H z2iJS=8YGCj@txIA2o4TUlRggi7hze=EJ0fHfUz2}7-TIf0d)F`S6$$@4k{V$`3Ap- z8*Jh$E#<#}!)FYfByn}@z<}MqRP>@oP6-vpgTSyn4V)V{?p-PPj)BCsFzB>h>F#N= zmPa|&mZ$*~XT<x?PKbf6(!F~z82O5G6~LXN&&&gN4;(LQfsJV_t0S*BVq{kXsfa*Z z#l=(ut%tO>ey7G(8vW&gKbXA!NDmYdIBY5`N`HcexZd{7V2jZ$JGbP4f?7n9ULugb zL4Uz%*~ODL+2LqzjXfz*Lopz+tt%nCQa<wb5DK*i5ys{hdRCJF$q!0C^E!9v<L4!O zUBJ~Ry!p#9LrWLdte~{CG|0=v-%mzkd#>JcJtANUwXRC!rc4D8GY?Zox=S-$9S7P7 z5RQ@3yLvl#rV6k72G6e^_n^iFjCWLQ5|faj08rPn*pgTw(HnSWAYRc$II{aYij1ah z4X2u-;vuoI^bT*rNQA{_w%5mwm@$Cn0fC=@j13~Kq>%y<F)l{>xzC?(Udep;;k*-S z71EYfK?0D~#qPl9X9Qb+3(!OI@P~|)$$<=sqy0n!x{EZ^U+ysw{U*Xdf9<sC6~ge5 z(BTD?(3#U4JD|3KvKkU1YvcYr;lA>44u!Ne>^#<65*uon8;*MajJ6-@jfTF>JIKur zXu{cZ7e$zXk3iiXBj(%?c;9)Q)$X9R3ZPNXE*P(wYN)^RSkHqR%32vH5H?m_(;~h7 z{T|@Hbz$ZSJR6K#grFF)W^w=tvz8`r1ZhQTZ_R;t3>u<P#ry0);8t3*ItMV|&a3{R zKAg%Gv)qS34|<ztU7fCj{CQtslqPNK?S1D^5;NpqUc0Ie$l}Gbe(cBC^N8DW=E1-+ z0(y_!Uu}~dv`t#65mhcrWtE#xq<n%vu93tHi~_>qSG#~=7io*FIt+<G00OY3z6OG{ z2NWsQ=5iiAq87;7`29WbS4AwVB2)={F)j%N3Qus%u%Sp;mgnej4A3Bykm57I+S+}H zp&Rf5!co1~wq{|k^_^j)R~u~p#B}mxJ@F+kz;ytMdaQCxmtLa&bj0|out<~lK+oyZ zCM2lz4-jO)3#}p@yGyi`RDI&lX6MWPR>HplHkWm3LFR$a%oUMInOcJT??4Y;`ZM!B zRl^O45-EG$0W&VZMtfo63Bb_GWG@_>Kv$6?gOvgqJQ3k)6^L9&fwAjPh{c7L(X8oS zJiIq7zt9O&r~EhbJ#$cY!<D&w`x0EE<IsLKz}h%+jLlhk0>+D;vqD<bRoYBYXZ%w& zAl>8%01%<VxAyCam91rzJKsNzh8*Q7Ok82V3v`#|MfC`?P5%&}GK6<(rKK>86cM(l zA>9Lq-)pWtN~Tvlot>SBZyVMdCh-B09wcEf7R$Ou{sK(;!_EnafryGwVQfTVt*WoV zRvdH5b)mOctglBT+iF55zN3s4g=#zi7{NOpfzy)<WL%{YcgjCphih~4Z}{r-lzwhB zXp8*5@SeLEDed(j;Uxe-r7sE+*<N73uR!=78m4#6SmdmmT79_>X!H#X!~;o8jJIQe zbIagbA1n;e`E((-9W{|OY;W=@m$^U8qn<x=Q87@ys@VnVCf@VSpI~$5QER@rOe?OV z*Z;?PPU+GY>xZKf=*>mvD(;JXdcQ7K@}pE9#fS{X-0!u3#ttFc#pE3hPutks{7Ac} z9xU_rS}n5hNV#k@M$A~5x?w8ya_NwaP^(AAM}r_+&*g40-kB~k&9agsFVc)*#9m$` zEmk_Z7N#ajf{=F3hm9D<!|<nw|D&V1=t=RPj%Zb`R85u0GSW{^{x?P`idDI4bP3fI zA{0QV_zEK_ojYs@Mo!?ktd<V!J?}T)B(~d98+46zxTFB*bHwBFCk^6!HCZydi_Q`q z$YgE}K9ky{f>M33KI$VkSH6QSNOlq)naE$6`_-=X@lN3}USE63)RHYDFVOMOmiQ+g zbL=qq4NnTzr{&&;QiK(J?w*t^<n-DX=okbhU*<R$c^a@Q^A%RxxL?LB6#zoW-I+Xf ze!tf=3!X^mhh-wQA@N=^;KUx@^#Rmm8AEPQVd`@NranOFg4m|fqj^yCmT>0^SJWr4 zeexRUapgN3pB7GX3J3YKBEKok=`0OcI4;1Q_oe%qgx7?tomUR-pd<<g8Mdg#Q{{Iq z?L2w`h6~UYe<8CC&MyzwW^HLAVncuqDqsnIPJ4S~fMgIjmO%j_G8$Fql%E)I#b9Aq zlb#r5`by?4Y$6YycBzirXhd5yi6UqB=no|h%JYS@Q!SZy^+}O0PQd}J{bs2$mkMyN zmw`(M8WjQ|ZdZ>Ck!z7H(SYIs;EmCr%nA<F;SnlJVN<W+Faf}VCS<;0sIn0g5P7=p z!GQUi4F}=9qj}qzq0UeVch$e}Nmc6wKZx^&B@V5tm{ac#11Fb9%DH{I69AQ@`T_Au zi>%54rwW+}PCMF*g>rGyB3uEg)Q$8oKk{`mZMsJvrQX|RL4_^t?Kp<|p=LfiWtjvF zH!?*_NvXO!V*9|mcuk!1>yk_zgC2`ZbE5j)2!?ZlSHhu#c&@N0YX%o<lcgR6+fvr2 z)U-5HAmcw1(a8aU-;JHKzV<Mg2Pl&(@8hCu7OB1qJ*|K&!eRaqaN6JmOpxpsucFM~ zf&`KJ(eN?~Aw_Ty<oB|4(k{`*{c4E)jaoi_v5!^Ho3R2>q7EpVJtCp@FjxkuA&lCy zI-Y&OCr<n6{_Ac<5LbX0Aak+52_S>}siCqypMRXoI^I=0mN2*-aUl_LJ2{{8+wFt} zss<jY!2o-L`!!np*JI_h&i6HWpZXsFz`k`8xub!sRv9Jxi#%E>o(hW5HM64pbAdoT zI`sEe6sf%q!|g|f!?RVzWtZL-_B*k~Zz;^fwT!&q33kFFji@Uldao-tg$W!QnCV{z zhFNoS-3voz!hQz1|BV!O9avzNqn#ZcN#E<^?QIshJHUDv<U@a@Xm)mL;%py>7&apj zT$i}h{i(f88@wCLao3EPm5jX^*piCW0U1G|mZqVk?9O{Swsw=?l(;9APz)cC`#7@4 zN(ppNIekcv*C!lM1|E6O5NFZ9aIeA(vlwn!^Q60XLLwg};y7aMD~u6^0NnoEa?AdO z(QoQDl3#Y6uG?h%aG4{E5etsUj106*c25-L!A+yk$@A2COL&?;<=M0g2ffVn+zc2+ zc({n{-q}Kv>`u35P$2s4Bn5}9ueFT8;Dkvx%<mv5ReLmg1m2_XMR<>68E1c4mBp~x zFN*5_4J5**K;i;HcLl%Vi=JS&%92^Ef<UriDpZed`SHUX$kX<5?#TOuyl;+xEx!fO z(Q0Fg)c>e@YVt1@nEb5Y?Ei-ameU5hdvH|2C>R=UdJvQ2V|@}kN@3(B=p)z=Gb6GL z!PFOTHC9550&px;l-qR<<A9sw(KHVpEkL4ZdzW?;iKp(6({8*AE@_ZMIxtv#R9|r_ z4SZl7zm%ssYKHU-i0t?{c<&+cC5qZWl`sD~;^Eq2Def`S(ajN%1`gso6hT9>mjS;( zQdMdRBeNSww6Mn1OYr5cPV-db&j5>F!;+z^A+Nk<X$6lV*Ajv<j=R7nqIo6#b7L$K zvCpAGq0DtjNn|vmYPWDIQbx?6fQ&${*MERqe{qZ)7iWl5E+4j|G_Q?}9`ja^Jzofu zjDPJ{U5c<@3HVjd%+AK{PdKJ9{4%FN-nzV`BM8l=-|obt!guP_mUjafGD#qHnVWoH z*Wj~AvRmr+mzZk=%uG5tQ{voQ7dxLf^z_R;?f?M|fD8rZyaABIY_~Jp?}YT*W9ttT zRc_sEfO!j8e>3vm;yfTMl4~=jefedFy&K|#$Rxk1ehbzYgrtFcIQVJkS^B<>bZ7PV z+bk)@U{;~k)Jm1*SDuM}Gl)-*dO*7l7Sx|Q*!RXgzJxJR<c(}LuQLAJH>%LA&yzYq z`fR}$j2XhyX#*<GrtUS8spO-;00uSjA^ET&+jml!n;ko*2Q*-Z<sds0N+1lWvF(z* z-i+`Jd%Qn?s<LU3a-SW2T)&UE{#%^(zpdB@8GEfCV{I<hc?WAlso%ZyGiOfwVZW0J zlyYaCh8klvUS!M^tgmfM4D#1ArC+H@F}u3RS7vEpRJ|Y1*Xn8q@qC~mX@I4MsBa95 z&U+d3ZI#)95&QM^b-GLvSbe$!L(5+PVY)65*QBL3=D$Q1GN+5b<m2rP9EB)<4>z!? zKb29nS2+s`INa5{yX%i$u-8r|eG$s?T{Y+cI@fh1!y8}++}74Is5w2f-z+g*Q;vGT zz6R>L6HPn0Qh6%bZ=o|>7#qwZ)!CYQh2s+cH?i3%*yx*|+d23I5O3oO+?D}Vhuct3 zNN&pn6ppCa1C&$Cu`z!AkQWFeDA%4=exb><IrB~<caM}Ao!;Vn!u~l!Mf?g|m*8Gr zVD9gQRGzmvJUmn5wwXEie8eM~!?48ZuCHB%<N3imV8F4q2o8-yPkcG^Gk1EO?m+q< zdez_>kS{<sqIB4WbGh;emfJ&KOjvji66ub*2oQX@*HjR=wWvm|l}mi8bGoueoWN9D zCj!pg-1MOkWdI)D*`$M&kP0U(Dk5^2q!>R9cWS-^pd4#Q(78x2k3v~TDdY8S>iUm= z<ug%n@-U+>X*&q>DmO%Qj&=|}VWdME<dLFztj9<5Mda5eQ55RQ#9`BimLMTT<a$Km zda`F!V1R-X%ma@o{WwgP)-J@+zyI^qfpRb{Bc?oA3cYbEBRa(2nPv(aZNE7dEQWjy zu*EcKuX8Nx`#1_1j4y`t@E%wLswOM~d*!`oOl%Oy5K(RuE-uqRD{8M-1okJ$UroU^ zS~eS~wb|sPEJ?~Ea(ooc)Ne|`<w)B14gaa<)~^Kk7x$W*QSphp)dn|Pe?p2CXqgTk z2-1<*>}-H{5cF7CH<?LMUhncvGN9!d==TFD(B@PIE2|mY&*k9Z1K5ZP+jls=R2=U- zQ6=&FPc((iV5Da&jP&bo({K>w^*h4RkfrR6@Ul??HWpfDjVZ+Z;`xRmGB_UILCpl~ z2BpsXf8!T2mz?uUOQVe!1_Q2+12{v}NWQgJeFGiImoa_n{-TUD7z@G`gSU4E1W8Yy z-3C2TT!9;8ug`AHKE13A01lKaU<QM@wQSbF4ff3dpGX2el@Q6BPILi8H(|G06#UhZ zpfwU;=|Ri|V1Mp2pU9CnjXZAb?-cL>GST<#G$d`2X%A(R;6I!l$-n+I9BA@^uWo~$ z@8(mrf4YCzyWtV2wSOWae*#C7|Mr}KQFRItfU}X(=3}<PG7=sfKpg8JoN1flfeU7S z7pb`9X^c%9YKYjiej+JUH4l+%sSYxByKTq1Z*~%8d#9kfamus)*ddp@>qjBe2YwWH zb~&Ig#<Ha~`6~9Qcn;%!ZKq+@c<BzOYdg89dcnb<lQCp+)~M&AvL@;$r4sE)7dfwq zOKfR#DZFcXMZ^#90pLdw4<i@Gf!lN1;a%(2nOD?9HN&{xS#iIzKiQ67z0s2_M<=fP zK=u+q18EGCS!}Q(4G|TS`NLP_==WjOz_uw{iB4uB(I{_pz)zz=na5W;+;Oj|j!rT} zDUD(Lko?I^pw5868-~ab#QbB50Xsa*U&nKx<t&37GY;Owj{s7*Ma@4S;P;C>LMnMy z0=9%c!A0$J!=L%%{Ei2i$~#nih3R+JQG{wv2Cu#(2BpHu%1y~TlSuRZG8U<1pXx#r zKkV^E<n)HGiVBZ@fm*f+#%sB3;G^6Q^lh|`B+A>2Fb~caM4UA=HM??Lm8@uJq$b>+ z8+&{E6pVw6(wl^j3aEYJJ_i<M$9#d;5mU$9$N_>OV@|?BCBO|7`OrM3dq?+934%Qd zCR%%H5Rm^N5}x~P=x{04QvCE<G}KZAQW^|tYp>847%<?jMn8R?ra0d5;%f|sX~Iv1 zhp%s1c3K53#1+8~YVQa~rCV!9faw4@Mu9XTP%BjpM!lKK;JBX{n5(P<)9An#Kl2Rr z^%h}-iKuMbd9*&}kAv|FI+s9^!{A-?(a>q3N=8{O2T(YOGs(w|X@x~idcHgRjO`rS zoo;cCNv4&!I+^Y%Wf@&@SR`EQaBl?-@?CQ8%QnNVV5Y-DC8{R>jHqa^fwJtClldA| z<ky1w;kHQSwzNj|h<mYxQY|@)?+4Y?FR~qGr^kDzFl3c)lsMS-&>g0V$g#2k0tB3} z<p*VFei}m3@_ILCw+z$8n=m1tf9)dl-I<T}hOJq#fzP`SAkxOS~bu*k4&&<Gi0 zV;Z<r_O7_V!Flr#t0`X{Axvp|ih_Q2r!eenk8AVB7mz{qe8J;c8Qg<|$wShqso7-x z?X&4Pg`cuVP@&Ti*(hE3g3<E}GCyI}5G3%?sSDwq&sa!c6DNb^9SG|nlROzjsC+o^ ziLpHkbJ37K(~D`R%AbDZ_f+RAzXTi_<-^*>$@&KU$XK|>IleCgJZ~t1+jbf!d5ei- zgETbm?(U0UCY^yZa1PJb9iH5Vf98IdL_<y8_)_rWcAc6RxoHYPFen3dqt8|_{tfot zwC2>S60V#Cxl(q&#|n$xy5fM3@tGDpkM}Vn@?yx*qaUq}d!VQXS_6)d#(+WdtnosN z4c&~lgZhDx)a)48F0e-Ud+7-VHygcUslfN>JUI8rN87FhP9G=ryL-e#f$7i`NVKju zxXv@|s@s0nI6n$vD@j-51NhtN(r)9?4I4eKQX(m`Ji=A=xn9i70a8Mf?DRiNhxfi{ zg*hfA`=lS#UM!Bg5UVG>(bbey7EDFbZVA(eui~D1%*ghJ>`=#5A%yuN`y2A;aJx6k z5wCznqfvqd^9ua6=!AFW3E!N2<}7@(KJ=!JbQ{VxQ(_XMKpqd1co&;etS%8Gofwlq zrb@T;mQ)`)yW8q5Q2XYjU};(~UAj)@wijqHN@ZXCbm7$FL(A-HVs-D2T@DsJfjuxN zrw=4nWey@>K|S<f=I^&aNg0;Q+WivzeUM^Vt3~R^fmBMrpG><M7O7esx}YcNHm@~F z)lB>|Ng)zl1<Mjmg&`NF?=)+4nKUI??^a=*$%!Vr-ub>!tzm5%*-%hz;K+=USC~~W zcE^!Mbbz%*zOT<1fg0Liz{PZU__ZUpEM;2MoB;&2jaUA7+4vg{g$cS)jJErrrY31z zd+XZOtOUrBm@dh$+A@-ew<Ga(zmww$D!x-v(Ssq+3%=v=Y964Y6y8B`GO>-Ad5fjh z+QwXqXbnwrBd8pm)lnqTXrU(3#vH^;nqsCi<JVvwj=Q?%P|@N+@8e(neOPPuM`h(^ z#d?bzFX0e2JfY3w{{3WWa~C@ACkNJ2s#+Xo0p&|fFx0&bxL)nNKdaa|{0MF)ovQq8 z?xUkVg7rI`Q+uuVrcuwHnfcA-oe{0X(mgQNOmDJx4R7E36%pBOVLYuz5|5OI$hjsE z1Fg&nAb=EVq&HI<KgqEgPrjLnI~J3GDQC&XeoaaE&TZ&%UDOMh#cC?&n*6_Aqw|<} z+bh#|(PCGt<$HZF<0`UVk_0|NTMpDxi;8-OJ3N<yyzPQi8<erj+7RrME-h4VmJRvb zYs>hDJAw0p#`LgigZjRq%`Gb6MoD+vYQ)4cjHTYXh2gkfwA#vHVOk@4cOKGQ@vh#$ z=y+9gMtmn5xVnXCd-UcZNZbG{6y5ccLdlAdc^MAU=4!<0;VSj~9gSOL@lJW;p=!GC zQX5=}w~rQgKRF=G*E;vy5A1(p@oudX<eVfLRatE*oK*1(vBoFu?4-)9=V#axUSi10 zZ{#J!$>v=elm>P(VlI<ydem5ChEG(GKp8ZY!!SetZ2R?rwb7rt_KY710G4IN2%Nxz zQw@E*NbzED|7Q>#yv1~ND}dS08GQ}*rKNcqfn!(+ua?9gp4wYuMgvPmHWT0R!wKwh z2eDd@qJSM3O&gdn2zL>LW8adhkF=Ug*9{10@)VENQ-4&jgi9xees#3>SBeBGRX#OR z=R)-o7<L9?H{jg9rFN}B=I1kTqEauxO7CjI^m)qgQR=Nh<XhWI3KTV#1aFVb_H~-@ z4&bMcu$C}lEt0kroQ#j@CA0U#Z8i8OLm^<+>+J&G90zanW>gsS4JH`WTxa*&>A4!T zR#aC`O0+DMk$LyJtchIM9f$EiQy;d)P>9@|jBvjs^ZjLDr3+|Owia15C~ePaWAe<H zikGIp<SSj+n>O$%x`pg4=HHQO=gc5yzfDG~f&UxJ-F0SQ<YjZQ70)kB$Yn9Sx{jd7 zECo4*g+87}3~mcx#b(^CQ<M41Zj6O(WW1};?o2V!_MVEvO(~w?7z$?7dEAPi(Bu$w zHNtxVR8y0jiIo{vViiE{g&<$O(#*j9?RY_A4B<?F(G}c)#scxUz=lPTmuu$1UWT|? zc#FU2c8y&Sl8{O4;53YI%i<DeqZ>@14Pz<g&eg_jJ<#qQBjMq)QI5J`O8%45E++Mv zSBO8U)TRXRVsTGz;qi*+eSnw;=LlW~+f}@N%jf%m=M1p2uGObruBuHj<K3vi)?g3l zJTU0BB*v|!pH|mg{vt;N_%>hu{30-h(Te93;o_hW=i}3vIc$6_whewTYyA6O46Cqk z_13(M!^2((O@pJWyNcCrcfWI{l=t-3)CVD#5D=T9LqN`=_l95mS+Gw3e9v9NiKQi- zS0gH94~}5b<>d<ZS0zj2UciZ*r@rZXcEs9BpN~x--beZ34Hc`}Fo0I8542BFh@8y` zs%V1gG*ohI95n|*G6Qo-*@i%Pk9O);ZY19?iQ7IWGGc-C0>{)a5K@5=JQ?hGmG~|4 zBopcf>mQAo3(ZG8UhUPTCg8uXJWHsDCD3`Z8exkbK<jJB6n}X4a1ksl(iAV2AES?` z1Z4qe%|YfayEU7ljGP$vjemvN-%`W?gKY5e!Va8m$Y+xPJr$prUF6g_$K{XO_Hb<M zS7tCJl-_-pc*GPJ`HSl@oUKQ;??roU1xIY3n0%ThQm^N^Roh$Q|5M6jO6Hoc;Njg+ zU7^J%PuQ|nbKXd8O4$p`WLd3|w5LXTY)~?=UZiw+d>U<tehl2gtcZmE^6nfT*lhAz z#VQaB54jb1iTlg_PWYN5jHAcxmK(M+ga!@j??o-^bq9oJtW2HVOP33<&y3#$7r$l8 zqf!H~_Ajlrk_-2Bp#lXn)>A4PCc&L;L>F$@e3vyUuz7&m{!v0`d3RK(jFuW+(4gO9 z+MycD)_|S^>Yc<<mBX4K6!_Em7vrChWd#+o@Kc5ois)(x@yY2e#i@`fP})-Q-LxB2 zb8yAU61EBiVJ-KSps8U;saWUF&v}vS=vh(U^*}b7vQ&m}S%Yc{Kw?6+d<&3hRT9>S zCh|?Tce6yKBeTgcQj7tMlqi(>N#EPB*V<r2mc<{62*cO9vSr82=S-CTk)8N9HW`o- zey;EJZi^$UFN}=`pki8T#OQ4R*l=h3Bb9AYIt<+o8`vIK)Es&Q#}%IL*Ir8&Sk;+5 z(TanL6eSzWe*r{8!>q-@?MG{myEsFu_680?SOv!4d}cN+)LntaX*=NezSFY*avtJ3 zkR~0=h6)T&ck7&2De-Zw<O7Q`3B$b{N`ZjJqQEu+Gis^EJV;cqr>|jYO=@}a!AR2; zX&5?ezBfDp?G_V-*$sJCzM%F*Fmh>q17{x!l^;@BHybmX_Nf9N`8(S2s)@<=CkeHD zXU^q~5}Cg1;yw#cs}`5Yhk4j}7`wnRD(kt=<WLz$zh<(e(`;#-X}#xJmdK?gs%prl zGiZCalqMHrSZd0Dqa3jC7=(QG)zqNzq?v&{Yw_cDM&!D{BXGbXiuBoOTBJ2Y$;+_U z%!%rD5u{xQIVt=?P35cplJ5>{|Auav8jsj`DUAbolAaJ}U-1>*2AG{5A$tPIZE@2i z^r^-iUT_7GM-ua2o$YZIx6XT<(|1p>{1L*E!HD(7cyD~?Via2hvI=5(Iyy02=wPpM zkT!llg_>1_U2;yI>{tn{*~_w@UcI?ydr5$M0u!XauTM|S#7And>!H&5#Jw#yYlDjw z0Ai<whTbx-d27*k+C*wHEp5apz!>bdA;W-0aULYJ#>ELN>k2iEBS9}m-lbF(179J$ z?5J?sf)}LsA(qG_9`|V$S8_ulLc>#}0wlQp;8ic|4U74iHtm8W*+r|hKPP5jzwx{W z;~z*W)HRgbk;#X-D>|0>HDg!RO&6Kv1XKAun>(d98(bi70MN0}n1334H{{TP2X~ZU zrCbE|x6F?K8ouF%O|V`5@wRnqT&@_SVD@*Kh_1x%G)73VEBkukf*XiGJg;1!m;TSZ z!%T$_uggIG=e=5~U)YON#O~r>#yg14Z>`E}X*mT1$cZQe20c2(_F4FkKX~V`w<(N> zM_9Bdz=_8Qg9D#A0m%trkv6b`0`3Y}KJA;`)ew88S1qzl<?DP1c%w1MEd<&5>#{{{ zO9U6?@i_PVpIk<n!A#iBCuha>40h043i;?I#D9zQ9|_m<(u*0y^;GiKm(r4iH-ykf z`)G~!ljrvml-mY)CB89y$UF!Di_hk_C^YSBNHK==VX*QB)Rj9WIJk1JQVr-otN{^` zQiXzB8)t~k+77JkI7V7_CMH{Z`};1d_czz_W&m|xSy_lE;c4XptF`QhF@eBF+=h@j zzQiI1pF$PIQA>-i(3Q_7XTdl|`}W65IgkNf83lq`>$ns(Ia*cqEIojG=t=bT>;5;E zLOVmLQYz=FSNnD7WxemB4@H3nZuyYdyUa`U*x>Vbb{`J#!l!xv`_rJBKW)uG4#!an zu$BZnW~TH%GWh;qC#l4wINvXQs-iSBa8e&80cS|(A6T8*5VipJ{Gy`Vgpv|R{>5e> zfcl#@@yT$87!Nf#U<83UbdPAc2p&j--~9?)#-C|`rYv8+9%S?o6LTFZxhrO7X4mYw z2TfE|Y!nqSrcc8aMNXE=`Fm#o4=wy3?wLLC&Ghv}u1_uW`GSo4`X5wO5&i?Uu~g`9 z&z?!*zV!55H~>}1VM;1Y2`pW#;!d@zjEDur-(tLzzoD$au+jeoP!d4N-r_{M<Jazg zDuY}ybpb?h05f(FD3zik0+K}27v%on{j{Fc_2b7MB5kRXrHokS<QDH|AicQsmE@Mb zzU5ZxgITeA0XLSe8c+lOXj(k|-@(h8JSTwOJ5x0^=2mh!$p2ni)^80NvB3dn@`q_H zPwblez5Bg|^0qPV9D56d*432-@F3Fmf*ieqjQ>pcKHeYLXJ4Tfuw!)hPNV;5Ul(Wp z)XPej`eOm9RAAf;fM#U{-C<isF{j$wxBOb7&-c%guko9BG~T;6KR1{C5GRr`-DzmD zt2?~(ko`0W@xc|q#AH~f?kG$&az6G06R`_+81yGFcLd^X{_=71@)3~?V6W|&$d?Xw z5A-TUt{o|YC#j5@%tgcfU&^lhK$Z?1Izo-Lf$U~~eVF|A|I1+Mu)`58wQI$Fjxg_I zr;(cZ!F^xe=FY?gq8-~{3xJtG)XmSCAPx^T)VJve-afh4bbue%ckRiWBh)!@*l>o& z7q7W+w#Jv-yOnQ#^oq;K(D@STYN2zKrv)Qbi<4EK)g^!W^x@3b=O9HUMYoAFD%`P? z?XRy|vUIJCTUtws1+2%<YNd0eR(p;YmNrE!7CGJhUj3^LTf@j*+IW9^kgl5yy%W3~ z_8~8gIc)Ka>ak(<u6Lja<`~-b^c>4+XAjs|8LOC{Hv5@EoX5iN4%Tt_x!(5N`H(s3 z)adVERq@NNhnWr=sXV%ox;d)T-o`H`N5HrT4t@LPGui1gna;|}R<|~7lkP)aen$0a z508Yr+S4<;rSk@rbPbo=#YW}a{in{<u1}bl`Tn{$Nmp}??u}#;8S=Yv4R7hN6vJnd z8f{*x5n2j1T2!B&p|Q^?aPs%wZtJOtIbwE_@PW0V&sNyff1MDGms?z1?Aj6ZS$G>U zE!UUGPcfx<o`CXmK9%>V2Kf&<k^ex|&tQaN%J(ZBkLuIxG=l$3NLYkn8WRgUTOImS zqMQmXHpF0!L^A!aUryM6D?N&|Soo*98&q=g9KNyUgtRvor|I(c|C2N8CQ2L`bQj0a z0v@UX;=Nwt@G1pe=XOx3TH7P=IkNo=r^wK<sUflO7s3S!Tjb{<Co{O<M{}v4NNNq7 z?k@JwAWzfyekN(qize}XPFB8q0e&u|o3Q(Q9MxbVQA4upY*n%AjJ%B|dW=}E6ZMZ# zwv2fj_-CnMWpk69wAjo9;d8x&MK`saoCT5RErd4f-*2Gtg|V8&TH9JA$#;=39}7A7 z2Bt5>xyW(+qMDkB5dT^A-R$PAtvVl?$v@AiZvR5wbK{HpvcFy>>CLScan5Zccx9XH zC)*Nu8+k+f4=okN$-l<GU~_UNzM<-@T^MFrQ~F*bpYWwUU)cIdH!)==_2JQggTFrr zNlD(W@0yD9JF}9$`&E?=bk`hSuZ|B}t2K3AAVoDdhlWYjX-CztVm5Ka#~utDLMYUu z;=JAG(<rt|j~R!Q_}1}?Ac~Ixd-`d0W{p+ajbdh`s6f1Zri6p#_qKPDnXJeQ`t07< z_?h;l3x%T2y}A7hE_*(Y&u6h?3ujE?X9xyX_uPEk{d~BJcHwl(>-Bc)Ypae7V^tKJ zaGd@8XwXueVw)KiND}8DXCo;evcUx<WpSU0y{d16o(AC1arRN)k}6XDzWGl6z?jwH zj_?0=tdpFLjC@svza0UL-Jg3GyL|gzH9aMBxtOT*&oeFl3-c)Gzv00wfwXKWlr|;B z@0kPD$nT%XpE~)w^x%C=_IooAat34q9vP@QA0jFDTZ;MhFX2<jGpb6OS4tGjAN~)a C{M5q$ diff --git a/public/assets/app5.png b/public/assets/app5.png deleted file mode 100644 index 98c6bdc07d2e4cce62544ded057fd77cd65fcab0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173499 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%0DEHye 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笭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 Cj;;y- diff --git a/public/assets/app6.png b/public/assets/app6.png deleted file mode 100644 index 2286fadda0f3c43eeae705ffe1e1bef9d7deaa80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157311 zcmeFZbyQs2vo6|Lu;2lLLvRo74Fvb#?(PJ45ANQ0(BQ!xLU4C?cXw&Hz4txmxA%GX z{r$!pZ@e`|caK#)SFgF|tg2b{)i*o*tGomX5<U_D06>wF6jcHM;Qavrm{|m9hy-E8 zpbzo^<)kF>1yDIgcnCRxH5Zl>1^}vKkzWkqAm@k<lA2Bc0H*U_FQ_#$5_bULtx!r- zSjAoMBnwgFgL+;=7M}SRvo@|ZKSz7k@NgPB8U(wIcb}kA!#?(ANhUQh!-|S3e!~r? zviNK?;s1=uLiY*bXKt>4-lqs?=)hc3(&xDehBqG7S+QvXl$xV^puMb{i>mUfs>@Mk zl|t5$3NoZ0{~UDygj}e9P62>Ag#XtH&W!-Nsw!Fg%E=A#4=SBk-vkYZkgk%v5dYnO zA?wRAxCvFUKfzMTXm7M+AW6Ngz5nrq6IaeBfR>`VydL_ti9^ilE%}JH8xS@g`@est zj*5uSpUD+@FvHuw-ojkSMfjKK0fV9Hp0AzMzuN&AW&2Tck#VwiU=F6(?n_zkWhlll zk#dflWd2f${~Q*EOX>jdgXX(?NuDc)wg*59>s|8aZ!w}<42+EhQ|zwE2ymv!W?pCV zOEM@vDZ_HM{A0dSCm9dGku55gxZ?Iy_>ZUWsr|w-C?Yx5zMX8L@b;6Z`_cQ--jzVh zi?$i<M@bdKWW@O`7Mt+$1<6vR{Ok5Y9_i?Fm;4@ACg*GIwdUh<hNNf%%B=BT%~EZn zPi@qHjR)PXT=V&MUKW%GjvhxLc5*B`#g>_+_0U%H_vC!b=ZPufSWjj&QhB9MgUM1y z8IE^E&RKR5@S{7`+0$}&l;)ijcBKp>cPpmm%HppT4}}ZGNTiX!oNCDU9#efv-76?k ztgy0+_Z?z+Nr^mn|Cx}r1S<HB?Ck8Nr6sTXv*@re8U_Xm<pq8%ef{;};mE9F#I|K9 z=P}Jls6^BPhw*gDe3%2`=wNMGo}_hy*eiN--Ens0frSUs@i$XVVN(teO+1GTz4z1s z8FzXE8g`PZRb|T^rxZPjp^P)l9*oZ14A<@ovQqhF4BN7kdcaFYoa*j+XwDc=RR|s> z7dc{>*DhrFhdS|pW^UuuQcI)JV)7FaQP$0k^I7vplTRL&ms`Cn*W!VI7GYi!064E; zXhUb|V!C8&Z3q(~hdEU;vlqKhp}d05u>)WCt6o=oJSMvb$yVYVm2+T>6thw4tV_$F zoY6+B*fnQ!C<lpyb!7_`5MTTmF_#4BoBm3d=#>X+vKQ7mxq|g*VuIV|n{ROwd-Sqg zS;l44N|VoT<+M}b9lwrwx+bFgr>VHU9?^^H_N1bgwJF$3FZq;5Ccm}WTF0gG94Vh{ zy}ADzGYyo-bqD@*Ed5gHe;y4`A&T{fx8d9=mRLYmPA(`g5QkZBWG`h0%0rqeq2J^k zD?y`>vHLTih4fiWOIl0IQ!Cy-W6IYe1JEL-=oqvp-VraE6JA?su7)ci6+G7VXMgxw z=Bk6|<AnJs#wla*mqdN1^I3Gcwa~R9EcP5|8D~G7oJxB9FcG6*3)8q%Al8xspnd3D zD7b}4+HAsvla);~*o!HrXQrGrAg{W1J-B7Le954Fl({}4W2&tUQJ~Js-hJB@0pm)o zgDz-qxOOGmNugH3yLh(U++HNr!S#!+;%_6WA2@2bAuSr|RRU!enRbZiLNNdR%(>u+ zg<ofbiPR8%J3KtZqLMwB%<s<=i+mvC9thH`HH#15p=5;tRUmE)cgg{DlYKo_DMq0@ z^TqMd`)yEjeGn5Bn6+z;`v_c7X{~}|UDK_!a`tKpGiz~tyfT`dtQirUXP&HXN0E57 zORJ8*IAQ6cpb(K?7ZT4m-OF61<;vH(5l4$j9B9Lpt7ZIROvnm#7_3Y^YzW<f!V)^a z<ZHEx5YFk)^2EYv3Nfv%We-Hf6OwWBqgoR0phZ`kQ)w&p+@xXo$PQBas&|>ZTnMy| z{gu$a;>FRkd?Mn#u=LY+$UA{=>5-EwJfQuabkYXRhwHUO4=kKVAS(PMYas~b{8T1O z$z}ocv2z$JEj^-^ir61Saae9T_V!=annxa7DcCC<gsS_KT&$ZNs*z<poUmoxu++}M zUdL|M>J;zXBM)BVrH8SYT(bDT4y+x-zu3(0{r>%1EE1<0s~5y!jpGr{PwIrZ5I%H` zv-yM6pnfe%7mc#d%Kev2rvzHGWrqFGa4xy)QLN6{aIj~fjB7hPJNur;R7VKMpVo}% zkaA@7gnl$v&<&TS2OU2xAK&TN<zxOk=B9yiGncc-o(IvnTwOC52?k>csITZk_p>aS z#|K35Nh$bFzN-`*<Qb%5#>fL(M2NY5GBlnlDxY~5&NIRY%vq<b$|4}{x|YRR_3kn) ziXWn`4>usy!2s?xDu1dAa1z0Yw#Ng4h7ktDoz0}>D1Y0dN6i6ZnO06M!%qyy2%>tW zP%B)OJ}5;fQU$32eW-S@Q##xW56;#0*IvHWFDIw=a-rI-?12+9hT?EZUY28-ILjK! zoo<_nPCHGzo4Jc3on3riOsn+g%U5k$yJl~1R#Q!#jpS6f;-dw<LM2{bv2;ytV~a(U z4ptD?_);pIjRYJb&jJFYS|wI}8?@`YT!S(s43KK;Y(7+Z_cW0CyyIIls+=M1jcXX( zs~F}Gjdw0>{eEOHoVfEX`eMql0=PbDz&ku$(|UG=C0w}<Iw-j(;8%NTtlyv4*#n_Y zPELV-{T8^nxi^Ax?&_{R-it0~M&ZipT&4!S7EmE*72d;X)5W8G*!sCx+X$k*9@cjG zaSxpZw3$DdqApg`#y_^QjO*mE?=IXwJ<r4ZZ=(a$`Qa1Hjiz%%o}Q1~`Iu2hh)~sZ zIeU8Yf<Re6`Ry@?&9Z*scO>H})fZie8F|~BV3js*e{Hb8qUg`%VwX)RVD*mPL`a98 zMfft0C%am09lr^U4~3Y+oq?*?=PFc=f%754r~rR+)P+3624yq5CaJG=9G~B6n8WTx z4GyXf=ekiN;w9xh>jELAb;XAXvVQ-RkAd4a*IU{iJo!Oj@prl=M7XZE8<G2UBF>uL z7FX7XH=v(8G=%neLOEgXy?Yg&w-Qb%@Fd$ctC=1Tb#Bpas|W2bUnjNhT--2J{N0}z zSeoF+A&MbgDhrVNnkmdh=R;LszPG!B?WAB5@6qkE8}nOTwcg&2NUOP*S&AJ=;`TU0 zuoC>u&%nr2UeBs-J$AWaxiTNhS#EduTF`d+wm)dU>bge1Pj8{qsbZeoF^vuC#tdFP z=b3q!9<Euj*sa!HTU%A?wWqXw)1J<<_%^*XNzm11*$2o}`-_2@a>X`sm_%V0_2q~A zZe$hX+~mhr!4Q5~mz=e3_wLc1>2)hqJt?HHqlA_Z8lU{t2yD3RyTkqC44C^bm)czj z!4^N1F}GjpLrOb#$F_mE3e8NJM!98FNV$pQS_gM?p!Ua}WdF#{3gW8i<%!8y8;LH; z3_X~ULwBJY*0{@@_)m_P-&%YkyXTIqqE@f{8PQu0wNKhU+>xHV%+UtTnzwKo5nBC6 zMF1&%Audl(PbV-Q!ls6XKN=c1pVM+QJs`$-SBKvPI{|SGIn|0*KI|iE&mBq9UY7bo zFfgeXOGEGk{h$`0)O~uf#5rur4vQgoqA1&kp3qF-!O@O^F~_oVn7Dn`?R*n1!Yt(v zZCMWUlk#Kl3I~Lk7M5K4?z#8VPHkWy>;xp3Sd^dG(eF$(E!J<}$T$Bc0sp)@G_2`* zmm+#2d(!#%;a(5rG6jk~;Jle3`|;|KQIVf7Wf2S-$u(67O`Owlku$1`{wq3rapK2s zddp@#Wc$$$Hv#K`=WIM}{^jMmJVF{SRPrpjonUbpO>KYORTI4ZXs$z;xzTQXdLH|5 zHfR}ON1q00IhnMCpa1$={C+9G^K_H%yAHsICgRy6>`=q!!VKuc0ECs0+ueq-KK`;V zO_iJyx|LitCcvUD<1-nrvvjB-vpiWPnzq9$78%l+ylMTO{r#doI|cNCZ;{s)sIeR) zxDx-eH+Zxl7X!9@?uhNrB@UNfD5(hyu2Ah}jfSvz;#t6S`@oneQogSivUiPjnbJzJ z1xQoly;&8k#|E#Gb&uH<$An=C#|)@bhE~!qg}g=h(|@MY|B9W*e^1#AaY+PlYD~`N zPi2u-fx1<tiA#p?>)8J6>GRTR5~7pRYH{oe-qqA}!ZC)1wzh-i?SwGZbz>9BeKXE0 z>6(33e2NAj%Vp5+=h_X-rBQ0}JX-GI0I@BeP$SKd{V$2fWVR~v1i#(F@$z-i70FTh z6i6&GrcMxM?Xd>*#~~>PWhs4C8+<oZE)y$YRDA8^t1T!;rLcJT9Hgwgo)j5t@r@@$ zJNPH*lq#zaRhth$?iI&_LBUsU!1&H5DPTO8{0wMN90xzF#>+wHQ~C2lpP;&Pci&NF z22+{e2NbYu_d&E@0c*U&CP!m*CoJygY1%&zPSzkY?(FAQChhuOvs1IR-LjR+HWL_& z)Y{9H7Y<W59rafI;UBTbu~54BUX!zCx3Pk+zLWQT(@9^NL+yj;$NRdTIE9-Ht#9iX z#?5A?zWK>|j^28|_N=NneFhUnkzxp;RrB`8?EeWKw#p|QWRhN&aD(EDLoXb#C+35d zaA87;UljhXLEMynGo^*c@UuetKC|#gJCjhFlkb{AVwNFr_+9jZxBA7O>AcnsUG!^X zt(3;quhDC~;?t>iYcBi;0V;$4WK_AZ(J04eqv^@n@~s37-DKSO?{mHG=Bk|&2`SPq z`pgF<uSbs=#qFBKrwiaKsfx<DMv3@OVX?615xi56qZ|>7V-aV+cMQYd@53BGd=BcY zwnIExGTs+X3tv--bG-qfX)@Re!q&ts3QTiLg-%b5#Q1%04$rabjn|Ar4MJ-lf^Rav z8_{wRy-SBfI=5DHG$BUnt#5e=octku&yd131ivnm{*&8>>Y|LU1C@0%-=smAvom{V zx|(f3^izWxF=voo!xZh-SrAphcyMOh*3Ah2SvUi0Rm<cgNIhKVu+Au7;S<>5JCq%P zU$f%Sm^eDHbpUQ6RagBKZG2>OxTN-Hx%49-T`3cT#Pv2}|24;EzU7&Em^d)ME18;q zp>TcO;(AlpSZo*ibeP{ed_MqgHI!i^$gB|B!q@jpyh1y`Grk;<-j2K#Jj%sfab{mb zR>Sbzqm#wb8Jww^JG~!T>#c4s{AB?yHVZY9YVsI}<7t3^`X75@Plve1mkKx-7#Lhg zd@lc?_|)#p0Q^KfR|DR`frW(yi`7iw`1LCB6-G4oY%?{^f%l>|`<B!0s@xNPskf32 zxp!TE-M&h{%p@%j^HvqnuSYG3AcDYr3bSz&qEomtjF$?8=~~N^m!q~>K$fK-9EEwI z;GK)E1-}Ln6nj4x1{vnJ+PcOoN4uh?by*jMF=SkoWEzii;cPR++(!qs@k1)X?2yW7 zIbgJ|=gB1Eq{ZY8OXEkF9BKM|=ZE1=#_Fyc(C^||l(F*W2m<V2pQR}%b~3Q)UPy)D zbd-A`mdC`;8_>m;s-2#pZWx1DWz}&}Me#PPo_TGDG^(>En4AqLZhCV_6eF?qNXE?? zwAxI@ofo@oe;JOY`|GRn9V;16X9E9MR{u)6VP6on=knCWdKPE4{BHJa@sH%<51|W3 zRv%+nzveV0;-Cj-USGd^&&mF=95^4eoGuMJF@jF$NgpI8;s9$mk;n%d`u#}AiMio@ z#6ros-%IAAIC%as{!3WVLLx(Fp-(kPyJRk@tqIqa59Fm_cY}8KkF`KNA7{DO)lPUv zhp(q+OG$|(B(s9#RD^hg(Hs~U7%X0QHmvb&qGl75lVohQ%(UanuoKkf?<7|U;w}tQ zfr&sg-3_Mv^IC+OlT?xNrlgP<q>A}vos$slLMgw>M)tY6iH|1i=F5wMkbDJVLsK@| z?_GDI@eNDQ8$j=PkH9x0z7DP5{uJFm1K+WapZwJRYv$noNH>Nlw(%6MARtRmG5ocF zX_ixSQaB51QW9)ODBzxZwh3}4XVI|Ggu=co5MjEJ+t@tGZs3yHQ!<lNm9?Kd7`t); zne4v<(!Z+d`GU>ddsg9hptz(gvnfo~R8^f63V!<f`1D)N%UG@`hy1aI=d|O`Qqk1b z&c^v=Tv0Y3oiwu2HX{+><$A$ICi`f-Uo_eoDmXYWJ{EQ_(BzbCePI1vAa|HJt6|-N zf(E!Pr)leQW;<WcM9ih+conJF7usyQ_%a;b5&-N`Y`bD`zA?{Ymb&cG^A<J`?>ktl z<sw2%7YhfpH0%T{$RW6ma$t8NZXVj#KW3m$J>#I7+I^9vMb$1_*RIE9!(`tKIF@vL zxTaK5jo4>kyk!{{fcaym&)4cQw{a%!Dm+5}AKL>&=OP+r6&DxB#K=SDl1erW{o}_b z2pVAf?#iE^5^GTAzq91Xi?0sGwUvuj>pOptpoEi-fDq46dq44=_*?X`*mBI_0@O|5 z0Uu;Of{jIjkl?C%VGdHi>+FA}w5$ZAkDF_bE@=Om9S;k)ZaelZ*$9o8(@|wsOF<-Y zxOOA|;AftnwaEug_&`GpAM4t|*lw^;+m;w#H4tRlPjU^T5m}{i3_BrL)T9!ZR%-En zw@!$wHEUjfmIRQ0F}9gZ;IozXE!W8ye0REHS8FxQtDd9n#>B%S;(%Uyz0FMc=rX#5 zGOTvK@cMjPJd*Ve8+L+2Rg+60;gRbNQ`(E&T;OV1WZgCBRJ?JWi5h1JKLFzUaO00G zC)-nT+%Dk)`i6>fZYQ1EdhaLIKKWDeqd9=Me8WOg#)2u&ewe2ohCy4@r~~=iV|*-| z4qsM*gWif7x?8Q@7pX9#hXRO*kSOKGt4to_wwKC8$4QfYP)J)OKWA;hgP?6&l>Q$J z%jK6tKE`3xD#*|8Mt#Q!wR{gEde?&3Cg-k^Mc`uaINzdGDjQW=eZ)^Rz)t?#bpEjT zT=q%!U_r!zrUQoToA6E6PTR8g)6251vYk}nUqk;JEu8_Q^D4;3qOzqgrg-Gfcop}Q zsg#fM*qE%zdgi%yMbwsGV0jShwr|`$?z!sf7|g-EeFSTbsT-3TPPIkT%OOvHG*GJM z&sIt<p4z{VqpaM)zjddf;cl(ni=3Ps8E*jQs&3FHM>Cy;-&-A`670g4G=#p3c!M5| zey!F=I4nmOL7EWfnKs3~4NQ6}%0&78cs9=9t!{dGkTUM%0>Rvhr;s*kb@Q`ss8Y8) z+=8SF`gWKTXs?Vcu4o4`WSocW_MvXBZ!))(6uCC~l(QW@jvKmtV_A`qB5RcM6q@av zcyapC5Lc(#c8C50ivZf#+T=9ucLo#Nz3#^#i5gL05I?Cx#^<wIZiHNdf^PT;h!gQ> z{KOAFg5#Rjo@jgidQiIp?kbrIU<EO)Wu(PH`YKByz0-z7YCp{MMTNE24*gERhs2EM znKGV(BG!1ybj_>wtcqUG@YRW99-nUT+S%+SvPk*qqz^>WXsh2aH0T<1+#yKZX45P5 zQ1NdBjtCM(cl=j3f}ss}&kCBVe+b*0@$)`hK}^bbakTcd8torU5ilVy1X$g4$Az}Y zhBVL5Gb5(MeMbMsr~#+2uoFZvZgl>3h`;{N3n3Q+7DE5k+=80a|IC5Z-wtph@-J~6 z7P^odH@XlP;EwEH;w40cTs}fbe*ypJJyBsNQ2c!m{%P&+3JraL3J-no?l%zjAF&W0 z6qIu)43smB0m;9R+}tdGKudQ3patsx*>1<k@9+hpe?cG%{_nSpRr%yUR{s)^1^-$U z1EBoV0wNF-{r?638L@SuplwIztUxcu&T=8vZtx6rIGmvb7j0RRjZQZaS2zT9rDK7y z!6G0M>ObW=D&d@@uU7LmdO6p>KbY(O{VE>5&=Zj$bR3|MT93*wgrX&=W<9flRbo^{ zXYaA~S~aVJn?sk7hWgJ8{0MH5pKpAo+WDX=DT_OQx^z7`_5%~kP{QqYQRCBF+R3{Y znO`Wj&H!Ckzx<BUzAa@QBdlxr(YLKyJAs+^Zfnd{o5iVXruz(-Cb~7mcfG$4v&WBc zEQ1kp|8<juJ1t+(OWCorZdWId81{+Hul$T8)fTWL^rew=NQw{X`Z0)qq7ID|Yf&Bh zjTB0IMjU<udzb4p)Q4q;H}3pIN6~T6b0gKK-le5P?uD_V(%gCEwaT+pjxS!Rs>&Jb z#2w06vujb1TyE~UO#2zl5lMF^@llDCLxD~EWE02WApTCtkvGZE<4^Zy1IB*DkuQTK zQ9g6%d)J6&#i=Df_3F`TH6oLHWp@L$qz{?5&DJfnU#I8vy?hSx1gEnleW+>GMP^g= zS6}L+jXY(vvmY#)Y!6=Fhu5&X9lNfF<02g^d}zGN+uyB{Z;MY~YeV8+VN$rHO-a|& zjej%ReENM}nUH!kM<IB(>F`V-INuo(Qr&(oR<QE1<)sD`dxNXzv8!P~ay+qKdk|Vm zBcpGyZ|{~PJHEcVB~x09S>wc7J1ci`?qIwIQdbyS(2emfOnpezkZ?DP0QY)IpQM=H zlRwYtr_63RxQZ!Gam^)6hl9C##gGSba8_<&*&Qy<t-W0G%&%Yu!TJvObNS6<!b=UW zV4}cLf$b_2gvHbzAnX3|0erU&3~uni0{xQwQ^j3<r=L{m4z3BMx2;?!N%-O4mi<da zhOPxu*<{J!exU&gU90P=_}=C89Nuv>E~a8LCL=OdG_k_+QhR1{*>a1yS=URRDj{{? zy_wgi9{q$8Qu^!S?nZH*C+meLv}NLIGK#n88UE<6Utju5er=+@@@h-fmf5DaUuA4o zSYh?~GhOR0mi7OV+sqpqE8IkFDdiP>X?V8gi0<uw%$@}4IkntpY`jL*$q$p*vAlZ6 zxsZbNvTOVz(pzp9DLP3Q<(y}|ep*JQ{koYiyLFAJb6^hT>01J;c-;~3dYzIe7_A<| zPnaUA-z|+yK_Y=4oD&XLc|^EI3!<dc)^_)LeU-6d^lI6c&kQbhL<_X0mx76n>Gxi| z9y*HI9;d0;dwpPz8PA_6gx1xsu@Z2jjx)*Gmk5MS0!!q)=ijt^zq+54-@Y0vnBdBc z&9$kmHNRSDTR?&712b<kUU0t4j7;3=@pu<z)m<d>`qllOf8gcxUCpUVu$Y*R=|H)x zA3{c(-^`;KT>L=S@LaEtO_YXB$+PVGeCrmmfS>S9fNYtI{jOEcH%NEu*xaGF^W;vo zvExNGl4MK?$+6MymhHX@cOOy77{$iEsfd>Qd)+sHF71He8Ti5AOW{L0Ld{f`Lhtt- z>2v#2A7&#M3&vvV_*=BNh&<esJDqaboQT5eMV{VqR-|w!^GcKjVuF_hpl^zz8pDQ} zHu|5D3)Z(YFl^0mG%#~;#^yks_vxanf21agGQ3%5iN0{gPE&1&G5O9Wq~bs!&TWAD z{3|F$@i)Iut!*Ro8#QLF1vB6ZDs*v8!>f~qak~)g+WFLJivT?adtcvLJwQErZh1HR zJkHewXUQu{XyhsgiQh?i>;l{0-silyHqb6=tozx832ha^1LtQ!{(45dfBVvti5jRn zcfWmPG*)rG(3-5z9k)$oI|t(xghE{Ps%syT5p&@*zhT)l_sO^39-P*Nd;OuL8cG_W z!cdT*aR;$aZ(a@L82d(!v?O0V_3S?P-MwPTn{x34F~x>YRil7u8^SDI>y>KNjmpBg zx|b)4R)RlopV*+(SHl}MD_l*um{j==d)FBG5<Y|0YGj&6M1{Kwzc1;h+eK5wLUR7u zP2+x9jhpKnb^A~qV9k<jS;o^s`+c*U7g3wIzFhs=f+<rc5ufjh?QMCNc<soos@DaS z^T@e!y(-81JsU?s-=~H1Rvfw9$kyPgBF7;^r_0L{wzoQ^^SS(trMkqtsE~{R*Xgm= z7QL$Cr3sG=R1f{Oi+=kuDhoFf;JwYXcnOYrT;k`&yS_1#GiJ<dV-mmG=Mqt~$-qzk zb^{GaxxKc6>mf}Q9;Mc%*d%Fa-j(<Ie&Z75+?4|g`b`~rd-U#j<~Cl&*WdYgV<x$@ zz|=-HAuzfL2|dL+O~47B*YK}h>2)pu-Qp9DLyczwl-vq%{YhnIL?xF1<xNUaR%a0e z?oju<mKskXAu6<XOQlVPD9@QJ@GSuv`y>V1lzC6GB78t8wmre`6FG&7`81Z9lS%@# z)x!^U?+Ti?L$F~(SB5eDBZ&TcuY_Ma>&^U3>aAcT>2dR}=Nn7^0!6#7RHrJV*PC!F zRxg)m;l?c;wO|e0vO<8YmITsK&kkk7BkbQx6gK@Nj3;EZQ;Kc5APw>vQY-sl9$$`- zyX*&G#Gv2*pvu65Utvljc?WeR<h_8xn$c^o_@Yc3DJ5drq)oRh7F$oc`6IygVywuO z#4gdy{&~ep4l#Fl+BfIZk%SKI2><8fx4Qkzfe&h-c0^MWZnoUh=D*lygcV#4<_uau zg)j%gwMTZ6Gy~k6W%~Cl8BIT*`m1wpiH^M$X7HNx_;-J2cI3EhLmyn#ZJX@H4>QFJ z_K$vba65*r?=ZIPH#hr*+JWo3X!@*oWTs4gqx`nXK%h5WUAo9aKXizK89hI@%5mv( zkzMf&gSlo!3T-83H)XP17Q?cI;=%&g8vt~JESmTA$b|LJYaI)YlQPMd@v-LiXj65! zBs<Q_WWmRLDR_?7zwcmy#?sBiL*nv*snvZA7=j_98I||*j!kYQk^_5uTk0BR+7j9H zm}}hFKO0lcU{ClErYx!k!Bg#2rWI+V)Rr}?v!e=+AUJbW-ATbDsd>3O_#Z6OY04$= zXzL$syqa@-E*kyJSpBqKl$;gi<1hal&ZFBd5Br=}rjy7g6QAYoe&F*P&@(Klim%ib z^PTJHZVXv`wqDfF+IUUse93GZ?7o2C%YdG(085-lw-er41cL>%0;hGz4XXX7KAG2O zR@uc=VA_sChuf12(m9eYhUMAk=AGP@-ufo2-lVzJHm4h%V$O^&`31||EY^J?IbZH3 z8lY;j+8p&Ap9$RAnf0owO&uHu_WD&{amuX%UAIR#CbcdHKP)mDFRVIV6fKukbr_0J zdbE-}+Xd-nXB#l>%>P0m+|K?O&%9I<Xp|$^jQmneEOXuLEi%qnKdVG122XWu+<j7J ziaEWVp999!ydS0*MbFwM*)JNYLr%gvs?XgS;AlWueN%G&9C06RhFOQLm<Jl1zCD?4 zSx8+q=@jj);X88J_mW5yg9#s?O-hr)^Ewz`Hp!V^&XA3b%p5rlwM)>AvC+h8T!0{W zd=onMSKmjrsShf}LK7~0lY8b2j?`q0<m^bsOOlt?kM_uh_*Rx0Rz4c~xs7h-#uHcd z+%0eEwt`3G`FF<8<WO_rco4Cgm`<Y+av`P~jgmWzMS@z3>Ka8G8|!UgfZKF|qYta9 z$M0SET3m!QHFXDT7KhgBmlBaTf86+t_o7wRGG|w?PPeM$%B7=xJm&v6spyG%Kj;1~ z9Bx87#j7AL)$NNPy;Q#BG*HM)J4}{>c~kuK@7QBV5+8p@|E~XeqlxfJ$mm61sp3;| z**kbRyU#)f;u1Gl=Shi<0?z)^iIPhUrIDT?wv;Y1cAw!rQ1+=BhnNef*$z<zbtn?o z+*uFh1n5aZrXId@&-vY~6?eo>x<Fz`7NVM|aL$jRww<ldKq3)MZ*M`x$PkavQ}yRX zy*WLSry#*bpe_Ak$5zJFRwK=)#SSUW!kw-ya&~))1iZQvn8A?~WlK1`I=@(VuYhWH zc8bkdQBK*fbvy-HD<+}Mf9$`SY4kpguB}9^hT5SvT%Yed7!TeuH0m>F=ofim<V^jl z9aMm}PFk_@X3OmOk-HFT*&F9$&@|khW7O1DgZ-Un)I=HjK-zF6*6eLkSCo$r*|ELz zc;4sE7hilIZrP#?@2puya4L)B*2|I^*~HQ05a4-cZ{yDB*@&2fqT^f=>@8Q#7j+!H zEj-%VqM<zl!@Jw$p)gzRWqfpsf>&&I|CPh!>Fac(k$QRF%a|uv&&e<HE`3?okCLc5 z{W$4VXP&k-2XtK#W%l}I@SE>tPko1L*L7PWGeydr;1a#PJNz+kKcR<KQg$+~`|X*- z^H<mLu{J;ZuvL`hxXgad?{3}3QKvAe5>tJhfvacq)_y_y+f@b0yE}<I$F5ru3G>HX zkBslJKO|3zrA-=(J>yMVE7iQ=s;)&O6N7KB)Qd&?-?dNiPsHE7CUObtM*H6Vu>OPW zI7Fw<@53749p^yBgNeX21?&WgnVtw|w9YQtziigQ^I$LHfsS>dKcJc}k4R?<npFPk znb)8lSgq&#{qXhbb|#@NRkJ?^4sD?)IwKPgTdE;T#6sRkP1xcAM!ER+tE()c{4|&T zsoXukQRI{p{Xq8DMvh}Y0&L1dWTHWw{Z9=q3(qZgZtaw)E_<u*>|%i#YMM!JeW&yO z9j@w~_v7Oj4rQZ#vVr9VAk=`wv4ZcN3{sMk)ozyrm-KusaVrXR4Qk1tAC1nO0^u|m zYOF!zk*6t?Zu!Itt`|AI6xm3wf>(Avww9#XDoV;}V$3^SynQk6ze>p$^2)mj7$Iew z?mWMEWPGp^(PHseb1-0Cth?bU>X6+Sj(Vdfh5z26u|F}#>X#Zx;>Gz1Nr?n+VQtTM zT!qP>Dr00ofQ4H|Ls7|L<Qkvt^@9NH4vsWgBYQbaah8XRU@=2+*vZewR@Y~gWrFh@ zt%U5`kTw`?oa$yhzfglRWY9gEw2@JcPvfBd2+JNc<#(vMnO8Ng*q82#=;3ILyZbR} zeqvMHuGF6<B2hK1KA%~>U1`Hl?1mTU7!6w$!dQH7H8qB@@QfmC6}KV-L+<)U>l=<& zp&;}2rF+;X<tH>(BcLR}dMWlsJInELRPmnPSfL%mM=_9%SiOTc-se)3lthSL|7?)Y zbxp5<ks@B0+$ZEOk71M4-s34WOyF*KySUI71;yB?37!!YWoV&&-8JvL^qU&uEYUvV zSz99prKYK3Vu4MHj5ploE!(y1x|<Ix9I4tj;Xwt5qpJNM&&V%|LL?5twjx_MS(G!O zIhr91y{Ts_zbS)QM(J_d4i65au%ENa4+(dLt8X<Cx;ZZgG)#a&+Q?YLnlKusAq(xr z$(+T0lj&<NGLy2o6Jyq(t<fwJ?2_$=gOHb3_6V-fD4fPEv#Q9P4gy;vL7Cx|Q)1SI zj!l48o>bnBTA6joyn-1t*fO)8E#TFTOmM`KGXqCRef=Bpl-I@*0VRirI_~k$Z{eRa zF2>e!RA{|@4W6iZoyKdrze&PR+>)E&o1>*Em@_rkXo$>S^j~<&abISjhd`o|_I2rS zf}-T!lh10iznUfej+?IYea{1&-|;GD#b9CHFT=W+^&CxoB@n3pAsFIl|GIUgWo$b; zKKm{z`wC3AFXHP~`?xk?U$J4hGfnALb>G;rGQQcDhg>yqM?Ot|l`~$4+nM-r@!CQr z?No$1yP&6^U+~Wh(hq~ie8J%v7ZaTI@Z(q_GW2280U2%Z!j-(vBfVM{9v;x$=e?Jy z8>E^5*sFedJhXa$zabJg#1?9SI54JWKY=LRhXk+4DYv6^sHU{_Lgw}T*e|*uj;MjQ z!Hy6+y&UfIAbvC)HES`>+yL@C$0MM{SHkSFLemc+W{92|I@F_pB^<5&2k4b-a4h7c z)yFN%i%W^YYFTi{_oqn>MMLJN!q^A2_bcOW&MSz`IV#>2NBRVx9w(I~o27=fSjAdN zJQ`m|_*XuW{U})Q`DAd1$9H+Hf63em*5dM~8UlU>*-L%c8>;4ij1S<zRZ6^;@Y8H$ zwYJQ87lOLSs^5}v_$uo~Ih}gss5hy+D3E*n#kENA)^<EoOROKbm~?F1(~nw8LpzF* z+`fa1K_m)|)+i&A#x!*z^H_a9Xn8Km^h^--0?=~wE6b2L+nk%*edqK|1PV;)f9fSK zzgAos?VvDR1k={03eZhUZa&=y)X0H64%DTOWRK=D@#kvzR_WIm2?&JLlXi&so#lKU zO-qF790y!gJ`-bHYcd`4=u!@&E@siRBpZ1pzMCk>EU*QHh6bEeD^y)ISF<%oB>m2V z)?9Q{=&GJ(kT#i+%=tU0CZHz}sujrX?Y8&^fwb@m`7}TP;ke}A`j!$TPSI76Z~_J= zWNsLCfitTugw11RMvX_#{$2Q5qMUsi)51zw)T1D8Em*yYwjk?IFYZo>P@IT8L3dkE zU7MO%Es2ED=g^@OosMz$-AR4s*z3yqatrzlN`JFIMz6!E4L?T7X1AzfvDv-g!(0R? zPLX9Op)JCrD|ghkGd7h5-=7lljAVr<BHaz{)r3Q_W9{L;x~rqNxP-O57Co={yj`{J zxz1-hZK=*(p3k&iuV!kq`7^b*D(c(*l(7(7vBzS6@0Ng_-(EV~e-@1q%rhU2{zogG zfW&*qI3LWf+zrfeIp$jf#*r-MYnIc{As|I(?e0iO#2H3Z2+yU~pTM|?-qp63?`Ny2 zq}W-yxzt+7+J3`6bJYp!)@a0%c{lOrs5n(5<Z}S@DT*!bCL_mLQyVsUA>C=_@$a*& z!2C1yh2LLZUyRDS+;=~=9|U>n2s%c$5thm^@5JhzzkPZzg_&<-ufp6%Tcp={vySTU z)?HoFKOu)eWBS?H8fslOtu98(mHbpzq|aD+q6`{-*3wZ>a%z^a$~V8-e^9;y70&rJ z)_8UfPa1rGgbE6gzI_U2%`w_!R3aV(7i?Km>aj}uu{R(r=O~sL+$Sy=Uz2y&gEAW8 z(SAZwITEwrkB|GeYG}DUl&G?%o<b6y`q(abMc3oMZuNWmb1Nu`@&aZo2L{FCT7AnF z><KWv@IC1G{41ADMl8^rmFc+eC^;#s^9r~R)B>+d;MMFJ#Ve>og(&1e$F72tHm1xL z0e4B|=2$Qm9-#!FV%uR9SLdvxIPIXctN=kR5Jq1Hx{#B)SoKkJkIv4N>{D_KF!S!g zl<a)?$y`9Kx7Cy!=D;XczskZs0!Wm_Jw54%)c)PzkfhkoKB7{k68^>%<rxA^9nxo7 ze3w-rw^w(BVkg=jpfynB7Vyq~3Vm<Zfc*DzL(A>@V7jgZ3=)5O|Cib?z)BHi_{mx+ zvxY8Nd;u`lYRb_s<e6cd^7N>%O07D^tnPNWx^Rfwwhn^{i4#KF{!8~gz&GM*Oe6I( zJ=@5hkM%VEI7mi$Ijygc4kca>Jrw~*!{9~EA{BscR`Sc${cx-;+A8P>b7j5dEboz1 z#S;Pbjw<o5I5gk^ozR364pPWEgQ|Gh+t6OwsSY$d-^j_iCTd>L^Mn+#Z#i2Mw4?Ro zVxDvbL4&Q>U28~^UBE>hP8NSNKECl6$_<Iyw~q_+(FKHW&-3kdsYGv1%`pqQNXclq z5Z;PP1?oIW;Pwz^I2reYo7pJ$Fj|)|YIDY2cSqWZcQ*tvQyf7bcV<BC)?>Sef8pL_ zb7eriG!@(X4z~ERW`fJ+D|t;P8O>o(n_YY#x*$&IshN<itYhBs)suQp)w2<c9v*^^ z>@{-NK&x`KRY}EeOC>cLjg-}iR8Cf(d|6tKYz@tLdBpzE&uZ7Io)*9B!!g$<AiB)y z(B;w~VQBqx<}v?ueqcx9^1il5&{0!fN=|o<$}_YDvyBCc9qgoi7)J7VpZ~zLe|vHO z#m=~QrjPlogT_>+2^bwBN!!u>w0>$+LJy<Y6V~Tnqz@!&$T*LeCPo7%pH{D>Pn}xt zDQm>NSbSLSP0M1cE{-pbk%phRiuDPX(^R}#@-15rTb&ft^SLy4mRA|sR%|%om4ut1 zv?mZ6=37ZyT&h_yfU&r9Y;dVo1#Mykf#O%w@r9npJGCCT8Q&BghyL7l6s>vN$#hPI zgl<?^G}=JsJp08WjYIuUr|;9mZ>9xKez6<lkBjWA{r<kRkOKa4EqM-uH37pt6M`)* zu25qC3LI=;TSXp;V>+e~J+D1yM!{71v2#|~4c=e1nP1g4Q0&VvOOV_JB;vpT#5{F9 zZSE08Oh(SNEZ!E@-X>0({1|`Q-qiyaK;J!lH2aaVZ{H-vHU*GgzLsnpkdJ1Lvqv>r z?zfGl#@T`H*Vyvv^k<4KJAhXnqs%)&Ucz)$UQWB~)+!;2b$cLvc|FaGYLcFR+tcVr zs8=gOE@gwgydoSZ&yqyw4yq=+*UyWzmiA##`10e#kUgp?w8fMp;V0K_&MS4_$L4CN zx|+XqRm$uvf|_mQsQw&6k@=xdvd!{jg@1>;sQsciyyeHKHjQUmPCv)kf&s-2x`tpU zC3yBNj6-U4L)&*_M`Yiy{!DIhkui8Y<ZCM2%>|Bp@I3DV_F+XoAC`BFoSB#kTh~*l z)${MJgf!Rc1|84wUn|g~%>Gywg>hzIeEI3+88f1>kw?25zG6{<UH1X~{8$eZcfp(D zt5ihLsS$YniStpCp<U1a1ZnB`nDa53ZKbJ9sWnT3M<bQT@x0FrCDBJood<4VHFc6d zrPJt!c2W42FkeBhQ$Vtgc_>F;@tY2_BR0a667nNA`O}rlB~g2!D$swgv+w2kmMP}v z2ONpsosI%{##4fU6pZr8RFR{Qjjup|Giefm#+{(h$|yeavWLnyJ4a}XC2}QaY{BHD z?3*Q$NpwP1!IB59S!O&+x8V_O3?uc_fi>_>vZm}8+%^!NR#^Nia3vk8E)XV>pGx2N z?RF9>&nH_$F-d;N&b8n>B%KMWus;c--D=HWYsUZ4+gr8rQekX%P$9gWLwwcIr$^o` zXMU&JA;P|lr6znLlM^8yiWCE}g~6<!qO_lh%=4N2J1Aa<ZGB~DmQlON&K<0e9fMjs zX-XD|PzgO~Jm&Sz0G5t`(eb{h-oAAcr4m3(*J{l-h+SgaUk~?v7rlYRk$wVPQv#3L z-8S|%zI4R&xFjx}dHdq@vbTaVraRw{d1LOqMeS$p${`xuW#QJn>v@9^+G+zL<$(>w z+|Ay)rurKawsBenq$jsG$Zkpagxl3#sj&JNfmH|t8@~&3RJNK&nXJ})W{hl)Q5Ig9 zhXf<ZbxY#hE6)NKoPzGgK8NUpTjY`#<Y5stmX%%dKx9klFbR-N%1{G&&s=DX>nbty zL4M=($epO9Y*I<v<Zm&YL^STaSKC=+3up8;o&ax{A-lh{oA*~~cRN5)Tv0KpV<N!% z7YzuOu6_0>eN#!qK}C<BZ~(1~%s$74DT7c*y_k@AJzqPPd1l`|5{Ow1$;ENH#|4k) zMB23LuAtaGo_O7(J6f#Xs)u!a`J7&wmX8jDI@7I98Sn#xK3+eo*s7vzH)pKG<^b=W zD|^kfR%iNKg6B@5JW}^d+>S2`pI&70e(8Hkiw$N~V9j(8a=U<E+17BG2_XfgKflkw zR84ECE+D7;a+)EH%^j|r{e|v(r?bMnci#me;SASQP&-B@>Bda`>+yMu814kY-1il( zU+qIwe?W>WLVMxPq}LyL69k;FzH|*YRoTGoKbTBE+KVYT+}Lybn??955)U>i%RCf> zK*Ds_R|tyC+A^oKTlq0LywUljxq23Rtm-E>l~~4I(Sac5wSLNs$__BclH{R6!OTWp zQ_E_}6IT!~pSUq?C`<6jCmP$-Lpy8G%*T_gnSyb9l*HWbrZJX#>_ncn@zcA7P~PwR zlzT}l(I(=T>Yw~^-Z&T3;qbocFF#o?!qsM>Q>rrgo7sQiUVWYgM<oRJeDF&3$2fE0 zB1?Ld>YXpmEQ<KScLw<d)>cZkjt&tTwvJaYwKCyx-+XH0^B{9AY=I6z6UUPUbh}|{ zELwZ5mG!#`Bs5ti(j6O@%jq>|H+74?Q_-g*H*N4cKEdc*?a<dKevA8S&qK2Ira^sA z?%R|U9_<I~rP<Y*g!oLhgAGJKid3MCPxCUlsP-X1OYw(_pE4`1l1`Q%N5;u}mg>RC zOqiz#lrENu%f3cj*7NDqY@hudu9WtBF%c+dFu1}l8;K`WJ0q1K50DNdw(@DNIz3$n zO|L@;=mm7w!*;JJKW^wbc<&-IQQfCOB1$76Zo}em6WFFVpWX-QV+HA_6t;Ui-Hk5? zl`|jPM5AmTiI@l?o>{vYU+&`0V>x!`<?w+$b*DG)ez0rJ$3HNbT%RtDM%ho7Cnq;H zK!(!~pxj8M&+4X*QVr00bW}L2R?a3guYNuwD_R%TImjBcyOfnuI%oOc=4t7YRe9ce zmpPuw%s)V?+@LRy5I$~5JtSLLsLJYZrq`ZlOhV(UYGkIegWhV8ajaQ~hmB?5yymxp z=P6<XXQ;2Ny&EO#nQys2{@#FwQ`f|5hv;rq66MbB0cKh*0Njk+ba*=2Pa==vr}>AS zuIj#aFHaV~d>L-6Inzfl$lLPfRW&Hncrj&Z5xEh5xgnBxLKISRPbG||TE5qgZN+R~ z8ieeJgAnsbYr{J!DmG02n1l{Ld4Uc{FY6oMe2l%`suWCWmQ@Bvf5-wxIlB&gkf)`m zrdaQZS_apHzb;aIU96z$@Vuc4hRHnh=!sL4)iW^Iaj4EfA6ll)7%qZTAuk2}DvC-u zR@v%bZ_1AAfyZ<6dga>A`Op?dT1~0lgvO`7rB+KJILAKOMMOE2kklV(m{{LclV4ZA zI42~%VU6oLYkZ|jCMP^l=luN1<y`O>c3nm*&c!B$mnc)qJIq{I{AUK3At>NTV21q( zC4<`yeCee*dOeC%Gu24a!ZWhgIe#_0bZkFz?Ry(+?|>BH<9(Jsv8fD(w{JN`*P?0J zYq>Hs896zH0+$bd`oXDwEe5By+dbh$74wSU5MYc(`J{uY{K>zPNz#|o&C5T9h--%@ z{>=LhX&A1VUOc+ZL05N+$+l)+U4>A6$K2-awxxkFHLE?$gVoIo{jm*v5;TfNEr<?2 znMR`Xc#V6&r+2KqBkJH-3Gm&)Xw65Xy2yfpbE{nEa><*PIExX$C9Z%_GcniSkw>@n zqCI%h;R<0PCnWkU$gDj&`h{*R7AuK8XRjz|btci?@nDI&>P}XW({DGgPWi=6+h&y6 zFRUtcofZEap`+*arHk?oFfq?|=e|ApZqPnQ6euzoAk`o#wYG@tCYJ?07;35~XRL23 zB5;`@#X9v+_3AHIk-rhuzuTeIC{#W$y09lM&zb6yX~!qlfv)SO+^G8@6Lpv988ai{ zBMG)~2W{L4r^$1B`;XY3(2X=lp67*Xf`Y5bak7O!&#IWuBI^I!a@UyXAS>mvV-O_b zmLc(8eC^z^bb_7O8SpVN@N!XQr$rx7m$v^xQ`u9+F$4kNk#fUt0G9}OHWD4nWmI1I z0!B5ir*~}pH_~;9$Y;U8^ix97A)KpS1)dLnltJ{Z1&}fsxgjffv}UcOxdIz4*HT7k znb+O?lbdK5{KSs6O65wT*U3)*8i^fA8|W|`{Sb@Q0xUpM#Mk*?{5PFvVd`D~?#Htx zsiUbEpZ$ay>A`JC3H4FY|D26Hn|3TW88x@;+V~8u<}USs@#gc@rA-6kwh+M&8*M4W zh50%)p8X)?`RAc@rkX3m1DL53N)C<g9Ajn)fvsh3I7?c=lcM92cA&G1B&J{*OznVx z(qSPz!_N{L&)V^8s%a~P2JXa$R-gA>!X{iymw$rZ%RK{Lse=FBlUP%AHI1#2c6mju zXzjc(hY)(~EhM30<wS!eG`$EZcKf3IU}Oj-kf}5~SsK>Yt1`j-d7@U<NNU?FnvlwF z0}gFnd8RGO^r8R$Y;wJD1HgAB1wG%+b2J#T(f{<yBq8F6Yc2x_idi0NC>pJftbZeK zuR^t+BXAY|Z=zGQ66k#k4Bq4ZY(ta2;<Mx5L>l+j1KcpzVvQsfT86dVyo0s#bAN=~ zbq_d8OZ!)D*XVYXvYE0A+i2uw4sWlU_+qic$3o{BWb@9+?3G!(p^U6#Tzp4~dEg(u zVje$zD+Q!6cfIE!au)6i2I|fSyP0i7)Bt3W_1zlm|6*VnKHvLVb@8DK;qKz}_w*@z zLnlzU_2Hl`tAkj0ewIl1P5^6rwzmEr@J)FA$z*MuZg^}E`6iao2)9M>RQHTC0`Nqn z9*1+ET+vzSbfanZOMc-c1v?YW=9rY<*>Y=Y+fQ&k<{-kw{>7{SI_tKPOT50{sJgj) zU~t7ZOs{l0qg|(c&6MH4$zWtQ>B+bPRX6p>uXkTK@t25t=Ml$o&Wm7vS}nYbS@N;h zXW+u`Yo&v>`0sK2`%1=7{~r<<VZj_k43$Pmze4@J=>6B*AGfbmuthwdNl>|lAiSp& zeQcG_Z&UDJu9|d6|K0A(!x+s^gDw4Pp)?u{TrP`kXba9GCfbUD0VYYwLC+Tkn18eH zHu-G6NUK4p%2JkaiNZ1=eG#^zfKjTI-on5F+{LQ(56Q@M*8d_z0sw8(Nyv2iowU-P z@wz&62Fga^2O0s<$hmOXs#P^e10>t8tpBhN0RaDde+b7_&lLNY@i$4)ek2u6f3xZR zD<s1&(l}4Re|Vz+z;7?H=G=sk?h!et<ndexkYM>vS4;2mudVLz|FZ{N{19%5)>>11 zE&&T{Xl_j@EY$zlbKjQwR|6$IGK*?5?%+bx|M^D*8$Q8z{!C@)DhS#}2&la8oDrcd z{@r7D<{tvNmVnhy#zN}J$ib9_(6AH#{{Dja3(SF%aC0sMvm*C@_ph(~?{WTTQ-BtP z9{R5afd3moFehrRb+B7wTG~mo46&Jqmg{+m@L^d#mtD&#GjI_5L^ijX*2mGRy|9|t z@YmXbl+6A2HbW|0(MQ-$BVs9Ka>=Hxdsyuq-7kBS5+5xL%+84wd&XeX6IB#sP>!dY zSJ*;Qiy1>Svy<Fb2FVAVj^+tIHB;y2D>>{BJb7$~T3G+QBeLQ42(Q&wqP&OvC568l z<#ACl*hY)g`evGdZ+`Mxp7Z_-Ti0y0b+B-m_y|wNn4GQ0huKpoKyJGt`Gs!oY?gJB zl|>x>l`P+Q+Ltp`)2|cKL+=IsBuZl9N41TR+fwhV?=c+5kAkAah)onUeopr=>wAh& z6Im8EI7MIV@dnusTIJU>#kyh(S^ttSi~dCl+-dwS-hwQU&E^!a7wo&geLJ7&GnV8W zxDORW<q6qJb~stHW^!t~Xlh*XxzA%L!3lwKozjHZCwQtLkv<Bt+n2^LC(vwXRx{EN zXX_i=;~u<LP6e(My-0L!F8Xq@F?raEDXUS!b`IJaLU6t5VyutbD52Sgd;8=wgeL3X zSg_I8z(P)CyN9$B^)oSMJ0$H4LigU+yySKDurn+(MTt1R0{|x0;=+i^oJfc?J{$bm zliZDzd#oscKHuHG=ikVt$!(Ug5rbl9#qnqp127tkH2MOf=UVuei~*xcSHBlk>`jG? zeb1qXzUm&de>Lw=&>cq=R`YBSdf4i@GK6fM6j!KePt(h|-}s#;MU^ir9!)Ihr$3N! z^kv<JO3R8jBoWsg7_r3fI~Cw=CZY;l7ywpEKRUI4X94*o#V?{A@GlF0b+{2bnV~lK zJVUeq9!wT<or)se*U|O*a4HWa_vO^(*x6XHnw=CB!gogu%IvW7e7hY#9=&DM-7bh_ ze&w1?_{ePSl*qE(A4Mc+-ncHqD)!Lhk?=T1UOA_hvS%z?1a^*Y?|`afF0D}*fo#}v z$GlGxj?0oCmRI_wY5IA6(RTAAOpCpw{T4+m;>#wjC&Pu_l=)@ly1KSzT1E^@<Fzjn z>20=zo@-+N$7np$&sHg`;oLI;0DusV>S1;*{P?EAXfVEE;PXvKWK5e;YbQ@DpQfg- zwH51R-!@sIsL86cwt*G9Rn%Zwb4*>E=@ncScB^l?t;q(ansye^({><YzT~B~i8u~z zsod`I!oG4<E%;Dxs<v(4zHDDpGc6_qLiu`eacpVin&)B%Nv_@bM~{_WZ^5|<7d%5N zCn_38iAQIg9i*xdG|Tv1aWW;9j0seKwSVnB$$Ppkh@3wyw8oeHZdlvTWbI1C^j&|U zA(NjS)If%y3J(y<ASL44%go^#xuT#=Dn-smU6@E`wA|n(3^#)bjk3;=>f)PoE^Xic z=9OqKB{kNVFd%a+iy{d#<nTf`MH+FJR#aBVGSV}`L)utP$w|AjJyw+=u;%~axxiWA ztssH%&d))x!!SI#mm-l?M!sj-Ntj4gO4bADyXEGZEy#2*%w7lg&HQvk?!DyyBJZuD z;_BKpQ6xYTECh!DAq01K3&DZ~cL>1>5AGBR0fM``I|O$~a4*~?IEA}Y)r)+4@8194 z-4}gv&KPH$b-`d%)tYOrX|F!dizB@J`K;FIG-#J2-p%O1v7LECDN^N@bTDmW>grVz z5RC14Ba{BBo5yL1KNdT)gkz+PE$P<Et#W8dRw`!EaaT)lMxRH6U_zlssX$jD^&hOz zh7xBPbN+*RJDQXIj|0suwD%Vw--WlR>c2nDgAaI0-eCf#TZBvajO(-Q^lw{KdtEQn zq6ku2y*gs5_k6}n21MAEAW=7E5wA%7G|~!$PnNHliP?Ry>0dA+5nvmNfz#F{%#q-H zCVq$Su;<IFo7m0?o5_rJx@K@bz7k8Z_3RTy91eP<1F&{IoDZix%wN}B!Fx(^ki<29 z$RzyjL;5~DuS1Kv%H8Spq&Vsz3&%DibUj2kZ;YAG0PKh$y!;_%(L5p%%$}n`bR-~d zRVqg$yiS0^#Pjp`<=z*}dGziXf{4^EP3hg=YI-$W`@wHF%Z3z~1bq#b!zM=zpZC8h zIwYvk^#(XJI#IS|yj!Ib@oTKQvzm!k0O@A(af8!t$En|OQx$t6YS0^{W7L_-1L71$ zHMP0B?}#ovN`bX<%|y&@VJ=U}tj{`!nI=~;gLggaW%dc$4VxsawCT&m<dr2hh&$v@ z2&%gg-~o&C@()@Om!20ezZLJ|@uP2+$t(Hg*%+Qj`k>orltK$C3eg-Y<`80*x$uQ1 z#CZ9nbw=?Sg`U078CI6xh;sBHr$4UF-NLzR=CA}-?r&>2a&qpI*$awKha{|pWh^K9 z?4L)1u0-wt5}#)(!kIZ(T9~O-jNy2yygYk~4lVs}$Flb16)uHda|V`kve`dlrKyl= zR-(u>?MDOWjyfipGzG@n<&K`|G_Q`!k=PS)k{Ofge@2g49G0a{)h^N8E^FfuJx+Cx zS=F#!N(rFVeYg9;`s6fK?T<Nh^fVRf&9Ti?_7DT%7??ewagT#&?I>kYZN17FbFn!+ zvFl}XzP=&`)@x`Owey;B6_bm^%x~0DZt93jd0qq0^wj632Z~jSu_odQt#O*56@}k^ z`ZR`R`MP0&HMP=;*##pczjK7Q9yHQeqn>73JrOU#$ggVuqM6qabA`d^&h((U%s6j{ zps$;<E7E$@gm6Ni>0XG_@>IBOcz#?<Q_lE0H_sN4wg2l|0<jq4CrDd}{JG}X2C_5^ zSeZFXp<!iuexDt>uw_O^qFm26yEAl*Onz1`%i?j7b{koR$?NwkOE5CVNHu+b<}X{m zMfJ+gUNohkNjLP%vmAv9967Ik9O+1rwpmpY3jsy0*;m~*bWJ!jH_y6hpJ(i|leVAX z)S2A*L5F`&F_kz%n0ZI1NV5ufKgTZ3>E*9|uF5Fl>0j(}K-v)r3IB4_kLzk&<%t(q z!Oo@a!uOWHz$XF&&_~uHaanY`@oBf2S|udH*__gTcRjH0*OcOF?3Q?aK-#}$J>3{s z2!5f9>p_L~h{ivAk89<hOQHZ*!hNA?tTGl&EEb_h-jzPxZ#<rF`WzmVelnCh|HSpo zXKgqekWctjGy-YiG6edh13YoE%-<9QN+DYxAf0giByLQPVD>eJXSw<8Mx?^kY<t!W zw|#&lEuLgE23R-7#)#!%6FViGE+=(z^1UkawwodOXwC9wO+8WZSPx|3^j*b{N<*fn zAL`<|7Cp=}yX)k}pk8-HEWWl*LEiIT8xDq-Va0M@gjng0JUQQ5`q$R2>jl3;PM1Yf z%LZ}gP8T1Z-IbPS>8p0cbx4Yw1?Y~QVP#ZC#$c_S8?z+o-=<>+(sTO8FKdm{int@5 zyGh&GHJ_X<?ZnAfxzcLWeyDog4rT0|`B~;>|0H`DuS)z%@M`-d;A$2AR7<`fWh`;5 zq7B|`%@)4$rD26OXQMgEsZjL=pDb|5*@f=u-{zzOk-M(6l`7EfWYK#^zpny}5+u)o zQ?f`?cz-(yi<s~8ye0PGOVrVkBJg?8%J-j#0(hhQqZQsl4{_>5>qGgS8+;toqr`yJ z!^`=ao}LpC$s>JW8%z)-1S}gJ0Ij>8h>l0J^BQBiz){w7eCWHCQm`oMK`=nSLZ#67 zE!MiZW6`=JZ%QJGvCXcWGkG9<gwhdr5IyrIq*nGpYK7&8eCXAkX>DHR@*ibKvR3vd zXb1>>xqas8>5U0p)bvWi@)tJWK-eHZJIST57P8H2PP;)UhUXKUCQ5S~slwx_cw1Z} ze<lK3&;oh2l9ae<@u5Me0s*H;I~5v^=I+V}-WL3r+_M%axxXq_SPOVqw<dW~=zIK} z+v?`snf))LjVp2(Xov0m@-!2P-hU>C9SxLPjDF~F)2xd=?izL)Q1pP3##lXS-!+(P zl~nY<cbsX5APzOhuNFOh5jB5=kz`3T{HjT3dj8~QTpjOzO2b^5Y`Fy#{Ppt7MBd%I zWtk_VVGNbBMppZM^@FzMB<xe1Z7TZ;yRDR8nIU^4Wb*EcfmUDijl|)jYL2)|lo!Y# zmAs>poL+B%9A$|Mf}e&Ro*_dBIv&PjLD96*bBInWzdf1+JgjGo=bB?majCT<SI&q0 zr3EF){OG3Uvf5{d&8E$Z`=2^%BXZfChgAwBPz{~dyjp!p6MLgM>*}clYDaXkpPgr? zT5cwXmWeq_seK|MlTmw8ro<|eK1f(Q)KGH<|5RKz)ZiA4c-|nZCvHgGw#BGSG^~<* zy8XOLe2?Nd(>wdm4l?rqvSv*G!W)1L=TXoo`O6-vO13Yb{oqw_zkU>b>`TUElIZzW zOU5{2u1`U69>>7;uSwi|HkZkWBR06$l^Zai0eY&m7@FrPCfz(nKDuxG&Uuvzg3`Io zk<$PiAd$XBdy2Tu!uf2*#22+9!v@hR6@;V*h^;zulY3zWEZ&7i0WX}+vpDH?m1wDa zX~0&+AnqO0Ob$jf?v8IV_0WJJg_0fhRn&085mr*hUEbafi0bRxW)3ToM`P^(GiWwy z(n6Z`#9JbSX55zhV&j7`7S9V4boL_hQK4_4k+rxM858E?K<JP9(jp{kP;x0ot_9fg z>uCy}VuJv`L!(4Z-TC)-17`q8`@DeSljrxoeZ(@mPJII8r@(DPVck?N4zXln*!2QQ zFC0JBA(DNNygYon_g<DDb_H4I#ZKYXu$h468*xQ!P*vxXIa20_vwjOkTmj4Yq4raQ zF>+*3)m85IdyW}juI|Pt6>-itURAwncJoS<PP3Trw)~JCr9<c7zVx+dtmt&PS8tR0 zFr`$#OxQG~PgnUy&j(wDU@|GTWlx<yR7;m)<j?ToEzX*;`MRtPp4O#YI6hVVfll5S ziEL2Hgm3_ul{P3g?8!rwL=gW_d1FW-*>=4E@8qyOeCgdueo^S-tX38n+4}x*yPBhl zzcHCXPs0(bH-0i^i;I?0DufSH5o2iZXMGn@k(|lfLmf&iD3A9z)Az$#iC>3qgOGM? zJDFN%J+V3bt`mfWIn0v;J=;saMPla2#Qk{06TS}m(=);@Ax{SPIu3|U0t_aI4H2(H zw3Ylb`tNq-q+^~vEIRSyIJ6f#KVt0e<B4<|I4QrqKHQ%^Fpud+uH^IE!m{?gWB6s? z@u{`U$l2K%1(Az?DdVS#p^S8F`7q`Re-bfgY}(h44zAm-JhX|V<m2LnlAV6}O}ZGb zOu2$vk)%A*OPksmbMn6;ko<?`g<et4!DiG$lD$r&w9>QuhI-I-X4jZKZi>2X@PdW| zSr|QAI6o)?QGsCbQ{!}oY*ylGoaXXNA;*jnEZxbSmnoMJ<8N~BPPG}=p$3pW|B+}Q zst8H=Vl0->wQtnxt0=(X%ynjL<h+`LEEj-noQ+=@+C9+$f}-T3e=->Qs<41G7j?U= zYl1S$!4rQT1e|5!HCcPF4Mj-NL@yG}-bo+ER|@tEDMzDl1I$u9t$%uW{3?>7nkCuD zd=o46KEx=N6IGZVlJu6Ev?3%QlDh<)CFpupe_iI)+A?&n0BIuDfL<Ulr;WwL9}2+L ztoOccZQRrvmKN;oVNQ<`mrA$o0vPBa&9py_pRUFHECR`MJr}|#Oxet$yHz57esLYd z*`QMxx_0!Jh2yNdF5{b_*`{!r-PiD=TZ1nbMZ%z%X@GqhEPI=?qmE%F=d$Dm2^kCr zprmzFad@tN(88!!`@>&kNykS`k~{5n&VzUr@&L;)R^#Qmsf{g%*?nSV{u#JQDnbYV z!Uld5?45ydl4X}=jk*3@4c#2sHs0}$wCI+^6T{8rwRbk!F&M1qNroh~FW&T(Hx8x$ z((9|*bG1TO#r0F(a#@D>*G|o8LcO!0wwz`I4v@sFQU^eb9*~&N;1xbFPuYpqkP&UI zJmX1qlUWFm|7y+VT-_Yw-}tPEXu)Hg>+58)Z37FNDNFjpyrnsj&E}-so9u`oTFJ_! zn|dtiY`cbJOkQnhLVDkHCAP@ZcnYxzB=G7rWx#p?^Zb6Rt88m9ajT2xg8XUSMnrhZ zp<8;RGXB>ZAP0cSxOXLz8(Xs;E-WRbfDHJXGFvIh1fx|HA@>Mna|BXkKa+$4;#-Zk zY%-B0aMwoWlSo2I1T98xsRA{KMo}co-u|ALC>fIdD)YSc#L?xtCD*^Yxd@Q2j1i)C z)<FtpA%g9;m)GpZGI5Kas|{a<wcw+U*7WHc378a)B__F~dnRCOIk$bE9I8*;BTbf1 zZTn>YYiWWp6vM1hxO7CM*NjQIV}9F5*DRudQOIL_-0yrO_=p?uwq#DVN*|Gz3Udbl zDyOM+>7-K;E=7CRO}>jhf<Be}mwKYgwQI(t=9u%KXePd%o{NM{Re1#|Z*K3i+S^7m z+w0{yDO6)x1^t@ef(*aLOuo;B|FTj+<Z9K-vVEk=J4|Kyt1|Qb^@aUf5Jv{x2LLC= zF#1Yd(G)$O5w}>9U}gJ>_GdG~E(16~8+yddjkJRAapqmsSb)HV?&W})jQ-b8CKibL z=*i;}1t3_`)|`not7ghJHUPJRQ29dI8^C<l8&c4hOaq7q{|HkVMcl}^eEg#+BDWU- zK^Nl$0L^pa2$+C$a{9|BCw4Y5fas*&GgTJf<3S}wOro&hcG<W#=nzsdN3s)^d^E5G z1Mt(!5)Ma)Kp>E9^3&vwY@IYk%TcF>uNfMmQ?(y(&iuDaEX4Q!;scW0e0i--;eVjm zAO2@kU4`ZJ3?)A4h7W(7r=|$iw_p0Vix%DUH;r#<JM;4bnig<<{Kt_fR-&Rz?-d`d zpcsBI`aQNZ_&H^LHpYRw4L;+8HsB&bRp)lkQ-?T|9GR)<%NE^I<q0tBHtdOSDr&#^ z$1@2+%KaMfx7Q%ymnv1yg9jUE4=}1eLsY67Y5%rF^|z&OauK<2wrJHMRc~oe|Lfkr zj#FQaytE7gYK;nsj4owlL<G968mF$%e?}s+ky3&4pQ+m$8&L{*Su&HQv{neXZ}9%( zjWh9ID^vq6&XSO$hq^DZyS)_mXY1+fgdV~STlooZ$!i~)u4whuU}XXwmNRKcwhlIm zv&-(chAs!$r;YK@Ce;%pTMm%0rKPbe*4|`M%z)eW6WeGe%kP!uY4z?;%*T2+zw+q7 z$q$?Yy`R;fS$=ny#p$&P6>x=pTI-j0(ODkWp9n05T?OikOu;-Z8oQpmFhsQEM1M@a zc{^&;)byc%lF{x<%zHdhktP#)3WuTbyi~iNa_ZZG2el*}<k#y2O`-$l`E7e>*C-{5 zX>jfnH7svEs!)f~-E#<}tTQpd+WDrNn8H;h%|Ys3*9MQfBg9ekfa|k!PI%R6vq6ZI z)qTc?g!0yap&R;!A1O>$SB3OS0@WU&iuK^gB?-t7J)M72g!u1iG!BdHqQb6S=N*iC zi3%ZvO#DZ}OevyEi3&w9bkWiH3pKB-Q#@R58(#XXxGdC#<kn(^8|GY5_hL@GMEdHs z0=xOVhTkxW{C*H?8y(UW@EdYUQZe3GazO3EJy5}Pa6U8Q=OV=ku{^^{bXK`BG5QEo zr?%SPK(-}0*0>;W7i!*MbJucJ!M_JDp(<#53uMKe!a@<YiIHaojuWSr?e)mC%%(K6 z=T)iXn|4g35)MAZ*NPD6SHMy?xp$Grt=~9l+{_d_^e{VNZdxvJys#5SJob50y-8OQ zH(!{P7n#m%3?aJwviXAX?Ox>6k8&%O3w9l?_7`*zg_hfe4j&396>X37l-!2jDbChH znjki>lMACQ$9|jI%0BI4WZv=xC#6iaJUt!3JUFes=xfQVjkqc$sW3|p&OVs#qDqF( zxzS8|KzzAtQiE8k`8HV(_O)Rg3&Rqs{E`QKoV<HOEn0mgzgm7n>nC%wNN<&r57_fU zt?iaG*iBBPTnqHCOQw$EB$CZ+NX53(>h1>()jWKvPq+2MSDZAVlaSLlbNa<0$l%eP z^DCt=@E4eSNFG5E^RR$S`*3kA^18_3d4D87Y3d9s`)x*h@wsC-=wx8AIqYbncg3yo zYNax(euClj^||}Yt8ZT4?G-&3D4ZUy{otfkXT-b;s8gHfG|o_y4Lk|*nBB?cxd3!5 zhUcZ?jH+%f9Q;)t=eOTkS=O<`VTIKLzEDNcgDTGbY3K?`N#}dfr#KsAaZl=S#wBaf z_`g%X=}SKS)!i!a5HbDA{AvZPC$Nz-qQ6ZxXoh{??kxnWtS(47lTRy8LuJ8uUC`+Y zLx2cyALfj}D@~I#)#=Lv7;-J<?acAKi#R?vdTe9oH^`>h#O6Okbeg`g6NN51C+3}1 zpt-V6!+S+V(p@;xJY_Af=-Mk!tFhH@hn*I|eKyo{!myn7K;e6QYwy?C)T+v>s`W_( z<gyNysPu$n!}3F0hdHLD9LGl*R6nqv%13LWP|1`UOmHm5u7~7X8@t&0)4@XRlqLWk zlw;Va9zbw15v*aR75r_>^=JqnW~P@e>q&%@<Yg|}5P*K%f8Fg$GuSqEu%t?xguyB- zV4wE}G=!uC#yyZyBF+w`b{%y5Kc@KPi$O^|(NRX@luMk{1e|9K8#*s$%BFK<rau3? z2-A>}Ut5wTyB1w9)5mTU^vOAyiKyjS#vLfX{h;aDr&%`MxIM$T1iqSYeGi8hIi)yi zm+~ZRa(~D>1JhK%+%xu+!l&0Kn_X_}r+3+ud(oeD=_(<#1om0*57_4MSejXw4AZ!I zLH|@7wpY!E-so<#&=^D`W`{}v<J!N}udATl>RqzbU;w@=*zK^&Hf~`^BWQs`r~K}j zd3<eu{=WN?Fx}piT*`lacVsY0C0>+^p*Cu19M!1SYok|xk^bwV@m{(<D5XV_%#mlW z{1nl`G^RogX)|Sr$w8y&eqaD1ss_CYy9h5UX{Rg*N>{}Sfn#YCALLGK`n8!Uwv)eY zHl1mACSE1iyrUUNS<wXR@qI51m?~<@pf*XL+_?$yp<9<!6r9%CjpDxX(6mBx%C(pV zm<)wqH#?EEMK8sldlsO%d)Q3R^>5tiIHdddad&8)az8+P(;(k#qrx-pX9^}@CU@8b zy|*3@hq(GO8NM{DcjxwY1?n$(62(QG4o_C#>5e;p&Xv?r58Q_oN=KeKR!*wtiN3(( zaK(_JzoNv~j#mmekbf~=kQ?t}ycw7V?~a@7;4>GF!|qdrure+;BGhnA@hg$%<BdAv ztLlv-+aN4yS(_6(kW<q(cAM{gV0V|MmbfYDRpLDJ-tu807Jt63XAT9|`ayBu>wvIG z(k7Xa`}~FtvD4Z@$_x?=wPvC1ldoiwu16^Ts3#q+H#v=)&*N+N_LMkjK<6b*s~BzW zx|^{cBFR7EM<_n9`TT;r9E{h_{48qr+bN=5R#3V}J768g+UC@*-YHR>x0r;Uv7;Gq zN+%tvxF$d?H*D^9bnk-I6HUlHo|(^RRfr7Rb_M@AYIZ$r8qW}+k=ZA&ttA#2k(+Lk zuG(G-)b+f7@Uz%%ERI_&!`{d;VJK!xEo-Rh0ZlbZ#U|W0N1+jvAepHD;(M!6VL@yg zmf27k>sg8b2x{NDZn#BWZ;S`Ie6ivn{s;LD;Y1axX0)2%4+#<Z5B;g;Q0#+hxK#cZ z!0)S3<O=f*A|JpVfw38cK&cDbk3XuRoUNZ2qUrLpj={n3i9vx<J+TtRdkmZ!0yS0g zy9E`G+8sdFk_iAogd!SzX*dsL2i>6wuGKj^!kCpRo1qOW<^aWYeo`J@1;-m?pW(_l zdQ3zrW)PFTp29L3z><gqI~{aT_wAF6h$Iff5*0?aoPq}m&RiaZ9Dnt09FZ^!oqeM( zobc0#nAtrAj(gl!U;`!qi>`~Ps$uxrF<K{Zpq<cwmDUi;wRMJh{v1=U^n>xOgLcLb z|4Da|eWAX%JqInHtIV*@a>iVaw@X?t;JZzi$(*iqszIg%o~x$UM%z9qR3=Q_KIKW_ zd(tH^*Oj@N1zd@n?y1{=_`tbv{M>*`CH(o81l5Z&&4a9!UAFnTNKEuu27()!B=cCN z3gr#llQg<&zpa=G4<ywqDpLFYM3EX#=L(#`#lxSqf&^RDv2rud@?0s+-XI)3b<hc~ z(k%*0P7=Y%`8`+M&t5*@93CrH9M@IbJ&Tn4_GI@6O=)>^1Z4x;RLJ{YfHw4!3m{=g z!d%`>72UeLU9n5#IvyARdzaRpuxqoMEoqt5&6DUAuk(~IcqtXKhzJ$Q6-0$cBsNB0 z64{$8JtLVgiNC8ab_kc>y>*2Z^d=bf3b%giUkP?PTv0Gy-AtV)EZ7Pitv3VD<Ix<x zHYY}BJ&D&tEkd|Y=-K^wx`Aw#VW~j5%`rz?`wkqUBygpN+vTI|u56A?c4>E@c=Gp5 zaPo)mN9>lPlrXoLh^I#z2fboQ6aB-YM!Tr>kkJmen$NgN^6YWCg%B02Xpj`TK!H@u zJMWO6rw54}P?jhW-2V56rrJN_G*t)^@b88WE@vw0s(PO-K?T7=9n%M~z0>SVf+g}U zp7(rxzdz>1)NQUt7RGY436R#M;w9`yaOMWfhJSysujsEj@69vbj^IxziXLD&^TP*` zt+`rMb(=biFq_=Ks@3@q*Fn6*bwhc5LJT*jv0}PN&#C%~ov!Tl;jpP`kB2P_aiP%k z>S{}<q|(tP;s`8uz)Opy8JvAo<NkcXZBS4sEuH7h%PS(6hu$xYuI-tQRz3IXn=$?~ z!B%o>ER~|B%~}@4PKN?>++Xb|IF58!ePJ`berEg!@NRqStC4fQO$gn1AUXdt`=^6) zoEfjQXVcU^zV(sYcEwIlK0RIRlH=)wok%`pIFc^jioNnL0x382V#m;U2^udK>yt0M zU)@h3B`+yZN*4g$E89lX;Hf1Ag|^UiU*E4G%Oq*Nu;aL+2)TZ_n4$a;$5YCR>8E~V zz1nwdHtH~T(hHi6rCBcb()6*KxoKMHU{$1+Fp5>P=Wce)(9x^zuU43$)0GLx`KdZw zdj&*wXij0rmuA)K?e~>4%O3vSYUsKVMEOl`*Eyq=a2jUJ1a9zRn77LLiBk;kw$_6C z73*ae05$f=%!mUboW!I}m1*b3zCK4^fHEWvb6FNRdVeEZ-%$aWT9y+IB@}X6A61zO zgJTluILdzp=&C#+6$7(xF4$AR(+)n$g8|mpEB5%Uj7jXwkdFA060cFa;IpIN%VE+} zqXdI#-ZR!H$1Bn1OR8<#ajaZQ{{b{Vv*vUm&mWiecd<0{=d`;c5AmM<i%m7Adi5hz zWJjZ;;<Xb65(G^d500#5q@$M`B_O*L$~2hV;H}d~C%y3p<aqd9J+^$^En07grR@_P z_d8IsA-heu3sk^9Wg;^|B#YgiL6w~^Qbr_13t_2of);tJfn2cv{+W({;eZeabX^fn z{&VzV_J}23hob1GbK+Z-82DX5k5wy3p{;oUEf-&5-bp{{Y2ty)!^}txB>1>8CVT+x zEF51B{Kod75Bk*yIgP8QRK$z^)>;29nTP8w3HpBVso@gYHVl62dr;ChRh0dmBmT4K z4ETz}`O6t#$`f~uk}Ff(dxjgfWfxay)ds>E!CnIPu3sz-wqioUkwtdpJ#9GUKE4I= zgQCm@aNkGKOl{wmVBeup==1;ZqD*=7yjxd^dZQ$!827``I}X8f#AB>q^{+^+^Ipa# zHKu+X(pvs9$h)k5CV_w0%Qk+$O7NCrj54bnN8ig#C82$<!pYG1lAr%x^w6|v*pOrR z6RR_j+dhYBpi^w+Oj{;Ym0V!%dYhSWa*YVzoz&hOG(wLEK~>6}w5#Q=nG_svu+0&s z4D#}X3tC?c$9_w|A;lD>Ig<5b?G{9i7^10eX;;U&f1qU$PH2E}IsOvu-%{nRx>U&J ze1AwXEjO5aqyXvYniifxbyRv5hjY2n^65~qm_imUADk_GhjHha_p~x5IYtK%dk1`c zt~&+r>yQDklopvH=z{a9MRKhvg1(yzQxT^Pp0_vt@4;qQjT>0$@xZtu{CegUc#Ob% zXVFiMUPzUk4`b&%fkhv)Ob>DgZ=9(@(~fUVB;&#i+DWQ~hhr`dR(;wH&tMTrNz2G^ zpD&((V`a8iS1!@Lmw31BPquK80=9H+rQ~baWzv-{@TN#C-YDq26jHFK84UJ#K6*%c z|3fkp|4)eA)@>%3D5U42949?}3^?z!P;|DGq15r34B@X!LOOY%@G`3kf6Z=^cP1f* zHPhGT3F0Xo3>IPc!hFAEjkhQ$h8t7CgW@^3{9)bAdr@_UA7NA7#c4H(_A-|K=b>>? zZt$UTR@sRY-$Fu1^(_icHNguW!TWDg&JM^sB6>UWjIYkG)XlKIooDs7zt<>IEl>m> z%tLNtHy}+a=Bl^#Pbn^vFYnep(roeUykCF)Hj#Z?wBS@dJhKuRD2cdW7MkwPCaaY~ zx&yL=p5*@Ug4nT?r8Mq5<v#{t?>NKMP1@G1ta`QF-ag-%C9#dC32!rfjs!Zj#h;n> zu~`rj&XnL$(%CkYB430-@A%IM8keZBLs-i0diZlc;E72eB(k|=Npk=u+KL-1kG3nd z;idUdLxbhoz3PT)WDw<o+0ZS0KG*P`r<s5MH_;*8^R$aRz0-%c_h$U?$xbopQ=C54 zQ~xK23t#Tj3qM7_`~ApknAI5YXUEt%KrIw{?DWp#^8N@6;dggYz8c90MjIt0aJl_6 z!S5q~!x-9pUb0Xmn8f8vkk$;1n5c>C8~Vi}Y1xOJRx7f<Xb1Zpc`bivZo2iRI6b9f z$JhgE&wGotaj#g7`XIdDs0RS_kY?@xAHhAPila>Ma$(N6o%H)v=v<n}=cbVpe2mMA zYVXon9?qiiYOpKw#9(Pt;<SKTL2AE3R%_*Ze3|4PSV{9m*icSP(<q<F-u?!B#-?l3 zAgv$Gebuy2gDg)piVt$n3*p>7i~3N|?vf>86kz6ni5b*`v|y-pJ{5Ho6aGR*Be8TA z!|XjrlHBI|xw8y8nr8(=>gr7n!p}D~y}L_<JGyd!%=338(rL|DhL&)Jdc|E0L?;^5 zAcW)qk{VjF`Xo#LO?Rl$J8VTNg+K1S-`C0j%wE4h);Y^tVKq90BZM{PUR2$pfuW)E z?yNw1aX?NY(tF~y(9%oEOvoGa2zGr;znfY7W}<v~it_G3itB;hy6y}*n1IDhe)(He zWUC@z^$#+LnxtiuSASbEt?e^}9=xKe+gs$|jvjKakrJBsdElrOIc$$-5jU9P=tk<o zZa-DHmrr`^Tm9EG@$dV<*`dxLBzWnk$N*m&bc#u6KVm2CRH8;Z{6ks76`pzxqA*X# zaF|y=HqPymJz!;ED3+Q=8@Yq8T1$-xu8AVlV*wgYk8tLrC2XOOy<;b`drv_uoT=&4 zckb&`<w#UE@#SitL4*_8y?22UN4VPZKnv2!6w+5FoU|V1NA^3OStTL0j1aL8BeQ>< zlu`7KqVDtj#gXCBhfnd;)H%V=t+o~65rFkD{gstvZn`@%&H=0(>#oKyhukC}YCVzI ze!o5ubhoz@LwPhXAfm7C!6GCT^0ChKSP<;1`2<{1JU%gPwJ^#eEHj}@v@KL4*UoLQ z;G~uq;(gkAsk2+`a`VS<=@xVJdIKoy^!8mC8`O6$kiseDl6GZ33ResWx&_2{(H*=D zB<HQItK)ir9sC*eOt_?4=m=;WiIv{9*16j~Ik1AE*@VIZt%q?xOL)=E-inc+6m)BX znK<-uJIHC{TwUZ7-Px42aQ<-F-q;`Z?hh=G4ilse;-P0gLsBOEvoK;75}sSHgOIjo z#k(i1(kFVQRdcf6crggg2zN-|b)(K8MNUQ?H9N!NY$kPY>9-#1$K%hiU%JLGc%N}H zf~6Wl6|@=Q0@0J4Zs9YTi1ro{9bU1Q`<P;H+r8LoPwkr8p5Aik1X?2{o|6>DD-}XO zc#+KLJFP|Z5wA{9JugG`%XdG52W5Uiw>CF~)(tNler`Bms(QtW6@uRtY#)|qmu3lM zOn1phCQoAOnOS*{`%gBjw-Ire8){KH*0QcVdu>r5;%TqmiO5x^=X@Y!Rx*Q4k|v|z ztAEBmVi4l_;D1mvSb<HE%!zA3l{EQmU0=4$mlVM1)2KKPB)!VVKW;6=|K*I!;-knw zYdHZ)tvsAxI2}sLOY3X*5@US`nl<e%8d1U$tegHx`Tz>TVaz2(KN<Q0_UiT{d!941 z&j}Sj6IF&v7i)2(5LHH%+(4)mI_CIy87MIHNq(h=;i-937YlQDv~2WZuT7x5BwKt6 zr1U^HSLGomp}u8i0vWLN&T`>9%$++N%*+r#<c~YQyXtxe)qm>iiX9Z6v!+oT%{hZ- zF7)F$c0{Jg55@jL(d<9MP%SYAzuCL<7Y^%()#w4zLO%$(Lw1osX4(P)BAjVt6vq8G z9{hIt2uppaM<ISqa5dseX{V*)ZyG6l`zq!3_GGR0O97rpj(KB-o<w6IYUSzk9E!D^ zq<0L<#BL@$Nt<OV-FbE*+4UxW6nTas(k_<6{4u*kXIz6{%6;nV*OA^tc9BuCc&li+ z!9?R?IE-;sLB0M>!<TD#9suo$A~~(vu_*g=-?n%4`kDoTvNU(Fjo$UopQ3A|iQK0e zfZr$NN#som{`Tt`pQYrR&Di&lw{Io)N;^rg6FHC%C$0<my3?E5!d9W*t{1(8oGetH zI}?7PyUMqxYW4!b1uuoToE?IjQJtTggY2~Tn=oPjYYz{`(+#5g@2P^_K5CJUYSWag z-*Rb@bJ}4Ks_VD)EBy@(D*AbjkRvRbA1`yS))34`9@0E5FCKQznksQTEzchaImH}X z_CBiUUne!vocjxw3H(Gn@6_*Xayhhc3@S07e2PryJpcq2BR&ASRFFi3*Q&Dl(xsbZ z746%)tOc+|>o9Gbr=G*Y0nU57Mmf6k`q#|VC92NNfh}d&CfR>#694c=w7XJ}64O2p zX|-t|r{J||nNyRhu8fy{=?_pFU8H!TrNw(&HG)Q3Bqw^pbT{TafFR0z|Iv1ayt`w+ zOOmU@!0KiRQdKk-q~N9j*fl@M^8LU}dz@Pyy5Me?d^6gS#1>W^Eu1Qwp76*jPtSHZ zeGQYz!u<+)0xI9|HGQntZgAHxiM{w?9*(3sPzrXyA0XtDWQLJB$9dYT<2{EE8$_Aw zAr>OQhrT`n+t}^D1SG1k>A7#Z2U~%LR(b|iA<h<cz(Lwrh}6(kxVM7kN7A|UzD>ey zm%T~Px_$Cd!nf0L7GGcqQikgIm*jmz-h4Z%EG){e#2BZLJ)RE>+CGCmzt`klA46^$ z5=l&Rwj5|z50kdK53_QU^M2C?^_rM-8*<WA{-xtHUp0Y7!QAq)UbU(6^>V`}_np#< zm5dR4y<0GRTFYCBt~rPj*7#P2&s33Q*8`o^EXb1gd2!q4De_Y?7=%_wuj_1W*3E2o zr)gi-0RsfTSrVM^adc8pYa02IGxcX+M_nKd`sXbmB$J#UH=S_iZ@~bkL1l5qT4m0V zoE_m1DA~bezD%g`>mV&g`h7G^fpRQTBR0b1$t8_Ie;`XfXiUtH7h1TkB3%)<kL%*~ z_Nps5K*y#kVRtlauWku<wi*3&fqncWrhV$+kTED)!OR^TBCPA(${3aua&vHI=-lpI zHQEMQ67c4+uD#aB?c=}d+aEd4PZ*+!7weG?OOM#u(ZV>Nu{t@k+Y<wv<HTYy75g<) z?Qp;y>b6Iws~NsmK5pTQPqx$A#5W`r5>DUZwg<5)xO%hTGcA5Q{Bzylg4^+S$tq(w zu^fD$a@C0kLJDhrMyYE-HAQvE@0hkEtwTB|KHYbzv$hLe<;wTmF5fz2olTxIznyNs z={++Ri!q($(2<ms<a#Rhvgo<%RASxPth*hnZl@Al#LLub@qpy#54b_^a-4dv^@YoJ zUO>ipijy?{jK=o$(A2I-H?#P%^39i-pUcQQf8MX*m?$Sce4N4=!HL`VXG;?(!l-vJ zvm5vLv)}n`z9oclN_6MFYeYs;&>i+IZEQO&8lO!Hj*M>95~M;4PUt=m4?DkdaZ!5@ zu5Z*yn<XEW#JLpE--!&lYgJ4aaydQEyGGi&RWO@$h8UJg7r7`i_{IyIgVsL+eqxNW zRItN>*_|_QmBO}%>DfZ~(Ca$S(e~^5a=$jmd-s)c=M~Vyg`K|CeuNx9TkG~GvsC(% zGHjCfzb)7$TU?J1!o09|)WI_M9K8VjE#(4@HmpyNKtorK9H{GKLn6Ql&Xs!c$$VS| z5ir!pl;Xq9z3YcoNHu|&Rr*!T3jZC-(5GQQub#_LZb3V@F;^X=V*OqdcyZtQn>=LN zz+K`_q(X?iWuYUA%py52pAPo(U}Mus{^cB9>0?^9BkJgeeM<>V(eh|wG+vzwVvUxt zIj^9v9jMX`NSefJ`8LG%rRl6+^=`*`(Fp^DSVVhYw)eExm{JpBcOzA+ie25A#P6iW zV#c%;!xAs7E^y^^OiOJHq(7wo?mwR!DY)@8s{CoyXV&#(ya!gOO50X8SkT0Q406HP zf%AR}FPS+?F}DgjG?KA8%~h=Gc>p{?vin4%xdEMCUOF-M5$i0<k3ljvURaiX_*)Si zigPx^^5CACX+b^ke3cWP1t~|x0Ep=aFN@YJDME4K@w1T7ykLN~A{QAn{*e~EN3ldB z&@ko!1-$et@=*(~sefpCRy{B`xoSYI>O2-SO0p<iZ&xTuDS{5(<o;IL)mbUMzbVjP zAR`Rdq~#VUn=-o5+p%srUAy?{m{M@R?Wb6SR^zru-=n~#Qs<WT(UR~vBSeU!IdJ;C zllA*eu1lHG`#WiAfvY1<6YhXOR5C1nW@-Mi$(|vmu$H;CB@0Ju+zC7tUpK1TRTgH4 zL~!f)!>D+#oyS92NWg~M)6a-C`>3odYxy^cn}&T|?jz}F`U#Qvdn1d>AI(b2e&-3; zt~U)|Jy|uTZDpz9auMA-I@sh3Pj%$hkd}qz4`wy=>$$E|H<qXo=94+4i<7`e<nNLr zw7<*W-V?^9SEpW9QyfRIdCZrF=DTXVXs+T9vP&5hDPpgmzNaYTA3+1d?V*to0<NZZ zdSl6v`PvJpqb9D6R}1evC^o=8i;nm2Dx|Z$v)`VS9?Y~ty}znUdmRm6CyXrKxQbns z>#G3@mGc7iV%ee1iTABrbV2iU_(f|{8|3-8Inv!AwkRF*s(&-f9pi`E9i9pJZB0g_ zF@j^k{%NA0s&muBdW?}E?yjWq`L2+P*pHr2nnBdYq5MIg+1i4vyyk0el*~LFQP*hI z8HE|BggX4LlO@9v+<L3>2J=U;BP?-l58S>idG{G`{y`74J;xTRr_nS0G(lbhARy$* z3E~^pb%)tKoudG$Dx{7q3bYWSxfQ37TbLcMV-pxuUFSN|dXH_!vrN?X@!og)@;ATg zt!mXzT+hSe(O?OmzX_Xe7ug@5=$rQ*Cdxwsee257{3m=`*AN^$u&vX>xMbYNvsq7; zB<^)pIDp|$Ij#=BMvi0AuRU*fMyDy@5ny}j$^Q~*M=I-24V~Bi<?s^$5e~A$IVh<Q z1_=1~U|aWOKYK5NaH0!o=gp~an5~>D3%??nAVj!P>Kb%4G-rDSg71Wi_1fq76cUOB zTEBugsl*3%?a~VcX@2K}R3?ks^>UjjOf$cZ0GpA(fYTu<gS~S`kqh-a#dnx<>o6~_ zwm1}5<BY&tG~>{?<NM0iP(93+SpV$1>NHlm{Sc)Z-kMMvF`h0@u+?iIo3{nX4|z~d zXs;&h1gKB+gr)c3YHCfF<aLh`1d8i%-t1UWf$pKvJS&O^|G+n(BTXINPrI|Z9{If8 z^grm2gLA+s=I1Cytlp(Y*4y0%@*1qZ<pby1ocXRZEEyjRo+&yic_=--)coS{jLpJ0 z#P$$U5ix|2ETnklN>niBKRH{#cTi8<gE}g&U@-YSvFa9S;`wU@A&<r@Fne0m+=;+r zkPytlUO%Mt_6_UgLG#!K>CPfFgW`(Rd~@LPIL?+y#@l(Z_5ci(Z_cQeVWoyOcJbTv zPgT7y{CU=A5y=5=EuUBQ+5Y2N16z!{;ron9{Vsl{5WC@O|GdKXF$RvksrZW+QIRHB zXDV;uM|bl+hC1xG(@Bw)aQF5~borxKt%id~GH+%E&EV-7E7QX}=f08Nj#E;_*0#~e z7H52f1BxVO>3n{woX7CFDW!285Y?sN+I?W4!9LMY31jOtFPR%PSk!e7S>cHk7xLW- zU%SOLI3aL#78&l9S^b5lS8g4lnN(JXnh<?cFYv^sjrPU3AJ6bOJle|{e&4V?3ROHM zwENl56u#fy9biF{0rN|}?qR*~=!#wT8aiFeqy&>28I6dXYX)MA8t!OnPR`U0Z>CMk zY#IR(jSQy;-_*%c7yo10S`9xy@%|?TXqgc&onU2gw%%m1!6JrP<3Oa%bwoabwuTzM z6>zr;jVVy%ND)|=r;4360XRzfnzt+6PABb{2z2e5UwYeY=h|zue~-_*wXI7OIJz@+ zNr`ynnreBFrc5ZNX^tYGn9gUmD{?zyLRsk%thCk$bGfDR`Q)iv@G66M62!S6p)HQU ze8xHb)DY#cHc4de^SDWfyW;YGAqjWK^%4+s6P#Ek;r_Oe(nCCQmbp|?vhw|?`oVVb zd(rxETUzs$N$EWgk0r2(yK^@+BwJn@$QB9Uur9)X1SOIZ|5E_e7xn*?_Cg}9u8^LY zU)sXJP`ztuIhADoE@E~-|346j%UI)F5`R;aYRC`nYv?LH$Zp?;lRQPpBrYtXXQ@zR zE%Z5VVtCt}|20Y+5y%5V@Wsou_|N>E`5O!6&ldvcvah)RvjhdmQDy?;FZglPqvmWP z`!RbYhVQZSf3MN4$b90z=*G+oxA<uNk9s}A7Uq%oaxz7(?OT^Eb-__ErvLaYf)2$$ zi9C-vNAfcLZ)kg6cB*6d|1Ia}pI_IA{AXIxf8LECcJ@DWoc@0Jf0r)x|Ku%?Jl~nw z#kr+Oe~0S7y4(^tiqKU0?xXV5JH(owc>nh0Ul8dB<Q;+?L<KxGjktKtE>Q>rp_&8u zKS%#>L%%%58Z$Xy?iv>E{$h{C9=)(Z^Uso7JawcF_jn~2SN>Zu4g`c_^5a_tWgRJ7 z3wKFLQ+3Ha6JI7sPZ**;L*Y<AqepzQ0*vTZ{9j<Pf=Z2<rx&Co=-V6kanBiaO)x-j z+$S8q{w#cd%)!Y@e{nI9JJbu|_!ks#Tc?P^fV{`at*M!Ta!{t;=nurqAONKsL~V-i z?w?0I=gw+iR*>87lYS|1)4jJbDgQ5k>Z5R0(0xiY?6!MwKXkvPrDHsr!{t6K{#j-a ze%zGP>+q_9?D5n4?D3Y4=*9bJ-maF<|91Zk7i^f3sh^dBnSq&=*%)ml5+e+Mp#dSe zk7jWb$;cfgCo@`zg|Xt_pvy86pTzU%(DJG5xQdTBt#Xh-8e0)3*#G)&ejJo+!(VT{ zG$;dnWPb@f|Em@J5CoF{j&C*!`SJgs4`x^K+#2-BU1<z0JsfqEqh0SOZIIExm<zwy z+eKQV)v4Ge@_wH7Bw6haB9j?g<{1=^U^H@n^6bFPI#)RYz)*il8UN!C*@)S5fW!Hb zW%qePvLdtaIx*iY_2s+ItJRb8^%65^wqapRB?&a>20h+3uG*a>h&6gRViNyt83?8_ zlZ$C?&<@vNE+_R-5B{aVEiVtj$4-`uXfkByznmP;GJY}&)E0?YR*XsPlQVe3DI@RY z`R~mD;fLVA1l3sPJ;QAFA;Kvd-HL=JEnjZA-!EJ`7&ez|1*&GmPO9NidmDQ<&Nvh| zId3#(-^x~Io@-kgh5LHyDAN#&ZTG$p`_kaavv=`a67gL1DDv)+k7W95&Qdz+#t+!{ z%-P=U@_vu1XjrHI;RBxPfFfZ_D0f|}w@{!buei=W-NFytM(((8GbO1=1b4K|4p`$g ztxf##UUcF+Q+N4_u-$2xU%k{lp6Uoo_Aj8S0M`Ms2~dpsjXTuG@@v<5Js~33e=Ssi zWUZ5))pum2cEN6T*$qA!eO}7jD^!8$6mEX|dfV3dk`&B$Ag(CsBDLj_;0x3a>A%OP zu9&eq)g@w}V~BC1kX3zWi|G)G#kBfzBe*KHY~jJ|hEVf42i}uW`PsBrsT$FdiJx1S z>~f{&NpD}0@$2+L%cbRiSBKt<!TTs(cwJ~lGxdxR4oX^~>C)E99=4L~?q`L4h*<UD zVETc6)>d4AN@BHK$){7l?5Kc1#(b0+sI&d=b93q`mJqx3<MZ0-hIFS56==qchQ@%L zWy6fIWP_ZJ1i5MC+s5p(%dhBmaVM&4<$+j>=ijlOm9hCwJGc?ik?;tJa_IG*jA4o^ zNW#vNTr|myulZ33u?>m06ss+OY4T`qq(mBfGo^$qfocsVVfHvfMRqa>!f2A5F#lK9 z2ctk$aRmKWPK>{ACXpHIb#oqzyhx8DXlfC=^Q)y`4dApc!zGUgpWFISBZ0)H9S1KS z>ostuB^2S`uriSWT+<x(KRXa~yfAi76AQm$VI!k(Zz>(_%`dgS{m>r-6s>yXTqc;L z+ebX8e-oUNp%r!ZIV&b3eBuCwHvOI;?y3s5go8f_3{=|xI#5->B95N9)P0cJ4~L!X zhKS}kW`8DkL}WR$G?I$Hn$jY@0(k6yq0EFpy8@7Qg0nI14t#PJRb!rbb;zNO5>?%~ zzr3pf>S=44q!N-Z&mtu2XprDP+?yX2(ksG{>w5xZLXFBs?}|`>{S?HU{J-k~C}^Y` zV1y0)f>{2Vxi<2s<CXV~03jyeH|WsK&f?$IRgT}T<NdS)3BvC3VC2j(y1uMyD`lwy z$UrurqlR-QX?wARVse<jmEBLiyykr(t<mvhf%I;o0YL!n6B}N;kI+O{|C%R6_aHbL zs0;#&K5@Ex@o$S>78#UdT=BB{gB!rsHHt8ExO-nR7f}r`L&p&E1EiV(e+v7&MJ7a! zbu)%$Q>$twYc5!PZUYV2-R)0UMeo8+W!lno-r!KgB#gMBTJkRoU)TJx*AcP$T>eu< zD=AI+7@+Xn%b7Hh3;dOr<sZbKZ6y4D06lX~Wb!Yg65d<0O%zlqEW)Mlk<Yrkr5kAj z%_A?sQqzVpWbfWH_=cai6}QsgMs$szr+ypVa|No7d{KxSLvJ3I{Ff3``(GZ5_gv^G zdQzMv-%X@E>5=YEw0o{re@RjGu)I7fegEPW>*w^LCGttgt%K;@r?bv}?DxnTcyC2h znuWy%59<m*f6z66V?|T`$jqMBfDSJsa2_bexT7jw8KIQh?*Yctg|D()`2^FOZ&R94 z_kioUeM{j_6F?O*Hvm3mgG>~MX9(tCX9YJ}I67=F9B7{`|Nl;zEbGq*`pVks%Kf6g zFWwc#%2&$DDYo(1NF|B>{?AW<9EN{Hc_Rn@eEv)lP|vCwtp4%$y&T2U{&k110QE~2 zlB}{Ux_srESUTljJldB_A2}qR8s)t2|4X#`H0yIp-pC@Lbc8j@ePHCMLI+ad$wdc& zC(hEzuF3{~REgxst21F=c%ls4x2hZbpHJ*=Q%GwBh$7Dp)&HFdh3ln?TlAY)yxd$q z(?Rv1jymOR6I6gO0*#vUAH_1*Jw?C~U|kWHoGK|rI_+&}2OzhZvXS3n@e1~*7+?C^ zJoXR3N1%K#`{xe4i5h|GDnu^b7?z3`G`S{O6aWgKg*x|jUN=4GTTo04R_qX9Q-@{( z4|=@B*cD1R83FLht&@d&_qsXpA_bsi9uq{a#o`s`f^1IRa_Eu(b`E#($L9c-@8{%? z=C)lgUVR%VeyNrZe3kUNo1R`xc8o!JmgmF&(t05$ujNxLBMV}N3E;Ux|IscrO6mKk zlHC7Y(~Cr-)P?fM2x6S#!Kx~SZ4Wa4^q^7fUwZH!L7(MCeC+d&Nx+DieI=O7LsF?} zQyBUhO^kq$jPux!$BQoinX}oWF*3^YdJE|NCF=hisqX@uj|L5h^o1iZ5T3~YHR=}c z(uf~5_!`U~{J?m({%!NIwQjDgk-u<-dDN~~K%2||(kH1$bIb<$Gf*WLWcS|&xRCP^ zUFuth$PUa!4ev*ee{}N9M4HJY9KZ?`TkuUp_H|8(t{Mp4&#npenzf8(P4zlhL<PNk z9HGbSqde~J(4*;>X(Kj-_^TNcH$dZQNwHChY}=R9T1V1M+5f`>k9aJIZ(g3zS)=sN zjM(_n{2$(>WdW!p2uS3Ax;C2qFE9H4%9pRd=6j1(Y$j;a9)xY=^TYvzqf75g(9@V) zSfsc_+AEwOzAKJCm42II{QX=Opn5@lFu-a%m)&2E0@$x9l{g7+)1kD(p1}(DdJ4u} z4g4uBZyXX`seUs4g8W(-bW=(N0c9{j3iAp>%Eyye32_8H65Q%jlg=`$jb=xA1t^PF zWe~{}$n3Y)ovnFgzaE%FMjD1#FHr9=Wp)f1)TlmfuNN1G7oC&Vz=Q0*=sj9xbt<=? zy<l~&=E)?SG$@>)<mU|K$z@pIT}4KOG@2j$;B{4e@Hi~;_58cpMlY{>T6pG(d(4NE zFA*2fG1yr{oYKl;po?eu<KG1wOHfCJFL+nzxASFwL0*%IC724!{Rs0Ox@H4~1l?*S zWESkJepb;yNeWa;=9Usf{Xrwr1Rk8R>Qq}2F`Rp{IeKNhY`A0%8K%AeRtkCP_|fVK zu=N%0m@9{kv4FVz1$MKM<*tOeU_-_w<}=JThr>Ssc43|+d#r*9TK&V5O~;5eY|~ar zaR(Xn`z8ni<UmQHE>}w5cy&_+`PJgmmx$-)OSc1%1ay}9b69B#U<$7RSO*n%^I^8( zxS0jRgs55X9)bAn=Z)rn7p+Z1`NYMjdDWf|P`MZ8y^RbCYfBceDv$jVvrGAOIeS4< zRYF1ofmu;KxVfllZtVRbJ$4b-6|OVJD!tE+b++v_?u2J}=`SMZK~9BGV5|C$x3p8& zxetjCEdN-#^>!jm4K7RIo{6P>rJp@vw>Uz?FG^fT=Y@bNCN1rQkmhZj1&hp*Dn(u2 znrk%hG|=gUe2jgyzs0v=y~yXCBg>fpbK3POpZ(T@-fD2{SQYPoM8xR9tM4bP+P1QT z6RX3(BcW`0%!+l^n-s~L$}`wA%v+uahN{o5)~-M}I9`_+Res)^OXPJf858$LLjrZH zYXl+aD5y#}b|5CT`jr608C+AcD7yU!I`;ZanEyb^5HjDMzI-O)O%oBP#Jn*EO#B|o zs4*9zwF`-G*J-X3Z}2YCN_SJ!r7i)s?KDt6n8#=(H(dkGFiQII0I*X!gT>Eou}iBb z7c}Olt44;dpp>AO&tn|5o@2m`(BCMP!(BQe+jdF(&WEjC3_lPszYn&eeV|<UmXW^1 zF<}zfl-v{^6t_1VI={O(`oI$tXjxKhfyJ$VNFLQaoUy}g^kcKJbg|ITrj5L~tUSNW zys2xBx10qLn7_Kkw%0oT(OrWhJgPlARwWj@p{ZOeH}XsB5R<4PPYRKXrPGH7O5rIF z5EgIsl4lOgbRA{Pe7bV4BG0aFGSBwm89=VlqEC<<?W#e0)N;NZ(%-;=%7y+M8MJ=n z6XX*Gi@v72UrZ)8Hal#HfwbrlZFA3<ELx*ggcwy~9|oq*wwk1V?T+A`#n7<@7*{}P zcDszHwnY$flvhg}Q(e|cnlDqtHCr0ndaT!4$Ow<R@`OT~4_ZC@HP`nc1=G<9&ky(0 zEk9QR*qE4&?Psn?U~Trzl1AEf6f!|SpB1E?N~{#78tH>JDyp=wC3B%)US}kZwKQm> zYt9y^TY7Ba{Dh(Db!LX2JRVl)Ic?!}JspX$2d${Y$qNn3GFdb9!I8~O6Y^RFvGzxi zWc*g6XFn>t=N&7%heC?(%)C0VF~${~O9N9W==w7ZER2N2R3%hhOXG6Pzwe-9ERQSA z?54ioquO3;fpwD2?Q!6zBK5g)%&wWd+b`lKpmLs6_p~5QUT_yNX&c;qdE~ykbc4Ug z?#0=RQe6G+N^pp0O#P@RpWSWIi_kFj<Kr$IE$)+l+cjbS?Y&_W&FDYJj0iTEbB0Th zj!|Tqec7ECfX^cDO!{%EF()cg{>vI$`~B?nb{Hqx&qt8_KKg%<_m)9%wOiXD1b2tv zAp{HV?iM^qa0%}2PJrMJ!QGwU4#6E7cXto24YPU9ne)w=`QE=%Q#Jhusv5fY-uIGg zt!s&UvU8te)x=-!aZ?eJ0g8TTQVqjO?(b8X!LmUk*z=m(qx%c);-*P6i6((y3wArd z=50+G$Em*zbXD|H&A0L5>tw1_i+9r3sZ1b)E7TE$WBI8EOKDx`lP8;nY;#Gm6n|h2 zLh^wzbMb^2;e_9l6r&R6dHS-2z!%RUDF!UE@Cddn9?MWqH)+Ml-y097>*YarDK+4n z;zd27xz5J3I%2Ann@*{DUr+>;z4o=8>r;N_5|%0nlE+`VGu;hEPi5<j#iuQFe7{CX zdkFeWa-cQuy=~1Q!J=<joX_i;QGX#5M}&#@5xeWPO0*;!9?;gCb`pY)6jj^fl8jH! z6ECa0S3cq*QHvJdH+*QyN$=boCrP5W%rovaK-^K~B>r+kjKrB~nrI;^d32ohu#FZP z+O|W%63{DVzupMpkt?%<14j>)_94`#iG9~KL&6elN9nO1(L%;WLc}yjWb|N4@KjEM zGFQ-C;{>XctD}o-_v|wqYSQhgkf#jCmEtjFS6Q_?Jd<fOjK@5CdbKdjFKHEGi-^lL z9+KoxB<1b7T&Ca8G)bCIG~pLGW|MaW`CpADeN~MCe>U4?8Z-7a*)iJ1O_Zr5`}r{K zai!BXloi$Ny;%f9K69g{Ym95^p}@$+_=u5^vk^_<-OnW+=b(E*<L5<n)IVJ~@tfa% z-?;f}Op=>>t9YmIp6;jx5~4#bx%PEEP}opggXD0Cxx>3IAUtOGgO~qX0mT}3?XZ~E zgw*Y9);}(R-rwmXS*!pjl3X!sqXopNS`2vL-A(8+se>}N**ao)KvkH{5(h9c=a+N0 zh?b`zQ$`rj9{Tr|?c6rhn%EKEY`&Qu6Q%|^%-=fLd>#hibq!nz=EK{o=#;fx4Ye-m z^0PC)zE;+Di>WE!m|G#FAF$^Xl?JK%^7w!78i^rG++QHEV1YVN<J2K)fD3cgml5P~ z{u9V=<tB`K$n8;k??=Y1mWVu~>k?^x6CXdqBv<p|oKaqe<YgUn*T^lo{`g`8?&wb= z@3Yyw*}Gj*Ol6U0gJ8qmg)$TFzi5UHfB8~@!^g`sJNPIf(GK6VB{!O->l1xRh#`bj zHsz~l*0zD3!FpVik+Z0<>16-%ZQPQ#U(T*6j-I{U`li!CTuV#K1Xd1MU}`|ZpP0;W z_5@4@dRKeDDIsbd8d2XsZFaG-8O4b-)eCh%1t$lGqL=LjL9d%cX?*xqu6CVd?pMJ# zjr~OA^9fyAQ4%ebsh7<Nx5Z3nryq~h!D%}u*=`yHLS?grRQVELwBV+1?K@*WllTXK zr)8q%z=s6-N5V2FhP(vB^YG_9k!KVx!f4zuZj5tttM+Lf8C%K_5AriJqrFesYQ`~) zVzzh0XC*)=@T+kinY1pB|K|6njOyB}u)T@se;szDn8hUKA!`nT^PVb7Hdw|Y+#e5Z zUI+iPDr;F=XQ)alxW<BZUBHil$~RO~SvT2X|C+#Y?8hG5LMoeb{V#hNds@beFklEI zb*{UwjMZ^6`EmBoQt|c=>Xa~eiFX|K`3U}8i>7pstP~tt+EvX;YTEPgz|!>ZUHlX? z@Qn0xBz;jIQvnfZysfb`sPKL#SIpl0N8AL09%XqP&G3az!zh9}N*|cQrk^uj2<rqM z;F{arro+Dji)@7r4)Y?P+j=VIdTP!oj+}Y@g%pZc1g^ES(=%?8=k+=DdBi2p)NPOy zS`^sw^B8qJQyn!gW8S&f+G-NboYH=Xcb=xe3cYppyT@C$-kui&JB@)h$A;DrI9Om} ztyEU%xex@sjxkt!79@A=nBxQ?$LpAUhkm1MGQ?w1ATarpF8(u#5N&o&OWld1@QHw- zi(C73p0acP<?&RV=e#Wy0)b{C{V4^@-QQEuphf8FaB9?S;}`Rqkemb4LA#``7FB0v z`>1oim*;|e><_dFMSAS7-T;T%Q<wC>{%rgY*000HEa@}b{~Fr1bvm&A0l@t$OOR}* zLEp=bmFH1}H*DwE|3s-ut+V)@(P$`>GED{%7DftLUa{EFWF`mke1s4(PrYmGZHVGd zK+In3Fl@jpklwBX7amQ^7ghJw&lRSvIK4y3hC8%LN)`#GE{Mizr!<p`Bf1M1-N;_f z^2P#IcWpRtwS#C)+fc0<+2>Cxb?}60!~d!zd1{$Q*@y4W={;I?cc&{GU9+`yYWW^G z=!KR;`}OA<LFgpdyPi)WU=dZCs(mZ+thzt)JWskY!EMBVyq*?yl(VY*E$XhaN2SN} z+jw%Ox<zy#jp6xFdsPReEL_Rf{q%PRk4@cKh?FB|dQ-+)|F-R4V4sk5;lw|nx3Z?6 z!X%vGG6qLzzA&pA#Jn~%mx};2v=*kCNFM&hds{|0Q?UE1pzd}Y+4sbDUS=y;C=)hm zHw|Gc#zEZ{K@FAm?KlpPFg<X;^D?Tj@b`+FVx<P9v8cVWxgma7AIiqpJ-Ju>cu*35 z-uV$jXdW0fX99bDe`DaLlz4wX|Bnt;1UfZxBM_tu<G;n-rx2;=o`2k8xYbh1hI#ZU zj&Sc1A%SK+$*kb&=+NqFG{&V!CDIXB9J1fSLH@`1K)fjY@8f~SOY~jaKqs<dqsyeM zRl!S7E)wVuplpjo&4$zZvKE;bNwr`z1DM9txWL-w{ZwS7^CC;Gp)F6#qO4Wd3F2ZE z2zpPjMj%=Sipn?7Qp+!;bB2EEuG~PaHjwDla}C7U2VE$uxrrN3-ZxPIHFpYLS0}d! zNd?=;d(G=r=vfTd*o7<2b;~Bh)V0K%YjsJLATQVouSZ$0e<9}-NGIpes>n(PURBs1 zr&l8hb^m(AQW`BDAiSGlsd183b}2==dqXJzTHXxn1Kru)BM_-ChZD1()>0muOUXRf zN-zYLIOHQwUqlAh-}kcf6Y#;XarjZH;PG9qS@X{}`6|qk1PqKV?%>edcl@IOW{T)S z^Rsj*n>+o#*_V0)uwcVly4icsiJWVv75AO`&fFva<$-J!*Y=o$S%6h)d7Hnc3hbAB z9B<4e(`KMrvdm6QGE*IDLi|ZUFlt0~PLy1_Vf2NLOe$*{tlEbppeg<)4Kpg?8}ne& z!p<*J@k}Tg^MADRn&9dRjx&09_CtY*{%bQR;be};2gA>lk&xi_(ChRFn3t&SF`e9k zLN&F-wM7p1(SF)rnA6LkWXWg%U10r^1x$!3gdF_3fS4;JifEaPzg5sN4@~tB8}i_d z8093BVWkREd4VMA&n4!I?+OWPa`J#H3`_KG1)iEYK%|*sp3BP0&|*6iMz8Y5fgK#Y z_E7*i_Zfv%fRuyJb|0?ET;-~DF0~<b#CwT)o7MyYL~B=txR+)k8O<o~4wjbu!#UdJ zh-?y(NUd2_x6cu$%7Jo?rE#&bnD#DsNFa-{aq2$s?x2)S!BMU~vg?Krj64gY1~^lb z;eCI>#OqiRQSK)qVyH({PFN>k=dA8gA|~g^M~Yi&MGSgkKqt<nvoH|7_su^8<D>{1 z*#d~6TKER_o4Ob#Q{MUQi32c>idg;;>=~b;b^cq_^tZr`CYwj;0C_4Suo293eSvvs z@G)A2z$myyHmz;5Wq}a)x%#L~qcuwvaqD)Ry`DFo2H2Ud0gM^b-?}|MrS`M&KevV$ zLoO6NHAAG&s+V+RKWYT_$Q(^wYiEDAd%n)olR-(+=7g==G6t+7Wf6`GojD9hFsJ<w zV`E4<#=UKMm-3%wXb{~=a~vrF%-b`|E|_#~A$gRwS&eRETjEaeyu^Jow3Lue*39k9 zgZ|3<swne1K+|NYJCqrR(}rav%G}<Y?ygk>@t-RCX70M_y}z>kuNCN^)XTs;8K*~r z^9cRNC>)q&)aeF3dey&k2`j4r>`HH5Y5`j76`UjSm-{>ejltF0!O>{IOrdC(Xk{$4 zuNHv_aP%s_@S#$L=JgzxFsdiib?jz^J3Gg?9po_sftJFRqaB&*X*l<*ZLRe2zPTrP z2(x!`h}mUGq|<~@<1T#m6m+*;;25mdqzKmE_ZP6d7PZe1OK)*l8QvjdR-gETM@5!} zQNyztChv<xmp7+lfcaeKeejy^?fs_pWsn{Kqe&!d4~ujiQDkvXZ#(e8T$_qb=Zv(4 z5t&CWRmJVdU6X9<%Mhuz8Vx4`i@Q@D`1~gG1?_TZZ->AwEF*DH)uwsAFDks#byL}K zeN$SFbyZjI%SG;^_oEyTDg0d_-&f_faxvB)1-cO4zSGn<FfTXfx+BWJ?7Iy>$vTb4 z46$Tw2y4f6=RxY9Gk$w2b?)j8;Gu2FqadTCzav1-#zOO&)nlvtKz8eQ$VI4k!I#<w z#hoI(bNP0f5cI*eqri-koc2V$#FENsXBH^VW39K)3OIFH`roDs&M4FLv0ku~E5HV* zyuu@o6P%Ep>hd)7et{?%JS1Tt+lVptq-BscKm<KCC1AoTE4K>jwtqEWkNKVUvX5$W zKiA|+3WnJ~!dxz=MfLCwns_*r9o%l}O-?J~96#XvC*+D<3xv>|_PUCK(HmPl^FG#} z%Hw_MTzY>qo#`TmodYaj3<MA&JDi+jAuYNm=l!UJ%=USrZxyh<0D0vx99OWWU)%95 zP?Bxa)j>aE6Lw749T)T`<&%+(X!iDK-Th!~IFN=izz)F%%r-Iu$XW=_fd`ZA{zC%U z)Mlrr65eNis9ji)VK4!T{-<zRjiyO*>Th1x`)c~Z^FD$jgMQx5PbDpB=(sEV&F+&k zq2!Y94^w2H#wW|bD}ld<ItBpoAOA(cb41V%Ov;+4*JbqcvHPR$7_*yYep2hfrv8R% z#ana0k`wE$4165S%$u7xhVtvkAdaV90oR%@JXfb*oqLQu8CRorhh5}WVSJ|u$uP0$ zWdv)wAzE7gxK`VcvbJK)+pa6bedC%Itl)Rqh0J=Y4lS|2V<6Vn&#kCkDAUb4UV!TN zu4i{T@|EZ-@`Hss*y6QBo<T_>g>Q4Fq6x+yT~bjFPG7YdC@7-rht1G7F0Eia&t9oq z|3?^z6M#(R@@K!oZ#?YmthakJbYmn!^K-cO%fOszyALTP7(fCuY<(&1M&`!&lHmYw zItXtrnIoE844be0ep?C2kXY^;yK@G{b|Lr4t<UGh?ujtImX~FF(Z^Ct<S9<ABZtG1 zVk%9ncLy)5ZBvLOqz=h^Dq1>nTDIN(K+n=@yfIKDNkQIzQWSNzu3Qm3cVHeRxxGfs z3CkuE$rR3DBa1gr1T5vDUweMUDn%k8UKf~mYH|TwVQPzE1(5<@0*)O7NPPzm^c%)I z3qSyuAe6^dWGnw(o=u>DF^h<en9b|QC9vUMC-p}<l4`8uk04F7!s|6Bc`GxY?yC*G z#MI-vZ$(w|8e0U(>S_(N>Z&a%Lg;AO2)1WL>MZ(S6xdPH9BYt(u<Nd}4m7W*c3N1; zI^|Vz{L3hmfifS1Sy;~bNOy=!y^AKh>mQTn?eOdHuR!}uQSDg#%2V*Xy2Ji-Z2TT? zmV4=UKOn;nBm+^Uft+{+_FK+6CVR{<isJ`L_HpArq#LI,o=uu9>d1M+i`|4;7R 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�$3K;8~>$V07StD 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%0D)b9okDGMV! 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 diff --git a/public/assets/app7.png b/public/assets/app7.png deleted file mode 100644 index 2286fadda0f3c43eeae705ffe1e1bef9d7deaa80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157311 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,o=uu9>d1M+i`|4;7R zP0$b(8ngk11d_TyHOIf+$1V5|N6?nV41kfpFTemOIe-BB=Y64YAUXSQreJL%85r2* z<|_nz!M{fvL1c^n7hmAk`1bApZ@)P-$ncq_EG9%!KU`BuSE|Fs28s>ge@}vJtQO`+ z1mys<!0%Zjkyq*e`7i%JOqW1FED`?ixe*9p<m3PT#{d6yJGE;}-e(y9NH9bV0N4JD z(V7&H*|W|Qd@a{PvmbK}Car%~U*0l8<&>!vUQ73cPK_Q;Yv3teX*dSdRR71Z9Ji<B zMUV18fL<KjFV{KbZ-gz|9K85`QGrb3i|XI%TY>NzW(7N?_rU|2BJS0+{;T=iI0hrG zfZFVPE53nUME+aj$)>8}&(@vIsikAhm}d`KlYg@I!pi}|pnWz;2d^d`vN*5V^=8JK zRp8L}jN+U=p-95byxQch#52orB2Ajs>yHV4aa!bmI5UudAz%jFvdpt694$`TJzdYT zoLNo^IfizJp)T*GB*Zn&!cSHZZKC!9*s_`=vmd@|77-w!i3P595IdQ)mlPg>b~}Eq zgm1wEYz9dyMI4_91PTuW+HKTHN#UgOx1QVFm8N_NH^%^<Dt5JlZQRdeOWa$1A`4?k zgr?U|N{(F*bZ4`Vb@~t$ZpSk?8tfDr8u&loxv)SOu-#Rz2|yhrg8lv})@;K$Wqsow z?^15HS*S?vOOsM}@y~P~IArI9!(XR$e_zeRkUy3l%dR`n{Po-YDu=Qv#Y^sZXhr7P zNyz?1N6zs;1@3&u!9_zHAn6d2=MMcMyH0BT^HfRi2pXy6sY3)>MxETUGA*EF%e9AY zTeh<MU2+KORvMELvWaiOMmg(6Nw*6qczV@HP{1VMw-D*9mP|%FgVL+AG4Ls;pm1lB zx%C2|!#q(4TD|nde*9(d)eoMXH1+zZ{L95Xcvg+)$cUroVHrvf(Xwo$9k0yw)#d5j zlG41i<al(!M|qBslQ{0NPsDn&&z=7pitBm_3Xsbd^>vJhd7z9GB;B-A=BxW0%4=*X z&{WmZ|9$%b&IZpBiM6e{Hi?D-8PE2iODy3Vg`RynPmAEg5i*?%3!W~v*hwwhKl$t& z+p~i-bt%er#I@%NdtV>KrwlDFZmk20DuiJi!FjWByA?BbB}pB_go3A@I1lkTC`B~D ziv`xouSZO&swtCNwDe~1W5pUf<2I4hFn_chXFysbP4!euA{LT4Il9w<^(|>0&1!}Z zP|cb#|47HIPzwv9zl1a<nK=#qUlN!3kHiU@Pf*mh0rlbcO3RmaR*yJv?GxYF_s%?% ziB8P2ORKMCZ4^OUkQRo%&3YQYoNX@t3dlrFeG>8)?exUK#kE&Bf&&>no@WL(Q*F?1 zXch@~YwBC;ZVmPIbk1mPr0H$6PIi0EPVN_IP|>lqY7x2VDwDJbcP_s#O4}7#g3?s@ z)?@eMsp9Dcbz9)vxQz;Rz9Aq=XtyU4)`ADMgThG`b_~Wxw#3o5(T8#Du#?{_{-o6@ zp>Q*5Swuhm7@8dgZb>|qaZHz}?&~S9%~fPw`VK8E$_!g#=4b&ztcR<Kf#%;t#nqX@ zEP2Bh*@0ks-1JKLkr5bUn2DF)lP^lsmtKimpwIeQ?$NO_<Gb)MXt((Bi)S<7^5oy1 zQ@(?EIPx9TNzDAMH@~^nl*C7HZ`|Rsl9D>P%nJ0sV<jYQnpYOuI8rv2@BC~Gzu9A* z0A`PmO`qAEY&?D3UQ@vJ4pz^U#e0l9*EYN!7PJwRq8+Sst(>gm%TUsI1}%!o&2Ign zz4#aoY=!Mwiv<wEdHWS5>lrr9@gOO^Q6gBx@$m<*Z5$&51x1>F<;BJ-U{{<qhyx2; zZdTMymC!4iMdZ_+d{$xhFKgwcN0o(KZuXF})4))LBRs{;8cT3MYWiP<eGdNs!1Dj~ znaY$gfq`^-#jm4C^d1+H8#^)kc*@G9(EfDdJ#pTO`~sL!cI(IvG3Q9dx(o8qQ|YJb zZP^FpRJ80NY?h3+VF4IhsA-;E*pN@9(Ym*$AD0#E5_4XtMGbXOW=RYi`N}cYR<qY5 zN&Q6?z6G1i)z@z>2N$C{AOi*7Jn!-s4lCP785>pKA6w-Xc739h&Fscj=Ia(C5MmXz zvn$Y0?c!FlOF4}qj~}ewU~5@sO3P*`49`tL%23B>YpN>ESr%=%=k*h-{=563mVWdn zDxyePxj)tp?W1-E%@jMkhq=69YIlc*wKD?!EmaH1Hu%BD+*Il^GPXusE}j?Wntcsl z2UDVBL3b&rHnD2emGWb2x>p$+?=pjn!}r#oQOn~#lPu%xR#M^(^3Ce8eXyYUK*rw2 z`c9oHk{-@=j4AB23?R%jXh<!c71o}*Aen4yZF!q`t>&vP&)D~2HggJD!O_`yIlKln z_!8*eOnZeY`~~``xRq_PZ$|Z>T7;lmBA`;^&y$oVLkbQwXf$A{JG^!_`Rs>PmN19k zi;_2_Cqn7FURJXymyfR>y=`=p=Sun!s}2ULs{uNEL0`#{Vf?sEy48O!0dCREmxfsA ztsC%k0S=Pw0RDFuGifinGhRAvx!kJh3k|7sA`W(95-<Qdr5qT6X@d39gQ%q45XiRW z2B4j<c7l)JMUwOYl!-@pu<hYMV8cq%2V%`?y;x{%h_U>B14uTMq=#5Y^-`ovn2g27 zA)AIDg{42_zM{(Q!de-;1I$ym|2tu@J;G6j<-$yrgxj?KJzm&5GX^4TAappAg5v5R zWN*yG1(tSErMjvmVF`^a%#xlEmk4-WHRnxxvT}<3$}G|Vw)VcD4Z?tK&@5b{TtxnZ zbj>D!YLnNjcGn-_ThdoAR~@Zc87Dh}@vi%;LuakLwehknSga|bQYvh^Ak{h?s{p^; z3hI97Hs?=%!XFGI;ovbMr9XWavqR4n`RHnH6Zr+JndY5C@SXu@?pQ_h0;uto``fIX zvQr+18RuUw!HHmnZ#N~5@)@N(>5sF@6Oeu>q1<<)67ZlYWVP$2`ePC0q`w0J@+Y<q z$O0|E0Pk0zhcjD1fv0zx2E2IRMR7~2r}J2!T}9^8*mqo|;o}3wxQMW*rtPTEgMG)w zgoDq(-v+GUSr)NTP57po@Jsh1^HosnVAj8C@A+j6MZbUx8m{Fx*kK1zzxd|3iYLqZ z$UXnG*A6~PgSL$|&<H+i@re<GjH|E~Le3c}Y!3dG*H?Wrb5e%7xvI0{$S5bDxo7$4 zMorL;6r&<!;`a55p7PH0q9?0!x!L`-uRqo%=QjDqS5fcyRfiLLfHbj{s?lW*+_UOp z`$faoJK}Dw=VI2oRFXL9?&ZPOWnYWOAbq{dT(cLai%{=rx&Mj_Iv5i2@y@nJ9(S$B zbpP8)pX{)OQBnb+|F(02tX(v6m;O%=j*M634?+_{lut?nk0*q(XZ77vnBCH4q;aw3 zPK?een)FquzdcqB4r>G%7qfn-4y>WJoHtT-*K!Ft*>0f|8QNHU4t4+Do9ylYebDfp zcZR5?iYFF)S9TSRqlA!&Xdu0!$S5zBc8YC#oem{Ps>SCDb64n|!3Y6i!nQ{TXcxq& z>}azohlroVkVBMpPrNXH*QdaI*vO!jx6|@~F*7#ZBWrEc^L6Y9a8*)*KdzWJB^^Ex zLrbx37qGl|F+zX`{lU)P7-{x(3r@X95|-Fp@GeoF)q4TI%=Vu6yJ3o4f35ORAsO{~ zjjrlE_Q=s*4yDmleon_7^8m9aSWOIR*BJ-FgF;$Ot=Bx9UveURescXxwzu!-$q5m4 zd1W{C4U!M8N@+TA=Cjv0a7L(S{jtL{{{qEldRS@b<5Jbx!VAl*&q3+A%=mJ3Y&)*o zg*y7@l{mGE=+W+lx9a7ttaeZHTtF|RNC3;&u91$Ml58b}ij3$Km*>cLY}p04LY2qi zX!6qEbyIn#H8w%&k+LEOk>NtY-x~#Uk@b+tZ{Q$3U|ANEc;-x<okgYyQ(a21zC+%1 zDo%<q(e@@U>tDi{_2v_OaR2IIYjM>T!hE#@4}$6@6r}j{A^auoVL&aZ#oP6#db_Hz z(bIK;PHULMJ+pw({MLG>^#BE{0`>~Ipt>i^Uvu(t=Do9@(N?a3s)AiBZ~2A^ILAbw zCJn-blC{nBme&iKe<Y`KhUX3Df0qbk1%3o#816}-b}>6yKOI-qcHc;_FvC6%i_m@) zq{xE?YG@!F4TDAiw+3MXfBjF991|=$P*(E(C-8r!3_{_RSu5=}XIi%7skH{QcS<;$ z-4O+wFlQH`zIvRmry91te;fpTmxZiQS|C>SUh~!mMkaM_+d7>_a=5ly(IRYU^!hmk z;_!%dZrAfgU(R)PejX`5&JsDi8?FwOsyZ5<I$t^28}$I!3y$RpAOQ=q+zS0hfg-o> zHbN8sDlvqB*f#7<epo~Vc^gBj+IF-!s2$8&BJ6iO+ufF(pGBX4f;m|4H0-*KYE`5T z;oU1<6i8HjS63r=e%06MIUnUollOshZGJC9aDljoBDO&N4E~vnY%~4ts8Lt%@$MMh zT{9y(`sy=4!`4}M*_DR@si|ksTq9`s+P2QPis2#mVIu?VHgsIuUMbuJJh4pr4{wT* zObBBAhcN(L(EWR66Q9C&b#!CkNI{0}wVaQPuG$M1g0@<|^Tjp@i}$2eRrg`nl9U~L z_fYtTp2$vU2WN9(s?sHA&CeIMA+|zxG`93l(#q0~Th&lhaf&8+B6eobX5dhq3eyu^ zUuCQ*Ef>9oC8Sy}0Dwe5rWg*B9Umq&@C!*)s=BV?-MXxi#@wQIAwxkh$@yghC6E8S z`Ue16W5vm?_G<gXFUsrq9qc;tdcBuf{-R3cZ}>WRI~*VWay@SPXyQ_c1d_J5e|Gh~ zb8%g@Xf&ETt^!+cKX7~;gFBlq!6rYz$R62o4#L#Czh8azxmn#kSq+xg-JF*x83Vp* z0y&0z6vg~Fpb~bn-M<v~CF6*8K6aHSUfXG_s9K`Xv?#;%3+g<(>6)O6@#{t+;W_P> z$35Iv=~en4O(o(>;)QS!f1ZE**M-*(K_38dW<wH**7N>dJ1MOYZ7-f{+pFNY`ndoB zrZ;=u#&Yy)Hm#7Q<1|wy1V3f9CdGNl_`_yzQr1${{nh!~;S#}jK|=hgg)u_K`bbFR zV~5shNpx^f+2N2;$S62BDBF(++Fn>om+@tkj&Ad0%_}HuITlC(j&%?AU)liN9Fu@V zL`s%Dd=wZ~)UK^zq4g^k9C`1VVAdySN!yw$!YW?yjBurMx^d@aW270QM)!;07y(TI z_eB}h4}_0(b}rkrnkia@{-U-9#tJeQH&Yx2c}I$CImVsS`xHP<i7CZz^dkN6l&DF| zjgh?J<6iPcf)>xCj7?GKh=RdRlmdm&w3VYpFCNSgo@JOVzZqc+S}guMi0=;BrvIwi zh48yzHW+<gp(Ja=l}ii!>y5M=t?(RU_YlErI2SJ`!lmu;Cclj@{0v$#Sm0mV&zyP` z&}PnhG>XFmR6f#$dpOs4gZ428?VX-CP(_B!&;33bQt&qviSxmloez{FV1ovsm_RDB z^!n|#5ISsNNIE_w%YfrX7yseu%3eaqvi_Pz(aMy$1h6n-yzp=&n%!X_6du&l!2IBO zcPyA$fxd#y?!r&-ffYFCPybKFhF2UfSv}tCxY;YeOS0bWSGn1;R`>I<wUvFWj=WmW zyt##=yQ^&sJvGFq0Lm5k1$`T*kHDP&@;WPbPp`6wAmB69jA(py_Wrcbh<WhesSUJL zq+QLWmB}YX!h?b^Idz{LkgjZfy>8f5viPYSNxMG<LO}2t3hlYM@HJ7j`_%6}#~!ow zrI=XX+^?E?8$inG`IN^N$R<L_u{N&Uk2p7qf%%PS{y37CRosUA?^qwk4uV8nw1{t@ znBxC$O7{r~q|je{@Mch1v0}I>GlDLoA{N31j>a`xwL<g^)@X}!-yTy<lj$cROKV@7 z6}0+-3Z~%AT2mo`YjI(g3UF*rBo=Z^Zk&vRs0)4LwQSs1tr-h*^Ms-RcE{>j-1%%G zX7JC(Pn^$wQ1Q*l)lsKduCCYlgFWv$jgj!LsI^Y*Q(h89DDe8+y}Tj>LvZ5)R@GIh zN#RUClK%Yr?9$IN))C=;rRG@smjRJk&~1VeHlR6ftX=yD@n6LZcd{pqVpLVF+hE*S zy<!>)7&05KMa;b@s7+whgw$YEyJF-tqZjgj>xeK=)(pXf^1)G-fmv^Ra9qK7-nX0d z#A!C%?D5htlI}tS38eXy;7ifakPnk^U>c-*lfrh5^=t6`rwd*Ate{MX`b!WZ2>cVW zA!^`?XXbMXQ=H+keM;(*e?IHjYAUe<7MSNlguT`_(b5;N5NLqcY`ce4@(PE_&5Hli zfpXc;B%Nela@tmVfmk9-O#x|yiGWkzJVN>~gIlZ1mX)AOEBWmfqAk}g+1m;S#AX`d zA?YaY3c*oAm9hdUfwBz$Fy7rGVf#4Vkr%fd5%#Z`e_TS%Kh&lR{I|s7|Km5OAy@`_ z58LdEmab$K%;_~del5#RCy`nT24UwA8}ozNydSD1uwR~`%_`jYzzNf;oaHJpI1rRS z%NsN`d8d&nvf~8UrwW4@e(?=xN&4YJS8L}k4eoaE3C?AHT9smIDPwU!;4}*4^2v&f z7E-gpr8?2_txML#50KXNi8v*aQfbKhxV;ZM+^qTjxaJm}eV4KeUGO_CjTf!v#JL@E zC+{$lCp8$|QIC!yTn6(i@jXj@U09Y{FpOpI*SG<Pi9q4HKnE8-O}?@8Y`JCDJe@!P zd1clr224`Ho?g}&n#98c<?;{^@SuKjQn#7R7+2ScP{e%*ql$Gy(%H3f$kdK!b)+jw z1N7qAe{T8g<{8FpN?<tTC8r@@D+ovE`&PvEi|kTwdiHD{jF~x+>DVUS5srTwnM1hu zpcz;#{L|2}XsT}=(~^4uu<EdZxM#TQ0i9ncxb3Je&#+0trSSa;HFvvXt=`g?->87> zqPh&~7{EpdymZZ`|0R8QYxTQcuaosd4(@q5t<aq@vGy+ZvPdPmlbTq|l0zl@MNKYy ze~NQkNV=?H3k3fgD0+KEL}M75yg?lZS*SrG38n>y&Qooc65iufuMLdHn}L{*slCtY z$EUq(g|ljv)1US(B8$U^pv|T@3VdG2=sSL^&3ofA!4_F6)-@C?He7%CT%s6(n=QVt zQyy?NXe{!lUO+cr(l0SlqG`!-OH0DC_4K*+g>3#aMbBq*x$@u&oj=s)hErM^rZUqH z8LC%%RcjszRaoX8U(oA>%NSiLTNzDG9xL++t*gTCe(OuJ56*5G-Tkuq2rcRA&NYxK zY$ZnBigl;KQ2Tqufx6=doj3*Kt!Z;Fy1{iFq=(7BZW7`>DS7|shc`|n88(JHGc0pz zslSS0%E`3Cd95j6YK6W{@=a*1Z`A)-ZzfItbC<E9Fo4YNQ|*Nl;(VsipxrQcFNcW4 z{3;b`ppACG<V^y?^!Y29wz_px@|EZ@HVI~VDQkAMkz?3h8NCC;Nznx<zKM{SU8&x| z?|kgCsW237YQRyigS8<;U3JKzEhU|cx%794t3xN$h!1$Y2LnTIp}996;R~#mBUJhc z8^bJnQ3gkF0!H{W%)!6=9U#7YMZ9MXc%xvx695)lWKd}0%~ap`U{DuW+1<AWXt0I4 zd$qb(kes@8)@4#MN85_EkBa+ITHB6O{Ada-fB#p7ZylY;Lile`?0b*~+`PR+pPy=3 zfqzVN1CjLng$7BEr21s}kZIGW1sA73D8!H+#R81GCbZRGq-RqTdi?$Bc_pEZSBr`B zAu_l=@4;n`f*7*ZDezWh$EOjuia*FvTcBs(ypQ<z75g1V{$J%Qx}d71d5zJ(kR2$y z2zCpri%3RH?d9vpxi(`oKZbhy6{DV&5i*r+qAkm!MS?cu&$e4)Ji39|J14=!farba z;%`vwn+(V8SifoQEu|c6tOYd>FrgPtBPvLVfyp|SQXxG&jrV(ybyZAr$31dguE;+( zsJRb+kCP)5CCJ<hYgO=1J%yasD~`KBl25-c-C-|UOasSh1<#1qCvkJgb1g|w34X8D z(hircIN5L{&gdE5v2a#St{uw^u}BD2()95t8e22-N)d)fZHG1dw}5^30I>Q0GKgHr z{3p1XQAJXt+geZGh}0V^I^y5Xf_VViDD>@w$X%I5AyezB!dQF3?M4(osPu`b4nms* zX({ZV-jD3j8;xd62>I@_@*%t$m>ZWz2e4aK&nAMYuA~(9ok)XZpk2cE98;(Z<`Q`a z8V;UI35r8^eMajH7#bYz1Jk~Q1N$S#%M%hUl`5A<5hkXu(fvOpI=Ye9i!g!E&$q1x zzB5^;mq@{XW6|fEM>uWu-}Ml~I&;q^3q7vEr_kzN*}}AoM3%2W*Xw02*T?Lh{2y*K zSa%01dI{^&Z0j;!y?!5`CHP8Dx2R-vo3MF4!|{;qmpc(~r&^E%i~R|;`Zy2#HNrKe zf9{b7gRd<>X5^2d(nhz%JG%o!8RS$um0%}$##Sx|Nx48Sl>Y`2tp6C_LP(>iYNxd( zN7o62)SBm=IknXYmgW~_b~yu*b&{3%-1gEc?YD1#uc%Rrn=K{E9#PgQH;3y+r(s>M zhy96HCtM^5m-4-P&nI}STkBoKK{LV$f?wraey5nzLKX6a{|zLO-vP}|e&!G<qDdV` zdvE3fdbg;4?%cjE@Xg)>qPj}o7JS>H_ElUtWq>(%SXs!A^0fB&f_{pjX8g~!1OPfb zyol28i1^lnrA1s$L(S4n&L-gh2W1BBo}F#8u`j~6w(bW4ws)9*&k|D3m}J04w@kA2 z4Ly9m;*wBtABG1F5-E>^ziO3!{XCQk1@ZlV`a~gYPQLPv`2buGX-jj98v8l-ZPejY zn@#@IVZJEvd!;}Z{DXtSx93&)n1Gor1*K*N;YRo0;T$X^KnMR}8?{R@RL_YREpZgc zIL2^y{8A~T6<D-oW>JF?#3HOQ1RHD)TpjLShWN}Exwogm0#3lnj>V`1Q8?KBgoS__ z{rjJ&h9vwy7NJKrk(<W@ww)1|H*1`No&!z6h?3Nejphee5Q$6P3X8#KGm$GUN<t!u zR=x+m!29=wg%EN}FDp%jx(Mz7{ytz~^)&g__Wa&cFPHg-c#Cw^E%}b9J4(SiLC1M6 zz3yQ}J6h!V+=LEl%(?X)>za!~nrHrj5xoyhnK+I4%1+AGAJ2T%FHu=rz8k+9KGg&~ zMhX?6H+$?7&8A?e%mTd&rxFWLbzEK-vSHp!5cN8g+&&+Bcnb3*D=PPQJDNFnQHY;d zsqPZK=uF^^?yBOB4DvJGq_|%_m(gCU5$&@I=EL|7vk?^Ryl}=o6gSv9D6L(LJ9o?| z$<MyaJF$4(^WRfjC*-XRJ9d&*5h$*{*s5p)<*ahtzDEf_<u*_Byink|sxA#$7QB)7 zc}=#<-^7j+eL1{ul3;IWsm)nC#rBQz98OEGgL&ZuU$%sb63#|Ix4---VR*Q=u57E@ z0F$#*ld4GJO(gs@)vPN(pPkDL`LsGGam%?LhTJuQoY6yMX3OgqBsd?=xg39&X?rHA z#;YlzrhvB_&_CwWw%F0A4EMD@O<8Ql<;|D6p#m}f|0xu*DNwO$vuBs9C>4*gnmyh$ zkLF76?D;k}V+c@oiQlx}vg}-}m8}48I?zNE*zWcj2{g!2m_BAzL52o2RMML)v(joh z2CQ;0)1}C$S4uLdnb+0lwCq$hELUn;HIGMOWfsx=Ex{zm?!F@BmTcG%@4<HnL9Wp8 z&Ks^9CQ+><=Er3B&~mlLOlqIvu8Y-_M1Zx}g1Y@(o*Z7$VCP?1e-IM!DG0M}$9qJ7 znZCwIliMS3Z*xA3?F=vT<^!fWuYI0OF?2M2C1pClt3W^<5D7fx>$@vZB<)BLeb>rs zCu(pPTO%qslSdt{Y^l#IFo$K+{#6cE?j*{Z5>C;a{Hku?SQyHM_sJWtrv~@&lH=+x zGF*s<Zl(RZI*X>el@#Q%FU-xgWSi<02jE+=799h$MK|@A+hOjh?2mKT*_@=tYR4{m zYpDl`xo{5kB9j!DaDhgn2>B{j->L>@8J(StuwI<i$!siM_^7=V)zfZ^r7>$WrU+c^ zWt7?D&Xo~CG*@k`8CMdit)1m=Ih?j#p%{=YotL;*R?{i0i+U>2M4G<VDFvQ;@@qXy z0tM2y2R7yf8JxC`l%>7#h@eW{lq)Y*?gEub0F`Q@zIW2+vKx{4@=Ig#^gQb<d0|ns z996&!oRB7+P)RB!{IX&g*;|nNC2GfBI<#Usu+VP*rbxyP%`vrojTcf+Z}=<@Jr{3@ zaFtU2s=<ZNQ%#=I2U*msbJjENFW1EAA8-+PFNVd!neMZOoP~<ZNIdZ#7j+<WC7nES zhj)G>Cc|p_pH9jGHgD1%D!AyyNF())0f$iAqPa{?O8ShJQqvEYrPG#pO0VuxT8FWf zS7CNM*}hI>GOwc!DK1YR2WnAzPs<Y$(r*^}zsCgB1(hsg18IwYhh!$Sj(ogEJz=c# zrDjvC=2?2w3ABF$R(hfARXj7v!k^B*<I0(F7&SViDL#6_d}GT=F>xI)P}6IUiWq$j z_fKf&PICR5Yrb@bUGqE4>rx#8;sf(UrUpe7f}`fEhKh5M_Ud%Zj<qcw-Jb~#Fhv(> zjaTIyYVl5-KS)P*e*yPlnt~Ez;C$JqhJgP4muc)ht6JV?)M2~d>`BK>s>hdfw(yQi z_%fvh6gj+B8E8)@8dc;%XbAqCR)3|+lS+TB?#7ZdEQ{LF@Ej=k)fj6zj^nIJ?r-|& z!>1jV%r;43Z*i$v&VLo=lm?9ojyK#6^^-dDHE^+KjjZ7D@-Wu7a4b1fJl#9%cUlM~ z5Y|}I^W-AX3b54yqM8kc;OwbIe(hAgX|TB)<5ns=qGh=MVAN}^n&b&1$l9Z6kW0lN zbk`?oQo*%ccvH+yEtIzuW-;fn3g1IgAH6kksjB0_B{4)QY%B1fRzoMxemJK@am=-m z)RGY$ox5XkyOJ%Tv#x8b_Vm+B3a(n5edXdIHmzD`cr=-=BILR5-E{*JMR*hfJI{;V zGxu7<VD|$#3CK()(X>QKN&7oaiL=nz)2;f^lAE+U)uDw&R3%XY|4#^dt_;1Fh4WjK zkEez(W(gA)AAHbB0<xiOSNAfpX2h&0-=7?HD(ta-K!_aGI!LP>WHkMzej2G;-&SK) z;<}Wz%93=SlqJ>1Jb==gQd^Z!b=i@jP`B8_*QK{&6KAsl-t7zj8nE40;;&jtXfZfK zM<KtAzHa+VS%u%;BO&gR$Onv7%Au_0r|Lr0!qW7Bbrz+<qDWTtH6qO>mw*}`g!O$7 z5%m5cMphz~dfaeqt(e2&$w~P9KxC3&dz434tn;Thb2>XI3=)W*`vb5SvHogbVRmBh zf8qR5Wgs<E$9EuTmlIRSd#tGq>2s8tAVd^Ef2XKA?^{}kFcw5dUihvcsX?GPVjRfo zwg1#}smeyI_}KH!_Z{qR4;tK4swXckg@KP2c(+BX$Y%c$F-~A8BFDUA&jbKQcc_Xj zK+a-sk(IW9V*g#G*(zAtpIG}NsAu_W%fwOp<P21_wC<Vh1S+j(3(#EjiE&_q?`IC% zXN{;<97$Tpq;w|`Vr%yZ16wQ}xRhK-hNWAf&_b+-tYe)3RStG4OK4IvhVIWQ7o?Aw zpl%3m{$cRkA_?Yo&MWq}iyw;O1P=%l3$lE(#}jh8g#ExG>X}??%<CDvKl2y@%v`S! zUia^hiZ5ORhs5TgS~?$b1GIcZDJLSvYnrEMdi<^S!Ulu>)Vd>iv(g7hlcyFBQ?~~4 zMqz_z3P#hDA*_?;QZVCdKjoL$^isL;K@w#Ai0eH;lV@N3Ogm{&5(G+8sOt1Bt%PEc zdiiyYHl9mmQ+{PL&UWux1Q^u5MUI4=+>+5FbtsxNfeHw))C*VvY!isV6JM$@cuU)^ zt)7pQt@!l2S-tq%kXUPzxZn*YAzWbEisKi0H)I7eAxUYYi+wlC?TZxecaQ`l7iq<( z(Ocy1=MLd7<DGglm1XMGEQW3}6Ac*ez<CJ=<$e3T#oi=)`&#KAwK~r4t5txlx-mpN zBEySe34=r?Un_07U+{6dS}ZnH6gUEf4((ZvkQ&;-h>6GZZJ%`qed^kl5*=B>TS{;Y zd_tBvC<8(<z3?l*xgwhAyE0xOzm0v}x|yX*u`*mWNa?<Y$G2;!j=CeMRhgV}=@iqF zOVe(5IyD&6H#jVua9mFI`!SJ!pD=+9Fh9lqt6W#Lhy=Q4YR16hPv>M!q4R7^)>^d? zm(1n0Jk5P5OVV2Q)akd#kfxMHbuna48L4pY{2P-fVYx)lws|eTg0Eysw&e(CId>aB zn{Sc6szZ2I<1;DJZ&29NFX~03*rJN|OD$vV51<vhQaLBo@D~gHb={^%U+7+vgGRaO z1VY9%>ytM(F#q6TC6)eYDQT6?<x4;L8~WYzH4ojlG}rSCnv8U@wQqjpYs0Rh0BN3~ z-nf{gVxat-S%D|@Q1E63(-)2e*LznYj<g}C1=2WQ+S4)}`IFhX8C|yP#10qJdo(K7 zyb*H*$AQ@UAhM6U-!0GVx4tB4o&U|RnQ~Nuzh>j=1}#pyew#3Rq?Na=L!nOp!;4%i z2Iw=tHm23SY<vFvM_c&iZba&tXHIk=S+$*L6GS`{53rB_jt{?QvJ9|pMg6|oW_5oH z-g!kTIR%r`g@`KFsq(ylIA$tvuNwENe&bPL7G*b&WREJt(|h&jU0PnLjgJrckHXi~ zfM%n9p)Y2D`*{W2PZ{@Voq&4w*C9*!TIcrt-(Dveg3=})alM~Y{471+^Y0VNlZW0p ztPp!heiT(W<YVx>IuyC2ks;XM{L-RkiBB(d{Bak~is>&_Xa}@|xwq;D?vo#3Z@jP0 zQlsg#W+<t|*Sh|u8m}4}bD@zhHh_!w4b*O)l^f)b;^1vZ<>9Td&S>kDvZ2TiE@YY! zYk609Dt)=VC}rdXAGJ9u!%aJ}qJ5_DQ*%6`<5ulF_iC*8t2o8${zC`OgWFSHa;;aA z7u}42s-Me+toum(U1ieT)Y?|WLFV~`3B&bR9eg<n&SFXkB$(2)cP|6ff?W2kCr(sS z>Gdd6ipMg#M;%hL;LrHDA-Tv*(e60^>{QOdg<#xK*gEyW?qna+Nt!I3>l56i0)~%+ z0<Dl^*e}36sfVgBw<S@jyXS~1acn{AWTluo{`6W?GtwPi%8Xpb%^)XiB#eE0#n0u7 zUvrqZ)?d@(qVa$^C}v3bwrz4wSiF}9bC551Sa+M2&ZdN|s<Lii*4~E`ZG)rYiyOx| zz`AQ)wL*MhT@TFS+40kRgcOnPm$I<ca9bZ&JHX1G+!VW7Sd0eI+^=+AE3J*)G=+57 zMjIbU5V#k9Uj@bOk}L<=R5A!dSmp%iu?TS0J(C^ZTW?{Z%+Pq_Dy~wgp@qHWh)^jv zptp=%EpIRQE|^-5Wyl4L)p-sOK_O%>!WB{FILv)~wsmT%wAE@eMfX=QPW6MJ44XT5 zCme=%&Kgm)6{C87YvwmOYD(eA-*mB$t0ksg4?W%hM=>Lx3Y5qUl`UF{L?oTOW1|b7 zPrLJb-v-Q%a4XI&IwpH^EYG8m=Dc*f5z&P-|MQp;oSYLmVb?G0iGe)Iy#s8;_(Rd= zgXylBbhvrGa9b-qw>xmmkH>{(1p?oMM^4`zDdSwOgzkD%<g}jL_0C|i5PkM0Pl`rV zk{+4xAeJ>_q7%vsx|G7pOCaWR-D4T>GjKAwU>V`Y>@+6o`g({wLabM`IZo=UE84F0 z>oUNFG=#bb=YZkDwT?IHb>L8t#)EKWV{y_K=)nJEZ_{AN|Feh(Rs!#6+{jhN6HQ_K z*13UKfE(K}{QmW$oTFY3eyEWDjoC8#45z&K|Hbv{?IqJev){P}f_6>X_q?mW<n%^6 zVj=h111X4_gXAh0wm$sDXfkoms@cX_*EmNs0n;GtFvQS%>D3BG+d^)g@O<;GLKc^$ z2d_!4ZghxI?9mR>#C+1#mWvn&euuK1zL$paSe`f{LBCy=quMztX*x!vHCZ>r=y;QC z4wI~#8h0dnqn=3ZiV>}-##8-N&vG)z7VgEKL}G`L-_z3z&>7}u;kYbxbd5x_d?S(K zH1yniZ4cl1z~*y8_PNKC`v&f=KMgK)UhXRAZeKL+Tj!Uk_rHyk2{xi`qZ3iNveKml zPBEtd8Qa<6+g4gGnp?io(@Vwz#u;e;4}~K=_V2~|#LL*G7x{m{ITb1_Zh+S$^XtPc ztep6=lf=V#gPtZbnuFTShP<y42G~gU5Z3sWl<&A`boB%lco$NtCZHaPb|HZl0|IHO zjC!r%<umhq@Xz?}PYGK^c-gaI4Pm=0!&{p#wh9|U6Al#+6$iJ1anFej@Qn#~?~afD z-bC<cqhSBI()O%ZUhckYB_X<;KZhiq{pngfF{))JvcLx<yN~buRCCxm#9NdpF{yjU zxY%yvzSvJ37^>B+>Ky3?R#JGst81xhvOCnUPdYezj48Z@V{f1$NPP+%?<iR0IVZ`a z3A8z|k8~r;PoRvVL(6Yv^_nVJpa9J=Yd?ib=*<c5jk{HAV%82k+7URfml*l?(6tUA zg3PM8#tE#&KIFnVhFXe?-!K2p8&ofY-!x7t`5>faxk^QW@TBT~p;%M|u(ojbYooD@ ziaYiCTK23i{dRM1RL6B`_52h<mYO8`U;HFetUe_$mb3F7b$It?`=T5#ZZ4U4eL79} zQj#9ec5V(JG64izwy&Jd)`7J;Le&j-XTO|ohone%1`|SbTo|fd**V3;snhyGX{=9+ z<|JqFb7%n_`p6lj8D(FMI!K;GgZ#^8J8)eIJ;DMdP<rl8NZn(pFU;+#bU%L(UQ&%8 z#pdATptIDJ0eo=Vh}K-u`II$vK&s=UMv)aQO<inZj-}d5>TJL>kV_~{)M@(*t)mMF zIl{+B{X!*|m=XRRXMN{kW#XE7`MUJ^%m$a=R&-Y&F0#lrwSAPC`x9;>NM-Nh)Pn5A z#cAsYmxpvUd2hvviIUUH*>}~;W-V8w^As^BjzEzTx>GZLXWsckO>8*~j!<zmyb_g( zAt^+NQLL-8lmh>F(nZI=a3dl1>8S{rZ1!^Zs7l5h2@*72<HmYdnv>J5N=hWhyG`Iv zx2FUQ2rhPcBS4YP!EJdD;c2s>SCZs=Dq&aaxW>Fy{WY_n@j~ohNje5@w?VvYr)SlD zu^|ktF;^c-cH&k*P^fdG6r=Q+Iov?#wG=`W-)9C%FhP4@cZfPtbwDLj-Zn5XZPBD2 zbv=w?p{mX2d0ItKwAa~G(6&2nsQu?-JtU+@f<vh<rU;RbhmlLy021io!SS8taWQ+q z!5EdGfHJEf2}~5^n4B}&7M9v4lg?uWxi4RGk8oi6*nW-7;0!g7%8<_|C@J+^wr9|@ zO8BX6{~ccRdAK=l`bB|Quj+SJ)oxtkscp1?NCZ`WzKm5;W*`C6c2-pZ=BuccjP|l( zf`+3JDhp}_Lrn|rakhF0O8E5){=DFmnG~N&QKy`>9_lbicYOG|PiE6nwAZhqvYvXb zqyGnrAj<>J=HXk%ABJjRy^8PT&#ky!Q@bzm`vH|UU&4MJ_SQ%zp2^eQU970J9J;;E z23gmkm3XOy^N`AY{Sc^%Po(D8uamfIQ06>he*oKE?h^P76rPO8oGpopHJNe(IDAzn z?UjZYZijr?#Dbd(k(yNr5m?1j4$byiodT&!IemF9|G+o)Yg`KwQ<F6g<Uq;m)$U9i zR|W%FUY+Apjds~RD@%DSigq$6n@fOR80fWW)#>uHqD;X}z{QT_8>kqaC^blAL~~Mu zMb(c(rG*j#KKJlho~2{@EB`t4KqxpT?^r&%r!uOHk0Fm&VnQkw<CS87<KE3wa^f4m zAx7Ux^&EbItX5qfl8#EvRu7$v`Y!=Av#sNpl8}EVF_RmRkx?hbHur6}%|Ltfj<TqV z&zf&!;M2kK@f}Y!TrZR^hMcm`XRORH(Ud{hU{-_Ana+Wcp{(U9&npbb3}Yrm8AYMp z6N_Jozfd_rB&Q_XPtegLtmYiE1T}n$L|813jn3k$rgoU%7~8IUVapHC-3ujd?OR;R zYNs6#uw3fZ1j;u~-609ByfFh|vnu;34=@r{;U%><Ey&a7N&EZ__|Kne`J~lUhb%sF zr@6r5O=n*8?*)n41!9tM8Fh=i=v1ImNG(VX(-PO?j1i~_Ux&QkILL7}N~3dvIRK)6 zMxME^MPF4u?K7bCFboY>_RHxu>m{O<*u$fC5p`b1O&SIx^!v>nWdZ|8B26ia-kaXY z!k8KO^v$s2X@-5eCzV!v0&knL?2(pHh?{=PI#BMM+0{pJKLrwA|75n9RzEAP<irqd znGoUQ(B+;l8y=<`6advrEdso@nYNVpha%)cv~lF_E<<^hsc~f~BM7VR**V=)8IA1K zc@1hMar`Y>n&#VmPoL1(=arE_Aumfc1-jfwr2CMOO#^u45k4UkQJ&*#ZJ8unHv`}2 z1GdQyd$r)iUkd1~k<C<mFTVv=LV!=1twE=2+XMH<{tg|*5+euSYv+i!<C!JUb>yt= zXA=ZX)Eu?63(hVFi8(QxoJ9>%7=+?UK$@LP0xz5O1ZZZYI(mxT)hcSxGslp`J=L># zuCwb)7bhWbPA4jV{e03PDu^~(StZj`*}rc(C!ld{8p_0nI;$QMm{})W6>jiS<eXG9 zqMDmZ4?v|d2oV+&&N^H2^GkZgEN(il;uK|A3m^=fvpC~p<K6RCT+1GiY46p$OK-0v z3RXZ^7hhMuW3$-zR;|Rd91^8iYa_tSMqzKtsm*&W&CGGyh1sYgb^1T3JFBR?x+PsB zfgr)%g9mrl;O_43?(R;2;O-jS-8Hxc5<IvQ+~q8?f7#u=caL)}dh|G4WQ2?4o8MYB zYu2oK-YVTQX^eN#w565FP+xp^x1ORyG|v|q7;^S!4W+RD2z<Kh3!icm$lmh#3OeUl zFhMdUagc!)DqLAFYNs_n2DyA_d~+PlX#TEUEIu)Nd8*S$x(J;ynEOVjV{P&av-YjK z20v0vDICZc_gI4|ercFPgFSo~DHhpotSXA^h&OwA>Ew1PXrhz?Vyk5$)7{*iB8dpX zD(+Hjc=2UXESpemYNhG<2}*#N^&_Du4xLZ72I_w8lNVY;K6)C+iZbSyiMv)g=2S&E ztp`e2{7S_UxPoQdl>1)g_|IDFb`%dN??=HU!4k8_YK+76=BpBZsUy<aZ@RWN=ad%6 z0vl(O6I6|*ij^nkUw?8gA}y3k3N%);GbAD(1rNb4w#G*8a%5-$Ik8&F=7pQVFD8$C z@I&g?lXeu{8#yJl4ot~6ja<J1yJN+n8UQFJ3NaCV;;J8w|E?V<2Smvy6>&NKjE9|S zy?acJ;HglN5b}?`BXBG17#4RQVsjlrBg&|O1d?EE7+-Z~ckCc++C%YRoPCWMqJ4QZ zys9Sq!Cc=W?P_Sk)RJPn>mbEM%1P9YI$Uv-4L@b1;9vWNVsk!5x+!5P&#X#)K{-xT z`h7v^mrKM;wB`skTh?~X3`*fs|A6r)kf>{kmhM?*s1ZW(rsuEEW4Q-KUUGHOn&A$J zs^3p|lX#X;h9KQz%<4Jw+~)j9m>F$jbteSYG8%TVBf=y0pr#H+#@abf+Le>-(i61C zGq!ayBnn!R$x{j`Amnkg#mB(Izle?LAkG(QXYq;a%*3GOG{go!Y#sDO2gozzgio~^ z)oYP$4jb?nyH~lww@`>+jLUiIe8o@x+Qx1`7Y!<n#-MD#e~6azc4h^b?5GO&)lM~J zbJ3P(#8%`uJbka5cH=6Y9>6{nze;}#5<;~qVuh^t0mVKnK1p7>XZ9}2yK1kiD68Y) z@nbajmP%yEzl+3>GXy|R2O;2>DDo-t5{xhxEYk1HoK8?H>>tV(hHeARZZ7k2hQsDY zPR?g1n)^fxvmNT~W2|u%5E{0;+fqM@h<Lz&dOXfRGlSJLuVn6fbb_rh-AMK}X)?e; zPwf@#S=$NH=>066=gaN^DtxvuYb~o>?h36~b$wHN0vSL3E75ToaHGBu@iw?{OYZ&3 z`^qy<0Fe*+P9kH=MsnaVCE}0ziJv*FG%Ra)L&VT6xEZ}_sv+`cII4ik)!g9`Sz`)P zH#OGF>ZP!MBn9w>)1<Tk>dN$FnLbhQay?ry6gB=4f)hjvdaTY=1Yu?gopEP^fI%9h zCwJt{iOYPcd(z<%JE6OfzieW2jIUcDLXs29o_L_yB-i1((NMJ!b>rfO1M^9xQbj_) zw?)-%zPvVQnj48`sb)OHJh_uALk7PF_CsMEjpcOj9F-M^s6m;NMcBTSi*7+324l=9 zPYew_!_;TgYE!1ZxR#*pdt>Ir3JJ1D1~azN2=g90JB~ClnFyAaDFc4lAiynwq#(3{ zk=96I<RAf*RlhY(V2@Uh!&MaR?DEP?Ko~f@6xo3hcVAVe%fclI81rHoBmNCqB-UYh zX7kI3f)HQu-U5*1@e(M@V!yKFm};O57zN+Tyoll~O^YOyK$1$&@gvHND0JLr{W}Gy zV!+ZkcGJ@HRFGi|bWiRHa!~VPWi9quvyd>IIyW-9JL;YkM4Pp#21ByiuTxS^DUFFm z_%;5OZ#R;8r!Vl3vm?`+TKK$Nt;lw&I^CR>ll>|U0$+(Iza#GeC3xV_d$bg1AF`(z znIiqE(L5AUjFyA6nOq6{Id1O1M?laT@k57!5PyCXYRUr#S3d{hQfF?aK=9k>w^TvX zdHt2IN3#eW`LG)nXU3XL(<v4x<Zon?C31e}yFvz)B+IpjqV0+h|BR;>1!e=Jjg_ni zUlT|D?#v`F=MEIoa;&|JZ8eamTmI}PC%mgZEV8`(cVW^9_o2pTSH`rd49xzw96tb9 zl5yY?j~vVow8(PO3hLt&<c#dD`Ti(p^Vw%VH|)3g2(P7eZcC_a;}!O2*<Qr#HXB=J z9u>w@63n@uAoSk!24lYq;j`#zh|6vd<ZMefzBlJ|K6fKWEH-DPG~ceXeNPR>pKo-# zE(vTktZvUGs9NMdAnUL<@6f$m(b0829)Jz~yyR@(-W$Pix+~VDmsP#YIZlqLUGqFv zeYsCpUDI~_X&{b}8aQ9F_(@-51h-ayAeO4hOrh5PdV5>JL7;sIa--y$eHN%+tNWXW z3lxrFuz2i}&oFP8TED$Oc?kWe3ub~oTWbm_6<u34krvOIoqj0K4vM5f$l(+5;;))U z@y1Meq!4!ysny#J`Ykalw1hmKr<6CQXvBnNuQW)vR1OuLUYn>Esm~&&#RSZ9_vJ9_ z?bb(8Qk;XzDCdT2u^?X+*)bt(T#zCx;JuJ5`l$>@Hh+5YiTWAin>k*+<V{&)|46QC zY^pOAy?yb_>FLRy<$0S5qyif<uPD`d0@N#~QJ&C66PnX&IID5B`a!y>r<MJK^E7qU zo`1d6aNAuUTs1q|!0Oyxh3bCKJ%+n`hV{8P6sFzv5tD>^-dlOOz}Xp2SNn*bq4sz_ zitagSM5fEJPY=x2Id-cN_~!lKaUY|~BnG>B9-6G{7;bxno}vGAUAVB+DI?K|-LQ22 z^5_yxpi68WFEVHHZdP{ZGgSiUS(7>ExzHz77X)K2nfKA=bUd>kJl$-4ZE&T@e2R8_ zo`yOJ6^CI#^uUUAe{<+ks&-qUIV;p;C~pbvd2zMRQ3-=61r{PJTyUk$Aan!CO^M*( zL1y0pxK%tgzpuIRl{8Wf^<l|+sJ8*jO?=o{!gelold#L$9Cxy*rn_LtO2*`EJS936 z0cx=+3x6^k$WC}A47C+f28SCnXSS0pdUm6?{(O>Aea5)=;Y+8tpQzYiZ&HSKosOQD zJS{3T{Fu8wEqUZ}6OqLxRhLm#E6>K_veFIlLxXN5Z-fF&OojWp?NXd&IPgS$eswY* z)@|unOBey$iGJJ&zEHuLyC$IMz3db(^tk7bNI{EFaQzH4;5OXQ5{VJt6`tfpn58Lv zTpIb<DqYP68<^+~Q%(M2=cG6RTgXlXk)H7-?;hN!%g|{rM>K^z)-Yr+y#_0*2+b=i zuxQvTUXi3;ecUWxxJa&+LCqnlttH@QW!mYm)HiQC6v9Ue7h52>8P4D_&EsBRm87Ot z@kVvi4qz*LYhmEEKt)YWjZWWqUu=NEA!OR)irY3&x2r~fv^;@6W#|r^o@l*tZNW-# zNN`Bkqt|5lt#D9^a@NUe?H4Bk=(|FdxdNfKN4mX5otJqM**p({&TW(D!#rt%3$Q!_ zUCuRiwOVHS9aNXmMehNmAH0>ax^kuCd3^D^&!HWdCx0>C?IsmEC`7+Yu2{A2HEb2v zARCKFx%ZtaS3c+0%P6!V8eI?L$wVY|4lcWsm#3#|Ox)!BgOY7nM^{A!=gni3B)aF( z5Je?igqhdP%{h!Kk$5oHK$+&nO|e7ZnocdihB$PYVq!=1ZX}dW3C%_#Dkpqk^6Rq0 zrhK^40tkIYs?1-{n;RC0Q28`LHAt(TOjh!$Y;uCn0Xsj2ri&k$3$&)lDogen{v`?> zQmd8BKJeL~g2Wl4FC?85S7q4KCWNa(FB=0H>~hH0dpG{{fUy4Y{X$^PRvB1}#tQEG z<*Mm4QN4W*pqq*j95uEO)(pBp4*iUSTrckxXxU^m+3;4V6Z@ST|7Ll+qP=moPW!rd zwp0CbAJ;)MAL@6Nh18qE@r^2*Jvt>S{`f)yawk*!p5bvx+&s&$cMDatOE_yJie*ML z7_Q?k-15?>Z$P>)_*EguqU4fv-PVIK*6hO&5p^aLReA4LBI{g&lD{{GRb{H(v|;Gp z2KEUH*gprRi`d&(gsVNwRAuB9q)GWKpAEhkc*zS@7U6jvt9HH6e|YGuK2!PH$mV-C zNf+0{Q~!IXi|;^9mJ~q`#hX3CcsUupj~?uf^qw0x%JnPh+P%PmX0Ex#S^#1S0CH5O z2a(b(Qv6GCG6@Fk<4NG6UdBa~Vd7pi5*i6#rE{YlFY?%dVcqrEeU&Q@<}@^b_sb$k zFJaD^AmmAya2=G#(=?Av#j*8aEv$Q74R#gTtwVk&_tvH)isgoEB%VL1{sk{4yJT7H zE9O)k^P_Pi$_Rn|ueaB=Pd^T^JO$~_CcZC-lOFCPCS@3P4cvY7&D8+KPV5}SAwU|Q z6iqIFX-z}SS-Tj|gMU~@yWV)hS_DWfZc8U+y{K^53tyT0b*j)N-@ETLXK4LKfyoH8 zPb-pi?WdD;o%-&2Y=__Lv}@B2syDb7!S4|?5X^eMN51^>wT}6cN8|v70}H><d0pn~ zv+mn*n@!-gwSPJ&sW~an75DpLitOBq+TckuW@$M-ortIsz|S%NaLA@cn#xo9f{Z=N z#)qN0H*4IY^aVINQ(6DET;f;DwM5uEx3#K%ThBb_K3~3o4omZ)E~O(Z%qbTrA&&5; ze_u<>7q6b$Sm7vVY65Z>KPXcDcAr#}Kj5ATOeeXIc-PGdL>eEu5D*t}Zuy>FYu1Un zG^H>tks5rX{u*MyPsO##r(Vm|p{k8mnOJQ+7Y<a$)ho1)tDV35?9LS&R20iqfQ-Kw zWSwwFsOh_Q$-qpB+Q{J$Ja2u9#s+9^H=DiEZfRwR-98k@(vrD2lt`berQ<W2m8d3I z9|<a|sxsQnm1sl$uBVMbrV4iSp^VsW>{f=HbnL3*l&h{Jyn^NI*p<$#tj}v&it^eD z!G`>FZtnXG2H1iX@(oyhm!COiY#L@RH%#>@NGp`w{dlv|shqrc(2yWNil}Du|I%@o zh61vyT~u(7=AWk#aO?$;aA|KZld(<pX+qwZ(HsktvuUymG3ya*UHKb-Q}M>k3aTbI ze|em&e0+>rfvG(o(NQYtYiToTmsFp5W)lO|h-{pi1}^F^hw}JS00!*$WlGsUz0~0N z<uLns<Q$<Z4v7G5>-3uL#dJGWLEhCHdpCxz^M1W&FNG@k33u)OhD~eZ!0!I2D!Ki! zc3_A+ZX>@-gA4e_TXr=8Uq$UKwrzD5+&T?}3?_CaJogg|vopmbUe>>fqIccJ(<CZb zzMeO5M$r*Tm3V|sHEs^a@l)*?5%Md%IY4K?#BWMHGF-CppUA(nl7zzcZ;sQv15wO0 z{~N_)8UQ3rb;vo&wOQCcKgd-awewEnOC@xk=UUNhrj^-C9c=I1U6=a!^u<+y9$X*n zZw}!6-K_;J8l=X6Q7640ldfQ<N^+{^7x23Li?MJ^pjT|x5m$0lpom$STzqtcm~arE z_eNKpTl{~B2I_-qtR!zYI$cQ=jfZ1zs2cm8>%fLGHQ7CH1jMKBJT9=bD%$1}Gvvtc z3%#+pv@}JE2F3e0;P=XfNrhCxd^)ljWYeU6x%@@faa+}u+VE|fFBUm=;JXY5o!jQi z!+<2+WUUi)hflT?HL9{U5GhuuK5q{s>uUc*%#dl8L7|<Yqvo^=loBkoYH?2Lb9+kE zeLS?)(f%n`_>HBM)iC9NQPlIsZ`75|%;Ripv1c!jb+|-NqWgCF@NhNLJG=;6w$fT6 zs^lO*?L&9Hap}!(Ngsf(ot6U%A?{%?=H#=rs;7MxBj3kEtHF?!;xKkJl8DB!9Bs>_ zvDBx^0ZLOF1r$OGpqOUiEUR8WWK!l{XF6%}qa~BHUrP{w%HUabqHH%W0k*vH`3Xi@ z!t*2MkPXA?*B>4q4jL1FlmveQl~#$VR!<kCV53c>?lWA^t}hEK68beRx$17&IB@@j z^s#M~lqY8hy%e}OWJ0XP7W(SdJy0{?5CRdoo`#t~^_Nej<CsPDW79WgKZp;=r0KG> z9(e0a#42+}z_U`G*K_lO_nGJ19IQ3(A>@4#l<`5N=!%^FcHW?`K^Vnv=pBuj@Q2c? z15;Gpk)u==ty<38ABZ=j&2?ux#Fq;O6+u$<^)x)cVP1Db22k<sj~rMfFd#)}icNp* z$=$%eGM-aGdhu#yZrCtWYo_y4B%9*(lM^rR^x4RQxe$I?%XaajM3Ya;ByTH_f_5_- z99;kP^u7MNO(b21Dx~2)KEloUzkbP*u~_$Ae88;=U397~uh8X7fRT;$^>j{F4$TH_ ziZiEtMojTZX*3mS^Yj1nhsQJ@GGF%H;<owFZ?vD@8VeIQZ@6c24S)Du2nBuKXwkyO zuhd_SF0-#o>?Rn647rp>g_Zi0vmupc$F1IYrLpw{8d#-PgzK(+GcDXq&P{-&(JT+= zNHZZ+WbpTcMEOHJK%WVGf`#t~YqVMC^<BM^W&3ZY{R#0L_;6I$qNr0!o5QuXvnL~H z5~U=13i2fhJ%tO*$8NPpz#$8>oxo=gE(lB)95wq+QXAfdDnCrxLfDq1=|N^rrUelA zh4NWItzke#Q!CJ+|MG~mW&JH~%GkgsXRGYyHps1~h*LaC^|e)eMV3S@)ZMc)hUfPh zVgP2Lo(nL>;a&hCxrFsfj?1z7varGani=C-w`g;TC7m7Qc;5iJ`;~7*t3bN(bIlKm zQV8+{6`gDSO$)AONZ9`K+_=wpp7|m>X<4jNEq`IUSxkRiKmje2qRb9^va)3SF4-vU zHk+I=$RuQ<g!4ITb`hRz(o=qG7^TTbo~kz)+>;O~BcynH@@#Q#u~r3lJ(!cPzF)=W zSv!1#1lgypA0ApKIX>_Aq#T<}LYIZQ;Gqnc=h;J975MgY5C@MOjH+tju&DSWQMelc zJm||DfE0~o#wM<?XNyI%&=3>`?e<)EO;@PY4_cWz5RE_rN-*);$~oGzV<qIc+=wYW zPomS#=EV{N<OT%|{N4r6POxpQSNT$^%gsr`;h!pspB#VF`t%J((WV$)o5kq@rSE>K z=P7)dSRT$dABs1?iR+^xRdloZtm>TYB0clpDwPUk9D;M&D2ba4{It-Ae$|~JeX?gj zA87UV;^y#3=FtYOi=#0$nZ%Z74x++?lR;ce0<UVN&wg5Dm>X}cNP}&SMa4P0vZw`C z5gU!`ODfEw7RKRQqRN>1cPlSs@JX61KXP#4(`1>*OevQAy(j+`@ARR@ab#(qM)_{d z%LDHx_-h-R`T1GFkBLuvow;8)J-66TCJUs;pfz|j{E`Ok=W!<#K2x2Ce5JW2bRA-6 zL=lT?=PkGsCGH<S;fjXw;M@R;3IJ*u9legdkZMq!zjZk=?qkmLjngHEw<{3O!F6mO zd9d}z{pmvqOHhKZQ@$%$i=krk#kF4vR5Xfb!!i8i6p;e6YW*O4T{MPtmAhcE++RD2 z3{Zdn!Sw6H4CBUfI5UgeC{T0fr%{5qMfcAqO>J$8*Tgs7DS;|Rt?1v?Q^lKj_FjA` z5Mj^&MwPqPz)uh-LxX<5cZ<ab)zH9Rx0npK#i}@_B;_V3?pr{v&jA!iBu8~;>xZ|* zY?*83EVj=F6{2^Xu|=V8KH^*t%s3yr^qkQ|FH>hm3lWM4%WW`TS2XJ0<;r?ofKayf zXIne6!+8q6*5|2{Q}(am`cM!4w4QE~zY%CDwA6f+Z<4c{WCJpVN|ef!fDPR_RMz^D z5y(c&IZYdnQ}n!IiGC6msGA=Aq5BtpNvDB)i%00HvcVfu0Csb9DLF_x3=}7Ux;y|- zl%T<l*ytm><{N^beGug6z5#OGv;E+N<}#Z$MvjWX`L%s55Q;5dUS%t+px7(8PevGW zQq=PqE{nK~gm3NZ3`_q~BkEE9O)?Y(jWLj@x(Tfo?AxO>ZHA-46vk14yPbScE`2>k z?%Xzj%f9Fws@IoOFG0H!h#njf1AbFHtsyKlaE*#27y;fzhY#9Arm29G61P#OA!C^6 z6Nl~k0UC8>WkNYXsm|>dE?beJ&sn&uS67fR4^_X{JD!kdf@{1$;NryV68Cj|&-tsC zmpuk~M7|=9Tjc?`>_91f=21ojvSjZD|2QJLF9-j&42iF}FJChsf8qdXiftIVZ0S!@ zgs!7e#S~vrnk>ha0ow>Q<$xv(A>+R><hXUy3DsbcLpiC<dRM!w5tpI46ocOS2GoVi z@K98Ab?UivyM~ZmLWZ;aLd*i6zwi$!(D2U*&WTaXLXzpeRbqRom)y8*HztnOr2+F3 z)`|7R6|-b8-^P`7YN#IlDZR0GuLwM#l(YT&!H((fn8jZnL*+)=qtt_Az!1PiogKfC z`uIKHKND-38NxlFk>;saDI?16-za=gYVvF98Rv(3(i(nPj6TQT6%;$RBJ`1hwFzIe zNF^v(j%FHaI{D1ysrt=l2FX#-#QdT3(P&Bq@c4bgHy^2YPem+*D^8?Vo5D(P%u7NF zBpdRGClc4=nGU2R90YIbQeKPL4-N5%5p)}v^NPQMVnXl!3X1eJ{=?T{&r<t{vPot} zkUfmz9PP06Mk=yewHJs_QDVu>))Hku7}SbmV#XcL<xGpb%2tnh6d^b{MduaDCPCMY zF+^$6v^^H}p8vZ0q@e$Cd;;1#reqF_fq1@~oE#Gg83ncuq_A`|nDaN*f(~wmlY8PP zMNq&=4DL(4gqIZajz)_9JVYFk>}Y8O?ejEi77L}iJ;)Mik-tVJYl{2d&IFM6|KUt} z#*E>+2qetT&)(1wJ#cuKfXNDqEjXuzaesVx6ah<QIrWks7Ouf3c)KfK2f-^D@=vlH zreCSqNI$%@I&7_&Ps#Pde4RG+R~?&yp*tg$oR%_+UN)7Y>+}ugs4=z3wi+IV9mf-z zm`)D{Of2bTKw0P__OfYZX7vUIcZ|TT;4?1Yz)LrkwfI&^QwwGA*Ey=)l8L4F%C?&x zO*r}pQyZrPOcdAV;NM(Qw{FAV9i~L%H?(FYKkTd~usIz~W-KcbIrHe!Cu^#gqi7WK zGTCPNItQ;tpxliinD_J?iRLFPzmPE!%w0>%jBu^COeAZJe9_QL&^}(%ntD9&G#pa4 z&y)!tdCS78)1HXIS-&)hpRYefNIvdB_bk4vong`Zr1)#=CI(s*TZWKhD?2(yl52tT zqp4uW62&v_O#&N-IqakD{kAhoC~GMew8s;;qW1Y&5c>H{z)j4z94|RiZEi*MEc59k z0VPdOqBti_oP{+nD#kZ_Oiu8QIUUD8v7WW)R;^@LJv&<nw2#fOv^&oBt;}SQ<VRn3 z(5Vy<hyNk4qBl*=Z(b*q=FQ5v8$?Cu>kr%+ArAJ^oiKiVZzy5uCCi|C68eoib$Kjo zKde!H7aTmc91=w|aY`khE!vJt2Y_;iQ-C+0vO$o5+cWX*SM+u>c0u^q+=en+rBsy# zVz4eZ#GY}6l?HLGYl+>dSoY0Q|Fl~_9%|fZFIe(DjnR~__5UmraaPoHZq{|14;0fo z?vJ9)Mdog7PP4mSzGJ86iXP+jZIH!Ix>0fvx9JCMWLX^5PZ!-9H$ANYGSD^^rsip& za5p-gdMB~xW3-pL4(xmkH{LG?oDhqJ_8q3tvRLRh!ii1PD-f}2L;V3&|D9SyLVS~~ zDuWm<fs7Zu<L*P7M$^OjyigM=&*w6<2-;Zo_aufg?+XpeJcKKKzLXKCpH}t3*ehP_ ze%z1jU91;Xj!QXCm~W2MOe>A`%2x1`N!`ud&tRKbeli|_?Qn;c`2;ofl-QJFnm&;+ z{G8{pHFa+}O1;A5dX7JTO`%*X>MBWf-0q&vBu|>9<mYlVlChgUOi>ttPs2a!(47O@ z=dY<FP&yW2<NM+?)IWi5;C%Y=uy|Q1?~+_77I%0vL4is!<8PUW^!R+)5iK;KM{dWo z^#-%8<xrh#Hnq?^z|>!5kvJTH)q&k)U#APcn`F+7n=248Zjxe0P7%P|9N9qBr53j( z)oSJN_m!3~zvm5+jZV;D3DEZ^7$pTp7J9Cx)H@xNX8C_Dlc^#l6PjL>7~lFa3;C1} ztA1~qG+Mue!I4#HViEiA)gkZUMKGpOx585-%~k{PPh*%Y0dAhH(C5|l89X>{mXyC* zVir%GgbvD1{c~N(flNzlG2!siDy^Ddw)MSa0H4vFRt28iFeyZCTgpTY&3Un;6jAhA z(~}X42L0zEim?R~*IT@Vbv;Yk@Aeh*+v=-`JM~OSa+P!c@0t&BnV%d10=T;t$Jms6 zBG74fLksh~3+$(phLL?2UMG=wnaLX3ILEDV-jA)-x-9QU;1(Lt@uq{&2(UFkHCU#6 z{L<IeLxLrhZ4X`6GKd>WD~lSp$POIAgqKzdvzlt<M^-2L4Q8HHDioY>Psbkv*s%>E z_BwCBmZO<2)#EonLYOIO%8zSQ;pqQJzLtMfUoDkH82WHW41i595=jGJ$W}B;jqu(+ zDl}aX<Zj26EjR{ZJ9~R3Y9C{qlj8NG&)E&_irNgiP%s(R))Sr2!b~mECQx&dkyJ_5 zlRo3xY8tISA|aU7laAIYQBcrUM||Uaf8*^*?QG3m*dFi1!Xl~)KawTjhn_J(r+wu5 z02zA?)EY;2-4OXou;{*oXg0O%zS(c@zyl;8d^sm{bUHs(b@F0oB`Zy6BahsesiRu# zymM{I7)NuK7_3X<>f1SNek?3gz-311gI2i}>h2JAWfYQ+_*${uK6h(POE?R{xu+KP zo8xReR5(PGlFP&>C}j0j`3lUL9KcMCoCv8xvibI|#7dA+ZlOik&q^zSLx6gKjcI{m z6BEP@i=hm-moMP3SlAq!`>jp~{~^ftfsHg^x8#LE&(U<?Pua4z+?~uWjT!#?`n5yh z_}+nP;5C>?(uUU!k^zlNr!*rve^as7xwAk3QUw1$EZvxb)E`1P9)g_?V1An7ToPm- zX=$hAr7smD=F5GP9$xJ6)G}<@_e1JCCN6|TK-N9jEoUz|<Sv090)Zf&Geirk`JM;= zjp~|#-Kg!?t<-6CR#i^tZJ9)=dqb&-nYx7d+9dy-2;<elFa4l6g#1f|&PDxO)X?Uo zIA_rKvVJ0_IV21Zfr`>iBNFu*rG%H;vxmR3OAq~|;(SS;0_*m|aSq$lnQD+LlAmig z0OOPki6~+$GlbjXivx<Ye8XKYU)G}N1xyvLpci}!lVKz40aAv5^~U9y9(~8fKGNra zlocL|t!4MK#wjLXP|*orHzv>K2RE4>-heSDZL-NP5e$f)uWqA1W%_oNcnj5Zp9y*~ zc~ga>1#cm;fz#Y1qHDaFpI;wUnU=Hjbj7Bud{u7DfNLnIu&*^SLv$s|E1AIVyk(hR zAD;vl$hl<4x`J3*^??|@veZGNGvd?Gu^#0}6<+!FkMekYj}Ua{r+FWFLYlC1QeL4S zwS3&`n51*z6K&-P=p_TgtJG9G?ZIWLbuJmS-x84u6VG%xmuk^9Ijmf8?#vANQ;>kt zd|*$#3(PIW;`bv6Gq058ut@xg^i-*&4$ZL&8z4ZO&TMd*ew-VvIPRb|Y>**EA$!|a zlQOnF->T~-CYjH2jGkuaL4NY?OC91_3!p%M7`9MgnK-G0b5-v$BQ#`~)!!uSO!%N( z7CKTUW2=WpH**{_weY_#+kiZuTl*i9ZG6`1xzpiS{7i1(8`CCj441+q3R^WT1RNNq ztFlU+AoR}kfF2Mdv3#d~_Fm6lfV7HZeEuq{@4QCz@dN3pIC7a0Smm+)Wri(#6v^tS zz3U!c_&c7$i{_*8V%XtgYV(Xx;(eV8<9d*a$OMAwo^Dx^d*rW$SL*(nN8mPyvd_-u zYgy8kq&}D*0rl9(`EXGIC=GE7MVsU^lEbzN8{rN6;?X7N-qF`a=ksT>mc-BaX}xkN z`!G|DRKqc*<gwF)>>*5a+vVJjjxAj_CC0*!9Jm;_Pu&gzYyhaxF`b&e`6c`yNA<sI zF_3&}(f#N0iMUW0a@Mv!{b)zgNY-z#P;=N5JJ4NW5;r}z$E4yn^tI-bfSxPlrExv^ zk_0at^AfA1`Y7^|E`~Nlf6d6CWr^>zZ^UW5rv_-6V)<BC+<-*?;!X#5&j>7@TKpW; z6d;~vRsFi_Exc1$e!QRtC%XG}@K~hanZrpwOWd~Qu#x^3T?T3gn3EF5NU43@4V==& zG?>#&wv)z#v<&tkr46g_6-~&zrq<b>%Ev2HLU%`Q#ntPP6{Iy4$uQKrv(&wU{JLVZ zQoY;6U4$|;IAhq3H=x0BJ4n)+0HYm8Tvaje1VO2O=prjP#B%pPr^WF4x+l~0z}*Ui z;onL<l&G>p$N#JTev0aga0n$`#Fbk9ew!0d4v?()hJjxSN_Y_86snXjf?nLB$%Wsw z%K3>;6M9X!5yu`qr8zd7$GLx5UZ#1Q@CfL4@pU^w?z$+4>su)18-R-9E&4bv<|8Gz z(|%!8gZadTxQpLN&KEYV%O*^9^#w-52172p^z*I^^)D*u72_`QqO4{=`F)%gFQv~w zuZ#IZJprH`fX$L1+lpCu^s@!Hz(JzsGest=PsHZ$rZFgS_BjlFH*dZe%Kk3*OQrBJ zXXP!?h#f?$<Rjj3^yZ8Gy==rPB%7PdrS2RPmUu?`v6}4qmuUa#;8vyu;J(xsU+0~9 z94!k!uuOEXZ=UvmJt6i6xm<K9B&XA!(%_#(Q=;>DNYWp(a(Iq1OLfu4`R_xFxR~-@ z`GxXFLiR3j#djA+sm`DO7sVBPN&v=5x%)X=Gt?d`I=&4nNlIWpMM?yA%EI_z1uX;B zb9Jv#P@3~m?DNwFSl78%l)|#QcF%d+nem+%_#Rr0b_b*P2;{FDUZM(ZCBytp&<<*a zWrk(nld*<EQ_&6T!wW6^0YIACCTamiuyi(+jBpL_vU7Oh$1Cd2AH(B<Tv#x%NV9Ey z!#wlcE3S_$aW)Hlwy@@agN^Z--Afa>U5;tXD|h`sAnqfn8KgPl>F?}(>oJHw1sT!p zUfHqu#d`fg8v*_fkW@q;^m2VO-JxM8w8Ec9%enI8FO$PwBU+(Afkp*DU2ISB0?Y;) z&ZNRDc4>PrvW53%nuhKbnoz#cT(%2e&IyDm@j$U7SA5U<r%S{=zcZ<_)ebQQPr>n2 zBGCmU%=E`@`#ZUY$hM-ky;<Z8vE-ZH8|D1Ej76s+X>hGZAd`Q>l_+JGVtk_pihYbW zq@EuIC3LArYhO_U<%{Ejie-sL*nY%__QuSz;-P><e~prkw{_PU!fYF4EuG^asJjmY z<mFOa=%dm(X+%4#6_w541m*<xFQZJCy2j!+X40d?@1UnX#77042RE3At~AA9URLSz zaA_@liYdrMrbu**+wm3=ukIsXkAgfXs*JL0g#9q@qSFyK!OO89h?|OBGde+fUAYrd z0%css&QMtK^zFWclh_Hu?3>)yp!_HU?L4Ua7GrSkjCIDTCQN{Qg}>bJJwBAAY?~UA zt<F^jD%Sd5X>ku+Cf<|k)mS0H%JVg0)0OLsBKA{$<Voq)g*-HbGL3a_#BIKe)UKM7 zVEE=u#d!}Kd$@qtbSf78nHbkU<xzXy8N(+^Y-2gc12jv&oAFIaxj%(~zna+V)y0Re zSndw28@1=1rwLxc|E_wX^_PKKQePn#byr??wb~c6aw`d&e2Ibt6#?3#a$3!S4);!X zB@kV3uuC^WcCnRsC_Wy>wx632dykJC{tYp3D>-c0kGEqjL)gw;T!RcbEkLh}W@t*} zy0xofG7=K`tB3%Dv=QTjh%K$1!dOcQiZA+is}o6B#QbIUZrfH}kN3{YBgnxTK##2N ztF_;FXoI(=ka%+^uf08qoR!B){JuXmG-MY$U+=}nz=iyL4`DWE*W{~rKlEs#`U{{- zZ;R=(OetE$I8jCmT4@$krM5psz`{$tko<byUDne0j5v3iUtab-eE#IS2Y<0vvxtW; zpDzH5E0jx-Cn?uDxf-;@>~_vb4?R?|TIG9-ZRcxzyE6*_Bdj0!*$l>fQcI1YS)my% z$cAXrKwKW6)2|c;%Hxm{HeqyA16H56xzQLr7E5egOI;Cz5qHdr%ot|TG<{HULMiQF z4dd90M6#t^jOV-x^LZ*QHYkZ{Vg0Z|GggZRN-qlTD$@HiMx_a(I)R6ts0s3%A3f<7 ztm#7om6f)4??p}EMM;22@Xsk`RE_zFi8$cXa%WgT`4OdeUE|I&Lk`@@*=3Fp@U&e$ za9y0Ww(YT2!aQUmKHbuBH1-T}ad}i8U`2i~iD{VlV7!=zr@faKhelvh(^q=w&X1xS zD)rC0I=P)@;-dl}k;`yWsE}weGVTdp()B^CX#XopO{>l0795Bo1k+I}i<qQ<>t5@p zTpDph0anYcO>$Z4h~+xz3-$^=xc$5mcY`4t`^Ib2Un4b>X{<F#HDFGjI^B?(*>7@t zipy%CJ%S3+dXZF{7om2qL~C-?CLf-uV`dVSYtzIgiZXi!>P_2gTGkP9bA_PbQ>#Dw zs+I|gBVWg!ONd$v*9|Ho@(@9?dzj%Zkilw$-s|*CMn<!Iqd)~TIg)+Vk`BXjV;mpn z2H25n@^Y)qa>=75$`34^ug2jl@?LOQH2g$92(z$erDho5+Ha)7O!YrTe6gghM-{IY zTAK5DxlR3aH)J6`tv*M7|BcSsT}@D@O{+pKEIo}6L8>VN)fEvmI!5Ub-(jt3Xoh1z z0%%j~?CrA3y(Yyt6AZ|JNJP(%;zYGsN}iHYS}*togA2KAEe%Ais}x8*oto-r1|x68 z@hvKg(Q}ZV8jK}79ME|uz_IOP=+v;D5VP@aGCJIOc=|`fdMe)z3C>_)lyD$V*R*e@ z-}(Ep?8oa&zPR@l!aFRAI0=uFzuB{mk<V=+H4cVt-ewE?F`p^Tk^xJzrSre1i#R;{ zXp-NfKVWW}F57$7CZ!EI0P!qC#RK5~$?n!!Gwe;<fZ7*_(i`a6w&<RncsQVOE*H*L z1pp+1e+u0`XuE^n9==^s^_qnU%#SQRBo#Q&rgHDfUr%axk$Pu#e~GCG`A^B~1deFZ zq*)=O{9{16jmPutGi-`Ry;vLTr|H<J#x9IPMZm`@bpt*&AUEQJD$eeyW}LnQzB|iV z!E$I{>AhAZw%jlYkQh?oh}X^J6E?W~IbMU8>Ly`}?Jh`?zVAWyi<dC$+aQ5M{n^IN z7vWgirAbwJE{j~~Ded4MtE8cU2+)<Zy*xI5_Jx%Z-uDfSu$iGJi|=Tc2Dx%Wr-u0I zd4eJa5j<<~qcvk-`ulYlrJt^L$g1+l2ubi?j@k{%IJXb7Lf%#8<Hc6Ak{H28q9{`I z=(x4#RN%C@W%UgU10ir*P`bBm22n<ueTMPNyW{{-N_AJE%P53)JE1TRQ5(pl&6h0i zV2FG}=Hl4J4WTz?K_&HemE@HmBX74~20!FSymxm1)nLo!Q>-bTlL-Gn&q#oWTKdS< z*}_#RxBf}|caHkKuXK7JTJssc%ZG1Ii31njR8ZvmNC!+c-76paTjrAjtcD>3ZJ|Zp zm>H{XdfY543Q>Ph@atS72U<2s4kpg$nN_OoD5nW4Rn>g~dLX^vH6^IqaE}@ml1n^< zM~-3w5ksPbf3Z1Yf?%A@Q|wc!;GFbbjtO29^7i}6$`x;F&qnf2<W*wq3K>W1e8U=N zoG*c;RmAk4_$F~ge^kGtj56ybvNASV#dQU8S|!FI5bYa@KDXZ?ZX@!Pnm2Bx4DiLI zgbl6(s#%ZEADP@XhQcoZ89I)7vuq{)(FaU4@HD6jNs&=w#w=<`r?eZ7-OuR;PSfui zc7u=X!`mZ?uQxEKl#mojA?tNOJ>IXZj9hum`Mq;BX0Lpj&wn)emem{%e?+9y1-*H* zz#hCTBrPV?V`5>*e~6J&<8$ECQX%W8O`$Q#wbaVFGz<QLSR|8z5z+}C3EbloAOx~u z=jTHT#wsMqr`MhB_Y>KbnZ|CKO!NK})5>LjKOJS%Z(;0uXKuQ@o<AeYvDiGs3jZZk z<Ggca>^(6t&{KuSXa5a0s7Bh!gcFahzis6M>H1Pd3b1pW6fRktbJ6MTln=8f^*r+9 z*n)p){mSH~SoyS|Z{|cObIPv@8;uz~sDzZQ!IedN=_<d4e;5-m4FX9j<Iq&N;rt&? z0qLE+J*<Loz~|p)XR9`U{E20^BZDt3%DM<(P;56c(>M3sQvJ&wCdilXJFE!}PactG z-yq_aO8vAktw=L{?GF_vih8RQ{%{-0cB?0@8kof#cHLfwFXQq~a?qfRhE{a*L}E@t ze7X4AgBN=aEhk*Gwy>2&UC&#<6{8Qjps?n5)`Ui@qc_Qsq*0<cBn3ETg*iP5FPfJx zerzCu!-<1>vH~J$pgWbWh~h%2Q7=KDOI>*i`n$Ku`QcjyYVoi%ke5Qh)fb!zkTh)_ zP=%57dpnA#=8z=qgE0#b?u0g58`7sxsa1AP8>h8tY9QKo`wt6celopnn59k667m!A zR36;yG+;2yY(Tjo2u?sa@S~^bHfEyiU8MMVuuWY4g@QjnkT&XCNIPEg4HBvNYj+Le zw6FSgnzzmp-3dQk>IRP+`Iyz)^xw7ns$Y%D7m;ii?JmPC&U62^z%RP3=?N>v#n zo#*ljhmTLHYwTl_W4n5a`|#KWq=+0Y*^@&jqbuJmCt_4jEHc$7S0J8$y=Vw(F@6!p z4FG@9K%TJIiez+xZbm<;*)RvXxZf69LYN6TYP`Q63aun+RFw(~y<!KeUxgIKGz0BW zZD8I<ykd0TnM!q5V`F@{6#TUp(5&ILFfYYiwAc^bGh2xKfeS`}yD^tx4%za14RHnq z(4YNx7t-Hy$_BoK#)!6bVKRE9OX3B$$+R6ZN3EaVUp=q&uZ2g^zDqhHe+r=(tEqhr z5@`w6#ZH<wnQd4QlbDvPu}SQIDRSuU$DHW$@ZMMFCL{#G3^-MvkJ@}8d3V-wcr7w( zND3|+ApZ)kzx@;S;dzCLD0<0i?3vB#;y55fw*Xj*8E?U?%>@>+Rxv9Z`((<=C_gIG z#Yc?6+Z*3+KUn>0th@f>g0ktjc>3h{)rSl}D4=u8??T`>Dp)<ywgUVn)CzAqpwjkv z{vhmp#BimLOTj7orXfY9#3OjA!#y%u*(B=C4G2g?WxL-ozJ214HrEmkDm(C5^7L~s zUvA0;IallQvIbG2$2t!KvKc_2G5!})75|D<p<5vf&gD=tLjr5AT(QUx|0<HM*+1z3 z5gh>~<*Zd}V6IR*oiuy>gFO#K4?RWsA63*%KVrT&$0iJ^>`>ts>w9s<P=rcIBRW<M zn`Q!3UZdw*QUyJrdTIK9T*r&ZUvaW|O4eJAWiJ4tm$+KE%Qoi({$tfb=n6<Fdxc=v z*I#$2#PS#+KrRNo{%XWZFZ)NWx%)DR>`nSq2~1Rvmr-<Eg*G$Ng4xBrXs$vq`OVuR z&_0O)_*FhY12R3&J0<!0azEYuO*UlxsiNA4mIfqrPx0x;4_%&j7+qe**Vs+Bap)GF z`)IV56Tf_)W&pVN*g0$3d26t!HKWT`{jzQCaYI)(wIL0KZi<99Y_ZE}-q!bQX0%GP zHAtsR)&?~q2m`+IZq)Z_G?K2XwVQ#x0|@O{N(Bt*2~vp}i=&H(ajXNdQ?Sf67{X(7 z^2;Jiq(HkrF=YS6PK~`1V*q+rwk51J=-XA)_xOZ0Nu4JrJDmvY-p{OBvRpM!<I*)F zZuJ)EuDB=BEihd{NBwg1qK41zdF8Rp(F9TTc;8o5Grz2c)Owz%OXJYu#=>=@*@U3u z?!I4>&NEJA|E<y2)3{OcH1SYe{%!K}vuasPQhq<&0-$oZB%oT(^Ofqnyi0qzv8`hl zk79T12_K;AvZ}d}GC5C-hp&6SS&GJ>a*7ZUQ8s&LLgZMYRLZk{2N*c}W(Jkq2g3V{ zdV!+Pwvt`^@o`agF-&xMkx?TUAY*7u`+tEjR{wx0FOyYN*0{+LiWdPG-&>iVcGTj| zJedCLTM+)Y+R$o0h}a!Z&76D?YAAWFA9~AahRs@@4~Dv4NH3fqR#(4a)?63r9)F61 z5pH^HT5uC-ihIy;xVw6(rk&3VUg`1}Aj<Lk@k7ntYkRcHisw7wUz!goEu@CCL|sPN z6*AboPo-0sV?sM67KgwkLPXO*o+*)nKB`$sQvz6jV_dWfr6LHu>R7>_>=a1dD^lx- znBzHvpl0=snpi8Bi}vJRPty!VLm`YQ(50d#g~`)@ZeDO&S>drvgS0G)uXSZ)Bhz)4 z+WDENQaq%QCA4;p%OUVqy`u9s4DX!S&ic)KovTjgDRMxe$g@_794<DHc%sJuZ6vpq zz!tZ&#y40|57RY1DslUf6#hB<=2Qg)q==;z`9G>I*p!9KpQ_dt-=fb&^mCZ1y<Cih z>gP8Y>PP-`jP%vna}oSddf$5<+|ovotNx+YS#JV15`MY6_G+|i5o_+Fjcqx*&D{)@ zXXRyFS%Ewnc3|pPAj^C8`f?v`rF7Qv^(htgu&3EbmON<U$VhQaha_rWO<0Q|{%;e0 zr8Xw}z%}mP`w$pfIt6O<x@ubAXh*#(q<$H+na(<8{G!G4BWa_>V>tQIr$Hmh(`mT0 z<7}?OLfO~4zY+nrt8$)-A0@ICe`=9A-yw{v+Ud&wP(i@2@s{dP=p%u|w7T67Ks4i& z0u2pP1c>eE&4bh)dtvF?Z=(hK#ng7~hWI)!dtqN5dtW@xlpm@h>%(KAg?lA4*q)!@ zk|WW01Y-4GGBD5y3Xq|Ml|_@g=$A+%<uYD=7RhI%YgFSjC?+Imkw(4R#m61FFG?AX z5X=+{>|6lIXbdC8)k}E}_(J<e3Kj#g+#JA7)%D~4?WRltHzn)yZjWzbhH+_=ycb-( z0$qEd#$-r;()rrdM*pRGJ+H6qyZ3hza^C`@ZPBOU2kJ&0!d^~drP{2wnS#Ky0;+Ia zS_=m`?&fCiiL6T<YJR%f2N^Ld5+#T9u(o~}5Rh36V0Rd9^gd?C``nJKAL~9lOL_&D zv@xb$%#YTwI&3P`Ili1-?Q}hw%%2dHe8u^2Hb+OT;~tEa5Jir|ya%c+3VAYh<@el& zdhS<=boF{3Nq-Y}zU{uuuY3S~Js55HrXJ%V|La%dE5@9nymQVb?Mi>JZx2#SEaIF@ zV_7oDg)m01!#I{cK^L`ZJViHCGKT|Nm{E!B`oS9i5M|Q?^?-h84_j12ZC)fp1r!4* zCxG+%SBe2uju%;+dG!y-_}X{9noD+8dnU2$$0gfTT{X8vsR~K4+75ROtC&*{yPu$D z6=@-zF0Y2eyUylhT}yEN&0#`)gJ11(_t&%3HWY|SILF_ve*$p*J~CkSdobzxO-l<F zG~1xsc5VY97X^~F=rg|VpWZr&2F-fvS_Fuw1TS&VgKMON?}K_*pu8?O{P%Keu3xTX zeU0xzb%(=ZU>w{Yl3(i1rM{GC0v!J+-T3OYvx3F-I90Jr$3wbq6*RRidU%z0$AK%| zbLdr1b9S8jSA{xD3*i;Gg}1BT`+O>Pd4^Rw&tBs%4^3?BXHo1I|57I)P;6$T6g;R0 z&p2c>u!i`}Nsa?-HlZwP`_(F!vT;QJ<u}z~$Vo2eaWqv+Xtiu1gS=EdRB0Ag72mcn z{)6~;-hi6W8dZMSOux5(vm5D&fyj_!{7ARMa6`Tp|K)8E-~EsF&LHHRR|U^;B=WF` zv{FgBAS@C%XW%DLI6#^^#_U9@XXMJ%xnOWt?CZNf;QKUQUKsrxj`l+wc!4^+BRZ#p zR9$Xo1HPW)2Ade-g=U7M4cfNtIipoNFWjkNSecF!&imyrzc2u?=a4XM?st1Lc%t}H z*tg%?uSUAsZ_mqMaRX*qeC9Rsfyg9&6n7k<;5C%O!OipS0Fz?!Fbc$p?<)Q>JgDAe z^?3vpWDH^Uddv)i{?DK4U8`?;m*F4VxQ|oZ&jA6-b@BIlR#(w>p_O=_?%wOYal(v( zyNw|ND+F5s$^XLfBXKFcy6*!s<Fko2$0VEU=rS@^%KlVQV7gM4b<H3J{dG|OAh^bg zJFy+prY*rwB8_@|PAA77mBXN~R^vaVq=$m7oXdbYZ9Hp5#P~Kq)g=Z90!-IPR26`i znC3@>5A3!HMor5ST+r@E`oG~clW0K6GK~7#Uw`av8jO{|A5VbL-1XJ_B<Xr*k07PY z1D!w$#D@nLY80fkXh7)sq*nhNs^gdXC#wuZU1-0fE?rQMUkhGo306Vgm&eXwT?~S9 zB?(9M7q88umy+<mD13Q!=41b975@mYv+DU0X6p6gF-swV?~|gkWA+O_=k=4r3t%Wk z^&su)=`)MSa7-Jt=`Bh1iU8yuXbk!L=i*PyPD&OMGSp0?TKp*1Dh2SaQdEvwT}v{r zVFHdaqOT!jEM@uYmr)lm<Veitn0C)^KP!uLy~*GYLFc>}uZiof8?67E4&?iTbI@C4 z{G)GBQDpDrklgfU$t2Y52=1%IpulMJ-G=Sh2YmSJY2fciTh_dZ`R+NGpILmIYkE2S zO#abJ9F?+#gw5!?gXAO$bc67MHu8zds9l6bF6&+{k=KyId{fiVvL<EAhl$Ni!^e*e z8gV#2z+FshOl{~oyiL-}%-@0cuqLWUuTsyxb6wsx1hUk?JA<vN&e(Tv&psrEEsyd{ z;JR+A^Y=gy=Z%Fd6rRjS;IU_{|EuQ&Qq)t=IJAoa1H5P?plL?X2z&~|(Qv1bVtiCJ zAZ$=vNPQdS@A1ua2D__O>_2exfMSUP%`WpABJ;AAftsHxh@lO4!O}_)V6r#f#8ia7 z@wRX_O>1M}+A;y4*FR=~l&DC~wj8oL+!>^{wo%T^EfUL@s{lO#xfbtCuX4QKS^$7X z_K{7(aHp~iAseYhVZm0)?0sMR$ICK$pEVKM2`qZ+zo5#Uzsd4IXyfQk(Z1hjXSO!r zrN%IOsowlHwv@+?8LJ(_qjq^2luYvwU^YTFwz;BUvP9Nq1yYVOcrWK!bvw0N$45{N z)Whquv#mYtYbQXyNyj(n5?wwpx}uELf(G7i((sSz9%OVkP0qZ!!ZtV(yNo=vHzh{< zC+}h`ji%2H65iLUWch@(&?QCm(1`OtoQF<TF@k<iLisLXlQo@E{im7wQTYFHlLV1^ zU3hQn+t-C}s`mN@VTsy{!b(fCPNaE^lWz3A0`a+Tloax(&~n5Fk#Yw93;7P`)MO?e z_dOIpnX8)H`WLPU^pwR%L`z4z1`V2sbt_KHHP!+ZSJjNwfb)be&Hx%!9$1!P5T5q# zET(eSCVTkbFRM9#4A{RJ4ctMp*QU6FFat&3pwiN;f{YuGE89)FS5}71BbdfdPB=c2 zvP6($<Rl_z#muf-;o=I$fK*(m%zylYdGE0=4ZTf6#N~lIJ83LC0z1j60B>NR9aA2y zP>;mYd~|0=4`pBQCQXOxnbyXis4=O|7n9d?S};B)?Ad!XB_cceGnm(t_@@q)0;!v? z8r@e9gPdl{E05~88~#1&&Fcz<mC)rDQqzDwR&6ng;!zvGkPHewy<%O)`7l6Y28>lm z52!9G|JaN0Yz7Qb;6aNBObfDWxJt6dUzZ1!SrWun1_$%?OjZnD+4~NvAJ<l4DFt2{ z^Pk6HG@-P;PB7^c4|SFJ_iH>YRCry|-OyT5Ew~A$>B-jgHzVSs$N*GIE=5B!LUtvG z9{oe*(NyZ*lneAqot}-j^>lHd9INp0tEeV?VdOp%hggBQ4gtOlPJ(JiV*{UOvIKF| zh1yBxr()c1MD0fg09;Rt0_KDaHT7?oVF+}a0<5an-~zkk$PY_ILXq_K*T!PqoLVN9 zNe5(sF#wzUsQ<AxN0{w;z);Z62^#FkWpIA;D-Ll&(9sV(jg?Rz8t4TB96P~?bMfUN zAnO*)q#$v-3iO*wZH*_WUZANIE>dwhZHb=HH&$Xm3<&HG;;DlsENWg(Hk0xWy%lA{ z--+_50sSQWclrUhh&5_MUS&gCvZ}sm#T_=#M%SvSlNMQ7#fCTTxlwcR!6zW_4m<`m zZnO9n=ukrwj*C{Q{YU%zyX^7<Ypq`{#7CeSxSeif1H4i9ass6BxO}ntc(c>_D&H?0 z)|S)HL!zHj<2@w`DtBRb9H=6lTa)j{+TEY$&^)MQwtlz2Z)5lu?e8n^w7_%!FZ<vB znpNsM&h{Yh@|9wrv4a^Vd~x#9g0TP>|L<w#L^E+6jnn5?>;#0;f=JVmUM8AFg)j?C zS)n)^OX7!gTj7wOrXdsh@byi2+<n=H1L?=Zo|9}h`DJ)C`DJXpnQ-G2L>9ki$YoLd z&EgdSxuB4(SD*6Gg1$qQo~tC$5BNfMPkJ-jlg3T<kFxzw>J;HiLu#zHK*rJw@}4Vu z-JNt@GK5rztcgCu?$Ox(k(WwFfxH1lqza^h3z{%{RH)6;y2a57cCjpwpxTkAFT!~I zI+`{=C~l`5r83&(&}xBTvDF7jGgw256lKscGeu<#jru$B865Ti78zjyz4GLv8fryL zk4-ve(irJpzxH5poL&j-UD<JC8M<a=-Dc3PIgD3nKNK)FNOU5$UlQq``sg>&{-cjR zhRnb3qmOhpBC7x4qvUE6ZsQ7(WKm+is2M&yvEZi9x;g@pe*+3zYmmh0j}437g}oDE z>($3OUURioML^<)^!CTR8C|F~ek@ydk9W>dspg@2nhAejM>1WCX90Je<QEkLV8%}8 zFJ0|o{x;^yo3S&BzKzb3^Q6MI_sEASYNsqRekyP`SzPh@XcT0wtR%Q)XdP==Voj1q zggCOVQydZ<3wreWY5xCbqyJS4{ck>N7nmWpK4mwb?#V655bNIB!c*j{4CVa;j^<v_ zj}H7zNB93+67`^3pBaaKv{aCoy<zZjDEL;2^bYn>E$xTyBtd+VjP@K8B6EPY<U8+% z#u>xb|HIx}M&-FJd%j43Bsc^J9^BoXKyY_=cXv&22oT&oxVuAecXxLS?$FP>_F7v` z@6-2;aqb<rM~~(M3_iS%%(v#8HLL#hs~*oOzNR0!KE{ob&0bMkYW5$?W7jj?;Gj7~ zEfSlg{)+a2P3cl5<(#+0f=*<J?!R`d0vXBi^FU7H3CwK)j#Rilf1Zk;F!qFuBB6pH zxz}janDMD_oaC?Co$gxsVkS(5>*1}O=sACm(pT!DCJ=c`^rEgF7Jc0$<t2I@QH@u{ z0tOJ$f8M9fbFd71IzWjD-nzBoQwv9j`)T&xp>6@!8^(xzK6%JVv)IWkp3R@*r2C8y z>E9}*H^u0Pcr5FK*5w_?Gr)|7>ULA9S|Ufgo;#6Prxc3qV#vGg%4W;5zFEg+Ci$dV zoy3ImF}hmX-R+FbW6;!2lu<48mBT(NL1r)i(6X37mQp>>0?PB1AWvW#Fw>1x8x;Qz z$Tv5au+G+ordT|sy>5vMm%ni)MUOeHH;>ES^0nE;=$-(w0{<SE-&XS%FkhOl0FX0K zsiEg5J6aS*JK_A6SQ+*4j|U{mWANtF*C$HOwL1L?jz}7cUAP(ejhIWh!`}xcL;Hc` zrcJXfz@U}A%o_M_0`p~=wB_OJtsTbRnG>?2!Z}~B;mh78V9@=hqMPVHAW>mT5OU_m zitp8IXd7okO)?rkNn}+LQ(whlJGMNH4IJ&Q@KtG<q;miOVw0#le04I7m|QjTE3$&x z#uRpoAmdHV?w9fT|BRMzV*ZaLt-=5f-<&gWVj!|qnTMz3(iPr}#uNm(+{bk$M0r>e zd*K%V0K>%vU*wNoKwD~`B<Jhns9ez_P&)5S;x1#m6nQT1$p4oh{K%XVZY6yPv*-i^ z+w@LZtT^hvmC&3xUkPbW(*P}?2r9Wh%zFkolLT{vOo1hP11N47F*WfKeD|RgYT(=B zwDM-wvnzr6k34*J6OXq#1s6qkzX}LP(85qGNz)In_ejg(4VbhHY<-A6%Xoo%;%=b& zT_XMpip+{=`lgw}3@8I1i)|QyRJ`HE@IG}4Ra%pMzBuVK1F6Y@+Mt-)5WhnY<JqL* zrf)b>=E)?dg1&vcKl4oZ-J3`<{v0OR=)~ua!aNNl5_Osxq=$=x>sy%_Cw|g__UM-m zwX5!Mr`3@p)sR^UAGPz)YAbR<puf>fCA9n(Z*}m8n5V~#hn2TPj-*N%KTcHK8!JQs z%<PSxgUueIfu1!+{eHf-{+6$8j0Zv?ROAN5n|;v`YCy|x#m!qS^NHVp!yC2sS<aLA zOv`#=?fy1wQRD+G4qrFhRDHCnoG5}F)408kS(cEl6>gg)8a!0JOMceoLj=AAP%00| z4=%kV6n<-93K`YuddnK%u0C;pdLtegBd1ewa-ve9*}kg;oX~Q1X6jO7)TX6MkuC+7 z>liTa1XJm9YIPJvVY59$5RvPS5oOqg#hkJ)X^;G%%<Zb!hTe}wR3z7KIcAl8Colc} zI=K*EJ3pvbz0@y)4cYnKmnQ`iF0WSMm&`<)F&+O_*S4_cmF?=VVb$@9zO_>^8g9sG zNp09}xwf^{nG<HQ6VS2|0Fw0>($>#DcM7Uwm73L$`%iJ%nTVzZ1drjCs4dAT#}wH) zyI3_0JqW>Kh9x0jBcA7eIZ)?O)OK=*JaUb)_VFJz7Wo|9$9fn;u}MV9xL0xaiPd68 zKhRGCEF(3n6X2C;k;nn5&bu!at*@UYLrVT%q~8bKa8-27E^l>5(blhSh#BSX{Y2U~ zv-JmHl4nKf6IZ~Q#}r$Z{j1AI;}D>b!5!!p7jY*lhv6nTkFLz0wcGxcU2hGm-jV4z zGyToKOwN!V8qm=ZR+^d13c)oUho(gsG&%3w6Z<X>WdAJAld8(`7f9Vd*v`mD&2=X= zC`!K{kQ*?U<|;)*Cs({kp0d<B<Dka(=#G?!$RK9ec=i78QttO79zth7s>I8%lE9l{ z4oEF%T}1w%s&9JN*f(LEKlj;0q^>mPmTjQa|7!e0V6v!+Z=IHyH#b+vcc7_ssv8Bs z4M`b#b`qqa+8o<x_;m&okJ_~H7V;!~bQwoRN)e_Nq6Y9mHPTyLT5IBO&`rMFdPe?^ zsr`Ru+haXiJj#g|&|aH0I>^*IlDj`e{%qQ0d++Y%$N=|9iaNCN^7To<KVuoFo=Md= zdWoY}CY)tG+j*y~z5gq@MA)}%Y`D5nfcK1wk<Q3!tk;~KQ-sx;jqv)`pM30R{j!rr z;w^@Bs5Urf5v<Wn!@?tU(Y+}+=Yb4o=`$%=xDy&%O7;V~;1Nxkt7CEIL-mgqj&uqe zkDb$Cn_`kLp-*>`j*}A$3d~CMPlK01GogW10ovbu1;j3c^GX3Y)j-sovPBa_T!~Y2 ze%4m9XN9%tfPJKuAe&0UC*Ruf4mwPH9QBtUPEV}oA<&1hykP~WF~u-Fjo{MGFsQZ= zc2&(RkFM#iA6Kq(kuw;naRGT{H_~gSvdC;`qJBEtB2{tRJk_DX(>(;Qp=17%7oJu? z7{6BhmN?ZVMy89p*M8hIp<tZI>;9bS1FaU~KEY?C>wqA0{@{o06M~t7D4@=FHlnW_ zprMZ`ce+HK2&Y1kS{v%i;4-+xqFTGX^-!Q^X|xj}_fQzeJrsn4IFv-jpUN~Q=F1~+ zSaqvX{r-X*?0~owgWtVho{}B+pa?}%4_hFk)bBtRe;en2TD69%Afx%K;3`~`mr|61 zhVRIUKC7bwEF5bPX6#qVzbLA&WV+$vMk|)jov==y&i7cRo0TZ|OmDviGirW4Mez}7 z-n#QieTm7g?|@GSX+|Z5QTB|vo*~*WKWY_l#jMbr@Njc1%`KeXZOabRWaG!@HI{FD z|MIEBy@rP8cvbNqMfV+wjD5FiwV;U@)}}3E1T!p}U1U88Iy_rKolCw0+x0JW-z5`{ z_I5n#6@<9zFO9}{T;dct6t#Z>iytZ&Q_mo;1Ohuu8x-8q$zK-}y=K`n?_2JR+3ag+ zZGh%IL1w3wfp7MA4G}e*`)RtQWeiXq^BU>Y`zwGhor;&#Z0NZ)+liOEe&<*DYLGo4 zWg;12J1c0RJudfa&FV{!CL41cKrEp+RnXfSx0irj`qWsJ@m;O~Bhaj(IPSzn#a8~< zEoTXV<={8b=bJHD!1h?g?M$4YhR%8uc8K6X6%5ydR}|PSYmdP-&H$D;0_Lhp)UMC= zwUKEqJ{>otx@643?E~WFn=`yZ0#D06!G8hP^W4mf#YS^l)W_DZOo=|ik_Cv7_sG!k z7o%CB4K>E~bUm-7|N5G~1Juz?ocIvm(^w%}Q=CD~&TIexD}HQph`q)e^ZdGiGZ!BA z?eaB9U~-+wW+c_G`&YQB0)^aX;H3g)o?(vplEi@G&(8FiBuam}ZeKOteUrK8Jju{6 z(<*$Y)|%i60I(Y65Bhv4bbMn<?flw<Dw0EbqBW+u(vLUxL4fV5LAV7;UK~{<9)PG~ z@*c<^qnI9jEPMWvLwEGa!K6i%CS<|Di#@#QOp&oeFMb7J5!9&Qn1r(uB2D-;cGqST zK3UDXhC-qI2WI^SN6=qV>i@EQnp=-CCD{$)Ty&8lN}j`FPYD38-x0S=02@^;GSF?a zXK}PxWpaUx%<Bh{0-^IlWB2OJG7?&rB|2GNO>KrczLE(5VGhR%U1m4u=<VVB6seg< zuh<lh>Mik^zK8{hQf&=V@4yU$YICGGJ%*47N9&sTPr7mARnF2H?{?kGsgHP$kK=?^ zoF^}3R%TJIlq#~%bjSgYg!FTjI8!0J072XSFkGVRPuzcQ$U>P!w10>T3MFaLOut4^ z(hG`OQUmD-;dzxFOhsofUDm`R6K}@<E1dp6TR7dX7GV~{Ir+b@Y+91U0Vk9u3ouO8 zU0jDGUIhAg_u0ZAyLDR;&8&h1tL^~KE~uaWcc#Y!r^&rHN*G(B-JyY<*#YV7l$Ay0 zGc#;#1tGj%sS|)?j-sgYe0clARxs+AqA%&Lilml9R<7tblEt!{$G$PcVLoMv(9t5^ ziG1(Ep)Rb8#U1kif#vBZH{B+spACL*Z9RXg-~M=P<B1=oP{tzRFFvZi+`TuBj-myS z&V(G%CJEe!MW0XFQ^(U_uqdfl+%!<R%st{L7jl|&;{aTO%!RB2Pc2yZemRZCVO;fZ zJG}8=56_Ol{h+(!hP~sFqXPnMVK`?VcD(4(2L6$#;D{aEwPD1kVzG=X_m?qjTBjdV ztl3oNyafC{9^c=$(oj%o{-SCB++BBSS+t>PN^pB<ZXQ;g_r%bgytGN_lpu225RzGp zjWXm2o_ysy`!I8Jj#xh9%Ky0k5Q-9!+eDbDjky0YqO`4{_+>W2M1(ujApaZ8)z%wO zsY@Ei=5z@-Y<Z67uXp$w$&w}2;&c2KIjL+=YZUK{BE9THxfD3@#vQ>I`Bw_cG$Iih zxmwDaH*2a#$Tse@@@V}wA$;=V(%<p;54Et$n58aV00pz~aX@nh;ggoJ!E1dQw}S2z z6+xgD0GiG5k1DR%vKFwXYrj*fwu5gS6GAiBC<NDKaV={Twi4Q0q|nS>?%yf?Vub-o zm_ow{fDR@Ja`L||4tR6I<b81AS_1mfBJW0{Nf^O^8T$dgMjwMB@{aiyj<*D(<S0=& zX?pCVt*TPSkP;+i($7zhbCf0*#h3Y_-|<>F>WENf4y1@|f(@Cp#<o|;-b5|l2}ZDL zKk?R*PZ~YmQ<{s=FoQQ0(<cI5w^bQ3HBhy#NRh~zH5Oog&N^pZj%%fK7%jPyT_HW( z1=R(~PTc9y&34%x$$d?nm&hw@UtQi-nkM$|-pWW*u~hn_r(=gx4N;X+g1@DnojxVW zJZvOJjnfuW;tz%YieTEHBfGpp*YW!<v%E%X@IL|LGoRKz8Yv<Z%h2*4?OvKt-qfg5 zAVUf7=J*N3`TW6(r%YuCsVaU>f!!`Qb7+8ITkS#!Xlk!=ZL@*eAm$P9B1LcD3TB^M zl>b4~sB4PDNC@b+vF4ih(t-W_|LUdxPt!{S%YTFaE-1xS(&84{RtQBJbn{gkwESC% zh5h_%`M=AAIk1Aobl8hbB=udFHG**AvH)5`IXS2P;LiS)FTUKz*U|wa(S&vN^K;E! z+O=(!fhr@qhLs%$mWxV}viK%%?cC)=K^d47cQnmR=>WtLYE$z^=}$D_&!Dh~DZqTH zOC_pQ;$-P|ByFafT!2mL+ez<35Bg{-`hLDTNpC>2)Bc$;c~F@I_2pwticwFEE_(xx z6fs#(vZg6RCRry?(3kkGz>rWy=QA<EX$sSSgADH}3*#>aPMac!JSz>ZLWi#>B?XR! z>u6R(c9^V57)&n4sApe+J@Ex0Wu>!R0V3~BeF}?p{d3m4K0MTKf-59%?P|4*ST^lK zLba=0iK#Eh$CKVWFO33?)GJpvBpefKQfqPBfJTF7312o|ap8cxM_(hir?Vr;Iv~(> znoSQWH4pe9l3os?E6Hpk;oQwd+eU1u$7V_Vs<n+o1A??fy>wc@@8l_n2|iv&w8~a} z0q&AM<jiYU_D+7-{nnyfv7my4LZ+b6lCGwxg?xN>5sdDc&7nHuY=#+9>9^^$ar_H% zNSl@L;f3+4%@Md8k_7+2H32385=WGrw;3P58y4Lb2c;fI3y#5lSprEROG*HCCd!wV zpG$YN4xHmQ&2U_~0sJYd0sHIWb*{;|zdz7DywgODE#ftX9w5yvVM|5E!EwJ6W?=`D z#Q&$;5IQ*C@MMER{(k9&52Z--Y86wXQ-CE}ihf31c?*`;L(IcOe<J5RqvkNV>)^oi zp6rtV6>*=;Ip1$_rU0-~(Z<*=c7b|CIyvOf48DKR*HJ&y<e?U6Z#dhmW*fw*5q0o@ z`DXvgV!t`X{gcI>*YXQLQk->K3;W3YB<k&JNqS-kGw_H^|MG~z&Cnobyl`L+q%q@Y zi_bV{rr7i=E<%Cr_W{oblCgXQk9b0=N$D}%{6st&lp~Q&=r0SL?ki^^a^98xrWuN1 zih_pmH4FTQh(OFRVm8PNl<>;|lFalVvOaKZj)9XNt$75aS4;f6qSW^vB6d5VC(vIa z_OWqU<VeFUFEb02B*A%xcM!~b9wV_hzu=5*0KyS4c|#ruGD&o`|Luj;K+T0>H%-ie z=0c=V9EVFDppDa+z9|?GlXNubyhZ_H9X+X56iO~IdPK>|zf?oNK6L&G#Tuqw#1}Wi zI%i^0#8LYFQIgPMI@lf5xBcd}VM)>X7hEqD-&fE@l(cRO0lVR*7yG*iMB(k9p*#BO z%y->mi|Rel`NFIr!weJ<j_Z*|pil4WdJ}@O-IOHs1z^LlF@M+T(Fgn`OlN{k=6}NZ zPU#8<ya6f6o4$z#4bVr8L6mvmqeYw@kXZC||Nh}k4{a5+M%U`8A69I<&&$h))ki`w zYSzOAqJCe<-FTEgAL{sA9yVJbXi^^#pt7xUw|nd?y}nEiy>eI_)cwLn+wKkBil=MK z+(8TTg-Cmfl7ouuAZ8TV@}dCUyRAi29Eg7Cf`swFm?$qj`wk+24MYBi_u#&VsJRoK z2hT^lG;Y7g2vb@@{PF?o(Uc>0$Gnl~#f*$heqhk&y^KvX>d-~@pW5yx#`s;?8xMf6 zOw~mG7;~%s<mUCs(*P(kK+-a6P$Q05ys;@Cc7E4p&qHyzKDqX4em!Adqh%X-y_8^V z^T<hYZ$523^m@6(;r$eEttT=1vOxEwcb`?QQvOQxy9*G<;%ki6W&?_En3s!5`L*IX z1t$9_qaC)R?a1)Ijdn7=<TtPrR0T=UJJ#ijfFe9_qy%q<;s4_c4nWk9KQmFm2DR%n zuH0nadP{l3-F6`=JA}Cx>SVkJoK2%Wp*@a_^LO9?4sZrw^}x^0Wr97^Pb5|94XIOE zSAJiF00YV&RIt!Pt?zBB&%gI_GFC~@w!h%d_NYhwo=?VjsiSF(pP&D6c6%yO%`0(Q zD)Vx8oe+=LEaom4)>Z#B<x|eBKyX<0*m1z;)65fZn>K5q*kM2)GCZ-$!w)!+^tb&P zg@D8Y4kWfay}p)R0QYs%jsCl4`H!9%f9uWArrA2r1q2oPfEHuzi(JT7W(Y=fj<1Vr zZr95s$Cm=NP1WmM5i6Su4%8o=txaGb`llf~@gvzw6L~yt1h~%dJ8ywP7q1YzK)kFq zfTFENTT`)$=AN%oilp=8e_pTVlg#21veQN$Rn|@2W{0w)3n2etie2y|TJr}!mxHa? zof`<RhkMvH$9p9SQ7pU-U}rL&!;0<lkig-MY@%Ulk^8%%{5HPCaf6fvnX;~`o_%=5 zIV7OxDWKRnqd^Q$Md2Z#;4?M#xSg9$DJE$hIM{E?timpJ)VjC4^>=##)@=USMLF?w z!z*b*);29(ofkbm3${hIr=e44J_B&&*`0QpfR?_!wL{GMfR>~7BB5NBy83C##^(~> zM$KNJe2om%^iN}PhYkd-3w31N8NlLF2=Mqp3dLEjtKdQ8e={YLAZl86{Z~-VJfF3@ zMzW4o5>7+S2&c{d0_R5)P{TW%#2+`_@RvrTK`m0RV$0FQ8IUYa?S^wmtNPBSBo5my ztlp7gE>`YrmMuh(6-~?nknY&-@AkY0HCycXd*eR0*v$bRla&f}K;Iv<dZU#Pti~p~ zA*+)YIB|ve1RsNXQyEjtq9t98(4d)Ux_kRp>=|>l(+Lub+dh@sBBkeAj9a?-J10!a z+u3(ivtQvtaHuBoZ<@5N`C;ucR4sX2Fpfs0LROn090sbuz+Cgjr}ZZN2{%Yav0q;T zhkD+Z(B=O-w!ZhBla_)~_oB*+TgT~kbo{BtKoE_N+eSir7sOR*@z)H?%d5yi(Kf6$ zP}_~Qdc@zt9)b#gtUP@`G(Q8T%mid6^9bFhWdPY^1^mDUWnAcAqpUtotNZt#v4w(; zeN;bZw?&3a!}sG|LP1Has!cO6${y{**~+JZi>eMb2yzxxKcCNBpQiaV*L%hnp0#72 zAq4&y;Bl3$tf$}aJ<ea?VnJ#Xp_#P_PqK@c01G1X@nq1T-io=Rs>=>swAC!6iFQ}y z;V$N^!XQ3g+bG_UomJ1f3r&e$B%_wheBH4J6zmJg?g|AQ2*;?n^>g|s8{I`gNc)gr z=|oMF6}{am?G>&&XvVUvJ1x|f)a-_ji{&r(0k7AaKHk5h^1HC<ta#?W)?s{Urgq)Y z(Z1Wm@P6(MI&H>yd9>tvDe!E<F2i@d&0Pp$ba!Ht2L>ycHpRGi0EbIpA27?=6-1u7 z?|G%=DWJ^?0DpisE0B}(CzBH>7zZ}1u_;LRFDE;Hu<>cA(akMKEfd~GG0)ulMQ+t< z|62Sw?v6Ue=y?}&ncX@76g0-Q5N%!YHqS^Xmo7CqywYOB#uI?_iHJ3B&yhfD0<(Av zW=aXEV(gIB@b`Azd1^Lo#igYIJ&&-hQ1-|0&s%diT*Ypem<Z|l4rj<LmUM5qD!SZ( z2^_?Q@ah;8DhRBJOhPJHq>`!5(-(1Ts-98bNsK40b!24vZORG@XbU?TGBH}d3-ZBY z7b`^DpsTQ8#7$4v1p~`r;CIxK*A1%I0s(swLftFNcFSt?%v*pn4RUMFc|EHcatcol zGl6JOqJ}kh94=TQeBR$7wYo)sobNuV&o{zhuGbe&&f_d_u>_GiN11t?I};md-r7g% zO$qKEd{#G3`Xd!qj->sxi+&%RN7HeBks=Uo{QHl!cXzgs5vt*JN3&8#;5^Pm8{n-> zl`)I#@t@Db>E>ET8X2PnNX{t2UQ96|xx^s>w)#X|V2aSDg--dyO4zHO7yW6zAzsz= zak9Fc3#-ejL^<{Iv84PHVm@!x-AINdopNwf%Ax*=&#R1&!`q0K=-($87TP{Z7mh%S zn|mK`gUVY)d2##LrNP&S+Xe4kpcK(=dyod?GNH{PX9R&Up?z%#0a*ipw~z<6f(6~8 ze{2Qs0m&Ay6$l4U@>SQ^-aQPS)WNjk!jqhf%z0HLOOUtcOlt!!3X{R!t&r*g?qBr# z@8CQpGVBP2lHI;9Q~>%`N8b-%SYMDi;Eb*u#ON_3h%;%f*IKgN_CksuYF(%RD0s#f zjLWyT=I;gM>)*A8p%1pJ)d!b=mZUv%DY$(GY&$s9vfNe|c?CY&Q{(+UjR@_8A`jP9 z2|k3<c>R!;JF4b(+cL*N3xNgJRW+-E*{WWu<^;8`VDMmLkXmM{^Y&JA*mV0B#dCZz z)}NPb(st;zeReQFjgv6gq4QPb4s3FvI8~oc=K$FL>#Ft+vQyCD7vdJ%@f*7K*XuC4 z=RUwa`m*-Zm|_|W<N=a6q&6xGf#NaKlgc20gnjNQI>;G{SQ`X%lz#*f|L~OocUck* zR0mT91T8MRxEH8)pPgoPsFkhwkmN*3@{3zfp!7ifXiUbme}TaFtoJmx{9Tc4$3mt= zU89fkHGATOZ(sy;^q(PW)M=>T*319_K45TZ%($33HX00jQi>#V<zvUylV{-JX$csM zBDzi<{>wR+qOTT08ph*&v*-@S?ae=&KBX2_tcFvnSv^ghD8sHL@$_{%&xMq*66XFE z(E&MSW8N4u1V2Rl*Y`gw)d~;%K5u%qg%z*`daU^7A@ITEyvGB@p(O6%|44a_mV#V) z;y#-l&|Es!Gy?y3v&ZcM$`lv^hroZj!Z6qEAHK7cJxLrcJ<)mp;~xfScU}K)mm}b; z1qS%eg<T-u*_e!f%{=;#JBG~z4!f>!6~(m&$ehpBi~biMH}xUUHR~RmmbC*pLvh0s z7Zs5ptIzP?rBI>-MEijg44-mAXS1{*K#m3eKd*{^dyJ1jK`ZDwcmsij-}(SemjOTN zA2k<4e%_#~$W2awYUQe5^e_Q&y{r=X!}(G+{Ta2PX;zV`p}fBC?th$LpM9ahbbA^8 zF5B;ku_y=znE3whj1UsWx&L@Is*SY{IFhW&R6sqZhi~)+P`!f-`d#3+({S#@OEc9V zgOKs><20kOt1KFGO;9+BtJF&YK&VT#5xY>WEVg6Rb|Aj3E+YohY^B%cX~ey!{<4>x zS~h!RWaK%_^=?k=2mb~uefmU+L@oq>3@4Ok$Alht_63?ML-hrZ)Zr!>%bc@fBr<o- z`#rNmz7qz$f9x2b?lz#Y0l>tlgE(yrJs|7g0yOS`yG#$lz=PJ-wJ{1sXX9Uy1R$lp z_fuW4xFKA$D_y>fpiHB>+Yg8udCGOp6Ijjzqa2wpr+*{cAIla)`!iTuqzYme;=k=( z!vHGd=iNoe^ZD@2qQ}hhGdHCh-DOj^N2~a>3uFgEpRE>rWR5<{7cW79BPjDOtUMfj z`o2~l73Prs?mriiGtCu&jM4e+E<tMH-P>r;Kj?C5{6;3`Mel`}Cz6cG!Q-w9qgYuS z3%5B|fr@^XPbH{`Lqp%@j!>hTiy7^yp!-2;RBZeXq(%Yymo`ebdxkm(LmF+olt@XM z(#^dYIfZNl5;y8w$&X(!SJziawX&1|O|`|`$E+950sv-5Lsk~I%suj-kLsLtEhu7T zDI!{?nf25DjWOImsoiU)WjT_Q?Upmd1m6|>6b2er$9zPIa{58+=CyCv=%+r_G2Zq! z2t7PN19HDOC<}J1-O)srFaFqAL)&4}s&Ka=*L-x!)y>yrty7ZzAju~=&y<3+3Dw*7 z#o#ckEId5O&2Jh@7tBZ{jfKpLmu)Ub*2c0-L@k7krgd`Fd5;FJ$*VFcc}qo6t?^#; zVaeL;?C~AKE+V(QXhg=Q&w5-PvHkQ&fqbxLt9=BkZ1LOwh;W|PAc>Q-fBF*WBq<W9 zt~EB~<zO?2&3R)YeM%A4lunm8R7ySjZbBy`iEXUCsgqxpA$IKP2YM_4eD3xOThx6) zosGTx>iMWs+qAs&3`EX<Q53CXRS>wucP;JxnrQoT<3qII%fVg4hiT~O(Lncl1t4uq zVPijuez$ftbVQ0^rO1SG^#GICw7n(wqorp3nTUlPUr`KWEH=PIy(SlFW;Q*Tcx6`j zhHWhru44=?tqaH0hRZdOZ!U~&Ddsla=2S$5OI=vS#*-5}SwZ�$3K;8~>$V07StD zwv=j4JGwX`dV_|`-+xp@nKus$jXa&|qD$H3sU^Cg$4!%`Yx~B>3pXq}Y-(JMOqiPL zh;;%6*jCneHSZa_67oo$pr&fg@nJ1fiX(Ne-jcxJYpsc6QdynAnhL0HAxjQ#VG0fP z22hlmj=(!FbypbD>NVmuYUWPN1|CMYFIqR7SMG3fNxP=!0nyIyvTw(HwsFKXRRWo| zkucSpAB5w~;&@6lx!j4(Pj^ojiD^A@t*<`L@Jo6A!iz_9sGLEU9<Qxbsm*#r<F;l$ z|Hao{Byya4&_gHTF~Ec;_|2^J@;(Y>he25JJCm`ru}^=Aox3U*i4ZDEC$tfkxCw1| z{YJ$Wl<H@znMZsBJ?M1kZ$;gV6Hr#7?n4^S9#!Tj#%aTkcpN+jD;nsET8s{8gS$CN z(LKXaGnKkp_eKp>g+d3?;NSI`3!T-IO`f25^!F-=W3jpI;Fx7HpI7dWh%C%Btt8Zw zHs$Vr1+YPmA>H5vm<|vWXc{wA_SX_xxC{P1xXORfgv50|%;xbFMR!}~*Ys*FPbx~) zU&TT=4sxGK?zKeip3+fi?~iEM>VE-xiMRA{FFEc?-eAlS1<mSc7$2lJ#@tra$^6-2 zd2eJXHH7l~CqzxL$}WH+lBQ6)G6heLhlJMph_HNtB5wq+<sS37NS$wxiv1AP`F8S9 za`^~Cw?*;tW7U&3nFF}W`Lw=yBu*V)ufDr`kate-M`|5k-DkB+uCLu%n4pt_XY9V! zXeA4ITJ9_P^_>5_`-9QkiJDSP%3RTT$UiUp7r8$}8MQQAo7c~J6xx;4U+tv1%wG6i zWG2HnXfmS}zan=~)}%{$Y$Vk=os`D-0UXvG?a!CX=(<TFuVg6af7LusDtw`!;5e#; z9~=4xV+OBKPw`J7MRJFJv_YU>6R<06#jWIk7B7q<P&t<!sY-I#<I`WA9b_o@CGI-# zt(?P(*)lmkF4nUSx&eA2b0`D)3_wK(M#k07tkr6|?=8me&MEcto8|sOJrI$_8g=wg z9}j!yv1=zgV9}X6>w~VeLZNcYOzgI_1O@xMTFOW4cMx?(aV;ceBaj5cnBu4tdT|U9 z>pp+1%P0|sg`Q*%;8;3FX!kP+uHUuCoio=;-+|YY<}w(7ts-^|D!tUp;5l-71E`*U z2g37)T+7#1HFHmPIeWaN6@qzwCtGJ}G1}PqxX-;z2dBrxQ;WqG@;~vU@6bIg?aX-- zIA#;tSr?s19`Bs=+Rgc6f&H3>Gy5GCFLMn<jd|uX7+Y60oQrSK>g-7yhlsfnfQA~7 zntsIn{~IPf?HJc}zUR6`2)h^Q%AZ4{mYypvX7^R;=PY<5&}h8SVvAxY?PqZzN0Ytm z4S?xbcl18cZ(m{yt^qn4%klR4-F+T$6=PX$8sQ-;2$`3ppP%vZ;a}0wZuwQ-@`Wfr zL32bCec{|x70Y4cL10bNF#dDB!J;CGnVZ9MMRg<U>1xm)^q{1$C;Rwx^4H3h^+!kt zA*va8mTjYWlzgk;v-&}bqI7m1aWOZ9_Lq0BBvM-7>I-T6gX_C84P5dAUjkYL7_6H@ z%rign%lIY%l+|rPMocrn1!D2YSu8l2f&#k93R!$-I&wvRMn~_lo%Q@c&0oLmOFytQ zJBmMs&r9-8wEfayyZs;(kZ+bISqNl}jN#JV_<*WPB1@MW5=t(nU#SANqEH0*RvfXk ztWP!fh7im=_;H$!OdK*)<T>3(k?1)vcaE!CBaki35!K6N@ZrAKb6=4%WB7f&Q9RHM zi~(hr<}b9;W*xB#KZ!T)uGr$YYOQR^?(1>`L(eA*dPXt{iTugiJ0DZ?>0^aShV1G$ zO-a_HV-VE!R9qJVNH4N=oZgs<3EvGUhMTf?-wLWsm|}|hKnPiUu3G1<jdL~S(+)=e zc@Q+afWl-oSiA|D6UvN1EMAf1S~QoD(!%<E#4)@Bd6Hzmkj}T9$W!0;1^n(e!?c{r z4~v+<G8jjnYZ_?acrBTZnP%jWtDthKIPjCNcbl3=f`h^W6NHx1N*2edK!&K%Qa2Ho z9F6{w*Myw0eh%=Dpv6(D`PgS1i%Y%~=VA%y@V36Z6gDCB6W^;$^09}dH&l)`bD&a9 zxE}zEs$CttQGqFCHRmk9vkQ<4@`?6&RcOc7^B?_rh+6e5PKSXG#2BY}El*FL^r(kV zHqoEIc#h;^Az9mx5!9|Ta0h_lQbhG+Y?X%hncTbs1YmtO*>!xOk=w09G!I;_-g1wk z7~i*o?u;LzApwlxzk;aGU#oA)bmda-SMPXUZ&R6=$e8vx9Cn9O-O?ANjjK%c345Vb zWIqUfhWjQ#j6#6b6CeuaC#pF82@4J5Lqf1F62>RW;$trIs$gG;Z@!<%2isV$Iz8ql zrQ}-Q!i)N?ja@glG!+*Y7ZrzHoZ75;sfaDMhHn@;(TE@fFz{qrI85eqngqzdo&{eR zDCFaLJ}uH>OTon6^m(>1u`#8Xv}^?R2Swp&44edzvi9lE^O?&zwFI4DK)XX52J*C3 z=_oSi-?LBgeYx{IT1W|+FDN_ge8>$_Xf75^{eBIfH>}z)K7aUylTFxi6mi+WOrKlA zXMu7l9p9=ZPUvK<MqFm>*SP+xla5I^VrF5&!KTx|s(swoua!xqI?jUi1*H9>uIIyr zuzOg^9NIaPoFSRi+ZCmiUpq7fPwBXmt37icM=p0mYNrQ&cww}(X&IZg?5cvtVD+(n zNYiR}+=XjZgC0^o{%(^uM>`}KfG9)F#%rJ8!Ap2%ER`Jei~;>H%~gBLcx`}XJYWKW zn2b%Am|Ji8{NkEdn`yYzbj0L!Oe0yy*13F;?|7I`xqKT@!!~RQ83tuo*~rT^8#Tu} z=i3t6yghQ5=I8=_Htx_QmP|e!VbAHY*t@*^OLsvzGWPg4<ShCwEc}AWIl1QJX01s% z&XOns87^dT>m<H^tqL(tmd=5uAQg<W-22rJ$=`6L$vs>?;Lh08%(kFXN#5imu5-bK z|EzGDSc!9+pN@Wx))rQ^-*wn}KA3!kFS4;#GsMpNN><P&n=|qq3=2%x70zaO1NSyK zfwWl8LWcBx)7|S_C;9C1LPuEi3|0Mia_0~%U2-}BQ8y#7743NR=m&q#H$^flHcdB- zQ>_xhH{^n9U?aHS1SjkZ@Jdoz-tWsqHj>R^S>2Qp;9J-4Yv9V{^2*i5-B~fv^IgiG zvm$-`mUG53ZDkyi`P;u|va66Vq~f<fNBu1h8vYW@jX1<3Ba>Iuz{<8m+l8AsB(93} zRf`VQr;JG=tznfkQIpZ95%3$Tq|+J3R!Gyb-Mi<ZIPPv)X1|I?_TCeR>gRzgvR9D} zvax{6@<58B-%GkJMDgL!hVJpC*8NWdU3|@7BRrI`N}ZOi8z+~$I}c1np}a&B(g~V_ zvH4SmO#lUWusdSM1ezC%^-Cd_gI1#;O&G+O!WM*#<4~?c7oOD^o<>tQhp32?>_`4` zm$%Qq2<*S*lyve{91QUAWdTU_X0$7y!xkPuZj+LcvP*EnJdJmpIbFuSiPB=8QqzFC z2TMLIZw-?YXbkN_CB^rVCo*?j!LIxIApAKiLc*k=ZQ$wzTBZJO%aO0~1$vwnbMwRN z$E{D4O8_B&^Hm={Za^Gze{-Lh$Y=Mmjr_rUI9Zhvp6o0t%BsrcER9Ym9`ordt_z}w zw<&57z3)vSS(tmvAVrd;+Zh<=9na`0o&l=0r{AJg_BTr&H5)rQ5oh%dHaxKy9DzO# zQdKBr^a%nXab0G&sexuP%8{w1xRl8m8p?sBjl?Ft*18t5sd%XIY@{;{*xY=GkWBq> zzp6QwyMkrex%pFjlFN+b^Y2E@0tFZ%);{K*+j!5Y@b>Z#floff$JXU}-ci-hq%49? z!D(6dQ9lo9FH|yiu4~gz8Lh#j9D8jNcKWN3syQb^xT*5KdB=~<O^;y|BuKtYO0{2m ziI#p#tx_JHn|lItFMY|6=#Y71xK&KML>Haj+`yB`rGQw~{`*HwT$IBFRwP7KOmWu< zoC=;;$$qwXo&9X`g3VR4F8_()LsBYzy$ac~CgSB7LbZ<8o?Ehy^Kaf;AGez{0I?+K zOmqpF;TAwTH0Bkd2-P`-fiGCD#VY&#O+MxJO|>HMXV^o9#&Y1QhIMJX&yz)Zwyl#S zy(E}fZj{5iU9EG)_!}yJ3~sI8LxiR#s+kGB$<7Ph#i)FJ_`(tW5V=Nkdf5!(#`HkO zgB$$gin+s6NBB~UeF3~_zoV9!yZ-~9^m$6QZ4i~0%Ln`!o2o@HjhG1+DP)q;u+t_a zE-fYxm=qQX$&$9kg9NFJpp>qYhIWdd7pAC)_SQHd)22*BKosXAUKh3lF|TW{M9fU_ zaCE{k$_D%o&Fe%Al42D$@diI3I6kzrOqj`MHBIaaA)C<;?(<w1XNo*9uPGi^j)hCZ zA9!FhPzYEazVfcfFs=6ZO2*D;Hcj~-PmH==6o$NNVx9`UMWf9xC7w6pnZcFpTfe-1 zTxjFiIVZDHrcD=e5j23#Qn=GJ-)VTOmthgSG+c}Ps9ZjuB#!VdguZL0dol%epy!ND zd+o@i)GJvn(ZLnX>w95herdl=7=aVLOj46=Kv>C;MrzD}wf3#55xApUaBE1TNZp0B z%mveQz|u%+R(|Q@J@0`Dhuh}vuwZ;~9n1xyG)Zx>_!W;2$Hiwgr)$4Ee729tjlHt% z9yAw~nz{#e&DmjC-?rw3d9)xo4?aq^Lhf6iSHoOd3XBkXoN-l2G;_)u*vJ4jPbd|7 z^#0<z3(48Q$pZL$$bI;T2`>A^3^phF>yo1JU?49M;o>Bva&)gyZ=7+2X`&`Kwxg|0 znkzl8A$nV>Bm^vos~z#@q3-+{CLa7l?aqM>H&Izu`|QoOdAv36STbWaUtq>pvIkol zY40($O379VzorLNfvHcgH?eI)E>Kzu^4WP@1#_ks;*E<df2MIb59eIK^VWXV&oAD@ z8|S^AMh(r0$!koAbFqUo?E%WE4Dt4wmnBFizs&4RkI!?i&KRym&oM52O(r9vPhKD} zT-;hF)QG$jbB=O|*n80=*?2RqIRBgl$%4hAfQp0CK;!{O!O2$BF62W%Y>jZ(xPq>i zUr&^sbckYFRkfE~Ps6YTKMSqwGWTG`xGeSjK8O(c=P4_zml2p$I+|{^C?G=jg4V#7 z{j9uUZn%1>W4KUzO+?_!8tar%(%NYBmFPO0k<AyqzK09*>Ip?i)zO%x7W?SQ5}hN! z_9i6@6PsV+mOG2umlRg*GntO!Y)kax2~og9=TghwW-soDQ|-ucw9#6j37+=N%tD`d zU1@Xw(4?bJ;PLj1qQ)Gd;ln47Aa}Ktvi2wb2bicmGi%GO1f_|=)w9}>H*CJ7T%k$x zM|ErNUC&~}R`FG}*lP6f*x7TdpNPN3V;HHC%^ppywNSb$ym1s!f#T9!XwJDS@OG$; z=JR2S^az!CB}(3$73%8x0n5uag~wY+Jp{XKqNnSYWOhThu30Oj=ru5@`}#{{5fk0K zCJ7k`L&8OFgma|N)?Q>AULR^Y5;T8tg-jh5+ZGx)R8pHdIt-g(OFIz=z+_Gm#nU&L zv~=8q^}fyM5V-wOOOSE6_NL3j*gI}_+CV3HL6%RIRMF8q@7ig%vszhlu*4G2j%KFu zIH>>q#etS)ipPT8PO^dGT3Oj}*O29XWke^~gWB*ZnlY`tSAs`DVpbOUyjkwSY5(dw zGO5XU59gQx&auaq<I}ie14S*3F-;gT!V;<9%XPy)9^897m`0aYzpxVa=^o(n*K2a3 zRv#aFZE3lbE9m9!+j*Obk~9mmn&obFHhoKlqIIE-y~X;aA_i7!{uY7s_GeJWX=BrR zG30)1teCY>IZo5(4SRWr95Q@&YAHAJ>5t>IdPfQcT0by8aCOABej*T@RCF3IXI2?c z!-&?kt=&sRnM23oS_?xfK;?H)=%VU76{pbNjLx7q1a_WMp@pX9Ig+B4Mz-`(6AkI6 zAe!GtEbC4dauf9%PT}u24RQr->A`D$7I)0h=xcf20~wV!<I{pBgt%4xpXD)Pr-oNz zXfThX2M!UMg*@Y~o-`8PtbQVhE{jOS4!K&jj=T0nXmt%ccz%Aca}Xpg`JQP5KsueT zPU1zI@9R=meS}Pu6Wz95K19VtI?8?$h=(hJ%PdrgKc#*qaVX5f-k+@JjOo+q5&a0+ zv}c6kMBsb@UQ@6;iNOTtu3h-GvTP&D(YXB@4zv5Te)WLnmX^+*hR5J@_@@rfppoVR zA{#bFR|#Y3yZ7}cWffdb?Uhfm?TYx@n6U70-}!3y;6BVMqaU89<t}WxkzzZiN@a1f z->S=%6R`i}3SEv57_9|vS5+$k14bZDS7Oxba!4KFSYJv1>bMhFd(+HTqN(c>{zL#W z^Q+hB3ogx>=A88cZv&-!E5_{_d0`s0qi+&~{qt6m&$F_h5ltWF2#BJvtc>4`gMoD! z%o{|MbH})vRbc^9+oCUwo3z%Dhe#5{%29mh*@i5k)c#u8fo_S~*VTP1Q%eS-QP_|; zZd=z>&5cI2uq@oCnN+YiMI;uPdVEY&bEyYrJtvI9d2fc`7pTJ{A9)?(Zu97V;^(7{ zbveq#dh6#m^&};xd3^f$P%>Z~ABC8iL2{yFUA1MLG%2HU!NeSIv=evj7(jKAElfkk z7E0v9jWMu0i1InBe#FEA(d2i8<bAH3psnkqpzYhi$u0bT`+~(3jYQYnJo0kQMWr8} zUzXb^wP&NODr_P5^Udl_@F?AgQFa1|hj|5^#70|?c@A_xL$jb%77#z5!oip;>WfMA zK=1kc<vzXY%^+mHvs22760DJeX4&li0X=?ojn;*(<!N6ZA8LA(!NX@yX?~bRNa`40 zqv+L2WqFzu{nMn=I;x<l=1HUet=HKhmb>p(E7Wqrvi*guzf_=VDOgi8JF8i!12fZP z0JAT8fU8Jy;1#EN|4W9%-l!FcTa$BCv7dQpayQE7KED!AqQ3s}=>F3~7ci+B^qpbn z?<wa+c;nquchmQ6bcHT`wAK#t21L(BUp$_}i13izF%~R{fem4m!@bD@e2T1<{pIGO zm}S@qdjOdw4k1$pPd{k2Y(y+$H#$W!(n?E3-PGay-DSzc<e)D^9iOq6n-M?nt@D`9 zGQX{Z-~%h4udzG-(8Ek!_n9{J3AmI_kT2np>L5Hg+Kul`HG>@UX;JaUpaTz2CS9aI z-gx=@%pR+AXK0?Z<lKVwS`~cTrbrq((%t19lgw!-VuZD^E9QjP%^$bYcJBO_kftO{ zz3EM8SD#t=9t#N0{BtK%ObX8!8yAbGG--CZmMwoa4-dsJ;OKJIw}UKx2xi=n&t_uH zwlLVO@QST`5W>{xuSHyNhU_HY`}x;%s7;ORVv}oPqFS%_+~8Ad%x-*zs5JlvS26up zD<|r3YhhMvrF7+dXdi?otj_xe@|;c=?2b|NWX1fI5>DoDrS<s^;n3xre)t7gL>phL z;R|$m8!KlsjhXmG?JOKG((ZCztXz@ppqs1C;LhRIiM^>cOP__MPSQ!cH88&DSnB<` z*QxM+4>{g71>2H6?kW}GODKd(8MFO&n}f0o?e^VH!J81q7O*DX=ZeP4=iC<7(|B8o z)GW?!=`~9qRXH9ve5}xH&EfD=>*!b0WIq>A^QxGV<cA8E1@E0hufmzD>Nk@oDh<pV zf;WWzSABd6eM5*x{wv`ma<o)Z2R+Uv?w+7MQb*#tGmN{zy$8v2g%CEg=h~<^H70T@ zExFJxUMpud-%0chyms5b6mB6h0qIHWR=mVBTK&)bYrGte)B$uD6n<1CwSySk0{Blu z1VcXcDwV~$S3JJEw0mC8#|PGSR&gmV@7?4KxB9rqRPU?1Fqyiq$0D>1@!&JIqs_7J ztAwXlk~0`)I3iDpS8~`n^@{ppu-sEE7Su%#G!<`XKFfg7l=-ww({8Xo8xf-N^Vv{5 z4ikLER8bdJOlRw3pbA`VX`0ku+B)B|66{+PKExZeA2ZKIq;-NYy(k$Ei<-n0h({V; z@s>YZs#qfRmTTxH$)L_^$~4`|Ql)`86ut9bHH-Y&$LdByTF0~W$*j<h3ze@Y1WQlw zRySUS0<CjbgiNX`Ow%^*IH_#9|NDJ&fk?ib$cL0pqc_4+b6<aML-j~=DNr=?Tz_!d z==<tn1V#g(!-eKov}0y1;7r|3wTx>tAzRzR!EsM(rYE<gSvX8NGf3Pl)z6-Nu__a= zAd|IvL@^=N^}s+rSNle17c00EWlnFcPAgqo>amPDhE;W5yEhf=vcBPlUC^<B`v*(N zg^%N*kfxM;wav;4ggj+m&@iZ_SXGlL?x2*)T|zM-IYaOu)oFc|>#U~1p0CJ<4W*4g z>}pIhWDa<c6}_F;H}xh@Pd!a0x+&EaE<tD8POtbWeP?*mr-HSh<-NSHa!-7)P03?y z_|yB%c+s464D|R2vNJR`?Y5kc<`~mh*#w&`I)4nC&<27^9ZW3FOlY|YeUq4fuE`v& z(|gRc49vJ%+9I39n4ADChSZwEmNr&Rw}{cT;ubbcZ?{Bd-TvVYXVPO=+<%>U4K0+~ zu_fB~BxuzV{&FX_%G7k?%H{f0LB^?XeLLunO^gHjVGii_H`<LzaQQ2kUh{^qwxARo z6A~9)i6^Lz=Z0y{n-k753AHGoE|JEfM;9~_4-XDUCSN_D+k9b0rjhn^QYh{w$k}}J z_@YO4tp}I|T}~BZ$Hr5g+#+*B&WAB!_mbw1Tc3VZ2gEiZH7BK-L{+uV-VM$mTak1; zvXxFwv5YgUR`exkm3)!Cx$Fq<eSltcADTUb)u#<*X>c{*NA&jugSP=r$L4+xMQpgI z7L3;XVr#oF<NgJlt6ev+JP#L;qir0M?WeoU<yJ#*aQ$Mv8^@)nM|?*TfG-_~FPcVW zd_cocV|!1LcH+0FjNX#bcKkJ~=LLK21}kzHEV*T$S_j77T>VTaGMbnimcvrwEZeBq za*VVJ+?b|z4X*IHZsu|~O9c+Vgk>C7b{K*?SiQqis~s&#*NgHXIGfg6Vu5~u<_uIG zy?Pf@Z@BPIXQ0lBZ|o}T<!qd3T*u6ZH^kkg0om4T@`WT2;WOGwc9rPv^CSZhykh{| zyu*p%>S-g&tAx@upQDoJ79ddJe^@&NVVcdo9RAcN5<%!7!iVS~hz4HM2?VJYAm2wG z?yjdDCluO5G$pPqM=glp2b97-nA)+jF@Lfu8nfgs7|NaOW--IN(fPp|7qJ0-k3RY~ zTQ~(1oEhxb?~gZ$Ic-9}fS^=y`B;8Pw<6i6k3EY4TE(3$u5wlGGvpr&*DkwKbmzy0 z%he7$b|m@IUd%%l+Vo8*pUrTz;lY{VBvl5(yBGEfU{N5*1#i`MBevEs(#~lmSRXI? z!~@%8ErKAw<Nf8iBT5BxIO~jT<_;Xrq={gyk-<Qd=4A&b5EZLvphC=(K$sFsu`_k; z6P5WGnxgW~kaX~Rff3Xc-1MK{9Ykz&A@hd`l=mK-?9An>EFg#K@Y&4}IW(S3EP32^ zzQt~RogtPovPdN~!5fS`z+8s(Rm!OG3HRdXooTMG=I*s>?1)gU!?-IJdy@t)l)(0J z5oUw0m>0Vr0p_F8-%t3-NDj-QbbG*QBF<u+$2o*v{G<tY5Hv)W_h!c^;LOpasURBM z!#Aw^jdfuINJw)el%bWjr00(U<YQBw@oh70DQI^z5^L~=z`Ha4^Sh_FnC=J-n5!6_ ztZVY&GRx*<dNpbS)6!^~9LE@mU%|99=?{_?e@x+ou`nQQqUUBN-km>;>jlLP&l*&; zDe;AiBmyv7aMW5E5!*j?+iqkoFmL~@<924{-(La?#X<8s-7fGFGwx$Q>qaL$thU$^ z`H7zhHwi%#UwW6g3KnWy5zF5A4{+tU>fav|K7`{Uefl!nu$Jlqk2ag<{fcYq&`{J& z*^@dm0soX)|DTU&@XwD3II9|?xu6r7d9g~etUoxqFj|yq3+)36orrRIzx{jwgeg5d z$KQ1KrqnEp!*2oBr>fOS+L$)0iXvOA3cNs+kki|yc_FH74u&9~Ljz#Bau&)&z1m07 zlIsWEA9(F!0dVHT-9LsO42(JEPX{`-?nYYs8#j3_q2Fw3;+ByD3~<xtx>Sji525or zoo~saL|OZ^ws!i>=c?;Sog2ZhBppWo)=52CES!}I0?;oFRws<K9J|VfQpan3#R(7P zK+tR-0|vdUqU61E7JcW6w{kBm@DMgJHlTsC#B%y`;Q0R>I9b<@b6q7NC&*?94lFc> zW!j<*&<*1q3USi}oEz!&Lx}ZLTz#}7N}L1YcB+T|vuk`p|Jw!L3T=j)$OE%*5zVna zRg3n!YDcbH3zFK2I;MMDt;8={4E}1f=|8^<$Unae{*ByTvo;Nk?~hyM-j^A#<Ju0N zp58H49^2LR3Aehu?5VuIJXX5vq(}NlN_nQbe7T)#|8b9h@@&?M(No4Bk=KLcE!G1V z*U6$ID^yyhNR9C4<{lb+&{N}%h8iWnW}XM$4ojwg6UdS!|L{IQpmkRtJI6~neR7TU zQWtt0WkU<2wf)KOW~@@(eaMP#)k+V4tKj`&LH#(5iWu`}bbH+aYOYsKJBwc`F|0y0 zRZ}ow72Q*Y$4kS)lfBlzOAYW9=BU_eITs%Hn4MJ$yzjy2H5;Pr#6pj1UY~TjPQbG^ z*xFz9kMUpXc-N7Ww7s0RJYJtqL$&t&Xw#gMU+aC+EKvnVaGFoL)`p$iqCTR>@l%rK zrZ$tyAHUx&up8W+;nzoS8#`Qb<+$KKl-Fly*8KRD+aAS@0QgtTfu~s$EIi-L#2C<S zfPqQn2&cIHfZXSKcn~?8tcMEccuCnEyCYSmT|sN_4-Td{FgzPT{m>JXuhPyrqZA3e zisK~PxnJ^Yo4ta~+0^~<7Yc_*B|3btKo~a0(OVa0bvMSBcTOpX>rdFFCvsA`kuB)2 zV{2?$g?hV<7)~==;_h-vT{%?fueXsFv{Ot})Xq@$%=Ei=yz6*$B6y;4v*dROD9k!) z3N&<|@kCD=QAcSpbcx4*G5sM$^c9xN8R1q0XD(fKxbNm+kmPN4Zr@W8=2TE_*l{GW znPF&`ouLVy&!Y)5BKbz{k=B!rxNnl=Q^nS(Qbbi0!E4$o>JdH5{5-?uh0m%m)kIR- z=q;UbC^`A?#blx52v0}Mz$k!eh#WR^>HTx+$ex*nh0K)33D!+)61m0)wVrz3+e9HH zGW=?WG$B%HorW7E9x@k&9sJfyzn2fC^?c)#>DA*Tg7Y+9_x_*uLZ2yHBWr7FFw`1* zVx6MIhO?#b*%Pr3OfBf{CTS|;lTpC}ap+j$mEy-j+h4~*s}Zrox~x{cj;79}y_h_f z@n0`}+ymG9xp`kpUnkOY9#`>wUh@$ht+-J$95SRsftd{dokJcr<5FPzYizX+H1J)^ z)sd{N2lPW0KB*hp(np)F|2c(n3MP!brNFP6B74g(hxH&d)gYFlTf9!Ar*a+9mr-oE z=B^xpu5tMM68?=MQYO}%DO1!y>>bWYwV;};t+rVM={4>2Tj|%_(v{7;W!RK_pF3nU zDypQ$plGnB<)O_O8_#0SLgI(cS*P?yh=c5Y<EgN!IkGSm@i#7!YO1&)<s=SuyzJ(j z1`1zLO%%|0#BG82rna|L;&p0(wBzZr>zGMtdK9)AMRj=_rv`ugCvRl^@_dI+R<a%z zkiIA_(}{)AA8zk`9!tLtY1uzUon7!f9hJYbI<)qMck5=HRKMQX#8~Nv7GYJ{G<aPH z_!Y}2_5&<pC!gE3tvaEbQ}_{J1%U3w(GS^P3Oi`Qar=Z8eeSQt{2gfm?SHWMmH~Aw zO}pSGBtSxd0Kqj7+=9CVcS5k>!5xCTh2RdsW#g_JcSs2C?#{;DeHJ-6=f2-NXJ-D~ z`FGbpHmj>;RaaM6KTox9<5OqiDqgYxP5M@ZbnidYBNT8XoV)y%Ua?_{`!A&(s}@(a z*}azPLi^<6L0Uh_JdWqmgf&n1iQa3!zQHw}C6_$)I<bE~c4`<GBI0_ME$NVPc<<p( zN3E>P!R*+GX!vro{3DU6sz!#c6}8v>9Nkg59qDrjB`oQM;8q4#qE_RY@O0Ef;_=Ob zcK@ecrKyU1ra7_aSD<SX$9}TH=jW3KgH&9mT-I`p5z{K~N35ktcKHY|UyXx?u;#kr z^E(r;Vu4)ca?J>R83%mFHJ3Z3ogU~T?L4?UOzSNRS8Q316B<#q7C)>i9Yo7d-aVQ` z!w!5C5!S|6K==0*M}j*X9EQ`5v?^<gn%DagBjG<ff>-enuyt5Y{;a{Az#5D)ErPH( zI>KzgAlTJw#Hshe&gdbnWu!@94%eWxVg{`Fj^E1?xdNYCS<2OqO?^g<ncbg7(Y1KQ zK{6)i=Ssa)tX+$tFSD)K@&~r3{&`H-we!?>I5W1f<~*-YYb%n}#!l_M8&P@Q{z%M2 zSC_`xru_tQ*;6(nwa~K!6(7nAz6a6u@s{Z?44WI`zCoC|U6{NtZgj`kIvsW)GE{+S z9a<6X$Rb>9xV@_NeE5=}Qoy3?k-WvV$F9c#a|+)=BxUaq)?(8-PLY!EDGFltI3B;k zmOyH>ksO-!fHMI9(UO0oEEuj|^vqMPxmzj%{^O(nCITCRpXmH&VZ;X(Mt?g5<k%pP z+T~JSd8Dh&RL9lMlWZS?-ZL-RSzfEFXD)VOgr}L1zIydfo0jdTUdk-YlQay1m%IDr zN;AEUl6izfE;hg+)T)IMcHMk7x1`j!K<-nQGcV;!%JfQjyNC?JA;6LFgJoy(NOqi; z+?i~_Yk-z{j-5qx?Ys%3az-*xLP5cKxB-(FUmVK4<T%K>+3-xNqf6*Z-sXF7Y?)ke z<KbXw?{v5aKcrQets%Fyi8FY#0U!2L<V6<03JenTfifwM7^Msij9C#3ht0`y{^vu# zdHm3}p!s>twK_NGtuI$HrE(kg5PWQ3#hLpQZI-`lHS@H4Q%OSu<CJB=QFTP_S4#=A zNbo%V;||ksKmDB-xw^;az^zFAf))q$^bOoSgJ!a^j4=9TBnrA+N!w(=QeHiCu4L@I zkV*;vtJ9HtC03uYeuON&alKN~nQ#%MXN*Z%tBOcC!-VJw07q2Vm~&kmCzrR{_g{A= zd&5;Cwz8dT@Z)4|cMd$yCLWCGDxd3fiNz_!XwF$4FE8E?mU^D#J`b+GS}^%e=TG&x z+OR<*UOG^0GRTqhc?{g}J~xhy>WHHe96Ul>^B(hy@6`?brpeXXm0teG3LvEeR$vNt zb03YN!aBM+a0kaNfk*&7qi54+BG``HL}p0q^A?(hq+HhVx&^uZ3I$goipJ4rzoids z6)&0LPWcyZ{3|<3j<c@GRXT~vG&oeHq|6fUw-4^}-*6z9%J00a>dr94w&HdT50f*^ z@VL}n$YLZiWtckrnNN4!$e4z~BT1LbHRgO(Bv;uIXtv&b+kvlrqvS$lW@5qsu|do% z5e)$0ex5q~xcdJ0dddmya=DGdB0(uLZ?dd;kyn9loaUetXTOU9xRI@r1=Kom$nP?Y zkN+$U=)lrI*avdK4lHO=&d2`*3ld<}GUW+)G!K5A_}r!X$AW7x6X|Ih&+FSw--ag( zYq{~X&ZMHX)8l^58b17MZEh`70M=$3SmQQ=3k<yZgRvJ4WM31%5rx>(-VP~YgT^|f zLg5Rk3&c!8DGJJ+z?{-*!;zcH_UQ;Hp<Mgb30s7bzL8o@$-4`~lI#cmD<QRC3a$kd zbq-dIjn8l5tp&Q-g+9wM%x6$)lR4C&)90gpIpFmed-Pc!cDO(G4RS0_S+I>`{oGGe z%i?Sa`A)iLr_I7)3q%?L{aa)lAX1*`DLkkZR8{483|W1jkGu{`WaiN)zesY~!yNcH zaDaF1K0Ve}0`gN*GRyo$XZ6@+NXTuzOqYhJ&YJU8T4O6e=Zzu)mCfoptlg9+y2*Wl zGo|>qI{&_!r}-64DMvTTyJ6-=2i|Ba*J6Hb%L<xkyQ3J@FSvBS?r(DWg;xY8TOTfD z+c?w~6x`k_xsrBF`^nA4)bnN+3n{v;Io;ZIejzx(HH7mMR;+4cv?MZGa+erg&93e? z(uzpZjFa_E^sOl$eO4tlm-+I}6t|%vm=wLk9N9fi;b8VHe(5%Sm|V(k6!^{O)b~_j zvWY8d5xH5MLb1%_tIt)5!)wp4<PTm$s`ShSJGsd|X}L%BT0Rrec-Y+(*~%Q~R%S0C zvYw#ykT+h#=6ui%HBY=XQw(cdTPk<JnanTjt$3?B*zE8k!6~oEs58*iv0n5;sZ_h{ z=dqaAvxMPbeJrjHa<{~t6io@FQ>mRQJQJA`OW^#beJ%DKjm64>AOs&0xkDgml<Y#H zj@UE}VJC@isF85rO~#6GQ4c?1noU)*<j3HjX8hRu<`{kB8-vvt&sLLj!FbGf{}rq@ z5a>`1(Avi>WUHy!f!jHxE+dhMq)<g)o_fScSGXNNl2gw4`ElzGu+-BL)DkbhGuz%R z3&UAMM2QM2r8QJ}ml5z(^Vxm3<9z8!kn~%Wa^`%I$Fr=m)WV%t8ojcp?E0xytiu(D z@~^EX$&=@+`&vK8E})$dz@WH1xCxRJz2G!AE0QDBG^Dzqd~b+QrtL`CthsE{US&d9 z4c^+q*~I>2!R=ZT<{>FGVBm^4notjSZFt_Pn>sprIf$VI&-tmAT%yKvY;=mbB9CPG zoin^Wl3fV<ZqxaP#A18C7o24s`farHWI1d`(OHiHs&9F_KjLB+P8svvyxC5lAhC1Y ztaU~4QrBsj5WVf6#SQwZ*|(wY!e@98jWhEC_QEP7O#N=q0(rsZ$8#K<9z&-nyOEA$ zJ`R-`S8^)s@eSu|PFEK=Aw;Z2X$L=*l9I!stF?R=X&K-QU5o?cK;AP~Yqk%myCT+I z=fFKzrJkFk{VW`lTi=(4GlfS+BBha?y<Tf>em(~6`@7+)rN{uD^`npewFI1swc0V@ zVzanyne}+Zr9v$b-IghrC$;2aL!vVTvQ(=+W;eq7gC#F=+IeNB6U^DMM<?&{9NxDV zbyB6sI!t5jW^Er)r$$^+Td8T0)HDh&NmnYdfNl4M`lhBN?+jT1J>A3k=k`A47>G&? z&TBK&`>7c+8fo0Cb7n8(l$F;O31bxv>b6XV93rx|2hY>gPb!cGVp6#<ZZzKyN}191 z3|9qf0RiMZ5n5C`o~|Bt`ydSr8Mcg4m{YjUO{#%mHr3q%SNNeuvJYL}<$YxA(Gm$F zNF^nxG(Ow7TMc^*)XfghGbb0s06dF$67Bf_f{M$8%~qiF_DkCzZuz4!+m|E{t}a7H z>HW<y<IA#sG=sdGvu9wgRY$tH*L}C8x$=1@xzp6)_~#`&fobphD9*dR7;MR?Pi8-x zuZrDWu^25jx)^~TSOb$tt2g%o6s}JORCXli4I23laa<4V@g}M&#Ln=<FAv*SaCRRW zrTc5bm)t}+a49V{5lEVcQ%IBeY}f11t_M{$lAH(b5esBwSw|#A_e=f(a2cETwUI@N zpKJ6&$+}=mo9?FtQP0Ye{6uoa*COmv;s{V0Mfa=V5pU6*hbxL++(u6)jLG9|8Xr|{ z93fqwOM~bvW$nwSY!2fyBQrqt^m7A4bE~p4HCe@dgAS<dzesyZirGKyq$~>XZd>Se zxqi=UHfeD&qBi+OttM-G80L!!&%FI@es8?Zp~FyYL<H@<Ut!?Y#WHKk{QTg@3x?4} z)Y&vAyq+gDMPJfmZ^?)Hm@9-oId$l6vGYA!BQU3Z?!d&%n=I*GZPL$kn|~CBCt`u7 zW09C!<-NX6@#DU~FAsSR0wDG3()CkEm}5op1{ZR}z(G&rVuB4cQ+>&dXeYD`k)hIZ zG(hf5ZZ^7g{N$8;|2T$4RK>YDI?JxJYIfDt_r<<KM=$lup?+)|qu}U^W#@YTI&UA^ zRl|S+ST=0SLum_Q=I@<EjO=r$Qg?w(Hum#J^l<9*7h1}Wj*$m`S5PsuUNtfcrRRhb z!QC{tUsisSBBqg93|7|(bFO`OfR#+7p5W-*ql+4gQ}+7~X(2#W;)~ylAdrHZ7a;b0 zu3vPJ5@wxE&TPrl4y}i=lS*^!YqX7l5LQXT8bhj-e1i$12S2!*a6cIop|8FENZ);s z{)Ln3o^hsAs!p8>b<BJgqZ-(hJXeH<PIbc*+1{K7?SM6<^NZ7S#p_L*=Uq2!nmA?{ zL@`M&*uCyobOA(+!N{n&%F<OAL*r=f`KcGVf-XL!y$Z+VdI5smMMr_+ONXo<?I`MQ zQfKASkVXV!w;lMsNVQ>~Z1kj=$&0*~)8^g%XHI*m5v3zv&AGOZE=<Ku&1cT)j%Jn} zm6~-4BOJUZqNn%=iD4y%Ey01z$(a&cgpC_2RNXJ-67{9**=i6d4n$Q<^*e4ghnXFJ z^*apST}=lxJguZH*rAmO8pv3z9=Oy*q;(70su9yj!o$*@>wJh2wFFcWVJ2-OifL&y zQusp*VS<*-UcI+r8bTWUQa@4})el*3ulxegjYyI^_4HPz?~e)JydQ%jmtC6o;4d-Y z{tO0`+$jF;YRG2_bOn`ygRX$AzvIY5eepsI6@Jx#&v*=YiIYb|Zsq#z?j*Qm$cDlv zkX7%=uBP*)UikIKp#&9uIEM)jGkUuGIE2;YEC5qbyen@X_ip3xqX>-M&!2hUc-T?_ z<<zYtjp0s|kkJ~m$XOM3bevv43wF$5s=+yZ^&)mbWX$4f9icvnsDpx6cM&}B`KAt; zeYU&ft=NJdU$oS>{01cU%NVK!xIp6ZU+v^+UlVwGOrQy>bIHygEiFY2+R$Z#ou)4T znt=cUMD`m9ky97rlBuBwd9Ek(Xrd2+3ZX0-RgxG<K*eZHq)2N*XKeS^d5GDL^BV@| z@8|-PqA9$X<VZ-|fXRX^{}Ko?dZm=*o5iGg{Cr#JCeqF(lT&vC4gFdjbNQzLJGf!w zGO({K=w7kZ$>c`{GZYT*Hm@a2?Kt1clZWCm>qGCNT$9_2n}U;gS0BGw*!n7B^+7Ur zRpOBIP{!lZ)s7Uj_a}oM))j;0Dc=oddQPAm2EM}#pF%-1ERFPI46>b>r}A>(&q^>X z&hSh=y17IQ{R7?K4f1;^)nua!&9G}=nMi-}>t1sn2PXA6tBBALgcPz;=q=PqLImVZ z!tT@2po-IrpzFf#8`=kwd_o)2QL#)XnyQl{2f%?Z#IsLxKif{eExD?9e&_TTRTtF} za{~uCV(e%*Xr@8=l@p(hHSyWN`5UjCmCM1YEduX_NOX;T52A<L{fgA~ueki}oHLx@ zX3~QBEi@6!8vSgu1`OT|WOe!fFEDgK;w{$@O!gfk_@J-94~k=T`Q}(Bt5ey=H9au& z)RApIW7`R3mf_{;%dVHMZU!xR#<C~YIi3NBR}&TNDCP2+<GyE7D7g+Q{v0?vc+v^w zU((CR1MMtBrjm(jJ4f1`V(%eJ)x7*O#x%h*EhaIksgK7&JIw>QL;?y7qrY~dn%1dd zVfC2RTXy{{<z*sLj#d&Bum)IJY#T+h?a|(=cIl|=!TQws&1pNa7+pzTByIGgo8?Ep zxuJJ*Z|ECx>Mwsn&KhPx$<^So)raF!@iMk}3TIKeR5RB91ySlJOSd)p3pe3~{zMj( z8=xQgj;03&+np?ZQG-tjS=hDSNm)%_-yMygrWgD-8oB`Ehx@@t-e#5xBwEafU(oB_ zPhwg(8nDOIc|;~>vi00Pj+>nG=0*<RN2f>Gpinrt-fEXDeSe~|>UY}4U3|e}%QIM| zysqn0qBa~`*q`>0XmxPa_As7{ZK`lh4)u6ze*enuoAcQpy=Q?y1XxdtSr6J@MBi<} zULp&TQkiSoIT{eMRq3~6#i7O~U(Gpr)}MO%Y$vH>PKz2ZJuhJR=QJV_$ClFWP8NrO zj}SQeyP4s8^*&tTBHC&CDDC^85H%5QprW0pVNhS|Kmn#Ds2@20v>k)v_AvDfjeB1{ zW5(1CpQ@=yR71Fyx!MzBlnqlA@^zhM&DrW*`4NNO!N+g?;g<WKOs%Ug1{3NA_W(wy zaI;ZoUe=Y-{{tMF>)kaI^WC$bmdBjhS3FhTVKnS63#Awla$Aw~&_dZPW-ufXc2|XI zzR<u0en_ML=q8TFIUwB_PRG_dWOl>wCVETmKKzGVD&mGGI>{p(TKa79pxDS{#jg*{ zb6bJ8S!<L%tk$^~OVckyn;hn+I5aG@g#!t$g{kazgLYQ<0I=xj!K`7GRVcWgc`}B3 z3_^Lx!%BZB48X$JcZbh+%d@4Dee4wBePK>r{D)BRNhmmasBdADQs++4>%m3U>hDV< zQ??k>I;T#@UOTGBEn7;c><u*O5KW6Pr&K9L09gL}nTDyi-wQc-bdHZGSTjT?n{@EL z--i!dZ@nC6PiUTB+Q#%u-lv#c1kI2OC9e?a;R_oi$whzM&@M`6QZ)f-;-~1Z4Z1C0 ziD-waqDy5jukkWLWX|`SPO`h0eE7>&_m{g+ECey&0MMkqe7FiHSv1`NfdG6<=k%<e z`HxVm4-GPk5ycccboAR@vNV?+q$W{9n;e}_3tM|gxU$5bhmc!wIbR58JG(U4DAd=B zka05OD_rDG#$M>p0Eo@kEAD3Lr;5a=ibd8R&g<cbV(_+cXEjUSdTN~I--H^`&SD5Q zaoF4_tdd@Af{i1hN7%KV(Ty?Kpq;D7Tee8_e<08`GWK|zUmtusZshD<oZAqBCxD)E zG;6>Kp=~b;vm+S{%1;BYpRk^tJliIVZm}D+awqy&NSs>~b8^PY@1<4vkgs=$4l{jO z(0jf+818c6wJ*)vb^k+XL|Js8+l=Q$muv}g4YP*x+wzRy>**d<GsBkQgRd}E(OX6~ zKjySjzS#2i@pHR@>rysM?x)*sq;Y#cFH&81vgorj@#pBoHlBYHWxYB`<I<_YIq|{% zHA`Ob*|R{<l3&Wev*1Ug<M@wQX*shB8IND5Fw%bkqQ&QuT?edh`q9>5Sf=H<Nea4? zStA|bY?b#1i4SP)XXEdXwM3_9&g_ak5OQVj#|F)&4wREtQ7q@6F+vssLODB<qoiXG z)<KJ}eYrKZe#0@N$Kj?DLz>UCq9#NfH3L#g5}JJa6UoSxwf9bYa|VC*FFORAyND8} z|KIS?iA1(vEH0XQgTvvoEe*n}wLhCM1q$2jYVj~|5PqLYd^JQyxYh`*>gK}$t|WC9 zG5l<#B=rDmd`xBFlOU5;1dA$0b;ZGAu>E8FW{OXYsfxV@doRFfrs3o5dPF!x-EPBp z$D>(cX5D{Na)#B&Z-GS!ela-}Tj;({X)=#*vMG+A%j7npj!k!Q8-rV+Tg>ubaL@_( z5rvznKd7N5G{>{L97a7aP4$0b&_n30tYyg?oagmoo(-+PIVXn_PWN#0b`O5QvShdQ zBjnV*kl^p_MrSu7glJUpMm(4(cW)P<xescGxqVR9yd%EvMDjB%VH`Wq3U;H#`s6ZK zvYFwx#8E9TH|ZtI1*dQTs#m|=ZR1J$P|>w;6Sg~25;it9JL<5WBmQu@{eN)K|A~XX z8HkPFgENCIY%sfH!`VkRdf0#ab!@&Pr}Sl7cf}KP%x=-A8H1m9V5z&PHA4H`^%uKh zHP!jio)`<MUPv4hT|W`q=C^xva)TY2Hm}Hn$~OB<P1z@|rfF?nEbp=lF|&Ea?mG*r zf@CluUuB!~ik!(VX2o8ND>EKk={986l~gBaLW(4!ot>stjEHEatM^G5q(<*>Akiuc z7Mj5kTRyu4+Xfc(_s6hXM8c`Lb>*Sj$|5wbEt72MbD+dsN<-b!S&tp03kt>>)t(7U z{rB!t?{})$5dzcJMN}sO&#nRZ96&FEnIBeaLsmK8r{pVG)F@sXT<lv-T^W4{$f>m- zK76<Kb+EtEUyi!>_{oJT_9rcJ&ky6?>X={Db~Co6V&QJ!`9=oH{?|6ut#4TBsKQXo zf#;L`Ny*GAo&bNq+m)ZH<&8^DA54|vCr2?c-uUH=mVxTo?hoS%&0*vD-oFUM>-nmw zT(ss^L=xmi%g^0)u9rcjDeL-de6`3CetTde!H6Au_G!7M6b4TXnIV7$?4Zn<HiG`| zMNNx8qrj0+-=CHt-{Cnp3F}iS36r<C;I$=q8(7BHW;@Fy1WVvENWin&xF@4eYV$I7 z0R~_TF*Ej-EU+)wUvOb42N7<>`6)uo^rMxz=!<B&RF`Zv1f5Hl3%&!`D!--AHgYbP zTd9SJ&0Jox5(#U{r#;%#R2s69pHJ6>!t0DUO<x+vm7P(I(RU#Rr7ZPmMMS(YDd5^v zNu=2vK$Lg~u<xY<%@9LV4t~8Dd!E*izZ|3InEjf;x_j_-Bsv40Kn<qBj*WI)p2Nn1 z6BpQmbBnOLwRF;JvRFi9(QlHKIv_j-I8k^y6jcIHj{@wE*71_HPz4lFQUdL|NZ=(j zKgcpW%2n%^Ns$+F?_wS)jht9%qB;3|6R+pewR}6`+MqN6%XPX!mveEk)7g)dP{#DN znGRKWYPd$7WDh!P<MM!!qN)RpT$C4<<r{hLjiAbu$K;hGt6E=i8rPTYXPG9oxGhcS zi9xFKax*nfV$2uV<ql;eN@@4K+jP!`>!|k=8%emEVAg-tu;`LoGuP1ARdpKjjP>i& z6Vif(mai)ZqHtE-eb2R*tLHHsa`)L|iMHdmeR%ZJy}gfe&lSZC{j!Q_$5y$eDt?_v zv+Z&u3EF?zugcI8`XcW5x{eMp#cUnc*uX3f86@~!>r-#Y!I&!kmZhG~{Z<c>*`A}- zr@GdA+$$l)!yJUo2eNg?+5$(hHu<}bg5z2JE|csKHOR!XIN^x$*{Rz;AraoREp7i0 z;eW6y{b8K15gIjndc)ugJtUppwmA(6Nxa2uFc3cay4qHtzK$|I9wME{@)VwxaAkwr zl`qQL4wJ%nArj71|2cUR{M;siz)2h_s`C7EiKu5i`q1nfruM2j)JT*px+N$msg66( zgDa@U8#&F#Idn^2!e_9rKOt3lj$Th+vTQD*f8?_EfLX=a>RRyHTJ4BO8cSr0!r}Um znfpRO{7f~vpO%zJfM05+Wlq1dpD7z$vSwsV+uK94v-Lt{O@yF)P1JfVe1l|~A2u*; zXck&g<<IQopM5)$_lARK>kY?KgRLVJeNM%_emd#Kj|$&a==Z3_`AGJ)O&E5%*gn<5 zoQ_t`GNZ)spamWaq8WnI>)#qJ1f<%P5&Pf<eo~C`aP)&^r7q9P*7dRd(f0A)Gt3mg z%GDMOb-kG(_rbN+6^Avxz7Em}rsu0umlCNw8%!nQ#9%%9fQrr~rB=|qfWdYlEiuCU z7;4m1yDwJ%*6e7H#M^qb@MK@glGB;;2f0v@GH6~CKZ)fRXCls)ATEH<Y9y5rav@;V ze;CZR-KdV@(QN1%f6po+qZ^pE`d+y?Qg2N~Lf`R>+lyQ+*v62%qu)sh#dyWUZQE@z zZmdBMtA;bbJo26?qv`$^YB)ku@GdJ_w;9d>KJR>$*H*L^zNhQeI4%?tx)zWq2|q-E zQZ8gQ3C)tT?Uu>utS+IUtE{{H=B(^)VjuuBOH|R^$V3cwZP;M2B(AM{<DNt$z|5Ow z@ZeeX2VMO@Bphi^yYA=p!d2jAI-U~OaeVyCdR;2vP@I?`%BIS*p_OVUZl6@y{*Q`> zGleJ|^PRC+=(hXE%pVGlzFd0b<ksC{`8PTSjlw1p3D8WlosZGoKpJyVAXn33NX`_r zq0L-U?s47YS}9Od#>Hhr(s=`|>OLrFw(broEv8tL&nw4+D5qulBkIY=Eh{a+6X|iN zg*b{G$=~@E$j!*zU3=ePEE=a*+8@1RtO&U)iMev6=%>?(oWmZv?bQpx{%0~E0h0k; z>g^)@+$wPNqm)`hxRUryNSG4yK&s`}<JnSI6fhe;2|1$*_Rzp?o<k}l#wga9Ke}&w zj8?@Z+!ruG#JW2h$<Eu>E%d#sj^I`B&iB>ZezQ;|2S4FjWcd5MX5UEtWL7y{BdRFG zXRZ23Lw)y~hMIP5Ksg8ZMCi+DlOMEiE@7jht?Ss_!2-*c;L7qk+6IxeXgkadW%ljY z85X&nXR&>y12>G!S4$RpBeu&_z9$G$jr~Y$$&xOz$RKMW`K5sv(Xb=Bd8LJ&NsWk` z>p(1l$y_2f_Ue}JlO6MD^uElB{!?2?dnhyo6bC{=^528&B1ij0xriRd^eI$X&vXtT zcRk}b9}BmPo@dRLelhvszbk^U74Df67DqJkya7)tLqj=nBzSM&^m1;hgP|pM+ZW6E z%h?O8Z7zQwDNKRjonB>53Zh(YR)tmC2GL@<od^WnMz`o(?vJE`(lf4|ba5|e)ka2L z!iVt(^n+oarv(-iopvLS!9}b`thdY-PAR>+CKRxZJK^I#x2vhG?&;_DZKQ-8t)`$C z-A*ru;H+F5i7_yR`^nizRE4^!l5Sh}55{jNi#bN^%j2iK8()<`;vJ|mr^~XVr@o0z zqwvC<<kvEd`_dRx-~V)IP`!uGR{V2EOe|{5M!$7mP&#{QN#)eJ)2-9&P<eG^y+35G zLT$@ODg~6tLvyN|W#=D3_SCipJa)sjTOFwVn%=sbLMo4*a>j{Lx!0QujW6AChIFf3 zMk_ebVvtQ-&2(;{UCuf~bZUq<eqBk@x9n<IB6^nuOM}cchlZ6&@1A~p*kla#S85~k zlgzChfse;ub#qhlzt5+-1l1p|wl*<&#fH+(kpLJOi5*rr7T!vFX0;M;t;G&6WJfjA zE$iGwe3q2KC?2-r3lD+NDXCDH{u14+5SZe3UY5aSql;gbb&|ZeV&qq6N*A!SWzQ+9 zf~!?WwOeAISu#QnYlR%%@V=<_Hh*dS3x*{IJfm4?9!aQ@5rh~^jz`;n2}_CIv_J&_ zO5VD>G_C`wO;^jqo{na|i{?y_kGGQO^ddh<$s_q3oPL$=WK(YzHdXT#r(A6=!u3ZK z*K%|fk+zAx<CV3M<3@B=bm3`yWaKrfK%o}546XpX*c6U=t$?=ol{0jj^%@QckR9hx z&Mo(xdqric9i6=tWe+Sn-JQvnedA-3ILy(bhBeN<*F7v4iY+^%sHKfp!XvhEptO+m zvV}()_nkiUGdH?JV_2m4Ur0o+<@9_;f9g<+6d<GyL`XizxPH-!3%I6*_sPqz_{n%h z&cI4oq=vUXJ3@8$ia9EO^C@q{{@}`t7oE#kSzUDQcBE(SHs|HyTxPD+ykX9!>W3{Y z`z1{G`U9cHV3-8T!n#x63WM;VBwL1tBIY6X`0N(_7@5JUdz`u8SJWD^>JnwW*Geuf zW5lpK7ZsC64B`AH6*fn-yw^`ICMCQ4$$SU1`7T#UzPXpAb1+AvjA#$}hZbeQA~--7 z?Ns#|KL_ojDd3?%^EUqtGai-c7<<35bMcJFI0yNlwEy+3@pn9dQKD``+Zz4EbnXkP z9wXy{`o_oD95lrDMB4mZ%cE;rP*O8sUV5NHd|KPgZG9Wz*uTtklp3I$609P*x=sI* zd}_c?G-q67l~xQS4`?RFfL+Q9WrsTz=z<?QB#%HSVB^Sj8C`BBUxoXAYT*Hr2`E_Q zHE$t)Ac}l&Np`o*@|^EC(VC6LiFjD$bL4Q{X}=!4MiYoOT8IC*5v1xO^^#7*L(=Fb ztvcq}LGD8@bLEuh>WtQEB#uBdR?7O0fUL|;)24jXQ-e4D7F(K?<}rKI!fdTY+2=`b zsurtV7I;pB-f-0KDDbky(|(@8V1byXsPg_Kxe#2)|5<G&{!^%Ps3(C>=Qx&@d--wi zv5~+x_CVkQcTKZ5NFjfK%>JZAZl<Mu=uKXRx*}$Gqjpvx<P4kjDg2o+RJ9;5qa}nz zZD3|_#|k8qr_}(EJ$ru6cX_(4Q8GE6o|LgGq6*N=Eq{yRO*_o9L|*9jNVtaP1yA=A zB)Ff-Pt~V|V;b2C!H(oy_jUNxYf#H*L_>8Ro|!0YVOvI6Er$^Qv&i&^aBAh}6?FIb zfC*5P-HE=GW(GefNPd`&hF+Iu+&SeR5i5J_QJSCur^hdtjhP~O+6S_x^9IXlFit<? zzD9dr5F9Aenl!9x^er`EJl5q4=crt6i<)kAsS`8TFO(Nghu*!@q+&-i0%y{TBc#2Y zH%j0R^VGKMXrP_m?IohYxfU!YKz`@)=WzQSkEmrIDgcnh!bi~s;t)4QItcaPCg%|_ zzbXdwS8xb?IXzEpe~C$@({Drq<8MTQo)@WAqlW=|O%dVObl3n$Yq}Z)4!6$8a7h0@ zun6$41cOb(@x#IU*nIdfm6WHZ$qQN@?k+I|>6d5dD!fc_vxH4ejtoOSY9ZuNIS^La zNM@Av&eNCsFs;DK-O|BF4BnofRV7@lp8G6DJBxTb{`TvDUq&z9%RQe%uoC!x;={uu z@odH4M!`<n0!U)wiR;mB)o@NysNjPpeUAn`77mIoXU^Q~AV#eq!$<^xpkK<J%+dwh z9a!LlPpc8V5Z}@j=?Xxvyx5UgkADK}2{^bh;<vF4?#wsC{-pmexb$#T0$Y|E$7z>& z=(A2A^|)xsc3q9wvc;z)%gv_<T3S4{XG)ceo7mSl0t*3re<8RV;r>>L@JK>i9*>3F z^=@GR5>s2nK-}P6ch7SwWBoH&rd-9@`DiKD`O-JY@b)FPXmO;+U*Iro(|^j#zp~AG z&9foW;iIMQAx&3csXGbZ^ZeHe_{Dw+0&iz;Fno>jw_B%!XC;cTnbVXUe>LDG)gK7C zU*~23r=@|)G0EzC2AXi;maLU9dejEVh?A^34>Lg!11(`ty_OAFldP4JmFo}cTS)A0 zTgzyw8)ilo?i_jaVT>Kia*^g|%=4>uw7`eV-N7FoYh=HK<i#L*(^XGGv@)T80l(e} z{}b2Fq=IswtG4zA`{&7i<893wVsfq;FU?(Lrt;6)5iF0_WNh7JP5isk_y1IysEb_w z$%f@E7z-0cUKK16*8HGI5?Qw6L8?^6R=*IsX3#FAvXgn2$i+Qn4;x~dx%H>&*Z-?( zTIj2w_7a#2C!M3An9=M80HnCW&fsUcx6K0vS2LaRFpv_~&%Cm&%PCIK<ZU%(@A=aX zAK8D}ajhG6F1tL?o6wl$B)&5X9#KF9R}>QM3F}&mn=B*-1UGs)iT84yOIo<nG&}=Y zc?~=M`F<LIzF*nQ3^%yRyd=+(vK03lN$~b7yYgUVeZ>y^y_6pZ+IsxPIn|oda&Cvx zEG}Y{mRJt+ZpjUApiB$|gxfP{j>YeXIxL^ICg~`q{)K0P`<oQ|ADDi^Vn(qz@!OyM z8d{>axB$OkCiL`J-XZWu+#Vek;aqj?`p;lLZ6S-m20v@?G7?Lpk>T;{M{N2dM17kT zYZiNt`S|;N^>6a{hyQ_$dc=drbfhMT=$3C3`!{-Bo{hy(pv%p>d^)U*od1qmn02(i zTr>p$ubk=*ck+v$!-#{I`&ohp|6&jL2>vycpcg=}v#21?^0NxDkzFS~T3|a(bwC_) z%fQTX9BPvK_%N9+yYV;USCxxy$!1^xK7i0N0Fk<_%eGate+$3aFzo(kTsi)zH6;+f zc@?bboy`6QS{tb~(Ce#hTg;!bi+WC6b?BTV&}A7diWczgC?@=T?m1Bj{UBr})-AKD zFnXTWi+wQZ%O7zF|A-r_Eh^ZZaU@5-KPPIr?+5cSE^L7(#pU`I*tW2k9*${z0C2Tl z%gtfj?$eF~S~r(aXj_TE03Moc`-akq;Id9WaJGB@g_)hcRB{w9klw6-HyOi>(8C++ zDT)+9Uw|c6LA?!mYu<(5A9luA4uA@L2WuR7SQ5OTegFPu;}c!edNtNTby;}6(5tPC zhAt|3Rk5ON{R8kQ9K;ReMF>E26`@pj4_9~Q4Hd)bZ}=vdA9ghLN{b5oAQb%ScYAcB z?%g`_&lP_`*OXXa&q>`}iN03g-&4Qr)Hd4-vRI)95Z}q{6wofJ!V12WKB;g1<;nE( zym(63wk-ukhS>M$`9&5o9+z_r`5bS+E|X}fm?s~Xbn;`;H+-Z{;_03`MK^K&K{cK| zg8tS(F5%7h!wshv@AbkSa;})01B)|>6r}Ea%JOa`N4;ln?D)9ldlq0o*w~i((%aBx zxi}B3pK}8zjMoPbHGGE>LR*;%Cl9tEdQPQos>%jR2L93>{lepOTL3PvQJzsuM(@U9 zv7wRplaQ(BQWH%#84ei(n6<Jzi93&4tNJak8#L*#i*;#1B~=1)wvX6F9$xKWPahZT z1L#BOF<n8aPBrEd(uHWsU-M@y4H)&jC&p@Z8^?jJx_Rmu+m9&BN(vL1QSGMD3mxW@ zysMWDL>hEhvJ(IqU}9bqx7>ej0(hA1>*dAoo^ss4-^K91j=)>V31N01=bD*OFruB> z^vJDlxEVmXR;XOK7m#ut5R7~2@_Dena+EEnqLMr^cQ!TCRWw)~wmGqK>wCCQ9jf#8 zSB81CZuIAYX_Q|UaozWh(8>F&Z|T#%-?LVm10c|o$%d!Q=rBwDZJ{BqDre)No>ztY zk(C90QQtMuJx*GfH{|Yj9&W)8?3Ug>gQWY(zvLG^4wK4{k5~YPjFnp7@Guz{>bU@B zi>YBTcTDAp4K$(aYg!0}YIwdlfK)DH5C~L$ucD-B);8KPE5Zr@-tH8L9<UYc-cnjk z#z?D|&T6MhuN8k)U#ru9J{x6qh_*bbI;-#%_t{>b8H^YE-d{#{NE@(q+F~F&R45^9 zO5K*ZY(-f^6VsSZ`atTXGMAT!Yju@2Rh@)LAB?t1Dc^(FHKi8rn*LtwP~Y>kiutq9 z8j<lA0Pz13iZm&imkXf!gQ=Tn?PHzaPC9%PvaOBZHm^VX`XX)8o#no;;+cNNrAR<i z>VP+kj$;k64tU^ezxOkl`KSpJhMlcb>b$_<8VEXrMN0LGKfZ>~^QF)qQlSPh(2A9m zhp}t6EWbIAWanS|z4nXsH`*vITi5H~7VkE8576_=(<Ne*Vp2Yx_daOa1x?;1ynY-; z!#>nlr8Yzz))uxD8SBHkiXF+E*+EXo%m(<b-*>i=^G1CuTr0xerj9Zi>qSfTagQ9j zM0=k)ET}!{57K*+8?Z7$08qWGT!8DT*yQ{?Vq7wP_6-%foI5}Sm3sXF=9EJE!B}wa zb(yN`(SDjg1E*$q9ZY3d%ti`CE8!ixm&cNc{-%xCbs4eKj?>Lfu_B6ban<_~Mgai7 zzp$iEVTm-I(KOQ%l|qTi(ww)WPa&zDP&PF0l&)T4=L54O(<&93&~yB-?X&n$dUd3+ zVE55L&Qi7M=7G*ekNbVxTgxOsajMz#k`q-P=Uq8tr%#X{-Nr{}`Q0Q{{*Kp?1RoW( ze*#8UNOkU5BHyF0krYIH0xS#w&ktbw0b8?^2+ePYymY(@0Oh-1Cswc1)MRkTP)gkl z3XL%2@>)ph<B2(@-Ronc5hAeIlPh-6&h_^I5^F?FWPm-(7{|d(N>&L!1Ha|kvGgIO zt+wD@c*ojZWs#c{Uli#2Ed`{xenO9wB`UtiM^pMu1lS{>j}8Qf5-`6%x3ky_WWGEB zwQd3sVF`o9OP3!95Bj?fHR4>^Z;yJKiM}TA4a|Y+0Smv|^Kd__@oXL}QBD(aLf(mQ zPe-t*Ik1*pZuQyXiZNyvDG*rPd;PIqp_l-)#tCBVD40?h(R%i)Kgx~;DElL7Us+{l z-?=wmKGk&<e&}Uj8301><#bVm*H;D6jcTPd8g+Bvc^0TJm%^WZtigv?hx`DkIiqZ* zHjjV~;a0LcP<}Yd-P|#BCgL~aqdBK2UZ(VQB9R3)kl#Y(PR!j){ly5UIaVE8*e~Ds z;(qnP!E=dj`MN)tHHFdjzR#L9`x{nQv$v9}l>4#^2Tg1oDvEq@^E!e2eZ=$wh#2LM zjUETwp_VX^8p$KsA+`A6r=jVqjX6XI@`lpMqbJ5X>A7{0z@;umjx|f=4;`G>z!))( zn4EJBeueH^T5EQ|jG`ofxMnpMTj|%xQW%1J8kM(-?~ds0W7?`A;FSL2(ilqQwEX2% zO*<@eBwCAXf7b<`Py*N)k;y))_(V$c#?`w06ZgQ=7&Jf$+oVDVJ?;@p9u`ZhT5>C( zPT=B-&buR(mis|ZX|?9gmanOG*SlCPMkOC9UAbz0_w@bu7GsC=-X#Nwm7=eKQpDXW zm8*cb*C-;K*-x8{bB`@SAK?CHXA@s3i4s+6Wid;beLa67g+;v)vja09@8M9q9CD<l zHa10|0&zuZdqaIQJ*~U&nr?dwZ;mA44Ttb}`arLdd&Zp%J^>@Y6t|upS1^xz;=*<c z-$h=fnl)&i4)yi3ef9iOUXLGH(}l@TUTorQ2AZ9A!%2^@l{<t-Iz+;Gu|EZ(5AC`t z#PE#pLbS0@4R}-?sRQHsn%~$jUAsfj0o&PF=JchP=T`8GNvbf3&N(ND(&rC3?a}Fn zXMRkhRrk10d?Y#saMq@>R_>h*LVYW&1l^sP$}^>e&%K03?1A0Hr!$M={(=cnLw;B< z(?5=X9^h6ya+g8AakwbTN{Xaj`!s>VXmKhA?&Jwr!(rF;Dw3f@_WAF8fE{7ugs5ha z%g2DQL6E!ue>CDTi_rgc1c{*^^75Nbblr{s|Ie9fkFo@eMi_~8U$)N)fL6qdUz{A% z^t}q$d{YQY8~o#4eUSO%{Yk33QZR56et&d+X(D{$g?tk`9KsLb8`B&rCu*=pyQN~q z-(uFdIdK9BUU=4ahj%Y$khh+_0;Tc)w<u+G*A0M)iZOosO0|h3_+um=dH|=^*3k*{ z0CxXqWky~Vn9yryggqh4+1+==02`IhG7Ih$1Ug&#YXYYw{+(3@mQjpoo~F>g{(GC0 zxtOZRqNk*miL?kR!-suWi?D`7HO-z~M$iYmKih(Wf9SlgY)?}@kw~~5_D$@tG1L&S z+Oxb3Z|oC&&&{B`55%%DZEjR?N>~uyQT|u!y_UV>Gh4oqSWC8@0D#%XQ-{}Mz&4ho zJibx1p&jge1%UwM)dgPelA`>uX-?3;3J|iuU{D|Ed|iAYz4EJ;w4P9rB_i%zp-oe3 zh*sN-gg$C|n|mg{2?peY@E;T7ujFh2oN#$cLm1Yj$G~33Sz;fd7}(o&2ltc~Lx1(p z0sD<T<ttck@vdFKU$gr!g=#$%=wqaOizcxUeXW;%rA!#uDDJ2qbF%*O*hUzw-1+jE z)_x=7dBNaMZr?N3J}Ee_^#9cB--)SE+VOn~^P8BrmYIRiKVcN*JiRYYb&S@9$Qh5* zzc!%Vd819;=QQ`Jw^0D(g!C`NrxyV5e&I3B02c0|7QRmAW3oBX>W6`fW3zAwxcY;T zAZ)9d-+!s{OQ7D;_ez>O#k)N~(;wr8CZ##!7cdu=SUM3%UON9;V7fQwyfOoK@tasf zDUgK5<`O>S9{OW1dKraeUkQ`n3^S;GSNTb6+253O*1vb$@Va3-V^ps(lv(`@l#&r# z%f@$D?ZfVm^)q_Mi=+a7)dZWGCUH;6z<^}F{!2>}Gi8X23e4O;O-HDGLQmDSCaYVs zYySy5d2mo4b+z})F6qQX+nM}mME`2WhJL(lBMkTkldZ0uLMQGk!IoKZof}FU-^XJn zD#vONrea{c;C}w$>HKF4QFF61cMKH73@oJN?a}jt;^-KXW|C$bru<<(ul-8_WPt4Z zZ{HIHO8id){I{L~>~o}lnK&SjG47xJk0QYK`70MFF%$$EV)++!g8`+P{EH3%^rF#! zna%JZubj63=;<9o+Ow35iE~+&92gMXGy5LPgSWX8-tG90HN7PHk7mr3g4Cw`I)H=^ z#BXvHY(A0!ul@S#wf}7G+$1RRdz#eP#IWiowb3`5x+(}Dkm(zWk2YLn6iPOx>nY|p zO236;{v*7!f9`^CNYNh|avHQ<zX7C@<AMPR(5(qe%I2<RNy-+lJ?-UQKQDjmCbsZD zW1y8(=cy-9^SoYbf}Nk*^S!bk9|2GoF@>DX3$ni|9QO`q2?Ero6}0@vvdsw0^xa{9 z^`Wg4A~v_g;Bi;h{&;_@(_PUhHvY&(-&To?HtLSv_+9y3y>ZbGKy`sA0Gt*FbLL-c z_4E$~X2cfI`)5O0YO~xsNizl7WBkt78AZp=CcjQP<x=_SJ^dcz21;k&xOAdM$vK^) z6eu|(qeiK0wf^)U5cU1qmznubfZ;`UJzQr0TsSCE(I_tdT3*Lh#a3NR$X4B5!Aw)g zj4uBg^D!2YUjVoLO+q$lZB0Tp9cbZ$hOcL$Qsx&!QJ^+fL?gq$0~V6@NTR^#z_3t} zFYmuX;O5#wvx}VFBhY;8;z#*0lj!7k)65iS>M<Vf>Us5l1ibhk0wj~_7GYQvPJ8s( zANxI?n<1ApmZ0FkMbMt1@}|KX=wTiohWkwW@A3IPG#}OMe3b9lDiUE>{_fac0hHf= z6(IiG&wg321pXP=-`M~jBQB=mFaFQV|C>i+>}48vgK&x_1n+ObV*#}F@1?3t43OsE z)4E{%*FOq9errI5@_ULq274iKOGkun|CB_a9tp@!gd}&+QE82;?CHk(vHh*groxkH zl?gXk0?hJ`X6i;~!5tZp#6Q#PaaxQ8(2o?6g{qIVXU~q_!j<9r{;#6n#~<e>LfTe# zu-eR1GC3`tQj7gpcJ{!%F(6RLmn>B&+?uRFDcpML;JC?``zq=Ezpo_A9DFQ>_-M-t z5=9|Q4*iesgEF6(tVH?IpEL++G1qDX)ja+fqP$&|aDywrOt%##t0S+9bZ!0kb1S7# zVy&W39AS~_-b=v!dHfCg*O(de1+Q1m%%}xHigS3uG{)m~9H0=AtaPcwhOFVjbMYbL z-@)Y8O~9wDBLcG7)e9FJOFy=^l~M$^KC4a&x2e(aOUsLYL>e<+m?0qRsB`0vJ_>us z73oKBvL7YNU~+wo`_rAa^7n@EvmIdj=URX(wTI*3fY-d_gIMe&Y#o>M1Tz10ItJia z0{Nc^p6Hk*fkI6lg<;3!b=W0&vA6)4zy5wL{*N;U{QLD|szBf=cmp`l!1Dq9F>ML( zjK6xkng;~Rd;aJa1GSkxrd9!-v2Q>sYTyxI{ZrIb5TFABPa5u@qO5<*0uuQ={=+Nr zZ&`kSipu<vr6}~LDEa??&F~sFN)~EwND@j=v)M)FR?<ww6F@|qJ6m+V8T7n^FI`A+ ztQ6=PU=}tVWdOf)qx^gj`Z8nN_+S(@d$(!QzftC-vl!9J;Gp1b*K?j_@WPLUoqbE$ zxe{*cwj<hedZ2%;0hu^3c6tN-K6w2bGT8j*DU52&aYgn`P6yc<K6UgPkR51ty+X(B zNlH1mFVSk<jv7)M>|RuZ<qg$aO#SRyAxFmpH%^YPXKjB-dYTb2%D*7WgaSx@$0E4T zKWQr!YtX-nPcj8>d;w;Yo5qpKskj)=o`yNaUd;?dIX*tAJIlLV{~-4Bitwb8_DRx| zE2)r5{c5H4Vmq=m3qvVCj9tG-@Pp?1FnSc?34=KVa#t79g!ov&im?D_-pMR?zhO0$ zfcLB3DAAf>_EWI!yQJghRQn|b2ROkm2P>v~jvd?Tdwh<pB;<_rrv?6*SM81+w3qbX zn6V0>8RLt&{UfQ65~983H!sr2ek>ua=HN>j9Xw8r)}=%UGuxItfom>1xjXqo*fGe( zxL&>y_sM26p7`(2|84}y6*M1HrS2GVG<~ZD+}LTL1zqht2+~dRV^}q}@90}_s3>q` zM}B|4wT|9FcS7~~fLVB4%Me}VCI`%l*K+NG#C;U=my(4D_xvzWy_U9Yse`ebOGU~3 z^sHI`pj>OA^46(d!3Ex4eIya@%Mwkd>gv|w8%rLWlk%cA%`GFY?zvLR2T10XbTOk< z3%5sgeu@fm${7%Qr0u*dHLnFy+c-+(&hD)wW+^O|jO2Qjb98^+^NvW(J`z_8eu6B& zbGL!bv|)<X;yOp1)S$%ta&o_A=7e&(AdrUsbpPu|yRJj9I&A)zxB+K&5hbI0V<RT} zTkPXgXXq^KNy0L153lmAM0LNXyKCF@0+>X<Kw;2LkI(8yZTV^rZJEt+)LneJHg|N* z^c+a-m(-?Y<+w}Iyx>Q4cK-;1uEt;NS&NWHQ@S24zQ;i?!#M{fvb6H4*{JQ~d9<b2 zBJbNB7zVzJtL&6US)(*<p3u->7wx1Ey7_SQ>}SHn8yQ9ZO-604BHi5PpzR!Pd0fX^ zJVes@K)nT=(xxum8?u2ibA2<Sg}IY;v=o$%Tc6546$y2yDm;0&T6k7-V_uq7*tRm4 z$sJeXAZc-@BgFvQ{C4VxiEciD)T#KLo4e~@Cpm8K40qc(JMd6Mlt=co1b_KI1KR2( zFRD}9Cj)-IP>XMhoe#q@zwD@aAd3?^S+odlIYBnDlT<tKXjucVBDGvmQQyeFH(V2e z&pc8q<arT)PHCbLmQZ7U^<q|POOE^7*Xxy`gMR*GW0z&d7*<5w9j!UOcV>-OzAK4X zq4s0UZwY|dked+@s|hHIqQLmIrLuR{X?D<<oi@r)u|Or2oOgz~K>@eVnsj_Wa&YkO zeZg%ebaat@E}@I(x(B4@+4mI#6MXR8?J^^?p_qKoGq<faaHnMSCb#KGL}`qDw8P@M z?%F^`Q^7yOV~EUKS)tRol=oFcqP7kHS;w+Hb4TAbd7_7lwnGl2KPPip1xP9__+F@= z6y>C*+aHVz(ZkE{jIW7*o~B)gi2)?|0f9c(z653mewEJ3cPrh^ju=5r&IY?qRZ26~ zD(HjTzA0`lsbHl4matfT`pt39`q+X@19WU2nEeT2&Wh~w;DxtH!_!G&&x|k>W3i0j zh;DSZIaVvKM#Nx?$wSx|_8f2IRYg=~r*sPC_!xO*)SyulIoGEAz1A}~8QbEp5DDB@ zy?K_|qs5EUcqq@JC#>X^FT0CfR&$E<;5<p?cq-SxYEsE=tAsq%G|k{mJ+sT<xng;U zVcjvxqlE!w2DyRyrNHpN#E}5g(7?UX*l=Vjr}ETVuhs8JK|>+mPiG=sm8j3Q?x#t> zWRB$|fstEze7(%f_k%{gzCCi<f@Xa_E%ozNzJ*u#n)zX)6dve5O-$l6swx`3gwHm8 zeLr!N?yZh&G=RA+7THTbLN{Lodn=}n7?v0|ddC+z0|f^8AY^`X^2CMg2T3^Dg9bUl zu;AG%{ES3_!obyHPWMl5e;nIl>TOKR@9nDWO2;J0G|9L>PouKY6R>@<sZt_x)(a2x z1RL`w;bWd2LMV}OQvGG_WKWIk^ASOXF%dP!kxi!5U-xjbbGTLZ*DJPmHwnX0&93*t z^C|Mlm`SfkOB6=ztQu`~DteXn$ypYrZWnG-`qszH1`=BOEl7gYqvvY&!wb?Z3kkmp ziO{5&+<Cai%=#p4?-$=#kenHd<OR>8?)yK}Y4eyqZ#n+?+p^}hA*T70xRbMRc};k3 zSnLn($3yB{t4T+Zr4x0>(a~F8mft$X_PW0@$tCyTzSFDgbx_!%h*pVWTla?(Lv%5y z5+Tfah*(cLhB>V~O8VjQL+)4nnMbd-)#_dCAv|kOwd9wb*>Jn+kNf}$4n((@BX{F* z%yM1#Z5vd7oRNU}K&Bv<!0UlNs(-LCusdyT`V$Uc;KGECTKg{}gbf&>2)ozMh;Af3 zSK`B=v^Oq;`DtUhGRex?=j93L*KV9`MM4#rp5UJgTbS*8$+`~G-Ak5DROZF`5sMW{ zis-|q5i3=VOkT<pk}6Zz7KL3QGIa7&F5m6GnPca=@&wo4_59H?I0moO3jV-2J`}fG zB*6;UO~wu}bfUHO4cmfR!I{IIo@rLsTdpmHAByZRGI<qkRTZHoS$S7dGouPy>HZk& zS&D;*s+IkSY5CqKz)h^O3(_?CTR5CyV$&`-r<uKLMDD1@C9H#?EMK$<1bQ`h!W&5} z%(gL53t%_DG&p9vOS=T9!xnY)8*w@2%_S7DB{k>(u-H4b;FlNckB-z!M8N66LDSLG z*+v>Xm`v(-d06E%3-1_b8oePDP-qECGJ1=L<3lVX3=3PpcrGd3Agd}B|7OdWx>q8w z+4<<S+yYv0&-Z@jX4xd*Y>aV6d%x~=oI*FAq^v!tI3uQ{?2M+g_*!06w{}q^d_D=w z{cvFS=Lksc?2nZdTE-sIC{0dh0Mtc!K8TENz#Z<uJR3eN%`YTAua^bKm-EJc!Q%$3 z*Kxd);`y$B?lDbdEhGC<MCf#5_`}sXKRHg(g8MSzL6rMb)zW=z4{_>D;EI7-r05*( zY!nL+U9I8WWJ@DyjZjC(UGHo~0$fv2O1?`)9bS$DXuh<ni}=yU^wP=Ygkcd|Z4%(V zJF4d&`k5bzsYc|#M7$?qF`$&0K+-Rgqnekvozsv<avE?(LU(QBNmEqR!u@5tYhnum zO3ahxt#D6S9<#Q3u%8>tPMRkTZiwG1fHfABph+>k!=>3$k}+%CFtTFXSas}>8E4jP zTClu03SLYK)5B~1?RhTsdxNgW=P8fF`;!G7H=S^FITQA&r2=~9LOW<r2J+oLDz_EB zs5knC3sWiW+MoHYa4h@Tdg<Ge|BJe}j*If^+DAbY1QbNN1q7tK8w?t0X_28zawG;2 zMMNZr?(UGGTR~#zh5-hY?jeR6>fC<gdEVc9{yv{`&i}-H@4fG}*1oQ_uB(iePsyHr zF}2iOi+ICl@so7y9FrC&Cn(WF8zy8?g_ysuD;2aiZ4COW82SEfyca9wVZAMZ&?}=% zKE)@Wy)eBi7e5!;We3_DJ?fYsmJXA8!bl)2Uff#{nDO0YD1-{yyL#Z*eR?|cDuMjD z8PD6LCVpRSU|PlcUAgBNCVe-r1`j<hf2766>(PGvstz0d<?sJRi-(Oa#j$h9IN0vk zN^x}x7(T<A>rogZIo|c8FX#zX1e!^y`ECs+GsQN>-2=@7Qn!sHImMQd$-}syWYT+n zUYbl1$0(atsVCT;Kv4jv2smu5Ya~Mr7&?oBJN4RclVf|fPceV^tA_uCe2X^{all|Q ze~YpDR9smtOyym%8|S?njzyo~=VpmoJ&_JBqlt8B!}Nh4M&qj;hdfVUA36DVB||j8 zmQ_+Bnr8R?5=rVk(tzxcb!*!P#gD@aT&!$*jW^>dDd7>z^Fp|uX9m|;^f?ZE;{tzg zTZE9ct<xBbx4OB{O18uWbVdq?ABJOB@?kfYSj=J{$?tX@maztVw0i*`FTc!iH%@zK z8H375e}6tvd2Ws<;Fgs+wHnW$KGdS6cDnz&E(-ooHu{8QtqJmaDiqg6A}y_k)i&`u z$(pjiXl^3+(+iYG3aaCc*7(8fBD)UZR^M#7lna6!<Gc8E_NhsF{o<Y@pa3gtV<x_C zykI&J-^@OA!&Dp6s#O{xRX5J^9;(VUpIkZPwD+kv0DlE%>{)3$pX{UNvilblLO5Ed z5FWw5-s|<ueLxlb{(-c2<aqi*guI(Z@01Y=-gXv9Z7bz9u!C|rJk<TS_|2AL5Jtih zu5cs6X{daMR?M{BiO~f)o0_)G-2+}PsB*S|thPfnvef-xP9nVVM|~cP%G3u1=C8Ao z0hblxU5<vMs+5zaFgm&G?b7)~Q;$Im1nEkuvMn$b);d-j7bMJm!$I$K`>(rOiv(~1 z$*h*!L=I*XIQ9*_62y_(#<KIoD&JuKc<ys2ettmU2His39kpt<VmzNzGyXQMB4n<% zVdq6x*Qsu$zpH@TrF7v_$!&k@R2VB#+jd}Qu#iP}OwD|3S&k}V-1rJ2@1S7q;}#H? z**f};r{7~fqm3K3fV#h!{ALTSt9Ozc?7w9jG`?toxuP#H;&#brm=yIo51TEy{P~@6 zps>BYmm|hUkD2@M)d6uitLF7)yvStK7!H_Q>}uX==mK&tOJxqy8Fw17%iC386aNIW zkh8M2uG_<Rw&wZLAi&K>!Dhda2eVu&YPxaENrnQ_{~(folJ9-*Y0~KeSvZT>IP9<( z@Cwc9xIF@~2*3B&Etkq0s0D;}hojbkdmdF>Y({)C1MRy#scIvX)~lt2ai}UGY`A<X zz_zSya?j&Ei9ncJwxJB&AbXpT%?X%EJ-6XtcI}jMWI8K>68H#)rk;#(I%*=zr8MUb z3{axf5j9g}qR4Qrl;RaIl8Q%(y(HN%I`iGpuf$I_sb%Y`e)3|U<{*~`EgI{&3}l9S zWCa#-EiT<N5}GPI-P5Kun2X!GiH>2#=hm&fy_(VSC3g9`<%<H<V)TTXgM4re;=<=A zNw7WJCJSY?DVHm^Sx4e3;`GJy;_O<H?muA~i;6jgUcUWE6vg=@?^@P|6r6>xom}`^ z$~BdKx#!1Hx9K8kno5K5X1M-Fz@_gNSD~83*sD5XXVo7vV*%;c*UeT57Tg7U)7Cyx z#slLtrzcWYH8#9P%{N<0Y*g_t%E7}lR~%N?=R-3pM@59$Z`U73VW8zHA{pGapMDrq zwl73Yv8K;A*>Tn;6hEKZ!zVw7Fyf>XX~;P{izk<X9kX(7H#IkN9?XcXRo8U4Tp1p6 zs*5!(`VOeM>hGvqrw-2Xlk>fgzT5xdq-=uz<^=C(eou%+N)5*@2jyt_Pbv}vRIixq zc?e|dx||l`s!*59e5kMKmfvO%Uac%I+-J|>-9FqKRP$oj8Jy6v8LS5|>*lG<hP~Ly zGmZ+?Pv0ChYpy_-1>c(X641ya_N~3Iu_iT#1s6}7YT4{rc~*v#C=Lf$Y5DS|*8B!i znSr5)v4h7OBZhe-XT#tk?+Z?;;kh&!rjkf+GqJ>)*U3Zu(j_LF`EEU8DMOgo19E?E zUlUF1v!8C;Qwb5b0+>UhUvN0Bk^t@>WaayV(fNN6l}`JAQ5g8YV9WpCfg7xs|DV8* z{@-|v6t?OL2Uqhn4Vwq5mNDr{-XzV{fa!0o{D0vx2pQyH3P80~2KOs0+3g=`?}B-m z2D~e@NUBLM$|~~S<@)UaGaIK)9kppF^9t)i$CLzgjR4y5(r)^LI*F89(4pd+WNS^? z>%5rsljqPyCZ(#mcm>R5A9M3(q|qOxufM(#01_PkhBu;R1x?ZmH$(xxwmVd|@1(ac z&M38-W|Biki-@zjYNHY*(K|P%&auPNR*W=HYwhhBNBSA9paJ<|>AjDL;gKIV*Gup0 zfKrNnc=25{tzjmaf&%V;5(`=jDSr~uwa1Nx^^WJ?gR5eNW9OpbIT*1?Z_pNyD;pF* zGu%dE<pc#6wm0a$yvZ}fl@|ZWM{wV{sJ5kEVoV}4z4!A;IH)d-jBhEfM%vuIUz*GX z%0%%;h1*Ph%T#subu0DVEWiima|e<)z(9QX`HyLQoS$mO+63YM=et7JZjYemewH5& z)+J7@9o4T(g^nk)==)MSetJZ-8>-DH56r^ZWQf<`bK4RoeysUM%Apa8oY~A<s8=~z zyNAWh*_+X;sdZdz%TC05dqf&C&(!rBA0&Evq*G^S=oVJc^sRqEQDOEV2FFDDIlrR& zDu&}Er~HQxirqAQIFv3+11FzDu+m9-aYtrtIYR}vDMM6G^G=FYjbI%1TvizIs9Nhb zue<f|*Nn_xSc>DI>WuC6`=cXtxWG$d>Xc?G??1Q4g7^%@E?!OcB2wfa{Ei-&NN~Vu zsu8=QSmECJ+kqSPw<pY`)9+kUR{b~Px2?exSXeC-f0ZKu#+wJn7S^WYflSm*=f+k{ z&77H*Dpf)0J9=h50%Cbin}#VrH)**_2F9~Pea}W>R4e96`sy;jWP)BKkQV##_si6$ zr)!m5Jv-KJN1rz{%hlwsjk+~2Q(eVMoz!p#O*Ot6M<PjRldIktu27|p9BAUN8D_53 zRaM7`;ALdnuvjz9F>N|D`HMI=vDC(lBlj2vQF-f|pYIPRq0}|fR${lP7JCWLO4L;A zGu4uJpYmAL35dwhDjAS)n>?GVhLz*x0=8Z0;95-;EF{SmZRbw6ipw2p_f2=T=fl7H zVM~$NNCSPJiBr*PkA>V5?~ao2XH4-jD&L->2fAa7Q5T~4JzT8%5~q|<!+ME%w(#`# z+t#WC)slOO1Tfj5=j&t1v=jZ7VYh~>Ohl$nSsVG}sH%3Jr1%yXkMQ`WL0wm$??Q>` zb0UVwKFUI03LS4KhYt(P(GCuLJ2O>46S|o<EVby5>3~_E^bd$Qw9xzPW;054Q>XXI z#E<vwJkyr&OVW4?L4VFrRBkGn8u$A!@%U5_aZ3JBp^MUc;wLd3taEc+EyaQNwhc}5 z^Kr_6JPMZ$Hx!7X`7QaZW2PS_pYLt<0tx1cYF!KF@dr}u!m}QJ+V2;V3Mysuh4f>7 z_2Bf=*Tjl0F*vaq;H-Bl+ixcY4Xcg(WPC{R>Ot7(1a;J%)I{1Vv`})xz~1i7(E+7} z4X(#}9G|x>Hr98?_y0oqGz@<}3F}}UmuUbIjM&b;Ya>}N`?kd+J2rx*i?1l#9{v<I z^Xu@<-s-V(t1Ibx+8i{~V0B?U8>(Qg;oh&65c!aaFv|<Vg_G}7QC859s3N9Cwlt_I zeBZ}s({#9@JFk0&Ewh3w|8|!k<&xD_>tIZR3T#F*jxiM@u&!;SA!m;HjFgipeMO_Q z>l?lMxZ*KjN~ZpX;;hQUASvg7OAmJ>=Q3|jcT)yldg_$Z{>9a@Q$u^V&TPVWagMH4 zD%YR2S%@5OWT5B=2-l^1@au}acL=GEB$C{B=|_)NQjf2>-q|P5t!KcHGwh|`;lWs0 z%^azp>pos2xbn1RKhjR_dhSIe9p&P?&4<3Rh^DcQ{K(d|ID>{Z@G7ZxeO|c6LeH$t zd~^Ul6X#$uXSzq387@?0eEcX$M#Vr|=|pLg#hOqIUHTis{$tYMP`>^2V1Udguu`pu zXHyFcIZdA<fnDR3bo36{etBWiez}^tklt|h)5dqHPst__atgk|Ou60&dlkd)b9N)C zJYIhpy=cEewqMOLUW=X(u6u1>N`uwb5FF6JgCWXLxbwF1w%pBDBBPV6hD#-J&DpXE zipG>MxqgK*&}d7s;H6l?EB~C+b&Iy+{+pC<Gu+}IjTBJ?uYNmRP7B<q)crnX95z~k z#H$N09v`?aTn_}BAI}OM-7J?U_s90`Vv*Xm%3Ys}-JFDi*`4ZrQ#Y~G_fCE+UV9xb zIC7=;-1PH30lL|WoPTj=TKw;z2*@eim1F>|(!vx>&Qda7mva3~)$n3(TEzt-yWc1k zMJzyPrT;RHe!(8QWPI`l45)5$OHdWmZbyCyTsZMKfH@f^xPGyaHs;jGw51=u;aEP8 z$ipk$5RXrBZ%a{G1kN^S`eT@AO!)9R3V%Qp+4ZR%<s3Pmpf=o-?2%&90Savp3GkU4 zMn4{^mfjnx2CQUwr@K%m!Y3WR*^JFA)u~rGBheYk6b(zyH8l0rL~Ys2v@F3dwP*sK zB)9BGr7xjPmkn@5`tU})&JpUA$SR`Z!hRq6_eQY^-BNc%)`!(jx3YtKgA?(e9gCPv zjOTB9oY6&HGLC01s`9x>Gjz3B11V^iCnu4^j1RHH8d~(rJx48gVd1vzrtIdk6a%QW z;Cn$IExu7?chAg1<NU-*Q(}Z?=Z3Q3Ji7NIgiAO1efBEOMuM<+pLik&(z8z;3QNN| zDkFZ*PpZOm541crmE&Y2S+=abuR9bDbmDTKeu}WPu+duJtl%?Yx&K)Xk?JL0%*nhB z*KYqAl`r*^yCr1yP<H}(D*6J&lat9tOMzgDZP^52z;|{$5!MWY9dJS`3qhkHMbyKa z`B^DeKG%F{elHtuD`6Btgv`@gsm&`T)pb3UH$G|#D(41<C*l(aHleC00NOsK@0i;i z=Ct^kZ!H+wb0G@nHR5|nU`{dFk3kaO<+I;4!#+y5+#(j5hEVt)RFj4S@?UBBmQ33L zn4MGCEBhAY-f#tdn!~<d!Nq>;;LTa*N<QEUoZ?q`euN5;I{xm}`0P%y({+EK=Bs*M zp?8DXo9&m=7MG_3<=dMRXXW)8JYydkvC(`tSt1u7*S8!at-3G!EfO2}i-A!tmiDWj zxxmzhtC5s%P~ZM*G$W(VTo!*&oun%|vmMiJJZCD`T<4lHdUKH&c*P?Yw2<bDn89S; z9Q?Lji!l_1hpf%N`CrKu_?lrfz(~4iOJoFb@@In-A2?>%8w|dakt=9q^njZ%OU1r% zg{`_(_Zi1nOk~eYo<*UrSh!9FEWKFH8$KMz_A5un+wP9h21ma?oU<*~`xV7>B7|)> z+~L=-3ybZLT__z})b{H)ss$9L5I87UYgsq>b&4+!UlE5_<9VsfUYU5ndbJ0u`eTjW zy0@4{hqW-6eoM2&{Kl3VV(2$8oo?&IXwuXpCWHyq`O+QaBIzG(HmeuK?i2`8obX(E zkoqombK(JM+HtHG!gl|K_@)LBv0dbZhCV&js~!yVl@y5%`3%vc*d*`}nqipd4H=Nt zeju+_7@gwLmO_6VT%A6KqV+Wxgfs)SqqT)%@1X&L=zN5Ot8CppY~{fhtEuB`EAO(b zX7LpIOkaIr4?WMc)2uyB8R(5jdNnPcfYzRm*@fyxqPp$i3f6LD#(c`n+jMZ9$Lrtu z%w4F&hPoZvsJWH9M!(&LwR|rDX*eOgSSLNGEC9*QFda6VUG89yCtn>$V$<_aZ%ntR zew+A-%Hh6ajy@0MM+h{wax8T23<#`=77wb(X5OF&rCe{eLoIY$Q+;ZO=Jvri0^{6l zN!-6()z)r?8Q02`3DW9U3<E9dGP%=e<!>x-Y;C;M+OP4TM*S3H&9|mP(oJj5KUiG% zr0H*d0W>5lZc>_J5FPg|Ev`2WTW{G3k|@0ey1X9!OR0~YVM+Se`AwZ3@PSw!wX3V{ zs{H7EN@4=r%g4ReD}du?b@D*-wAh4H@pki{b5$^(`o-$nzwF(3_v$5SrU^9KLiedw zf^{1V9Wlj9d*IXUBdNGHAQ8YvoAeMiXfv%wIxYJ~^ssf%?Lrk1c6hkKPzw@81a-jl z`Kj%Qhx*T%n;dA7`X@lHbzJi6E;4v78?@$Ro~J}4qoR7^J`>QFb_|QDQH2$1Nk3if zfF0;~^64l^%W0^B7(~MJL`@QsV_ziS*CmlZy*AES-=JAfdoc({426jKy8B@W?>R7Y zh`O#0!0EIFVH5E?6zl%J-|u61C9rzeRBr2Aj4Y~@eQ~kGh*zUhir#M`uHNHZC8(fc zQ~U~^G!)fVtv}io)5e$D!1Iyn*jz=61npfAjb>q`&;(oMTwk<9wkdB;n%kqUI((dc zzdL=fxP%}A7MwCOUmdqwT^~&cp3U8)OP-$wV#I9T-8XmEL>^sd28toC&OS!nh;L5_ zloQ{S_^n+KF9d{d@&Y23$+UTK`JJh{GB&)lr*n%}Cty_M)x}Et^`YFFf5GFR3@dcF z9G_+4ln068pDj%J&&?Gm@&gvxTp~~qKRNqzo50N&VHHW`uBV3H;x<N_C1nNgUiwgj z??x`iqf_i+3wp*5=!!NUDo_h8@pEjxeeeY?xL7>q(tFS76ol{$@gVRJOp-0pyM7Wu z!)iuu@eRiXeec(Zf&S)Ny5L-L(=DcIIwK9~pTt?olye`T?2*-kwl2|zzAxdwXiWjD zN&d`TU+(QJ9u`ixWw*2?n9sHqefF^OQN^1N{Z-x*t7m}pz(zW$i%OIL^F{1ynp)N! zD$147WBVQ2Jm#P^^O@)eCwny9n<Z~}ag@J!#5`BEc_1|_4y|`5z6bCm^C3w+U^uTt zum9pB&YX&ZvxT*W5BU!Z0E@(XH9Oy-tBo)ed6Mrio6hMkq_aX)uDBssPZ_9!CHSqX z)X<@0JaLm$wBEg&vbZCVMW&CyT`aFI56@Ml{Spz@75Hwuew@f`{e)wI3}xxQHDkEx z!q{@f$LLh>{PRNWroh4Sjg}>rGnAUYXJa~0H09>zTG;lP&VF!wAf|Nm^5!~<gDpny zY_1YGC=JWQ9s?ak+NskGS{Go+plg5BO8xb7U_4FzQp6W%efj$8KX)M@Tz7;&_~OVV zI>JI8owEJHD=7X>Ti+LIr}w1trvzH8&VakQJ@;8fEY}*mS8$Pfp;hv%FIE=t4wou) zqQiGl<Ybhuk6#&t7JNa?J=gRK8=s>UL&ikJ0wOjG-ETj<cF~YUpTW6wGythm1IeSQ z{ZC<!K-2>1H7CZE`Y1YGCIl9l`pf<uC4}J}nDS|^8L{APzNAr!22?I!gCH8hGyqt? z680IuT){;exKhNq;;|7Q5Fmr|_e>;JlnjrbosQbDxU+q<ez-tP@6r21{bWp!c5Cpe zB8z=+{Wh9jK*1j7_FaO!ZKPSp_lJtCitgm}nAuf^#@SD^2@*kD!^hPJS43GCElPLq z<7d@_GA1)H{q0oFBE;8jjTcLc?Z@m$-RbsAdkzs?^mMZ!X4k4as7@VxV>i&=a?}|9 zY(@fV=6mzP_r1coAW$>5UvBeV7d1~x3fcY1#@9eLb`g~7c;0?%W7v$n{kSkWJOE>_ z@e90Mc-_-Jf{m7RRkCSVCtq0nntB-+00(9zhT@%eg>!FC)f%xd)n<%0hs3&QEgxg6 zdqDeA#Q(gUkS2@GypE&3^~xK3xzO>VPie9Zcf?r+o31jKYuFKxsn%q8Q{Ba}$J#jd zi=Qv;{{0Jb`M_UEEnLO>LcaGlfQqL=OE=a|^|%sG7&)96@L4Vvzmlc=Vb1rXiGjSS zpQ0I_#B5@+o&z!FpsM9dH-|o|RL{lOQ1zc2&6+IP0RTDIS&44gQnAS&{bowML4Z)? zUM}Xbff@TgjXYu8oX&xi#2PD8((2RV)q=+0d_q{liBCs-rAk+RO%7PQxG|~{Kf`dS zZ}qd!4H2jBnFsvvc1rY8HhdtTxkWC2WMPvDzMIX2EnHk1kh+k%-(zvTm3VW#4rcS; zb<of<_4$5QiE2BpbiFzuhEfL7oCzf#c5#fiw_HuNUv9NuxBYxG53-iY`XG8c@VwCC z$~`Tevawa58#C;B_2%m_{#tgxoso$LbZQINr}Y8nQ;P-aRa573o>;A{<8$A)po`ag zivbsV14eTcSCK?n&Vo4VX~)-R>wy>bi>(*%ll!R4FuB`31!ppu8`tNCHwP8(87DK1 zjjE)8_5gFj+ap&;uQZK~jrB7pr^YBP|LQT*7#kZ?OmWyrzaF{7GN$+{NWIh_M`jbS z_ln{W1E>Kx)QuW`VVU&1H=i&w6U?Ycpb|26aSSx1J;e4)B(s%%pk2e){2h31buEyQ z+!U~CT}5=XO0q!-yGrK*S=KBrLRNNYxu89x4t11&LAwxGoW$l54`xz#hO~JI0`&il zqD#E729ZM{@8|1HL`On}|70s#9Amw6`Pqxq-FnUYI2yzD5A|9y=&@27A6Wv1!RVD` z))yOGq%a(LE+%`&>vIRX>K_xwR0y~!kNPXEFVuXg+WnE1W2Ex^OI9{<&K%x;%JB99 z!<`4uq<pACf4$7D1Nk~?U3KwY*A`wo9zI{4e(3*<+T>={LOdKpDQdJfESmQpsEnBG zFCHQ&#-#>#WC@eOB+kabX~p}BUG14O%vWc&Y^Hg8XwhRo@Lh2cbRJ~|@KjX0v~cbl zt3>~;l25{jM_n*CCi53q<wa6;dBtBhyVSo>n|<6+!y3H(Beo*NIqi|Y&sp)KpuN36 z)gOfSbSyZw!q`yE8-w<kY@w@;0(2rf)xS!0GsPl=)D(SrZy`95W{eX2olze>C`$u? z$sYoV;XTPiZ(C)H!ZV}nY++2|YB`Kv(fU*-b&7nqr)_yT?I?+NZX$`vbMLZBZC;Po zePE<BcH~QLai?FIRT}RVTc4u$L3qisg3n@Fb1O#*?S+m>2i?1Qu2=3gy$dAi;bTwD zCp-^dQVF?z)xg}xIN~BQC#mM761O)IWbFe1I}ZR)Lx09|>?PLn5Bl33LlA~{ItB@r zhVO*Xt)g9N7s1s<lVZFs38}U~hs8grC&QD!%P<z?le<~xluYRU1y45X6i<IFhX?rH zL7yV!?LnebOg@H}M;~)ixR?O>X!`=^Luy|!v_#Mzh2b&CxjJ(FChqw1`Pc{j=9#Ci z-5jFtAV5tZ)}6kRz^Jp}$=B{w>NxXa$c=Q<82|x&YXBtk+59O$ZV#&38ffg&VOY8H zo&@bx^bgy3?=+`&u4ix^4*LdhwXV!5?eNdb5bgv5qob4l8pL}8WHJGFE*|`#4o>FW z((vz=eR}UvP(hBfO$~w0z#N1tKd1$1<&1b=$IfWG=;j7N&={dn-Y5{}=yMwT3qCdh zDX7yo>xbim$s6KY;I}qkgc6j8UnA7vxPUg0tYG#c=G1w)tdr}Z>Xt{}w(GhqVwI$8 zKw>(P=D;*k`D7}}OWAAKnfFPhw;bigduO!g<|{6XE-{KJf@kqE+mo_!H@YqwroQj< z)n>?SPqrx#{W}*#z-?e!F1A&p$+q|RfpA_N24nl{oeGyKKn?#B7H~rTLYQ9X(cZzX z`Qq#9k*D%Y%}7gIrN25I4-u4Pf<ioXRe|2|huqH!i$$wHn-0#^`32hieyb9g-r56f zIIM5!8bx?1pKwDOJzTX69O)*;fYeV%&yGYO^C6e`w(_6M$#6$NVdn?+$!DpCRyz+* zR+PuPuYsm4q}ls*akc7JkNm<!0i)L+xYk*Z-mJdAJpx+ze&lqQt28RTI`R&ipjvc< zhvG!M^efK6tj1?Vz3~cO^A-KDY)xeiDW&^ZgV8{okPDZ>5=K-NGjh#V<;fyaX>&nO z3AN905Z0W@Ig0Cf2`}wm1n5BL;c6Svfyu^xN1e;ALUSs^og3Rk+BMKrBDISSA?shm zNUp7A*8d5Y|2GBz5SNv6XJOOh%Enh@Po~Rt#wKZK)2#ydy<5yS{6w>t(;p7B5Z>zd z%jp54;!_j-)%W25w*4Z&jui9WEN|~evy_#E^zVE4hrYKq_Vy~u;{0s<ST+Vfi%RM~ zFn(rcS5fgRd$WQcmfOqarHb0H1q_M|`c6V2z_w`MuIsx!0otXG2Zj*OcGCcvsPU76 zic>y~lGWRTTenAA7knF;qWqdLu}ef)8uz$^3WzekMc+F#kE+7Inh_WpXntJWW{aQM zAhTqHjxA{F7ZV&X&F!B16j_CT^Hbutkb54SK04>2H<g{v7<Lx5Oeb%Dl(D&)&el?T z$IdIk&QPM!BccOhX&*1vE?T4&{G`Q|w%G5<9~4@hY}ydw_v8zAC^zWNm2*U1DaTHC z`uWiBfqv6fla$dh54wP*DIrl$Mt~bgpFb%Z7?nvV0|L~(3NPGy%rywt&efcb&*|C0 zmmTJRkx$>Ozp09^o-ASjSNaQHR;q|wp3^(T^k3@aKS6~4@2=<|reLi9{@y>e<)^#1 zHs1eD-0_e7(=|=S3@GOVvkrgwJ>}59QP$U=9|iqC_6H&8ynICA8Pj=UYo)+H9LznE z+av#cfLVMrQg@(yH@$i;tu`WmH=6+MO#kx-6uJ5VRPDz+w|9xnKNJ=J)9030W{H%W zZu`AMC?uHie$$a@uV%iNw2CC;v+qWb;jAvUP<iR$kp46Z{*#owvwwC|B{m_JwlFFN z;Z2YKi${Us?+n-WDwT|tFbfRIgXxOk{JWZhv$yQRie_$}%KOrLHS_j^qYQbW9HbAm z5w#y|U1dnTz~J~#88d;ovFc=UQ8J$VWeu$+WBR0Sn3;U(tx><kNMAOQ*cYjcxO*}w z=(K>Qzgsb|%s1_>W!QYpAG&?Glw#R0lvKdGNW%T^TbW=Dj_Q-3s&5>8DE;aI(Im_j z!ig=4LM)#F$!Ns86+p4;!<}+JWDaG4@GH(ME&bJl!a1BNocu^9L3<S3SWfl#8%zds zsV1WlC^wSEW?{~x`MOqUuXG9izv~|}N-a;wxZvsLU4^7gn*6TGG+XU|iW?_T&zkcl zv(*sR+;mjQaM;HFhV4tn^M-VFM>}OH@5ctS+l^e4>m#*_Cy|POA2Pk(3P?8OxMQVX zZf~GzP~B%@A`3DZuC5Z8jY+hrB>vBWO5)fVPal2dy3ovn%T5(h%ljP@WHI(D{NWsP zm0TdsR_uNuw}XD@OioE2*)GBp15Ke1I1-d+ijm00^?6p(_0Y5MpKrX!zWqZ*182Yl zNK(RvvaXh{x*XN6b?97Ai%-91jz~ZK0vf@@V)U8)yc+t^ck!`4<Y>&)J`xV1GYszt zbfG+H`X<yUb+KlBI8q#J`oI6AOVf7MwAkvMWBkJN--E58Z%Svb2a&NAg6$s)lfoZR zgk;Rz9F14(xoCUsWPeZqB>J=%PFndMWybGadv56Y^Z1ZaGTp|~csV*duyBh?P#vS* z$1RzZew!cgQw8apic1uER#^BhEOcwGey%8a6s6hft?)aylKMYu1suMm^DF+-s~6sT zUW*Xn4NZ~wr((zh8{EZ>Cu)b{$};FyLt}|Kp{7#7-I8td({w+rw0k^`IqYGn1TJZ^ z&u!+s9tRl(699YkTA5X@GUK?KuUII=XXkOyk11X<%|^xhy=o3FBl_N)-@p6&iG|Q< zk6*SP(QeJCxF=PdIzn?07oB*2Q;FXnP={dGw2Y0AE>J6p6ajX}SX4T&_ap^)rXY6g zd!mJ3_1wP>-fa*+rt!*e;Gzn_!QC;{mX$~-FqL4u+2N2<i771S8PKLQ)BmAK!}f1% z>P>whU!Q+|8P|t)X1Mj&gTimw?UtGY&!`Z8*^)x{TDQ{diG#yuf6T}Ej5U<mLhySF zdC0+VhGwIC2A!HM;SoZmk3KM3X)O(M1e1SEFXk-%52;MW?5c<*Qv)3L@6k;!(XU~i zZx?zCwPuR3KIAo)>aKrclxPB3wKz4Ws_)Stn-(O>>>qtK4T(4gRspV!-bIpm$EupW zIk<cfDA+!Cb<cb3?SH<}2l9U<Mk*a!f+MzZ2oAhmQtT2CJwo7N;dXXUh`ZbIMB!Vb zz+mv_j~Gvy2BB?HDg3NqhdcBG=MMU&4X>Cj3)@sN;p;0LvxcpG#-Db?94mwWS0v`o z6V3an-gdd1AKNkfrn5Jq(v|%iG1k4A2=*wrf{4ubbt6Aosj!o8*_s!eUK)y%xu>`G z?>=Q-{!F{lYxIC-gl*?d;Yq~c?(M^$<Y_2H^J)?r`F6Az;}LONz8o3QObTe8aOA|N zYpDLMYEzZD2%_-e&GdN`(V8pqICy^BmPOUVt#x1FC3%guE1+31|Hb8RZ&*Lu+okw! zM#Vz*1NXMSPhToLLqzUMX{&66Y!fFh8^HzSE<duGmwFmpQ0UZ`LR$kh9kba>H4ZK& zFNU+{$1kC+@PXa7KZ+5|QZF&AuMJa{%T8;EA6Ki9)qiptHQ%*BOjde)?!JzA8$|~& z_cH%k@h0^CU0{<Yoy*5BA-%h|LBae~8MXBq#95I=j`>kDDs=0cRTehaJ0@Hkui3GS z9>*w8vA%$IK3~V=m(&G>%C9J=GQ5qEmdPqhN=N%fpczJpr6blPSYhC}P`6{^j-0(y zv750GEMrqJ+#T@5RbrG6w*Wo;8P{v8pUV=&*6bEB<#6-+tf9cE=fuo*0GFOnZtW<( z8}JbR0gd}sQ-~%%zzZ07;@9qbBT+;u4{b;17LcC){1)EpohjkHGm|g3!VflytsgdX zZz{`Ne-Lp+`<p=mXov9qCk69+X4KWG7gXF`@0@Sl|J--m%k`|*`?%7P>8q#Kr(dx_ z{Z@+b-=KJ^u7$*-XNtFze^<}&t7PKo1c@a(YT`<~E--J7UVPc3xy7Ej6bix@3OsS% zr1=zJpDPe#?zhzkR`co(%o6dN1pm<5e8us%Zq7>~Ve7Oiq0!N)3`wgW&rhY?c_zBi z)u|Dl-C9XC-!UI=7{-kNcc}kpEdU}8{is_6d6pJtv-Y&(M!*GoJVG=>t$<=GzD2+1 z@>aDHYsXJ|Uz;}89Rqra<}N)8+ma!{>2H48a{iW0FKqz{hfb0maOwY2`eB0Xah(ch zTB3a1TZ`NF?l~(t4qs@1>eMVYA<`$URk~iD^=D|`<=&EYqXvtG`Z49eYo{ygxt2XR z#hcR)rK=rqP-mYw7v^bb`U~ckW?Lqod?zgSmiEB+8ET)hrX7nT<-RT0!4eYcMs4^| zC>ym-5rrXgz;;oW&2Bs8RWrN(*mzU38p}!aB;XRfoh*rbvU1a7mhS&`M;L}ZPFW#C zHnm-94eL4Cp!Si?atYtQ1xVm5$bO!ko}k#;&g^3oqaBCr7;b8frsTDb_)8gyw;o{j z#!&dZ-F#Eoz!F`v5~@j0Z+vTZOK10b{r1T_ZFHNL)be3+OdIbp5pGo~(qLoVNj9oQ z=g^>OtnyhG_P50&E|zo-u6cLR(GxQ#qYd+LpOuD`s-K%B!#0po^Ds+~pJI};R@V{+ z;nJ&@ko+EL$Vdef-tOOWMTsX1>>n??Nq3lJ!dk#}>v@obMyY)hMhCnT&DJ|VOct+Z zGv_zGzQxzz3Led$zi+X04a~}yvCZyBMt)WBZ(q5lE5e%nU527?FAK;M8Saxgzav%H zG0mzpM68BP`kCyDzt{kga+NslQ3T!ahx;gd6ETSmv>iz{4863l?fc4FXUO>NMMGbE zOcWgb%)mv!8f2sJ4cW7ekXvvcUcDK)*&~8%Q9gCL=p74Bslf0l?`dy^S-gY!)IW^| zRaT#oN${@>d9|>FMlbR}B#ep|q=9ZF1uU7Df3n5~Kg;8?$o8%U)ShTK5u5JL7<A@> ze^$hs03BQrEtY;M^;tmp$okXJvD*D~_sp*RUL|$BODOT?uq{^Koj7u+Qi$G9tcrNf z8091q={nmy7}Pv>lv2L(!tQe2B~0rMayLHv1OrX)D2gs)mv)~PTcFVA${R~wP8Bod z%FbldUv(Af>}4&0=`sV-D)s`axd=}z9_5+B>en8x6?E(KYM;EG+X<O_2scvE+q5pO z-cj~<W+yLcq8gm>+zuJLjB%Q@$<z_FH9I`<v9fh-*Bn%I(bcYu9!U0^@LVgXmKaoY zsHUgb+6l8=YN=m$BH|{0nXbZT<XG-mDCEg1lLap+jAp$sjH&&+9ecu^yh^6m!wveQ z@03n1$Ys0^FME<QV706TaZ~gT&nAQhdo7+@CctGm-Jq=$mSO89)cw+9MJN7VE&XFE zR!MKmMF)(db}C@J4&e0Kq!)PeO9erdHHMjmx{YIx*d6O{?@^mFH~0;7d_mZk6Gx^D zZBS!^*gkuXnR_J6N(_?Fde+n?D-eGS+3Uu*fL`De^7-4hNtJyhKr&neR9O9fVZ-Xm zTm9qqX$XH@E6BQ8`*%2Vv*HDd6<9L6RBg^V@9KUlV%3}?(|=iQcfYQK&}7<Y?L>3e z2M??EKH3u@O+cw8diFimXyC_)!AbR12bdl1WwZ+T>v|68;_WV#?<n}$Z`Y8^t-J+? zcSP6{*++p+qD3dOiR@2)fxwT9WF^EyJk^W&gZ3n58jjxj-6r0<V4cRnIMZ_t)4>oc z`mPzzDKrZ>{mo3x>?1uiid}{##%%makC;9Czt$>DNUu%|Kic>$E{=^Zi#kq!Qv^-; zm9c(kk5(vJas>@3vP*(pTGp!#hI=D+))S|BY)t??)@aJasSWjHg{YnmIv(?o$}>Wp zGdr~wp6aWoYOvjT)<VLNFT-B*EH8e$_^iTJkJqrVf~5bsCVd(L8`ZO0TpzqNr|@fU zZqBLo*{UT3E$vbcU1m<5>rT=C7*lzuH&O!U%7jnsqenW6qSXq<9@`Cyr3bKPmE z9#&*CU#8H9Pnn#(*6hXG3QEX0u61}$?|tvFZ9zar+))5d@{GdI4aW{x1S#wJUbTYm z{w30aejX;0$Gan=64B2SRPNvM1H7*i`@XViS8*5}GuLf1=O1*1+hhZWc$O2>y1H0E zIq3yH?O-jm?v#`}7D;X9(U#A*&lFdCf%|ULRo2mw%qaZ}mTyzQaBqiRZ!Pb)Wb@t( zDsN_^n1znqkTIX4=){O5<lu(+uzKj5rb@SAN1wNE7}n!Oah(D(7fMAE%h>qOWq-8H zs9EdD*8U^R6(2D8BBo|4Zt?Wmq5BP=@7LGVFqNb7B831_Q%`#T08=en4_0%Vc{2^q z2^n~!OTB!QW<KMNSb>E&Ig(~j$~Q4RG<%1Y3lZ_ff($_}l#k7bq@U{z-zw0sZpdtA zm0bgEa1sT*Mm*_E!!i!2n!u%P>=$kL+2b@@ddu$IDucf96(;l<>)EAtA-R*ZM4p7( zznWkmzkA|y<D=*$>r(WH^(m>?KW7eSLjlshg0rFJ{q!5SXno92<@fd*)ac_s_V%?P za7-chHzNB2B6ps`EQ7-y7m4LmabI&yEF@_bkUfg!=(0=NNrsv>s$`c!U|mzVMzn$8 z&7{U+L;OkV`J{#6lAB;MS=IEijs29CrZZFle`_{*#&*Mt$PQ!&gJz$W0pt<~m^q{V z&-p5h5o7N+ySwM9z?&@2zCnPyM0E_*w24n&_F(CHe5jB`L2BiXlb+(n<U2Rd75-*I z?&&Ub4%}~>U3BdGk(yPd`?j?O^!%6B%A=~*acBV1M-LksqZGHF5!2Eq{TmE8n7TRc zg6VJE(y=uZSC=0mim&5}ef#M&AM<}8=ikUEJc=AcZ~-b+DNWCl>vT&zHH}KBZjfXJ zmAr{#m)cGG=-pb+8-z>PqmPY9IvWY^vV@a+&UH1lHZDY#fYxbH9bctMY*;!7zYjRU z9wkC-A2uh9X0JXrKY-<I>TwP}lojuL`rgxBY*`-mEj+R#`AqA?c*C%-AoMmVTP$+b z20era&6|@}^6PkP#nu>*;BAu{pB-6uQfSazdCDMa$XpgCS=Hs4JrvAZI_**`IwraX zG%^$f@kr8kZVM7qL41y$g(P;39T7Y5m8TBfK3Hn{Dl}&>IGyU+;$MEemS62vv=cRS zTto-`vcJN+R|p)q`G`JqJyiX1#hjvd+5>HCv|F=2MyO_;8}aVRhJX*)+z0Cc%!iCO zq5cN}-tH>ih<lH;gq<9%lua;N{{g8jny&NSc|_EW%RHX3Or@D$?38aM=fVlUFA%P= zc%=RWH{hW8x8FU-GOoi;Kd2ip*Es{)YF|ZQrxr@wsby<WNBmN|Q=wIv#OJPB7V)j% z$c{iP$j0SCj-ZwY=8@is*w{rJGvLL}(3<Pe!rhx`;+Qw?l=ykd34F!VroW=c)?cfC zd57c+L%w87;BTY8$D@fGC}FaH&l+TUW9|}4p6z=CT*!e5B6$FX3Swd=3okU4(E1`q zR|1G_1#eoO$4j<|Y8j`jhvG(y`9A;N6=L~*z>ccg)!e`@$n29}fT&TV`N*Vq_WZgb zYr=Ew_qD*q=0p~Yr@k$Lj!M1!Grn~p(JM#8L?Fq7k|`NR;Zxg(1$RaScPHU~316)O zkbL^f)&d4oW--$^v+ZEw4HK`467u69@3eYT8V-V-o!SdBd2s%2LWD&BC<@D05=jj` zK!1?{RI7~dR)U???#;Evz1woE>*Ww|&#d5n0qx4wLuvXxxn~Sa_`}rd;n2IZ9{%lV zxzk*hJ74S5Emyn~ea`h{`%%~$34w6mrq=b}s}y?AmAzr{7sbbt1JRT#kTmPpFCsYQ z?-C*@)hzhv7m$t16~iq^(>`-pEVAhA8Hf6p#tfuPovL55A-t3rOVJBUBjNkK?C+0I z{iS~L{q#p@h5i#W2rHr+ihM#((1_=X=k6=tkMR^UHx?_9!$v=9vw#p}k+<<O9G=2n zr>--w=pF`;=)qb>RvX4^j%9Z=_2}hG&uC+=iWH}CQsy}&92-EHiPfXew}x-_Tu2>< zt@Iuvw^unX31ffDs8Ya2(CDjo>=0k&2fTZYk<&QF0rhVeMb^yL(#t@4t_)aMz!ctJ zP$K_Rm-cs}=Z8^*owpDdbWy3a$&4*lk0M7ouev-63^##*<9LT2q}u%Q%;%Cv&^o1E z%c)ivd#liY^9&bszLO{WZgy7DKsAH>rNMw^u$Z}u$MQqp7MlXx)0x+w+Sy+q!y~=D z$42}*Z*6od7b3Q$2~&#aIhTVrLjk(n;oin3p<j2<)KqLF_HIt|Ysc62j`exR_%gVj zWJbY71CHb}!JNq-XcXcZ0M>1@)?o|=1)DtXpmD%w<k<#TDs)`UF=R&l3QO)ZI32#X z4eA8kdCFIe`iB->Nzg-(rr3JTbLCX((nK1mSAj53z^NuAn*8{ls2P20k7vRl`3bTx zG&fz&qLSSqu`9RZ;c))!m|-9FVjZX5a>uHRN<1j{HVETD=s;)rm*gKQA{4Y_braz+ z!!T7=A>1QDXck)>6H<&;U1tdZyhe&q>$A|BB>lT6RHm3pI5mIbySHyL-kbI6Vox?h zTSyqIWwMIt-o`J$^a)p&o$~pNjIxUN|C}Ls1df_|)MH&4`8fiaX**riG1p&biHGFn z^v66R+(Wt7)*D2*;FX>rprZ~$$hoiQ4ir-5$-4YrBRY6TO~a?!IEnpF7T&olZ~svm ztQB>?E0Jnq!NugU#$eV#n`NrZ)<mr_yQ4p=vYU^y68(~1-nMxgDhBv(vfv$DZC$Nr zi3b^+HNER)gA~XFz&z2!m^+cF{9%2o)WBqAejv+0^wPxdr}X>2gvA0`_?Fqtzw-{7 z(5tM)e4J?6l%nOh4%xgdK4nDiz=2hp{}e?NUOmi%{<K%S!NlALrqh63e#+PE$M%vC z7+se52jRX)5*2z1R86ByW*F?8`rPx=-dvC13Un(Y*H^M@>n?S5r}T$&91p7i1GYzD zKV@aY)Y+rVxozZ*kLiub#`AqGG*K~Z7mpfp;-uYD+QH|)%<hsrFKXCQE&ch#UHYx# zgUV?5D{9*36wMMm8Le{Yg&KyD96*ARXH8>tEG4V;^#_y3;JZY|RypgEoz%<l+$r{8 z;v`+Aw+|jFX#Z-9$ILFXtEaSfb7aXkUH+p=TT@fOXSz)?EDXLQ(U#i#P<B+nd;fg2 z%@z2qoW4J@xSnj<v^JZHtDpXuoPI%9JHos9Sz7x%SzXhu_W}@C;XWWHAs_^`zF<sm zrBM7_QcNBrvmZ7Db2cB4zE?802J*eM#uZ~IkCb@b_!hVdpNy3!UY=0<|2VO69yqAA z$k(DM$hURyi_~1~eG2md5%ZH)f{p^REl7`X5FG-Hu~#>p#XJ}SW@YWw;$hj8;d`d? z$FWmBiPYrpABb6}EwtG6y|7~6D)&B^hu>>g<2n_*S;QYZi1Q(916jW>=%CE!=U^*Y zPDbX4XapXgUQaxssFcRKJBckkYH(OxeJ%}~p;k+)NJBDj!#G(;D9#Z)6H_S=bj6re zhOFK622|`#Y+YEb-`9D{hA*NSG-mH5@(MT07dAWhxVC<o+7=<V8<_EKswyco*#b#Q z+X%&NWDLBpcuI)8%w@MpoxP=3f$FOvkO=6NQ~bTXloRZl8%VR4%WK@*(CT{l?kZ!n zNFOjA0&1+*zpt#86O(s2^uD+5({EDAPEfH8+(ijb1}dmB4th@qp5}VNM!)}6UlB32 zn_%9;biQFnLF6(kPzmp~v$3(r{=KVy_D?300b-&d17+2zA8A<E^ZE)XwYLq@hW$q` zJ7C;BuV(wOwbMtYlhr*PDAboWQ81TN$7oacqG($lf*YeJ6xC2n=ZPk>kEF!bgrDSB zPkQFxT}!t<e3M^0OIt%NC&jp5DnxqN7~AD{?1SX{j8l>pebA-#*YaP{STvvp`?b^; z4>t3h;fyn2VLPJg43{p(V4oG*TGTXuPK6ZbRXIQm*%AF)7v2@RrR1i|;%A|0X(dDY z)Mjex8}#*KM@j;ohs?dSlE<?l(O=BzG4Gf&WSNelC8i%gJ*gt}2e3_00D$B^)40Er z7vMnZy%~B24cTc-9MTM)W(9IKncsS?-@?LL3iL->jPl_9Htz!dC^6Bimv5UhRSd-B zeztGd`mevo@|}1G4C{o+zr6VsoIhlIqwPkg^6^<=`M~K5Y1VZbKn&HsRBHuKm0|Qb z;v%2qWAF(jHD1E7Cjs>2BT(J~jXHmEV&7jrakbA-KNSF>xZL=~!Ush8&S=0{xKv>! zbH<co5H(w~cF)Q^Rw;c6!Qp{S2>cRY2`YRh#Ty6B(mh<vnS}hLTZT1t`a3^K7U()$ zRu}F(rt-B|)O>W2M%MU4)7K2<XyK$BW;vy~b#!JDqm}tB>V7Lx%v+azeb@pJcV<+S z@UhWF2LMip*u#+4M9^QUg<j5OPv`UQq7zsrzTchrg?|yO_F}phHy3q9<IMw~^O7Ai zKQ@b-Pf`SQbT0peKnZ}j1>L+Z-+{1xd~<NiS8uU(BTh0FS?MPct_g)V8zHBrU8Lg6 zdDIg@NZJ4ZEZxz2mUP?0Uhi%Dr=6D@Kz(mC0lZ?e8cU>L=S+W6X-mGu(k2vmNYR3B z0hZlfhm*m6t~ZMGV^A&G7Ogeyt2W}NBTyap@BCcpzJsY{+Fg|bpERAkSX;3vGwKeX z-_ZI03+ew$H!4~BJ>ObsJHF^IuS|DemFBn)AIfU);OtL65H{#pbl82v*FXM3BR%~O zmg!M=e(_{Gp3dB8Xn_A!Vi?OD+=<7};z-z=V2ErjMn>t{njLPxx8;u*HJ_X^o}Jel zd6jwCV!YnO*vwwa#bsi$b%*|R%bY@V3VLNQK3ehiXV}X6<f1jQ?Xcl<Sxou_9_ao2 zIlKd!VHl%vwWc{XR+Lpd-2%5km#*D5Ga{$F-bKcZ-)Yp=Cyz>@=|#NW-u=i2&B4e@ z3L!WT$*MPgMHIUMrKnAUG~ZdubS_?{WzBu^DRy16Z<HT#qua@VD&+r04iJQIx<siA zU&MXd$>%eve<X}$7)t9le^i~E=lX~e3gRVJ?z<RO@{XP8kMy?LSWsgP*~!B$CHEX? z3JBxYebyq?!Yjx7#`|t@qk`c&u0<;3=n&YpLcFuyf>JXO)5_}h9kXODu$*n*BjLAZ z?&yLE@k8@JOioMch5O!hbgI{m0ue%ul0jzQ&y?(?0iBLE#>k24i0r6gpIjgt_CCJx znW&ew6O~b~7V8~qKF2RKw7rVTGWPbo&Msd{E2ItMZp@~fVYh8Q5|HNu1Rqea!Oub3 zXTvLG3U$dl5fTc9o~~z`rtMEr&~N~bPpXt}IQF?LFDfCh2_&lAsxv?s#DEjhC2#*r zI{wS);+lmj_j)?8iOr~RdVP6%6kMqR5FPL5)xC9YLJShFqEkgCAot#@iPlwW9mw;Q zc?#b!R2Lx;;2D(~5&Rg?5?7D=X5~j)t>j62oAwKU)*4v5|Lv=bStjF>-1=I4vfPE{ z5enS1B0DwVxS*CB+|~&7Y<eVi;G++@=@A7O$_M)TZim0GoWZ&VT_Hlt5aPVpi<8Oe z?U>6J<2p+vNdnP-sknbw_3v3~#o4zKyXq^dPcMG$^{=bTLkkTaZCq_I9eL_3S{Ixr zkaf%XVTy|1L*Bgv2)8}ds|St8H#aAHcPjcMEzs@N;C1!&K$}dYY^8*sq^*TbN-^A? zn}>wY2M+6orIpR=Mw&S}9qQO8C3A$mQxN{4ET9y38@uuI-6wZH2Qdc+1-WZSmp&or zP499(xRKfRdBtm4^|AIH+3%wOh<V#P3c^>?Wg+2w#O;MSm9Fk!Ju%0D)b9okDGMV! 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 diff --git a/public/assets/opensans/Montserrat-Light.ttf b/public/assets/fonts/opensans/Montserrat-Light.ttf similarity index 100% rename from public/assets/opensans/Montserrat-Light.ttf rename to public/assets/fonts/opensans/Montserrat-Light.ttf diff --git a/public/assets/opensans/OpenSans-Bold.ttf b/public/assets/fonts/opensans/OpenSans-Bold.ttf similarity index 100% rename from public/assets/opensans/OpenSans-Bold.ttf rename to public/assets/fonts/opensans/OpenSans-Bold.ttf diff --git a/public/assets/opensans/OpenSans-BoldItalic.ttf b/public/assets/fonts/opensans/OpenSans-BoldItalic.ttf similarity index 100% rename from public/assets/opensans/OpenSans-BoldItalic.ttf rename to public/assets/fonts/opensans/OpenSans-BoldItalic.ttf diff --git a/public/assets/opensans/OpenSans-Italic.ttf b/public/assets/fonts/opensans/OpenSans-Italic.ttf similarity index 100% rename from public/assets/opensans/OpenSans-Italic.ttf rename to public/assets/fonts/opensans/OpenSans-Italic.ttf diff --git a/public/assets/opensans/OpenSans-Light.ttf b/public/assets/fonts/opensans/OpenSans-Light.ttf similarity index 100% rename from public/assets/opensans/OpenSans-Light.ttf rename to public/assets/fonts/opensans/OpenSans-Light.ttf diff --git a/public/assets/icon_blue.png b/public/assets/icon_blue.png deleted file mode 100644 index 0b4b74e0f0b52daa7bdb6aa93873c47f7bfe3fb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2353 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 LbaL5-K>2?GuFT>E diff --git a/public/assets/icon_blue.svg b/public/assets/icon_blue.svg deleted file mode 100644 index 131410b093..0000000000 --- a/public/assets/icon_blue.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80.43 80.43"><defs><style>.a{fill:#106bb5;}</style></defs><title>wazuh_blue_iso \ No newline at end of file diff --git a/public/assets/icon_fail.svg b/public/assets/icon_fail.svg deleted file mode 100644 index 3fa9ae80be..0000000000 --- a/public/assets/icon_fail.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - background - - - - Layer 1 - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/assets/icon_google_groups.svg b/public/assets/images/icons/icon_google_groups.svg similarity index 100% rename from public/assets/icon_google_groups.svg rename to public/assets/images/icons/icon_google_groups.svg diff --git a/public/assets/office365.svg b/public/assets/images/icons/office365.svg similarity index 100% rename from public/assets/office365.svg rename to public/assets/images/icons/office365.svg diff --git a/public/assets/images/logo_reports.png b/public/assets/images/logo_reports.png new file mode 100644 index 0000000000000000000000000000000000000000..6d3a965d446fa6456d8ae5b8dc3724e03027d5c7 GIT binary patch literal 3195 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 + + + + + + + + diff --git a/public/assets/images/themes/dark/logo.svg b/public/assets/images/themes/dark/logo.svg new file mode 100644 index 0000000000..ea25e5d2f9 --- /dev/null +++ b/public/assets/images/themes/dark/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/images/themes/light/icon.svg b/public/assets/images/themes/light/icon.svg new file mode 100644 index 0000000000..cc9df8577f --- /dev/null +++ b/public/assets/images/themes/light/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/public/assets/images/themes/light/logo.svg b/public/assets/images/themes/light/logo.svg new file mode 100644 index 0000000000..a931095bb7 --- /dev/null +++ b/public/assets/images/themes/light/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/public/assets/logo.png b/public/assets/logo.png deleted file mode 100644 index 22b7a9ca4c15907fe74d4ed5d5563f7f33c5ee26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3659 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 d&ftFn3;wazuh_blue_full copia \ No newline at end of file diff --git a/public/assets/logotype.svg b/public/assets/logotype.svg deleted file mode 100644 index c313d82b76..0000000000 --- a/public/assets/logotype.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - wazuh_blue_full copia - - background - - - - Layer 1 - - - - - - - \ No newline at end of file diff --git a/public/assets/new_logo_white.svg b/public/assets/new_logo_white.svg deleted file mode 100644 index 3e1ea7f078..0000000000 --- a/public/assets/new_logo_white.svg +++ /dev/null @@ -1 +0,0 @@ -wazuh_white_full \ No newline at end of file diff --git a/public/assets/new_logo_white_wolf.svg b/public/assets/new_logo_white_wolf.svg deleted file mode 100644 index 5823a570ee..0000000000 --- a/public/assets/new_logo_white_wolf.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - wazuh_blue_full copiaLayer 1 \ No newline at end of file diff --git a/public/components/common/hocs/withUserLogged.tsx b/public/components/common/hocs/withUserLogged.tsx index a6719082dc..a2c541b0a0 100644 --- a/public/components/common/hocs/withUserLogged.tsx +++ b/public/components/common/hocs/withUserLogged.tsx @@ -14,14 +14,15 @@ import React from "react"; import { useSelector } from 'react-redux'; import { EuiProgress, EuiText,EuiSpacer } from '@elastic/eui'; import { getHttp } from '../../../kibana-services'; - - +import { getAssetURL, getThemeAssetURL } from "../../../utils/assets"; export const withUserLogged = (WrappedComponent) => (props) => { const withUserLogged = useSelector((state)=> state.appStateReducers.withUserLogged); + const customAppLogo = useSelector((state)=> state?.appConfig?.data?.['customization.logo.app']); + return withUserLogged ? : (
    - + Loading ... diff --git a/public/components/common/logos/docker.tsx b/public/components/common/logos/docker.tsx new file mode 100644 index 0000000000..110805592c --- /dev/null +++ b/public/components/common/logos/docker.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +export const LogoDocker = ({className = ''}: {className?: string}) => ( + + + + + + + + + + + + + + +) diff --git a/public/components/common/logos/github.tsx b/public/components/common/logos/github.tsx new file mode 100644 index 0000000000..1ba81aaa8c --- /dev/null +++ b/public/components/common/logos/github.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +export const LogoGitHub = ({className = ''}: {className?: string}) => ( + + + +) \ No newline at end of file diff --git a/public/components/common/logos/google_cloud.tsx b/public/components/common/logos/google_cloud.tsx new file mode 100644 index 0000000000..20392c5d3d --- /dev/null +++ b/public/components/common/logos/google_cloud.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +export const LogoGoogleCloud = ({className = ''}: {className?: string}) => ( + + + + + + + + + + + + + + + + +) + diff --git a/public/components/common/logos/index.ts b/public/components/common/logos/index.ts new file mode 100644 index 0000000000..54bc2ab262 --- /dev/null +++ b/public/components/common/logos/index.ts @@ -0,0 +1,4 @@ +export * from './docker'; +export * from './github'; +export * from './google_cloud'; +export * from './office_365'; \ No newline at end of file diff --git a/public/components/common/logos/office_365.tsx b/public/components/common/logos/office_365.tsx new file mode 100644 index 0000000000..95f3e062f5 --- /dev/null +++ b/public/components/common/logos/office_365.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export const LogoOffice365 = ({className = ''}: {className?: string}) => ( + + + + + +) diff --git a/public/components/common/welcome/management-welcome.js b/public/components/common/welcome/management-welcome.js index de73642487..9d7ac56fbc 100644 --- a/public/components/common/welcome/management-welcome.js +++ b/public/components/common/welcome/management-welcome.js @@ -180,7 +180,7 @@ class ManagementWelcome extends Component { layout="horizontal" className="homSynopsis__card" icon={ - + } title="Logs" onClick={() => this.switchSection('logs')} diff --git a/public/components/common/welcome/overview-welcome.js b/public/components/common/welcome/overview-welcome.js index 1510fb36cf..a45dd2256d 100644 --- a/public/components/common/welcome/overview-welcome.js +++ b/public/components/common/welcome/overview-welcome.js @@ -32,7 +32,7 @@ import store from '../../../redux/store'; import './welcome.scss'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { withErrorBoundary } from '../hocs'; -import office_logo from '../../../assets/office365.svg'; +import { LogoDocker, LogoGitHub, LogoGoogleCloud, LogoOffice365 } from '../logos'; export const OverviewWelcome = withErrorBoundary(class OverviewWelcome extends Component { constructor(props) { @@ -58,7 +58,7 @@ export const OverviewWelcome = withErrorBoundary(class OverviewWelcome extends C } + icon={} className="homSynopsis__card" title={WAZUH_MODULES[tab].title} onClick={() => store.dispatch(updateCurrentTab(tab))} @@ -100,11 +100,11 @@ export const OverviewWelcome = withErrorBoundary(class OverviewWelcome extends C {this.props.extensions.aws && this.buildTabCard('aws', 'logoAWSMono')} {this.props.extensions.office && - this.buildTabCard('office', office_logo)} + this.buildTabCard('office', LogoOffice365)} {this.props.extensions.gcp && - this.buildTabCard('gcp', 'logoGCPMono')} + this.buildTabCard('gcp', LogoGoogleCloud)} {this.props.extensions.github && - this.buildTabCard('github', 'logoGithub')} + this.buildTabCard('github', LogoGitHub)} @@ -137,7 +137,7 @@ export const OverviewWelcome = withErrorBoundary(class OverviewWelcome extends C {this.props.extensions.osquery && this.buildTabCard('osquery', 'searchProfilerApp')} {this.props.extensions.docker && - this.buildTabCard('docker', 'logoDocker')} + this.buildTabCard('docker', LogoDocker)} {this.buildTabCard('mitre', 'spacesApp')} {/* TODO- Change "spacesApp" icon*/} diff --git a/public/components/health-check/container/health-check.container.tsx b/public/components/health-check/container/health-check.container.tsx index 6e051433cc..9d6cfdfbaf 100644 --- a/public/components/health-check/container/health-check.container.tsx +++ b/public/components/health-check/container/health-check.container.tsx @@ -50,6 +50,7 @@ import { import { getDataPlugin } from '../../../kibana-services'; import { CheckLogger } from '../types/check_logger'; import { compose } from 'redux'; +import { getThemeAssetURL, getAssetURL } from '../../../utils/assets'; const checks = { api: { @@ -169,7 +170,7 @@ function HealthCheckComponent() { } - const logoUrl = getHttp().basePath.prepend(`/plugins/wazuh/assets/${appConfig.data['customization.logo.healthcheck']}`); + const logoUrl = getHttp().basePath.prepend(appConfig.data['customization.logo.healthcheck'] ? getAssetURL(appConfig.data['customization.logo.healthcheck']) : getThemeAssetURL('logo.svg')); const thereAreErrors = Object.keys(checkErrors).length > 0; const renderChecks = () => { diff --git a/public/components/overview/github-panel/views/stats.tsx b/public/components/overview/github-panel/views/stats.tsx index 30e93ae4bc..15fbd7aeb9 100644 --- a/public/components/overview/github-panel/views/stats.tsx +++ b/public/components/overview/github-panel/views/stats.tsx @@ -15,6 +15,7 @@ import React from 'react'; import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; import { PanelModuleConfiguration } from '../../../common/modules/panel'; import { renderValueNoThenEnabled } from '../../../../controllers/management/components/management/configuration/utils/utils'; +import { LogoGitHub } from '../../../common/logos'; const settings = [ { field: 'enabled', label: 'Service status', render: renderValueNoThenEnabled }, @@ -42,7 +43,7 @@ const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any export const ModuleConfiguration = props => { diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index 3c2f3dc8c0..24d6a980f9 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { EuiDescriptionList, EuiPanel } from '@elastic/eui'; import { PanelModuleConfiguration } from '../../../common/modules/panel'; -import moduleLogo from '../../../../assets/office365.svg'; +import { LogoOffice365 } from '../../../common/logos'; import { renderValueYesThenEnabled } from '../../../../controllers/management/components/management/configuration/utils/utils'; const settings = [ @@ -46,7 +46,7 @@ const mapWModuleConfigurationToRenderProperties = (wmodules: {[key: string]: any export const ModuleConfiguration = props => } settings={settings} configurationAPIPartialPath='/wmodules/wmodules' mapResponseConfiguration={(response, type, params) => { diff --git a/public/components/wz-menu/wz-menu-management.js b/public/components/wz-menu/wz-menu-management.js index e745d306b4..3d7105a8f1 100644 --- a/public/components/wz-menu/wz-menu-management.js +++ b/public/components/wz-menu/wz-menu-management.js @@ -138,7 +138,7 @@ class WzMenuManagement extends Component { name: this.managementSections.administration.text, id: this.managementSections.administration.id, id: 0, - disabled: false, + disabled: true, icon: , items: [ this.createItem(this.managementSections.rules), @@ -154,7 +154,7 @@ class WzMenuManagement extends Component { { name: this.managementSections.statusReports.text, id: this.managementSections.statusReports.id, - disabled: false, + disabled: true, icon: , items: [ this.createItem(this.managementSections.status), diff --git a/public/components/wz-menu/wz-menu-settings.js b/public/components/wz-menu/wz-menu-settings.js index 02b8560132..1ef9b388f7 100644 --- a/public/components/wz-menu/wz-menu-settings.js +++ b/public/components/wz-menu/wz-menu-settings.js @@ -124,8 +124,8 @@ class WzMenuSettings extends Component { { name: availableSettings.settings.text, id: availableSettings.settings.id, - disabled: false, - icon: , + disabled: true, + icon: , items: renderSettings } ]; diff --git a/public/components/wz-menu/wz-menu-tools.js b/public/components/wz-menu/wz-menu-tools.js index 7b5533d129..82e4c7e998 100644 --- a/public/components/wz-menu/wz-menu-tools.js +++ b/public/components/wz-menu/wz-menu-tools.js @@ -73,7 +73,7 @@ class WzMenuTools extends Component { { name: 'Tools', id: 0, - icon: , + icon: , items: renderSettings } ]; diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 30a9c4d6fd..57dc66a4f1 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -49,6 +49,7 @@ import { withWindowSize } from '../../components/common/hocs/withWindowSize'; import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services' +import { getThemeAssetURL, getAssetURL } from '../../utils/assets'; const sections = { @@ -948,7 +949,8 @@ export const WzMenu = withWindowSize(class WzMenu extends Component {
    ); - const logotype_url = getHttp().basePath.prepend(`/plugins/wazuh/assets/${this.wazuhConfig.getConfig()['customization.logo.app']}`); + + const logotypeURL = getHttp().basePath.prepend(this.wazuhConfig.getConfig()['customization.logo.app'] ? getAssetURL(this.wazuhConfig.getConfig()['customization.logo.app']) : getThemeAssetURL('logo.svg')); const mainButton = (
    `); - // Add plugin help links as extension to Kibana help menu + // Add plugin help links as extension to plugin platform help menu addHelpMenuToAppChrome(); diff --git a/public/components/agents/syscollector/inventory.tsx b/public/components/agents/syscollector/inventory.tsx index 5a097c4373..b44ec3b6ed 100644 --- a/public/components/agents/syscollector/inventory.tsx +++ b/public/components/agents/syscollector/inventory.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React component to integrate Kibana search bar + * Wazuh app - React component to integrate plugin platform search bar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/common/custom-search-bar/custom-search-bar.tsx b/public/components/common/custom-search-bar/custom-search-bar.tsx index ee34349e3d..aca0d0c7ee 100644 --- a/public/components/common/custom-search-bar/custom-search-bar.tsx +++ b/public/components/common/custom-search-bar/custom-search-bar.tsx @@ -32,7 +32,7 @@ export const CustomSearchBar = ({ filtersValues, filterDrillDownValue = { field: const [selectReference, setSelectReference] = useState(''); useEffect(() => { - setKibanaFilters(values, selectReference); + setPluginPlatformFilters(values, selectReference); refreshCustomSelectedFilter(); }, [values]); @@ -84,7 +84,7 @@ export const CustomSearchBar = ({ filtersValues, filterDrillDownValue = { field: return { meta, $state, query }; }; - const setKibanaFilters = (values: any[], selectReference: String) => { + const setPluginPlatformFilters = (values: any[], selectReference: String) => { const currentFilters = filterManager .getFilters() .filter((item) => item.meta.key != selectReference); diff --git a/public/components/common/flyouts/wz-flyout.tsx b/public/components/common/flyouts/wz-flyout.tsx index fa98c27a45..245376722f 100644 --- a/public/components/common/flyouts/wz-flyout.tsx +++ b/public/components/common/flyouts/wz-flyout.tsx @@ -12,18 +12,18 @@ import React, { Component, Fragment } from 'react'; import { EuiFlyout, EuiOverlayMask, EuiOutsideClickDetector } from '@elastic/eui'; -import { satisfyKibanaVersion } from '../../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../../common/semver'; export const WzFlyout = ({children, flyoutProps = {}, overlayMaskProps = {}, outsideClickDetectorProps = {}, onClose}) => ( 7.10')} + isDisabled={satisfyPluginPlatformVersion('>7.10')} {...outsideClickDetectorProps} > 7.10') ? { outsideClickCloses: true } : {})} + {...(satisfyPluginPlatformVersion('>7.10') ? { outsideClickCloses: true } : {})} {...flyoutProps} > {children} diff --git a/public/components/common/hocs/index.ts b/public/components/common/hocs/index.ts index a6ba03580b..02fd77a891 100644 --- a/public/components/common/hocs/index.ts +++ b/public/components/common/hocs/index.ts @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ export * from './withWindowSize'; -export * from './withKibanaContext'; +export * from './withPluginPlatformContext'; export * from './withUserPermissions'; export * from './withUserRoles'; export * from './withUserAuthorization'; diff --git a/public/components/common/hocs/withKibanaContext.tsx b/public/components/common/hocs/withPluginPlatformContext.tsx similarity index 76% rename from public/components/common/hocs/withKibanaContext.tsx rename to public/components/common/hocs/withPluginPlatformContext.tsx index e4dbab3ee3..767b96926f 100644 --- a/public/components/common/hocs/withKibanaContext.tsx +++ b/public/components/common/hocs/withPluginPlatformContext.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React HOC to create component with Kibana state + * Wazuh app - React HOC to create component with plugin platform state * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -14,14 +14,14 @@ import { useIndexPattern, useFilterManager, useTimeFilter } from '../hooks'; import { IIndexPattern, FilterManager, Query, TimeRange } from '../../../../../../src/plugins/data/public'; import { useQueryManager } from "../hooks/use-query"; -interface withKibanaContextProps { +interface withPluginPlatformContextProps { indexPattern?: IIndexPattern filterManager?: FilterManager query?: Query timeFilter?: TimeRange } -export interface withKibanaContextExtendsProps { +export interface withPluginPlatformContextExtendsProps { indexPattern: IIndexPattern filterManager: FilterManager timeFilter: TimeRange @@ -32,8 +32,8 @@ export interface withKibanaContextExtendsProps { } -export const withKibanaContext = (Component:React.FunctionComponent) => { - function hoc(props:T & withKibanaContextProps ):React.FunctionComponentElement { +export const withPluginPlatformContext = (Component:React.FunctionComponent) => { + function hoc(props:T & withPluginPlatformContextProps ):React.FunctionComponentElement { const indexPattern = props.indexPattern ? props.indexPattern : useIndexPattern(); const filterManager = props.filterManager ? props.filterManager : useFilterManager().filterManager; const [query, setQuery] = props.query ? useState(props.query) : useQueryManager(); @@ -48,7 +48,7 @@ export const withKibanaContext = (Component:React.FunctionComp setQuery={setQuery} />; } - hoc.displayName = `withKibanaContext-${Component.displayName}`; + hoc.displayName = `withPluginPlatformContext-${Component.displayName}`; return hoc } diff --git a/public/components/common/hooks/use-filter-manager.ts b/public/components/common/hooks/use-filter-manager.ts index a0ad870674..b383761a72 100644 --- a/public/components/common/hooks/use-filter-manager.ts +++ b/public/components/common/hooks/use-filter-manager.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook for get Kibana filter manager + * Wazuh app - React hook for get plugin platform filter manager * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/common/hooks/use-kbn-loading-indicator.ts b/public/components/common/hooks/use-kbn-loading-indicator.ts index 99899c6bad..47c4eeddde 100644 --- a/public/components/common/hooks/use-kbn-loading-indicator.ts +++ b/public/components/common/hooks/use-kbn-loading-indicator.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook hidde or show the Kibana loading indicator + * Wazuh app - React hook hidde or show the plugin platform loading indicator * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/common/hooks/use-query.ts b/public/components/common/hooks/use-query.ts index 872cf7fcd2..99b9098804 100644 --- a/public/components/common/hooks/use-query.ts +++ b/public/components/common/hooks/use-query.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook for get query of Kibana searchBar + * Wazuh app - React hook for get query of plugin platform searchBar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/common/hooks/use-time-filter.ts b/public/components/common/hooks/use-time-filter.ts index 2d91ae4826..d14a10b89b 100644 --- a/public/components/common/hooks/use-time-filter.ts +++ b/public/components/common/hooks/use-time-filter.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook for get Kibana time filter + * Wazuh app - React hook for get plugin platform time filter * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 73721b180f..f4f50202c8 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -76,7 +76,7 @@ export const Discover = compose( _history: { history: { items: { from: string; to: string }[] } }; }; - KibanaServices: { [key: string]: any }; + PluginPlatformServices: { [key: string]: any }; state: { sort: object; selectedTechnique: string; @@ -113,8 +113,8 @@ export const Discover = compose( }; constructor(props) { super(props); - this.KibanaServices = getDataPlugin(); - this.timefilter = this.KibanaServices.query.timefilter.timefilter; + this.PluginPlatformServices = getDataPlugin(); + this.timefilter = this.PluginPlatformServices.query.timefilter.timefilter; this.timeSubscription = null; this.state = { sort: {}, @@ -265,7 +265,7 @@ export const Discover = compose( async getIndexPattern() { this.indexPattern = { - ...(await this.KibanaServices.indexPatterns.get(AppState.getCurrentPattern())), + ...(await this.PluginPlatformServices.indexPatterns.get(AppState.getCurrentPattern())), }; const fields: IFieldType[] = []; Object.keys(this.indexPattern.fields).forEach((item) => { @@ -368,7 +368,7 @@ export const Discover = compose( ? this.props.shareFilterManager.getFilters() : []; const previousFilters = - (this.KibanaServices && this.KibanaServices.query.filterManager.getFilters()) || []; + (this.PluginPlatformServices && this.PluginPlatformServices.query.filterManager.getFilters()) || []; const elasticQuery = buildEsQuery( undefined, query, diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index 3158c6400e..7db6e3da0a 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -30,7 +30,7 @@ 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'; -import { satisfyKibanaVersion } from '../../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../../common/semver'; export const Events = compose( withAgentSupportModule, @@ -205,7 +205,7 @@ export const Events = compose( if (!this.state.hasRefreshedKnownFields) { try { this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); - if (satisfyKibanaVersion('<7.11')) { + if (satisfyPluginPlatformVersion('<7.11')) { await PatternHandler.refreshIndexPattern(); } this.setState({ isRefreshing: false }); @@ -304,7 +304,7 @@ export const Events = compose( reloadToast = () => { const toastLifeTimeMs = 300000; - if (satisfyKibanaVersion('<7.11')) { + if (satisfyPluginPlatformVersion('<7.11')) { getToasts().add({ color: 'success', title: 'The index pattern was refreshed successfully.', @@ -323,7 +323,7 @@ export const Events = compose( ), toastLifeTimeMs, }); - } else if (satisfyKibanaVersion('>=7.11')) { + } else if (satisfyPluginPlatformVersion('>=7.11')) { getToasts().add({ color: 'warning', title: 'Found unknown fields in the index pattern.', diff --git a/public/components/common/welcome/components/mitre_top/mitre_top.tsx b/public/components/common/welcome/components/mitre_top/mitre_top.tsx index c71ea72da5..a2fcfd4ed5 100644 --- a/public/components/common/welcome/components/mitre_top/mitre_top.tsx +++ b/public/components/common/welcome/components/mitre_top/mitre_top.tsx @@ -32,7 +32,7 @@ import { getDataPlugin } from '../../../../../kibana-services'; export class MitreTopTactics extends Component { _isMount = false; - KibanaServices: { [key: string]: any }; + PluginPlatformServices: { [key: string]: any }; timefilter: any; indexPattern: any; props!: { @@ -51,8 +51,8 @@ export class MitreTopTactics extends Component { constructor(props) { super(props); - this.KibanaServices = getDataPlugin().query; - this.timefilter = this.KibanaServices.timefilter.timefilter; + this.PluginPlatformServices = getDataPlugin().query; + this.timefilter = this.PluginPlatformServices.timefilter.timefilter; this.state = { alertsCount: [], isLoading: true, diff --git a/public/components/health-check/container/health-check.container.test.tsx b/public/components/health-check/container/health-check.container.test.tsx index 3655c2ce7d..a56eadb3ea 100644 --- a/public/components/health-check/container/health-check.container.test.tsx +++ b/public/components/health-check/container/health-check.container.test.tsx @@ -41,7 +41,7 @@ jest.mock('../services', () => ({ checkApiService: (appInfo) => () => undefined, checkSetupService: (appInfo) => () => undefined, checkFieldsService: (appInfo) => () => undefined, - checkKibanaSettings: (appInfo) => () => undefined, + checkPluginPlatformSettings: (appInfo) => () => undefined, checkPatternSupportService: (appInfo) => () => undefined, checkIndexPatternService: (appInfo) => () => undefined, })); diff --git a/public/components/health-check/container/health-check.container.tsx b/public/components/health-check/container/health-check.container.tsx index 9d6cfdfbaf..58de6951dd 100644 --- a/public/components/health-check/container/health-check.container.tsx +++ b/public/components/health-check/container/health-check.container.tsx @@ -27,7 +27,7 @@ import { AppState, ErrorHandler } from '../../../react-services'; import { useAppConfig, useRootScope } from '../../../components/common/hooks'; import { checkApiService, - checkKibanaSettings, + checkPluginPlatformSettings, checkIndexPatternService, checkPatternSupportService, checkSetupService, @@ -37,14 +37,14 @@ import { withErrorBoundary, withReduxProvider } from '../../common/hocs'; import { getHttp } from '../../../kibana-services'; import { HEALTH_CHECK_REDIRECTION_TIME, - KIBANA_SETTING_NAME_MAX_BUCKETS, - KIBANA_SETTING_NAME_METAFIELDS, - KIBANA_SETTING_NAME_TIME_FILTER, + PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS, + PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS, + PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER, WAZUH_INDEX_TYPE_MONITORING, WAZUH_INDEX_TYPE_STATISTICS, - WAZUH_KIBANA_SETTING_MAX_BUCKETS, - WAZUH_KIBANA_SETTING_METAFIELDS, - WAZUH_KIBANA_SETTING_TIME_FILTER, + WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS, + WAZUH_PLUGIN_PLATFORM_SETTING_METAFIELDS, + WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER, } from '../../../../common/constants'; import { getDataPlugin } from '../../../kibana-services'; @@ -91,24 +91,24 @@ const checks = { canRetry: true, }, maxBuckets: { - title: `Check ${KIBANA_SETTING_NAME_MAX_BUCKETS} setting`, - label: `${KIBANA_SETTING_NAME_MAX_BUCKETS} setting`, - validator: checkKibanaSettings(KIBANA_SETTING_NAME_MAX_BUCKETS, WAZUH_KIBANA_SETTING_MAX_BUCKETS), + title: `Check ${PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS} setting`, + label: `${PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS} setting`, + validator: checkPluginPlatformSettings(PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS, WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS), awaitFor: [], canRetry: true, }, metaFields: { - title: `Check ${KIBANA_SETTING_NAME_METAFIELDS} setting`, - label: `${KIBANA_SETTING_NAME_METAFIELDS} setting`, - validator: checkKibanaSettings(KIBANA_SETTING_NAME_METAFIELDS, WAZUH_KIBANA_SETTING_METAFIELDS), + title: `Check ${PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS} setting`, + label: `${PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS} setting`, + validator: checkPluginPlatformSettings(PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS, WAZUH_PLUGIN_PLATFORM_SETTING_METAFIELDS), awaitFor: [], canRetry: true, }, timeFilter: { - title: `Check ${KIBANA_SETTING_NAME_TIME_FILTER} setting`, - label: `${KIBANA_SETTING_NAME_TIME_FILTER} setting`, - validator: checkKibanaSettings(KIBANA_SETTING_NAME_TIME_FILTER, JSON.stringify(WAZUH_KIBANA_SETTING_TIME_FILTER), (checkLogger: CheckLogger, options: {defaultAppValue: any}) => { - getDataPlugin().query.timefilter.timefilter.setTime(WAZUH_KIBANA_SETTING_TIME_FILTER) + title: `Check ${PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER} setting`, + label: `${PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER} setting`, + validator: checkPluginPlatformSettings(PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER, JSON.stringify(WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER), (checkLogger: CheckLogger, options: {defaultAppValue: any}) => { + getDataPlugin().query.timefilter.timefilter.setTime(WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER) && checkLogger.action(`Timefilter set to ${JSON.stringify(options.defaultAppValue)}`); }), awaitFor: [], diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts index 5f8d786d4c..0647e294b6 100644 --- a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts @@ -15,7 +15,7 @@ import { AppState, SavedObject } from '../../../../react-services'; import { getDataPlugin } from '../../../../kibana-services'; import { HEALTH_CHECK } from '../../../../../common/constants'; import { CheckLogger } from '../../types/check_logger'; -import { satisfyKibanaVersion } from '../../../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../../../common/semver'; export const checkIndexPatternObjectService = async (appConfig, checkLogger: CheckLogger) => { const patternId: string = AppState.getCurrentPattern(); @@ -41,7 +41,7 @@ export const checkIndexPatternObjectService = async (appConfig, checkLogger: Ch const existDefaultIndexPattern = await SavedObject.getExistingIndexPattern(defaultPatternId); checkLogger.info(`Index pattern id [${defaultPatternId}] exists: ${existDefaultIndexPattern ? 'yes' : 'no'}`); if (existDefaultIndexPattern) { - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ checkLogger.info(`Refreshing index pattern fields [${defaultPatternId}]...`); await SavedObject.refreshIndexPattern(defaultPatternId); checkLogger.action(`Refreshed index pattern fields [${defaultPatternId}]`); diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts index 146026055b..aacf9f960c 100644 --- a/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern.service.ts @@ -15,7 +15,7 @@ import { CheckLogger } from '../../types/check_logger'; import { checkFieldsService } from './check-fields.service'; import { checkIndexPatternObjectService } from './check-index-pattern-object.service'; import { checkTemplateService } from './check-template.service'; -import { satisfyKibanaVersion } from '../../../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../../../common/semver'; export const checkIndexPatternService = (appConfig) => async (checkLogger: CheckLogger) => await checkPattern(appConfig, checkLogger); @@ -25,7 +25,7 @@ const checkPattern = async (appConfig, checkLogger: CheckLogger) => { }; await checkIndexPatternObjectService(appConfig, checkLogger); await checkTemplate(appConfig, checkLogger); - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ await checkFields(appConfig, checkLogger); }; }; diff --git a/public/components/health-check/services/check-kibana-settings.service.ts b/public/components/health-check/services/check-kibana-settings.service.ts deleted file mode 100644 index b26026e62f..0000000000 --- a/public/components/health-check/services/check-kibana-settings.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Wazuh app - Check Kibana settings service - * - * Copyright (C) 2015-2021 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 { CheckLogger } from '../types/check_logger'; -import _ from 'lodash'; -import { getUiSettings } from '../../../kibana-services'; - -export const checkKibanaSettings = (kibanaSettingName: string, defaultAppValue: any, callback?: (checkLogger: CheckLogger, options: {defaultAppValue: any}) => void) => (appConfig: any) => async (checkLogger: CheckLogger) => { - checkLogger.info('Getting settings...'); - const valueKibanaSetting = getUiSettings().get(kibanaSettingName); - const settingsAreDifferent = !_.isEqual( - typeof defaultAppValue === 'string' ? stringifySetting(valueKibanaSetting) : valueKibanaSetting, - defaultAppValue - ); - checkLogger.info(`Check Kibana setting [${kibanaSettingName}]: ${stringifySetting(valueKibanaSetting)}`); - checkLogger.info(`App setting [${kibanaSettingName}]: ${stringifySetting(defaultAppValue)}`); - checkLogger.info(`Settings mismatch [${kibanaSettingName}]: ${settingsAreDifferent ? 'yes' : 'no'}`); - if ( !valueKibanaSetting || settingsAreDifferent ){ - checkLogger.info(`Updating [${kibanaSettingName}] setting...`); - await updateSetting(kibanaSettingName, defaultAppValue); - checkLogger.action(`Updated [${kibanaSettingName}] setting to: ${stringifySetting(defaultAppValue)}`); - callback && callback(checkLogger,{ defaultAppValue }); - } -} - -async function updateSetting(kibanaSettingName, defaultAppValue, retries = 3) { - return await getUiSettings() - .set(kibanaSettingName, null) - .catch(async (error) => { - if (retries > 0) { - return await updateSetting(kibanaSettingName, defaultAppValue, --retries); - } - throw error; - }); -} - -function stringifySetting(setting: any){ - try{ - return JSON.stringify(setting); - }catch(error){ - return setting; - }; -}; diff --git a/public/components/health-check/services/check-pattern-support.service.ts b/public/components/health-check/services/check-pattern-support.service.ts index cc0d3cfcef..4615467503 100644 --- a/public/components/health-check/services/check-pattern-support.service.ts +++ b/public/components/health-check/services/check-pattern-support.service.ts @@ -13,7 +13,7 @@ */ import { SavedObject } from '../../../react-services'; import { CheckLogger } from '../types/check_logger'; -import { satisfyKibanaVersion } from '../../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../../common/semver'; export const checkPatternSupportService = (pattern: string, indexType : string) => async (checkLogger: CheckLogger) => { checkLogger.info(`Checking index pattern id [${pattern}] exists...`); @@ -22,7 +22,7 @@ export const checkPatternSupportService = (pattern: string, indexType : string) if (!result.data) { let fields; - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ checkLogger.info(`Getting indices fields for the index pattern id [${pattern}]...`); fields = await SavedObject.getIndicesFields(pattern, indexType); checkLogger.info(`Fields for index pattern id [${pattern}] found: ${fields.length}`); diff --git a/public/components/health-check/services/check-plugin-platform-settings.service.ts b/public/components/health-check/services/check-plugin-platform-settings.service.ts new file mode 100644 index 0000000000..89805fdacd --- /dev/null +++ b/public/components/health-check/services/check-plugin-platform-settings.service.ts @@ -0,0 +1,55 @@ +/* + * Wazuh app - Check PluginPlatform settings service + * + * Copyright (C) 2015-2021 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 { CheckLogger } from '../types/check_logger'; +import _ from 'lodash'; +import { getUiSettings } from '../../../kibana-services'; +import { PLUGIN_PLATFORM_NAME } from '../../../../common/constants'; + +export const checkPluginPlatformSettings = (pluginPlatformSettingName: string, defaultAppValue: any, callback?: (checkLogger: CheckLogger, options: {defaultAppValue: any}) => void) => (appConfig: any) => async (checkLogger: CheckLogger) => { + checkLogger.info('Getting settings...'); + const valuePluginPlatformSetting = getUiSettings().get(pluginPlatformSettingName); + const settingsAreDifferent = !_.isEqual( + typeof defaultAppValue === 'string' ? stringifySetting(valuePluginPlatformSetting) : valuePluginPlatformSetting, + defaultAppValue + ); + checkLogger.info(`Check ${PLUGIN_PLATFORM_NAME} setting [${pluginPlatformSettingName}]: ${stringifySetting(valuePluginPlatformSetting)}`); + checkLogger.info(`App setting [${pluginPlatformSettingName}]: ${stringifySetting(defaultAppValue)}`); + checkLogger.info(`Settings mismatch [${pluginPlatformSettingName}]: ${settingsAreDifferent ? 'yes' : 'no'}`); + if ( !valuePluginPlatformSetting || settingsAreDifferent ){ + checkLogger.info(`Updating [${pluginPlatformSettingName}] setting...`); + await updateSetting(pluginPlatformSettingName, defaultAppValue); + checkLogger.action(`Updated [${pluginPlatformSettingName}] setting to: ${stringifySetting(defaultAppValue)}`); + callback && callback(checkLogger,{ defaultAppValue }); + } +} + +async function updateSetting(pluginPlatformSettingName, defaultAppValue, retries = 3) { + return await getUiSettings() + .set(pluginPlatformSettingName, null) + .catch(async (error) => { + if (retries > 0) { + return await updateSetting(pluginPlatformSettingName, defaultAppValue, --retries); + } + throw error; + }); +} + +function stringifySetting(setting: any){ + try{ + return JSON.stringify(setting); + }catch(error){ + return setting; + }; +}; diff --git a/public/components/health-check/services/check-setup.service.ts b/public/components/health-check/services/check-setup.service.ts index 5425515a76..bb38fbae16 100644 --- a/public/components/health-check/services/check-setup.service.ts +++ b/public/components/health-check/services/check-setup.service.ts @@ -14,6 +14,7 @@ import { AppState, GenericRequest, WzRequest } from '../../../react-services'; import { CheckLogger } from '../types/check_logger'; +import { PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_UPGRADE_PLATFORM } from '../../../../common/constants'; export const checkSetupService = appInfo => async (checkLogger: CheckLogger) => { const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); @@ -42,7 +43,7 @@ export const checkSetupService = appInfo => async (checkLogger: CheckLogger) => api.groups.version !== appSplit[0] || api.groups.minor !== appSplit[1] ) { - checkLogger.error(`Wazuh API and Wazuh App version mismatch. API version: ${apiVersion}. App version: ${setupData.data.data['app-version']}. At least, major and minor should match. Check more info about upgrading Wazuh App here.`); + checkLogger.error(`Wazuh API and Wazuh App version mismatch. API version: ${apiVersion}. App version: ${setupData.data.data['app-version']}. At least, major and minor should match. Check more info about upgrading Wazuh App here.`); } } } diff --git a/public/components/health-check/services/index.ts b/public/components/health-check/services/index.ts index a61b274eb6..2b2075d593 100644 --- a/public/components/health-check/services/index.ts +++ b/public/components/health-check/services/index.ts @@ -13,7 +13,7 @@ */ export * from './check-api.service'; -export * from './check-kibana-settings.service'; +export * from './check-plugin-platform-settings.service'; export * from './check-index-pattern/check-index-pattern.service'; export * from './check-pattern-support.service'; export * from './check-setup.service'; diff --git a/public/components/kbn-search-bar/index.ts b/public/components/kbn-search-bar/index.ts index 079ba2da1a..49f859ad79 100644 --- a/public/components/kbn-search-bar/index.ts +++ b/public/components/kbn-search-bar/index.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React component to integrate Kibana search bar + * Wazuh app - React component to integrate plugin platform search bar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/kbn-search-bar/kbn-search-bar.tsx b/public/components/kbn-search-bar/kbn-search-bar.tsx index 7eb9fb1933..3aff560314 100644 --- a/public/components/kbn-search-bar/kbn-search-bar.tsx +++ b/public/components/kbn-search-bar/kbn-search-bar.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React component to integrate Kibana search bar + * Wazuh app - React component to integrate plugin platform search bar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify @@ -15,7 +15,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TimeRange, Query, Filter } from '../../../../../src/plugins/data/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { withKibanaContext, withKibanaContextExtendsProps } from '../common/hocs'; +import { withPluginPlatformContext, withPluginPlatformContextExtendsProps } from '../common/hocs'; import { storage } from './lib'; import { getDataPlugin, getCore } from '../../kibana-services'; import { AUTHORIZED_AGENTS } from '../../../common/constants'; @@ -30,13 +30,13 @@ export interface IKbnSearchBarProps { const SearchBar = getDataPlugin().ui.SearchBar; const KbnSearchBar: React.FunctionComponent = ( - props: IKbnSearchBarProps & withKibanaContextExtendsProps + props: IKbnSearchBarProps & withPluginPlatformContextExtendsProps ) => { - const KibanaServices = getDataPlugin(); + const PluginPlatformServices = getDataPlugin(); const { filterManager, indexPattern, timeFilter, timeHistory, query } = props; const data = { - ...KibanaServices, - query: { ...KibanaServices.query, filterManager }, + ...PluginPlatformServices, + query: { ...PluginPlatformServices.query, filterManager }, }; const getFilters = () => { @@ -95,5 +95,5 @@ KbnSearchBar.defaultProps = { appName: 'wazuh', }; -const hoc = withKibanaContext(KbnSearchBar); +const hoc = withPluginPlatformContext(KbnSearchBar); export { hoc as KbnSearchBar }; diff --git a/public/components/kbn-search-bar/lib/index.ts b/public/components/kbn-search-bar/lib/index.ts index 8ce8769cc1..1d6832df9d 100644 --- a/public/components/kbn-search-bar/lib/index.ts +++ b/public/components/kbn-search-bar/lib/index.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React component to integrate Kibana search bar + * Wazuh app - React component to integrate plugin platform search bar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/kbn-search-bar/lib/storage.ts b/public/components/kbn-search-bar/lib/storage.ts index d77879fdad..6cc6199d2a 100644 --- a/public/components/kbn-search-bar/lib/storage.ts +++ b/public/components/kbn-search-bar/lib/storage.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React component to integrate Kibana search bar + * Wazuh app - React component to integrate plugin platform search bar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/overview/compliance-table/compliance-table.tsx b/public/components/overview/compliance-table/compliance-table.tsx index ca95757668..d669ecc86d 100644 --- a/public/components/overview/compliance-table/compliance-table.tsx +++ b/public/components/overview/compliance-table/compliance-table.tsx @@ -37,7 +37,7 @@ export const ComplianceTable = withAgentSupportModule(class ComplianceTable exte _history: { history: { items: { from: string; to: string }[] } }; }; - KibanaServices: { [key: string]: any }; + PluginPlatformServices: { [key: string]: any }; filterManager: FilterManager; indexPattern: any; state: { @@ -54,9 +54,9 @@ export const ComplianceTable = withAgentSupportModule(class ComplianceTable exte constructor(props) { super(props); - this.KibanaServices = getDataPlugin().query; - this.filterManager = this.KibanaServices.filterManager; - this.timefilter = this.KibanaServices.timefilter.timefilter; + this.PluginPlatformServices = getDataPlugin().query; + this.filterManager = this.PluginPlatformServices.filterManager; + this.timefilter = this.PluginPlatformServices.timefilter.timefilter; this.state = { selectedRequirement: '', flyoutOn: true, diff --git a/public/components/overview/metrics/metrics.tsx b/public/components/overview/metrics/metrics.tsx index def528e905..80a24aef1e 100644 --- a/public/components/overview/metrics/metrics.tsx +++ b/public/components/overview/metrics/metrics.tsx @@ -38,7 +38,7 @@ export const Metrics = withAllowedAgents( _history: { history: { items: { from: string; to: string }[] } }; }; - KibanaServices: { [key: string]: any }; + PluginPlatformServices: { [key: string]: any }; filterManager: FilterManager; indexPattern: any; state: { @@ -54,9 +54,9 @@ export const Metrics = withAllowedAgents( constructor(props) { super(props); - this.KibanaServices = getDataPlugin().query; - this.filterManager = this.KibanaServices.filterManager; - this.timefilter = this.KibanaServices.timefilter.timefilter; + this.PluginPlatformServices = getDataPlugin().query; + this.filterManager = this.PluginPlatformServices.filterManager; + this.timefilter = this.PluginPlatformServices.timefilter.timefilter; this.state = { resultState: '', results: {}, diff --git a/public/components/overview/mitre/mitre.tsx b/public/components/overview/mitre/mitre.tsx index 83eed4531a..e71de37bae 100644 --- a/public/components/overview/mitre/mitre.tsx +++ b/public/components/overview/mitre/mitre.tsx @@ -42,7 +42,7 @@ export const Mitre = withErrorBoundary (class Mitre extends Component { _history: { history: { items: { from: string, to: string }[] } } }; - KibanaServices: { [key: string]: any }; + PluginPlatformServices: { [key: string]: any }; filterManager: FilterManager; indexPattern: any; destroyWatcher: any; @@ -57,9 +57,9 @@ export const Mitre = withErrorBoundary (class Mitre extends Component { constructor(props) { super(props); - this.KibanaServices = getDataPlugin().query; - this.filterManager = this.KibanaServices.filterManager; - this.timefilter = this.KibanaServices.timefilter.timefilter; + this.PluginPlatformServices = getDataPlugin().query; + this.filterManager = this.PluginPlatformServices.filterManager; + this.timefilter = this.PluginPlatformServices.timefilter.timefilter; this.state = { tacticsObject: {}, selectedTactics: {}, diff --git a/public/components/overview/office-panel/config/module-config.tsx b/public/components/overview/office-panel/config/module-config.tsx index ce63ef5807..f2a6964507 100644 --- a/public/components/overview/office-panel/config/module-config.tsx +++ b/public/components/overview/office-panel/config/module-config.tsx @@ -22,7 +22,7 @@ import { } from './'; /** - * The length method has to count Kibana Visualizations for TabVisualizations class + * The length method has to count plugin platform Visualizations for TabVisualizations class */ export const ModuleConfig = { main: { diff --git a/public/components/security/main.tsx b/public/components/security/main.tsx index 6d0da6b5d7..ff1ae9df0b 100644 --- a/public/components/security/main.tsx +++ b/public/components/security/main.tsx @@ -26,11 +26,12 @@ import { withErrorBoundary, } from '../common/hocs'; import { compose } from 'redux'; -import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; +import { WAZUH_ROLE_ADMINISTRATOR_NAME, PLUGIN_PLATFORM_NAME } from '../../../common/constants'; import { updateSecuritySection } from '../../redux/actions/securityActions'; import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; +import { getPluginDataPath } from '../../../common/plugin'; const tabs = [ { @@ -141,7 +142,7 @@ export const WzSecurity = compose( switch (allowRunAs) { case API_USER_STATUS_RUN_AS.HOST_DISABLED: runAsWarningTxt = - 'For the role mapping to take effect, enable run_as in /usr/share/kibana/data/wazuh/config/wazuh.yml configuration file, restart the Kibana service and clear your browser cache and cookies.'; + `For the role mapping to take effect, enable run_as in ${getPluginDataPath('config/wazuh.yml')} configuration file, restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; break; case API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED: runAsWarningTxt = @@ -149,7 +150,7 @@ export const WzSecurity = compose( break; case API_USER_STATUS_RUN_AS.ALL_DISABLED: runAsWarningTxt = - 'For the role mapping to take effect, enable run_as in /usr/share/kibana/data/wazuh/config/wazuh.yml configuration file and set the current Wazuh API user allow_run_as to true. Restart the Kibana service and clear your browser cache and cookies.'; + `For the role mapping to take effect, enable run_as in ${getPluginDataPath('config/wazuh.yml')} configuration file and set the current Wazuh API user allow_run_as to true. Restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; break; default: runAsWarningTxt = diff --git a/public/components/settings/api/add-api.js b/public/components/settings/api/add-api.js index e6ab633d1b..d536a4eb33 100644 --- a/public/components/settings/api/add-api.js +++ b/public/components/settings/api/add-api.js @@ -26,9 +26,10 @@ import { EuiPanel } from '@elastic/eui'; import { withErrorBoundary } from '../../common/hocs'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { getPluginDataPath } from '../../../../common/plugin'; export const AddApi = withErrorBoundary (class AddApi extends Component { constructor(props) { @@ -135,7 +136,7 @@ export const AddApi = withErrorBoundary (class AddApi extends Component { {(this.state.status === 'warning' || this.state.status === 'danger') && } - Check that the Kibana server can reach the configured Wazuh API(s). + Check that the {PLUGIN_PLATFORM_NAME} server can reach the configured Wazuh API(s). Modify{' '} - /usr/share/kibana/data/wazuh/config/wazuh.yml{' '} + {getPluginDataPath('config/wazuh.yml')}{' '} to set the connection information. diff --git a/public/components/settings/api/api-is-down.js b/public/components/settings/api/api-is-down.js index d9dce90d3c..0ceb1f73ba 100644 --- a/public/components/settings/api/api-is-down.js +++ b/public/components/settings/api/api-is-down.js @@ -34,8 +34,9 @@ import { withErrorBoundary } from '../../common/hocs'; import { UI_ERROR_SEVERITIES, } from '../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { getPluginDataPath } from '../../../../common/plugin'; export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { constructor(props) { @@ -138,7 +139,7 @@ hosts: const checkConnectionChildren = (
    - Check that the Kibana server can reach the configured Wazuh API(s). + Check that the {PLUGIN_PLATFORM_NAME} server can reach the configured Wazuh API(s). Review the settings in the{' '} - /usr/share/kibana/data/wazuh/config/wazuh.yml + {getPluginDataPath('config/wazuh.yml')} {' '} file. diff --git a/public/components/settings/configuration/components/bottom-bar.tsx b/public/components/settings/configuration/components/bottom-bar.tsx index 71e395957b..f304903b2d 100644 --- a/public/components/settings/configuration/components/bottom-bar.tsx +++ b/public/components/settings/configuration/components/bottom-bar.tsx @@ -25,7 +25,7 @@ import { EuiButton } from '@elastic/eui'; import { WazuhConfig } from '../../../../react-services/wazuh-config'; -import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../../../common/constants'; import { UI_ERROR_SEVERITIES, UIErrorLog, @@ -172,7 +172,7 @@ const executeHealtCheck = () => { const restartToast = () => { getToasts().add({ color: 'warning', - title: 'You must restart Kibana for the changes to take effect', + title: `You must restart ${PLUGIN_PLATFORM_NAME} for the changes to take effect`, }); } diff --git a/public/components/settings/configuration/components/header.tsx b/public/components/settings/configuration/components/header.tsx index 686fccf6ac..6f4ccce29c 100644 --- a/public/components/settings/configuration/components/header.tsx +++ b/public/components/settings/configuration/components/header.tsx @@ -24,6 +24,8 @@ import { EuiSearchBar, } from '@elastic/eui'; import { EuiFormErrorText } from '@elastic/eui'; +import { PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION } from '../../../../../common/constants'; +import { getPluginDataPath } from '../../../../../common/plugin'; export const Header = ({query, setQuery}) => { return ( @@ -58,7 +60,7 @@ const Title = () => { iconSize="l" aria-label="Help" target="_blank" - href="https://documentation.wazuh.com/current/user-manual/kibana-app/reference/config-file.html" + href={PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION} > @@ -71,9 +73,8 @@ const SubTitle = () => { return ( - Configuration file located at - /usr/share/kibana/data/wazuh/config/wazuh.yml - + Configuration file located at {getPluginDataPath('config/wazuh.yml')} + ) } diff --git a/public/components/settings/settings-logs/logs.js b/public/components/settings/settings-logs/logs.js index 2ea5061df7..6f2b50c448 100644 --- a/public/components/settings/settings-logs/logs.js +++ b/public/components/settings/settings-logs/logs.js @@ -28,6 +28,7 @@ import { formatUIDate } from '../../../react-services/time-service'; import store from '../../../redux/store'; import { updateSelectedSettingsSection } from '../../../redux/actions/appStateActions'; import { withErrorBoundary } from '../../common/hocs'; +import { getPluginDataPath } from '../../../../common/plugin'; class SettingsLogs extends Component { constructor(props) { @@ -116,8 +117,7 @@ class SettingsLogs extends Component { - Log file located at - /usr/share/kibana/data/wazuh/logs/wazuhapp.log + Log file located at {getPluginDataPath('logs/wazuhapp.log')} {this.state.refreshingEntries && ( diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 25db2c15ce..fc36440c08 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -41,7 +41,7 @@ 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'; -import { satisfyKibanaVersion } from '../../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../../common/semver'; const visHandler = new VisHandlers(); @@ -130,7 +130,7 @@ export const WzVisualize = compose( // Known fields are refreshed only once per dashboard loading try { this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ await PatternHandler.refreshIndexPattern(this.newFields); }; this.setState({ isRefreshing: false }); @@ -157,7 +157,7 @@ export const WzVisualize = compose( }; reloadToast = () => { const toastLifeTimeMs = 300000; - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ getToasts().add({ color: 'success', title: 'The index pattern was refreshed successfully.', @@ -172,7 +172,7 @@ export const WzVisualize = compose( ), toastLifeTimeMs }); - }else if(satisfyKibanaVersion('>=7.11')){ + }else if(satisfyPluginPlatformVersion('>=7.11')){ getToasts().add({ color: 'warning', title: 'Found unknown fields in the index pattern.', diff --git a/public/components/wz-blank-screen/wz-blank-screen.js b/public/components/wz-blank-screen/wz-blank-screen.js index 89e087c385..21a6b49a42 100644 --- a/public/components/wz-blank-screen/wz-blank-screen.js +++ b/public/components/wz-blank-screen/wz-blank-screen.js @@ -12,6 +12,7 @@ import React, { Component } from 'react'; import { EuiButton, EuiHorizontalRule, EuiPage, EuiPageContent } from '@elastic/eui'; import { ErrorComponentPrompt } from '../common/error-boundary-prompt/error-boundary-prompt'; +import { PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_TROUBLESHOOTING, PLUGIN_PLATFORM_URL_GUIDE, PLUGIN_PLATFORM_URL_GUIDE_TITLE } from '../../../common/constants'; export class WzBlankScreen extends Component { constructor(props) { @@ -20,11 +21,6 @@ export class WzBlankScreen extends Component { } render() { - const elasticGuideUrl = - 'https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html'; - const wazuhTroubleshootingUrl = - 'https://documentation.wazuh.com/current/user-manual/kibana-app/troubleshooting.html'; - return ( @@ -34,10 +30,10 @@ export class WzBlankScreen extends Component { action={ <>

    - Elastic guide + {PLUGIN_PLATFORM_URL_GUIDE_TITLE}

    - Wazuh installation guide + Wazuh installation guide

    diff --git a/public/components/wz-date-picker/components/condensed.tsx b/public/components/wz-date-picker/components/condensed.tsx index 7a00ac4b24..973fa64bbe 100644 --- a/public/components/wz-date-picker/components/condensed.tsx +++ b/public/components/wz-date-picker/components/condensed.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React component for select time and sync with kibana discover + * Wazuh app - React component for select time and sync with plugin platform discover * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/wz-date-picker/components/index.ts b/public/components/wz-date-picker/components/index.ts index cb29ffbde7..d8d45ff0bf 100644 --- a/public/components/wz-date-picker/components/index.ts +++ b/public/components/wz-date-picker/components/index.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React component for select time and sync with kibana discover + * Wazuh app - React component for select time and sync with plugin platform discover * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/wz-date-picker/index.ts b/public/components/wz-date-picker/index.ts index cea14044cf..2a7b5ead55 100644 --- a/public/components/wz-date-picker/index.ts +++ b/public/components/wz-date-picker/index.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React component for select time and sync with kibana discover + * Wazuh app - React component for select time and sync with plugin platform discover * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/wz-date-picker/wz-date-picker.tsx b/public/components/wz-date-picker/wz-date-picker.tsx index 75c7b29823..c53c6c4cf3 100644 --- a/public/components/wz-date-picker/wz-date-picker.tsx +++ b/public/components/wz-date-picker/wz-date-picker.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - React component for select time and sync with kibana discover + * Wazuh app - React component for select time and sync with plugin platform discover * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/components/wz-menu/wz-menu.js b/public/components/wz-menu/wz-menu.js index 57dc66a4f1..30a89b3474 100644 --- a/public/components/wz-menu/wz-menu.js +++ b/public/components/wz-menu/wz-menu.js @@ -593,7 +593,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { } switchMenuOpened = () => { - const kibanaMenuBlockedOrOpened = document.body.classList.contains('euiBody--collapsibleNavIsDocked') || document.body.classList.contains('euiBody--collapsibleNavIsOpen'); + const pluginPlatformMenuBlockedOrOpened = document.body.classList.contains('euiBody--collapsibleNavIsDocked') || document.body.classList.contains('euiBody--collapsibleNavIsOpen'); if (!this.state.menuOpened && this.state.currentMenuTab === 'manager') { this.managementPopoverToggle(); } else if (this.state.currentMenuTab === 'overview') { @@ -608,7 +608,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { this.closeAllPopover() } - this.setState({ menuOpened: !this.state.menuOpened, kibanaMenuBlockedOrOpened, hover: this.state.currentMenuTab }, async () => { + this.setState({ menuOpened: !this.state.menuOpened, pluginPlatformMenuBlockedOrOpened, hover: this.state.currentMenuTab }, async () => { await this.loadApiList(); await this.loadIndexPatternsList(); }); @@ -996,7 +996,7 @@ export const WzMenu = withWindowSize(class WzMenu extends Component { try { let isValid = false; while (tries--) { - await delay(2000); + await delayAsPromise(2000); try { isValid = await checkDaemons(isCluster); if (isValid) { @@ -540,7 +540,7 @@ export const restartClusterOrManager = async (updateWazuhNotReadyYet) => { updateWazuhNotReadyYet( `Restarting ${isCluster ? 'Cluster' : 'Manager'}, please wait.` ); - await delay(15000); + await delayAsPromise(15000); await makePing(updateWazuhNotReadyYet, isCluster); return { restarted: isCluster ? 'Cluster' : 'Manager'} }catch (error){ diff --git a/public/controllers/settings/settings.js b/public/controllers/settings/settings.js index 3ac4fb8866..9ef6ab75b0 100644 --- a/public/controllers/settings/settings.js +++ b/public/controllers/settings/settings.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import { TabNames } from '../../utils/tab-names'; -import { kibana } from '../../../package.json'; +import { pluginPlatform } from '../../../package.json'; import { AppState } from '../../react-services/app-state'; import { WazuhConfig } from '../../react-services/wazuh-config'; import { GenericRequest } from '../../react-services/generic-request'; @@ -22,7 +22,7 @@ import { formatUIDate } from '../../react-services/time-service'; import store from '../../redux/store'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; import { updateSelectedSettingsSection } from '../../redux/actions/appStateActions'; -import { UI_LOGGER_LEVELS } from '../../../common/constants'; +import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; @@ -35,7 +35,8 @@ export class SettingsController { * @param {*} errorHandler */ constructor($scope, $window, $location, errorHandler) { - this.kibanaVersion = (kibana || {}).version || false; + this.pluginPlatformVersion = (pluginPlatform || {}).version || false; + this.pluginPlatformName = PLUGIN_PLATFORM_NAME; this.$scope = $scope; this.$window = $window; this.$location = $location; diff --git a/public/get_inner_angular.ts b/public/get_inner_angular.ts index d82cb226ac..c59956ff8e 100644 --- a/public/get_inner_angular.ts +++ b/public/get_inner_angular.ts @@ -45,7 +45,7 @@ import { EuiIcon } from '@elastic/eui'; /** * returns the main inner angular module, it contains all the parts of Angular Discover - * needs to render, so in the end the current 'kibana' angular module is no longer necessary + * needs to render, so in the end the current plugin platform angular module is no longer necessary */ export function getInnerAngularModule( name: string, diff --git a/public/kibana-integrations/kibana-discover.js b/public/kibana-integrations/kibana-discover.js index 6cd6ed2993..fbb3e472ea 100644 --- a/public/kibana-integrations/kibana-discover.js +++ b/public/kibana-integrations/kibana-discover.js @@ -45,7 +45,7 @@ getAngularModule().directive('kbnDis', [ } ]); -// Added dependencies (from Kibana module) +// Added dependencies (from plugin platform module) import './discover_dependencies'; //import 'ui/directives/render_directive'; import './discover/application/angular/directives'; diff --git a/public/kibana-integrations/kibana-vis.js b/public/kibana-integrations/kibana-vis.js index cef5cf94d3..29ba4926a3 100644 --- a/public/kibana-integrations/kibana-vis.js +++ b/public/kibana-integrations/kibana-vis.js @@ -252,7 +252,7 @@ class KibanaVis extends Component { vizPattern = JSON.parse(rawVis[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) .index; } catch (ex) { - console.warn(`kibana-vis exception: ${ex.message || ex}`); + console.warn(`plugin platform-vis exception: ${ex.message || ex}`); } if (!filters.find((filter) => filter.meta.controlledBy === AUTHORIZED_AGENTS)) { diff --git a/public/react-services/check-daemons-status.js b/public/react-services/check-daemons-status.js index 19c4302842..6eaf1b27fe 100644 --- a/public/react-services/check-daemons-status.js +++ b/public/react-services/check-daemons-status.js @@ -12,8 +12,8 @@ import store from '../redux/store'; import { updateWazuhNotReadyYet } from '../redux/actions/appStateActions'; import { WzRequest } from './wz-request'; +import { delayAsPromise } from '../../common/utils'; -const delay = time => new Promise(res => setTimeout(res,time)); let busy = false; export class CheckDaemonsStatus { @@ -25,7 +25,7 @@ export class CheckDaemonsStatus { let isValid = false; while (tries--) { - await delay(1200); + await delayAsPromise(1200); const result = await WzRequest.apiReq('GET', '/ping', {}); isValid = ((result || {}).data || {}).isValid; if (isValid) { diff --git a/public/react-services/generic-request.js b/public/react-services/generic-request.js index ab86e54522..ea553a157f 100644 --- a/public/react-services/generic-request.js +++ b/public/react-services/generic-request.js @@ -17,6 +17,7 @@ import { ApiCheck } from './wz-api-check'; import { WzMisc } from '../factories/misc'; import { OdfeUtils } from '../utils'; import { getHttp, getDataPlugin } from '../kibana-services'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; export class GenericRequest { static async request(method, path, payload = null, returnError = false) { @@ -27,8 +28,8 @@ export class GenericRequest { const wazuhConfig = new WazuhConfig(); const { timeout } = wazuhConfig.getConfig(); const requestHeaders = { - 'Content-Type': 'application/json', - 'kbn-xsrf': 'kibana' + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json' }; const tmpUrl = getHttp().basePath.prepend(path); diff --git a/public/react-services/pattern-handler.js b/public/react-services/pattern-handler.js index c68e4af960..888356d0c2 100644 --- a/public/react-services/pattern-handler.js +++ b/public/react-services/pattern-handler.js @@ -14,7 +14,7 @@ import { SavedObject } from './saved-objects'; import { getDataPlugin, getToasts, getHttp } from '../kibana-services'; import { WazuhConfig } from '../react-services/wazuh-config'; import { HEALTH_CHECK } from '../../common/constants'; -import { satisfyKibanaVersion } from '../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../common/semver'; export class PatternHandler { /** @@ -44,7 +44,7 @@ export class PatternHandler { static async changePattern(selectedPattern) { try { AppState.setCurrentPattern(selectedPattern); - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ await this.refreshIndexPattern(); }; return AppState.getCurrentPattern(); diff --git a/public/react-services/saved-objects.js b/public/react-services/saved-objects.js index c19781023d..d1908e089c 100644 --- a/public/react-services/saved-objects.js +++ b/public/react-services/saved-objects.js @@ -20,7 +20,7 @@ import { WAZUH_INDEX_TYPE_MONITORING, WAZUH_INDEX_TYPE_STATISTICS, } from '../../common/constants'; -import { satisfyKibanaVersion } from '../../common/semver'; +import { satisfyPluginPlatformVersion } from '../../common/semver'; export class SavedObject { /** @@ -35,9 +35,9 @@ export class SavedObject { let indexPatterns = ((savedObjects || {}).data || {}).saved_objects || []; let indexPatternsFields; - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ indexPatternsFields = indexPatterns.map(indexPattern => indexPattern?.attributes?.fields ? JSON.parse(indexPattern.attributes.fields) : []); - }else if(satisfyKibanaVersion('>=7.11')){ + }else if(satisfyPluginPlatformVersion('>=7.11')){ indexPatternsFields = await Promise.all(indexPatterns.map(async indexPattern => { try{ const {data: {fields}} = await GenericRequest.request( @@ -94,7 +94,7 @@ export class SavedObject { const result = await SavedObject.existsIndexPattern(patternID); if (!result.data) { let fields = ''; - if (satisfyKibanaVersion('<7.11')) { + if (satisfyPluginPlatformVersion('<7.11')) { fields = await SavedObject.getIndicesFields(patternID, WAZUH_INDEX_TYPE_ALERTS); } await this.createSavedObject( @@ -154,9 +154,9 @@ export class SavedObject { true ); let indexPatternFields; - if(satisfyKibanaVersion('<7.11')){ + if(satisfyPluginPlatformVersion('<7.11')){ indexPatternFields = indexPatternData?.data?.attributes?.fields ? JSON.parse(indexPatternData.data.attributes.fields) : []; - }else if(satisfyKibanaVersion('>=7.11')){ + }else if(satisfyPluginPlatformVersion('>=7.11')){ try{ const {data: {fields}} = await GenericRequest.request( 'GET', @@ -187,7 +187,7 @@ export class SavedObject { params ); - if (satisfyKibanaVersion('<7.11') && type === 'index-pattern') { + if (satisfyPluginPlatformVersion('<7.11') && type === 'index-pattern') { await this.refreshFieldsOfIndexPattern(id, params.attributes.title, fields); } @@ -201,7 +201,7 @@ export class SavedObject { static async refreshFieldsOfIndexPattern(id, title, fields) { try { - // same logic as Kibana when a new index is created, you need to refresh it to see its fields + // same logic as plugin platform when a new index is created, you need to refresh it to see its fields // we force the refresh of the index by requesting its fields and the assign these fields await GenericRequest.request( 'PUT', @@ -267,7 +267,7 @@ export class SavedObject { */ static async createWazuhIndexPattern(pattern) { try { - const fields = satisfyKibanaVersion('<7.11') + const fields = satisfyPluginPlatformVersion('<7.11') ? await SavedObject.getIndicesFields(pattern, WAZUH_INDEX_TYPE_ALERTS) : ''; await this.createSavedObject( diff --git a/public/react-services/wz-api-check.js b/public/react-services/wz-api-check.js index ad19b028de..8086692261 100644 --- a/public/react-services/wz-api-check.js +++ b/public/react-services/wz-api-check.js @@ -14,6 +14,7 @@ import axios from 'axios'; import { AppState } from './app-state'; import { WzMisc } from '../factories/misc'; import { getHttp } from '../kibana-services'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; export class ApiCheck { static async checkStored(data, idChanged = false) { @@ -29,7 +30,7 @@ export class ApiCheck { const url = getHttp().basePath.prepend('/api/check-stored-api'); const options = { method: 'POST', - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana' }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' }, url: url, data: payload, timeout: timeout || 20000 @@ -72,7 +73,7 @@ export class ApiCheck { const options = { method: 'POST', - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana' }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' }, url: url, data: {...apiEntry, forceRefresh}, timeout: timeout || 20000 diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 6978134043..51c5c51356 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -18,6 +18,7 @@ import { WazuhConfig } from './wazuh-config'; import { OdfeUtils } from '../utils'; import IApiResponse from './interfaces/api-response.interface'; import { getHttp } from '../kibana-services'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; export class WzRequest { static wazuhConfig: any; @@ -45,7 +46,7 @@ export class WzRequest { const url = getHttp().basePath.prepend(path); const options = { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana' }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' }, url: url, data: payload, timeout: customTimeout || timeout, diff --git a/public/react-services/wz-user-permissions.ts b/public/react-services/wz-user-permissions.ts index 70e3e92d6c..a42fe50938 100644 --- a/public/react-services/wz-user-permissions.ts +++ b/public/react-services/wz-user-permissions.ts @@ -1,5 +1,5 @@ /* - * Wazuh app - React hook for get query of Kibana searchBar + * Wazuh app - React hook for get query of plugin platform searchBar * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/services/resolves/go-to-kibana.js b/public/services/resolves/go-to-plugin-platform.js similarity index 82% rename from public/services/resolves/go-to-kibana.js rename to public/services/resolves/go-to-plugin-platform.js index 136ade7d09..5c40bc1c74 100644 --- a/public/services/resolves/go-to-kibana.js +++ b/public/services/resolves/go-to-plugin-platform.js @@ -9,8 +9,11 @@ * * Find more information about this on the LICENSE file. */ + +import { PLUGIN_PLATFORM_BASE_REDIRECTION_PATH } from "../../../common/constants"; + // Manage leaving the app to another Kibana tab -export function goToKibana($location, $window) { +export function goToPluginPlatform($location, $window) { const url = $location.$$absUrl.substring(0, $location.$$absUrl.indexOf('#')); const lastSubUrl = $window.sessionStorage.getItem(`lastSubUrl:${url}`) || ''; if ( @@ -21,5 +24,6 @@ export function goToKibana($location, $window) { $window.sessionStorage.setItem(`lastSubUrl:${url}`, url); } - $window.location.href = $location.absUrl().replace('/wazuh#', '/kibana#'); + + $window.location.href = $location.absUrl().replace('/wazuh#', `/${PLUGIN_PLATFORM_BASE_REDIRECTION_PATH}#`); } diff --git a/public/services/resolves/index.js b/public/services/resolves/index.js index a5ec098834..6de138864d 100644 --- a/public/services/resolves/index.js +++ b/public/services/resolves/index.js @@ -13,7 +13,7 @@ import { checkTimestamp } from './check-timestamp'; import { healthCheck } from './health-check'; import { settingsWizard } from './settings-wizard'; import { getSavedSearch } from './get-saved-search'; -import { goToKibana } from './go-to-kibana'; +import { goToPluginPlatform } from './go-to-plugin-platform'; import { getIp } from './get-ip'; import { getWzConfig } from './get-config'; import { apiCount } from './api-count'; @@ -23,7 +23,7 @@ export { healthCheck, settingsWizard, getSavedSearch, - goToKibana, + goToPluginPlatform, getIp, getWzConfig, apiCount diff --git a/public/services/routes.js b/public/services/routes.js index bc139b79f6..652f8b2eaf 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -18,7 +18,7 @@ import 'angular-route'; import { settingsWizard, getSavedSearch, - goToKibana, + goToPluginPlatform, getIp, getWzConfig, apiCount @@ -114,7 +114,7 @@ function wzKibana($location, $window, $rootScope) { // Removes ?_g $location.search('_g', null); } - return goToKibana($location, $window); + return goToPluginPlatform($location, $window); } function clearRuleId(commonData) { diff --git a/public/services/theming.js b/public/services/theming.js index 2e5d8a27b9..e078643409 100644 --- a/public/services/theming.js +++ b/public/services/theming.js @@ -1,5 +1,5 @@ /* - * Wazuh app - Kibana theming configuration file + * Wazuh app - plugin platform theming configuration file * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/styles/common.scss b/public/styles/common.scss index 26655cee8d..0976bc9326 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -165,7 +165,7 @@ background-color: rgba(0, 0, 0, 0.1); } -/* Special fix to make Kibana search bar similar to the rest from the app */ +/* Special fix to make plugin platform search bar similar to the rest from the app */ .kuiLocalSearchInput, .kuiLocalSearchInput:focus { @@ -416,7 +416,7 @@ input[type="search"].euiFieldSearch{ background: transparent !important; } -/* Custom Kibana styles */ +/* Custom plugin plaform styles */ md-content.md-default-theme, md-content{ background: transparent!important; diff --git a/public/templates/settings/settings-about.html b/public/templates/settings/settings-about.html index e84b90abb9..7a0434501d 100644 --- a/public/templates/settings/settings-about.html +++ b/public/templates/settings/settings-about.html @@ -20,7 +20,7 @@
    -

    Welcome to the Wazuh app for Kibana {{ctrl.kibanaVersion}}

    +

    Welcome to the Wazuh app for Kibana {{ctrl.pluginPlatformVersion}}

    diff --git a/public/templates/settings/settings.html b/public/templates/settings/settings.html index 8c86dfaf45..2b39a0087c 100644 --- a/public/templates/settings/settings.html +++ b/public/templates/settings/settings.html @@ -87,7 +87,7 @@ >

    - Welcome to the Wazuh app for Kibana {{ctrl.kibanaVersion}} + Welcome to the Wazuh app for {{ctrl.pluginPlatformName}} {{ctrl.pluginPlatformVersion}}

    @@ -96,7 +96,7 @@

    - Wazuh Kibana plugin provides management and monitoring capabilities, giving users + Wazuh {{ctrl.pluginPlatformName}} plugin provides management and monitoring capabilities, giving users control over the Wazuh infrastructure. Using this plugin you can monitor your agents status and configuration, query and visualize your alert data and monitor manager rules and configuration. diff --git a/public/utils/add_help_menu_to_app.tsx b/public/utils/add_help_menu_to_app.tsx index 54a13d92d5..f3048fff75 100644 --- a/public/utils/add_help_menu_to_app.tsx +++ b/public/utils/add_help_menu_to_app.tsx @@ -1,5 +1,5 @@ /* - * Wazuh app - Add the plugin help links as extension in Kibana help menu + * Wazuh app - Add the plugin help links as extension in plugin platform help menu * Copyright (C) 2015-2021 Wazuh, Inc. * * This program is free software; you can redistribute it and/or modify diff --git a/public/utils/check-plugin-version.tsx b/public/utils/check-plugin-version.tsx index 4a3c3b44c3..19a1a49995 100644 --- a/public/utils/check-plugin-version.tsx +++ b/public/utils/check-plugin-version.tsx @@ -17,6 +17,7 @@ import { getCookies, getToasts } from '../kibana-services'; import { ErrorToastOptions } from 'kibana/public'; import React from 'react'; import { ReactNode } from 'x-pack/node_modules/@types/react'; +import { PLUGIN_PLATFORM_NAME } from '../../common/constants'; type TAppInfo = { revision: string; @@ -46,7 +47,7 @@ const checkClientAppVersion = (appInfo: TAppInfo) => { const toastOptions: ErrorToastOptions = { title: 'Conflict with the Wazuh app version', toastLifeTimeMs: 50000, - toastMessage: `The version of the Wazuh app in your browser does not correspond with the app version installed in Kibana. Please, clear your browser cache. For more info check the full error.`, + toastMessage: `The version of the Wazuh app in your browser does not correspond with the app version installed in ${PLUGIN_PLATFORM_NAME}. Please, clear your browser cache. For more info check the full error.`, }; const troubleshootingUrl = `https://documentation.wazuh.com/${appInfo['app-version'] @@ -61,14 +62,14 @@ const checkClientAppVersion = (appInfo: TAppInfo) => { {appVersion} - {appRevision} {' '} - does not correspond with the app version installed in Kibana{' '} + does not correspond with the app version installed in {PLUGIN_PLATFORM_NAME}{' '} {appInfo['app-version']} - {appInfo.revision} .

    Please, clear your browser cache following these steps.

    -

    If the error persists, restart Kibana as well.

    +

    If the error persists, restart {PLUGIN_PLATFORM_NAME} as well.

    For more information check our troubleshooting section{' '} diff --git a/public/utils/config-equivalences.js b/public/utils/config-equivalences.js index 7e26758d67..7bd6f13c08 100644 --- a/public/utils/config-equivalences.js +++ b/public/utils/config-equivalences.js @@ -1,4 +1,4 @@ -import { ASSETS_BASE_URL_PREFIX } from "../../common/constants"; +import { ASSETS_BASE_URL_PREFIX, PLUGIN_PLATFORM_NAME } from "../../common/constants"; export const configEquivalences = { pattern: 'Default index pattern to use on the app.', @@ -16,11 +16,11 @@ export const configEquivalences = { 'checks.fields': 'Enable or disable the known fields health check when opening the app.', 'checks.metaFields': - 'Change the default value of the Kibana metaField configuration', + `Change the default value of the ${PLUGIN_PLATFORM_NAME} metaField configuration`, 'checks.timeFilter': - 'Change the default value of the Kibana timeFilter configuration', + `Change the default value of the ${PLUGIN_PLATFORM_NAME} timeFilter configuration`, 'checks.maxBuckets': - 'Change the default value of the Kibana max buckets configuration', + `Change the default value of the ${PLUGIN_PLATFORM_NAME} max buckets configuration`, 'extensions.pci': 'Enable or disable the PCI DSS tab on Overview and Agents.', 'extensions.gdpr': 'Enable or disable the GDPR tab on Overview and Agents.', 'extensions.audit': 'Enable or disable the Audit tab on Overview and Agents.', diff --git a/public/utils/wz-logo-menu.js b/public/utils/wz-logo-menu.js index e39c1efd7e..ddb693e16c 100644 --- a/public/utils/wz-logo-menu.js +++ b/public/utils/wz-logo-menu.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ -// Remove Kibana Wazuh name and breadcrumb +// Remove plugin platform Wazuh name and breadcrumb export const changeWazuhNavLogo = () => { const interval = setInterval(() => { const nav = document.querySelector('[data-test-subj="breadcrumbs"] > .euiBreadcrumb'); diff --git a/server/controllers/wazuh-hosts.ts b/server/controllers/wazuh-hosts.ts index d0c1caefde..1b798327f6 100644 --- a/server/controllers/wazuh-hosts.ts +++ b/server/controllers/wazuh-hosts.ts @@ -16,7 +16,7 @@ import { log } from '../lib/logger'; import { ErrorResponse } from '../lib/error-response'; import { APIUserAllowRunAs } from '../lib/cache-api-user-has-run-as'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; -import { WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH } from '../../common/constants'; +import { WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_INSTALLATION_USER, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_NAME } from '../../common/constants'; export class WazuhHostsCtrl { constructor() { @@ -45,7 +45,7 @@ export class WazuhHostsCtrl { return response.badRequest({ body: { message: `Error getting the hosts entries: The \'${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}\' directory could not exist in your Kibana installation. - If this doesn't exist, create it and give the permissions 'sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R kibana:kibana ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}'. After, restart the Kibana service.` + If this doesn't exist, create it and give the permissions 'sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}'. After, restart the ${PLUGIN_PLATFORM_NAME} service.` } }) } diff --git a/server/index.ts b/server/index.ts index cf09e38894..6286a55684 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,7 +3,7 @@ import { PluginInitializerContext } from 'kibana/server'; import { WazuhPlugin } from './plugin'; // This exports static code and TypeScript types, -// as well as, Kibana Platform `plugin()` initializer. +// as well as, plugin platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { return new WazuhPlugin(initializerContext); diff --git a/server/integration-files/kibana-template.ts b/server/integration-files/kibana-template.ts index 3d69035fa9..84ef3ffb41 100644 --- a/server/integration-files/kibana-template.ts +++ b/server/integration-files/kibana-template.ts @@ -9,7 +9,7 @@ * * Find more information about this on the LICENSE file. */ -export const kibanaTemplate = { +export const pluginPlatformTemplate = { order: 0, template: '.kibana*', settings: { diff --git a/server/routes/wazuh-api.test.ts b/server/routes/wazuh-api.test.ts index 43b0b2f0fb..bf02089d0f 100644 --- a/server/routes/wazuh-api.test.ts +++ b/server/routes/wazuh-api.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-api import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}){ return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data }; diff --git a/server/routes/wazuh-elastic.test.ts b/server/routes/wazuh-elastic.test.ts index 701db14b75..34b821db1c 100644 --- a/server/routes/wazuh-elastic.test.ts +++ b/server/routes/wazuh-elastic.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-elastic import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}) { return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data, }; diff --git a/server/routes/wazuh-hosts.test.ts b/server/routes/wazuh-hosts.test.ts index 8f806a1c0c..226de54ecd 100644 --- a/server/routes/wazuh-hosts.test.ts +++ b/server/routes/wazuh-hosts.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-hosts import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}){ return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data }; diff --git a/server/routes/wazuh-reporting.test.ts b/server/routes/wazuh-reporting.test.ts index 8c1573d1fd..b56ea7b2ff 100644 --- a/server/routes/wazuh-reporting.test.ts +++ b/server/routes/wazuh-reporting.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-reporting import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}){ return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data }; diff --git a/server/routes/wazuh-utils/ui-logs.test.ts b/server/routes/wazuh-utils/ui-logs.test.ts index c4503c55c6..a48f02852f 100644 --- a/server/routes/wazuh-utils/ui-logs.test.ts +++ b/server/routes/wazuh-utils/ui-logs.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-utils/ui-logs import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../../common/constants'; const buildAxiosOptions = (method: string, path: string, data: any = {}, headers: any = {}) => { return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data, }; diff --git a/server/routes/wazuh-utils/wazuh-utils.test.ts b/server/routes/wazuh-utils/wazuh-utils.test.ts index 07c4f1e1f7..166e5331a9 100644 --- a/server/routes/wazuh-utils/wazuh-utils.test.ts +++ b/server/routes/wazuh-utils/wazuh-utils.test.ts @@ -1,11 +1,12 @@ // To launch this file // yarn test:jest --testEnvironment node --verbose server/routes/wazuh-utils import axios from 'axios'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../../common/constants'; function buildAxiosOptions(method: string, path: string, data: any = {}, headers: any = {}) { return { method: method, - headers: { 'Content-Type': 'application/json', 'kbn-xsrf': 'kibana', ...headers }, + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...headers }, url: `http://localhost:5601${path}`, data: data, }; diff --git a/server/start/cron-scheduler/scheduler-handler.ts b/server/start/cron-scheduler/scheduler-handler.ts index 1d7509ab91..25515fb296 100644 --- a/server/start/cron-scheduler/scheduler-handler.ts +++ b/server/start/cron-scheduler/scheduler-handler.ts @@ -5,6 +5,7 @@ import { getConfiguration } from '../../lib/get-configuration'; import cron from 'node-cron'; import { WAZUH_STATISTICS_DEFAULT_PREFIX, WAZUH_STATISTICS_DEFAULT_NAME, WAZUH_STATISTICS_TEMPLATE_NAME } from '../../../common/constants'; import { statisticsTemplate } from '../../integration-files/statistics-template'; +import { delayAsPromise } from '../../../common/utils'; const blueWazuh = '\u001b[34mwazuh\u001b[39m'; const schedulerErrorLogColors = [blueWazuh, 'scheduler', 'error']; @@ -13,10 +14,10 @@ const schedulerJobs = []; /** * Wait until Kibana server is ready */ -const checkKibanaStatus = async function (context) { +const checkPluginPlatformStatus = async function (context) { try { log( - 'scheduler-handler:checkKibanaStatus', + 'scheduler-handler:checkPluginPlatformStatus', 'Waiting for Kibana and Elasticsearch servers to be ready...', 'debug' ); @@ -26,12 +27,12 @@ const checkKibanaStatus = async function (context) { return; } catch (error) { log( - 'scheduler-handler:checkKibanaStatus', + 'scheduler-handler:checkPluginPlatformStatus', error.mesage ||error ); try{ - await delay(3000); - await checkKibanaStatus(context); + await delayAsPromise(3000); + await checkPluginPlatformStatus(context); }catch(error){}; } } @@ -110,7 +111,7 @@ const checkTemplate = async function (context) { export async function jobSchedulerRun(context){ // Check Kibana index and if it is prepared, start the initialization of Wazuh App. - await checkKibanaStatus(context); + await checkPluginPlatformStatus(context); for (const job in configuredJobs({})) { const schedulerJob: SchedulerJob = new SchedulerJob(job, context); schedulerJobs.push(schedulerJob); diff --git a/server/start/initialize/index.ts b/server/start/initialize/index.ts index e40444d868..c3c4d5b79b 100644 --- a/server/start/initialize/index.ts +++ b/server/start/initialize/index.ts @@ -11,20 +11,20 @@ */ import { log } from '../../lib/logger'; import packageJSON from '../../../package.json'; -import { kibanaTemplate } from '../../integration-files/kibana-template'; +import { pluginPlatformTemplate } from '../../integration-files/kibana-template'; import { getConfiguration } from '../../lib/get-configuration'; import { totalmem } from 'os'; import fs from 'fs'; import { ManageHosts } from '../../lib/manage-hosts'; -import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_KIBANA_TEMPLATE_NAME, WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH } from '../../../common/constants'; +import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; import { createDataDirectoryIfNotExists } from '../../lib/filesystem'; import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; const manageHosts = new ManageHosts(); export function jobInitializeRun(context) { - const KIBANA_INDEX = context.server.config.kibana.index; - log('initialize', `Kibana index: ${KIBANA_INDEX}`, 'info'); + const PLUGIN_PLATFORM_INDEX = context.server.config.kibana.index; + log('initialize', `${PLUGIN_PLATFORM_NAME} index: ${PLUGIN_PLATFORM_INDEX}`, 'info'); log('initialize', `App revision: ${packageJSON.revision}`, 'info'); let configurationFile = {}; @@ -198,7 +198,7 @@ export function jobInitializeRun(context) { } if(!fs.existsSync(WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH)){ - throw new Error(`The data directory is missing in the Kibana root instalation. Create the directory in ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R kibana:kibana ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}). After restart the Kibana service.`); + throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); }; if (!fs.existsSync(WAZUH_DATA_CONFIG_REGISTRY_PATH)) { @@ -244,12 +244,12 @@ export function jobInitializeRun(context) { const createKibanaTemplate = () => { log( 'initialize:createKibanaTemplate', - `Creating template for ${KIBANA_INDEX}`, + `Creating template for ${PLUGIN_PLATFORM_INDEX}`, 'debug' ); try { - kibanaTemplate.template = KIBANA_INDEX + '*'; + pluginPlatformTemplate.template = PLUGIN_PLATFORM_INDEX + '*'; } catch (error) { log('initialize:createKibanaTemplate', error.message || error); context.wazuh.logger.error( @@ -258,10 +258,10 @@ export function jobInitializeRun(context) { } return context.core.elasticsearch.client.asInternalUser.indices.putTemplate({ - name: WAZUH_KIBANA_TEMPLATE_NAME, + name: WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, order: 0, create: true, - body: kibanaTemplate + body: pluginPlatformTemplate }); }; @@ -269,15 +269,15 @@ export function jobInitializeRun(context) { try { log( 'initialize:createEmptyKibanaIndex', - `Creating ${KIBANA_INDEX} index.`, + `Creating ${PLUGIN_PLATFORM_INDEX} index.`, 'info' ); await context.core.elasticsearch.client.asInternalUser.indices.create({ - index: KIBANA_INDEX + index: PLUGIN_PLATFORM_INDEX }); log( 'initialize:createEmptyKibanaIndex', - `Successfully created ${KIBANA_INDEX} index.`, + `Successfully created ${PLUGIN_PLATFORM_INDEX} index.`, 'debug' ); await init(); @@ -286,7 +286,7 @@ export function jobInitializeRun(context) { return Promise.reject( new Error( `Error creating ${ - KIBANA_INDEX + PLUGIN_PLATFORM_INDEX } index due to ${error.message || error}` ) ); @@ -297,8 +297,8 @@ export function jobInitializeRun(context) { try { await createKibanaTemplate(); log( - 'initialize:checkKibanaStatus', - `Successfully created ${KIBANA_INDEX} template.`, + 'initialize:fixKibanaTemplate', + `Successfully created ${PLUGIN_PLATFORM_INDEX} template.`, 'debug' ); await createEmptyKibanaIndex(); @@ -307,7 +307,7 @@ export function jobInitializeRun(context) { return Promise.reject( new Error( `Error creating template for ${ - KIBANA_INDEX + PLUGIN_PLATFORM_INDEX } due to ${error.message || error}` ) ); @@ -317,17 +317,17 @@ export function jobInitializeRun(context) { const getTemplateByName = async () => { try { await context.core.elasticsearch.client.asInternalUser.indices.getTemplate({ - name: WAZUH_KIBANA_TEMPLATE_NAME + name: WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME }); log( - 'initialize:checkKibanaStatus', - `No need to create the ${KIBANA_INDEX} template, already exists.`, + 'initialize:getTemplateByName', + `No need to create the ${PLUGIN_PLATFORM_INDEX} template, already exists.`, 'debug' ); await createEmptyKibanaIndex(); return; } catch (error) { - log('initialize:checkKibanaStatus', error.message || error); + log('initialize:getTemplateByName', error.message || error); return fixKibanaTemplate(); } }; @@ -336,7 +336,7 @@ export function jobInitializeRun(context) { const checkKibanaStatus = async () => { try { const response = await context.core.elasticsearch.client.asInternalUser.indices.exists({ - index: KIBANA_INDEX + index: PLUGIN_PLATFORM_INDEX }); if (response.body) { // It exists, initialize! @@ -345,7 +345,7 @@ export function jobInitializeRun(context) { // No Kibana index created... log( 'initialize:checkKibanaStatus', - `Not found ${KIBANA_INDEX} index`, + `Not found ${PLUGIN_PLATFORM_INDEX} index`, 'info' ); await getTemplateByName(); diff --git a/server/start/monitoring/index.ts b/server/start/monitoring/index.ts index 00bc47c586..174ca27cae 100644 --- a/server/start/monitoring/index.ts +++ b/server/start/monitoring/index.ts @@ -27,6 +27,7 @@ import { WAZUH_MONITORING_DEFAULT_FREQUENCY, } from '../../../common/constants'; import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; +import { delayAsPromise } from '../../../common/utils'; const blueWazuh = '\u001b[34mwazuh\u001b[39m'; const monitoringErrorLogColors = [blueWazuh, 'monitoring', 'error']; @@ -35,17 +36,6 @@ const wazuhHostController = new WazuhHostsCtrl(); let MONITORING_ENABLED, MONITORING_FREQUENCY, MONITORING_CRON_FREQ, MONITORING_CREATION, MONITORING_INDEX_PATTERN, MONITORING_INDEX_PREFIX; // Utils functions - -/** - * Delay as promise - * @param timeMs - */ -function delay(timeMs: number) { - return new Promise((resolve) => { - setTimeout(resolve, timeMs); - }); -} - /** * Get the setting value from the configuration * @param setting @@ -297,10 +287,10 @@ async function createIndex(context, indexName: string) { /** * Wait until Kibana server is ready */ -async function checkKibanaStatus(context) { +async function checkPluginPlatformStatus(context) { try { log( - 'monitoring:checkKibanaStatus', + 'monitoring:checkPluginPlatformStatus', 'Waiting for Kibana and Elasticsearch servers to be ready...', 'debug' ); @@ -310,12 +300,12 @@ async function checkKibanaStatus(context) { return; } catch (error) { log( - 'monitoring:checkKibanaStatus', + 'monitoring:checkPluginPlatformStatus', error.mesage ||error ); try{ - await delay(3000); - await checkKibanaStatus(context); + await delayAsPromise(3000); + await checkPluginPlatformStatus(context); }catch(error){}; } } @@ -410,7 +400,7 @@ async function cronTask(context) { // ((error || {}).status === 404 && // (error || {}).displayName === 'NotFound') // ) { - // await delay(1000); + // await delayAsPromise(1000); // return cronTask(context); // } // } catch (error) {} //eslint-disable-line @@ -513,7 +503,7 @@ export async function jobMonitoringRun(context) { // Init the monitoring variables initMonitoringConfiguration(context); // Check Kibana index and if it is prepared, start the initialization of Wazuh App. - await checkKibanaStatus(context); + await checkPluginPlatformStatus(context); // // Run the cron job only it it's enabled if (MONITORING_ENABLED) { cronTask(context); diff --git a/test/server/alerts.js b/test/server/alerts.js index 3642c2e3dd..0d9a293dd4 100644 --- a/test/server/alerts.js +++ b/test/server/alerts.js @@ -1,10 +1,11 @@ const chai = require('chai'); const needle = require('needle'); +const { PLUGIN_PLATFORM_REQUEST_HEADERS } = require('../../common/constants'); const elasticServer = process.env.WAZUH_ELASTIC_IP || 'localhost'; chai.should(); const headers = { - headers: { 'kbn-xsrf': 'kibana', 'Content-Type': 'application/json' } + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' } }; const date = new Date(); diff --git a/test/server/wazuh-api.js b/test/server/wazuh-api.js index 9d72be3f11..7e32285110 100644 --- a/test/server/wazuh-api.js +++ b/test/server/wazuh-api.js @@ -1,10 +1,11 @@ const chai = require('chai'); const needle = require('needle'); +const { PLUGIN_PLATFORM_REQUEST_HEADERS } = require('../../common/constants'); chai.should(); const headers = { - headers: { 'kbn-xsrf': 'kibana', 'Content-Type': 'application/json' } + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' } }; let API_ID = null; diff --git a/test/server/wazuh-elastic.js b/test/server/wazuh-elastic.js index 1f655daebf..2143f211c4 100644 --- a/test/server/wazuh-elastic.js +++ b/test/server/wazuh-elastic.js @@ -1,13 +1,13 @@ const chai = require('chai'); const needle = require('needle'); -const { WAZUH_ALERTS_PATTERN } = require('../../common/constants'); +const { WAZUH_ALERTS_PATTERN, PLUGIN_PLATFORM_REQUEST_HEADERS } = require('../../common/constants'); const kibanaServer = process.env.KIBANA_IP || 'localhost'; chai.should(); const headers = { - headers: { 'kbn-xsrf': 'kibana', 'Content-Type': 'application/json' } + headers: { ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json' } }; describe('wazuh-elastic', () => { From a2150e2f35659194a6d1a7b0998d75e88ce3034a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 26 Jan 2022 08:59:30 +0100 Subject: [PATCH 399/493] fix: Changed some references to plugin platform --- common/constants.ts | 8 ++++---- server/controllers/wazuh-hosts.ts | 8 ++++---- server/start/initialize/index.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index ce42a5a0b3..62e3ff1932 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -126,13 +126,13 @@ export const WAZUH_CONFIGURATION_SETTINGS_NEED_RELOAD = [ export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; // Wazuh data path -const WAZUH_DATA_KIBANA_BASE_PATH = 'data'; -export const WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH = path.join( +const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; +export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( __dirname, '../../../', - WAZUH_DATA_KIBANA_BASE_PATH + WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH ); -export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, 'wazuh'); +export const WAZUH_DATA_ABSOLUTE_PATH = path.join(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, 'wazuh'); // Wazuh data path - config export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); diff --git a/server/controllers/wazuh-hosts.ts b/server/controllers/wazuh-hosts.ts index 1b798327f6..480adb9cd9 100644 --- a/server/controllers/wazuh-hosts.ts +++ b/server/controllers/wazuh-hosts.ts @@ -16,7 +16,7 @@ import { log } from '../lib/logger'; import { ErrorResponse } from '../lib/error-response'; import { APIUserAllowRunAs } from '../lib/cache-api-user-has-run-as'; import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'src/core/server'; -import { WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_INSTALLATION_USER, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_NAME } from '../../common/constants'; +import { WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_INSTALLATION_USER, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_NAME } from '../../common/constants'; export class WazuhHostsCtrl { constructor() { @@ -41,11 +41,11 @@ export class WazuhHostsCtrl { body: result }); } catch (error) { - if(error && error.message && ['ENOENT: no such file or directory', WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH].every(text => error.message.includes(text))){ + if(error && error.message && ['ENOENT: no such file or directory', WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH].every(text => error.message.includes(text))){ return response.badRequest({ body: { - message: `Error getting the hosts entries: The \'${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}\' directory could not exist in your Kibana installation. - If this doesn't exist, create it and give the permissions 'sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}'. After, restart the ${PLUGIN_PLATFORM_NAME} service.` + message: `Error getting the hosts entries: The \'${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}\' directory could not exist in your ${PLUGIN_PLATFORM_NAME} installation. + If this doesn't exist, create it and give the permissions 'sudo mkdir ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}'. After, restart the ${PLUGIN_PLATFORM_NAME} service.` } }) } diff --git a/server/start/initialize/index.ts b/server/start/initialize/index.ts index c3c4d5b79b..5881a93483 100644 --- a/server/start/initialize/index.ts +++ b/server/start/initialize/index.ts @@ -16,7 +16,7 @@ import { getConfiguration } from '../../lib/get-configuration'; import { totalmem } from 'os'; import fs from 'fs'; import { ManageHosts } from '../../lib/manage-hosts'; -import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; +import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; import { createDataDirectoryIfNotExists } from '../../lib/filesystem'; import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; @@ -197,8 +197,8 @@ export function jobInitializeRun(context) { ); } - if(!fs.existsSync(WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH)){ - throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_KIBANA_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); + if(!fs.existsSync(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH)){ + throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); }; if (!fs.existsSync(WAZUH_DATA_CONFIG_REGISTRY_PATH)) { From 3cee0fe4946d5b33fde398581224ec8e08105a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 26 Jan 2022 09:14:54 +0100 Subject: [PATCH 400/493] fix(screenshots): Fixed Agents section screenshot --- screenshots/app6.png | Bin 214994 -> 215576 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/app6.png b/screenshots/app6.png index 9017d74921a9bdd06f0dbb3bc6213153450ff44a..57fce9cbb1708b9be13d8f3ae88587ee9e7514ca 100644 GIT binary patch delta 168618 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

    - + {textAndLinkToCheckConnectionDocumentation} From 830ac00b4eca04887ded7765c78fa08f6ae46342 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Tue, 22 Feb 2022 12:51:13 -0300 Subject: [PATCH 410/493] Fixed agents details card style (#3845) * Fixed agents details card style * Updated CHANGELOG --- CHANGELOG.md | 1 + .../agent/components/agents-preview.js | 216 +++++++++--------- public/styles/common.scss | 28 ++- 3 files changed, 130 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29820db8f5..93a1eea13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,6 +128,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) - Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) +- Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index 8d7608acc3..50534813f8 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -221,119 +221,109 @@ export const AgentsPreview = compose( {this.totalAgents > 0 && ( - + - - - {this.summary && ( - - - - this.showAgentsWithFilters(FILTER_ACTIVE)} - > - {this.state.data[0].value} - - - } - titleSize={'s'} - description="Active" - titleColor="secondary" - className="white-space-nowrap" - /> - - - - - this.showAgentsWithFilters(FILTER_DISCONNECTED) - } - > - {this.state.data[1].value} - - - } - titleSize={'s'} - description="Disconnected" - titleColor="danger" - className="white-space-nowrap" - /> - - - - - this.showAgentsWithFilters(FILTER_NEVER_CONNECTED) - } - > - {this.state.data[2].value} - - - } - titleSize={'s'} - description="Never connected" - titleColor="subdued" - className="white-space-nowrap" - /> - - - - - - )} - - {this.lastAgent && ( - - - this.showLastAgent()}> - {this.lastAgent.name} - - - } - titleSize="s" - description="Last registered agent" - titleColor="primary" - className="pb-12 white-space-nowrap" - /> - - )} - {this.mostActiveAgent && ( - - - this.showMostActiveAgent()}> - {this.mostActiveAgent.name || '-'} - - - } - className="white-space-nowrap" - titleSize="s" - description="Most active agent" - titleColor="primary" - /> - - )} - - + {this.summary && ( + + + + this.showAgentsWithFilters(FILTER_ACTIVE)}> + {this.state.data[0].value} + + + } + titleSize={'s'} + description="Active" + titleColor="secondary" + className="white-space-nowrap" + /> + + + + + this.showAgentsWithFilters(FILTER_DISCONNECTED) + } + > + {this.state.data[1].value} + + + } + titleSize={'s'} + description="Disconnected" + titleColor="danger" + className="white-space-nowrap" + /> + + + + + this.showAgentsWithFilters(FILTER_NEVER_CONNECTED) + } + > + {this.state.data[2].value} + + + } + titleSize={'s'} + description="Never connected" + titleColor="subdued" + className="white-space-nowrap" + /> + + + + + + )} + + {this.lastAgent && ( + + + this.showLastAgent()}> + {this.lastAgent.name} + + + } + titleSize="s" + description="Last registered agent" + titleColor="primary" + /> + + )} + {this.mostActiveAgent && ( + + + this.showMostActiveAgent()}> + {this.mostActiveAgent.name || '-'} + + + } + className="last-agents-link" + titleSize="s" + description="Most active agent" + titleColor="primary" + /> + + )} @@ -418,4 +408,4 @@ export const AgentsPreview = compose( AgentsTable.propTypes = { tableProps: PropTypes.object, showAgent: PropTypes.func, -}; \ No newline at end of file +}; diff --git a/public/styles/common.scss b/public/styles/common.scss index 58b26d4b83..0af0d694d3 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1555,18 +1555,38 @@ div.euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom.wz-menu } } +.agents-details-card { + .last-agents-link { + .euiToolTipAnchor { + white-space: nowrap; + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + } + } +} + + @media (min-width: 1600px) { .agents-evolution-visualization{ width: 35vw; } + + .agents-details-card { + width: 55vw; + } + } @media (max-width: 1599px) { .agents-evolution-visualization{ width: 30vw; } -} + .agents-details-card { + width: 45vw; + } +} @media (max-width: 1439px) { .agents-evolution-visualization{ @@ -1579,7 +1599,11 @@ div.euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom.wz-menu flex-wrap: wrap; } .agents-evolution-visualization{ - width: 92vw; + width: 100vw; + } + + .agents-details-card { + width: 100vw; } .agents-status-pie{ flex-grow: 1 !important; From 7e1a91746a113145f8df87d485a32174b7318127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 23 Feb 2022 08:41:34 +0100 Subject: [PATCH 411/493] fix: Replace the links to documentation related to Kibana app by Wazuh dashboard --- common/constants.ts | 6 +++--- public/utils/check-plugin-version.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index a807327418..26981094f0 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -362,9 +362,9 @@ export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = '/usr/share/kibana/data/wa export const PLUGIN_PLATFORM_BASE_REDIRECTION_PATH = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'kibana'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_UPGRADE_PLATFORM = 'https://documentation.wazuh.com/current/upgrade-guide/elasticsearch-kibana-filebeat/index.html#upgrade-elasticsearch-filebeat-kibana'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_TROUBLESHOOTING = 'https://documentation.wazuh.com/current/user-manual/kibana-app/troubleshooting.html'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION = 'https://documentation.wazuh.com/current/user-manual/kibana-app/reference/config-file.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_UPGRADE_PLATFORM = 'https://documentation.wazuh.com/current/upgrade-guide/'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_TROUBLESHOOTING = 'https://documentation.wazuh.com/current/user-manual/wazuh-dashboard/troubleshooting.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION = 'https://documentation.wazuh.com/current/user-manual/wazuh-dashboard/reference/config-file.html'; export const PLUGIN_PLATFORM_URL_GUIDE = 'https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html'; export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'Elastic guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { diff --git a/public/utils/check-plugin-version.tsx b/public/utils/check-plugin-version.tsx index e61ad4078a..5c5ac90bdb 100644 --- a/public/utils/check-plugin-version.tsx +++ b/public/utils/check-plugin-version.tsx @@ -53,7 +53,7 @@ const checkClientAppVersion = (appInfo: TAppInfo) => { const troubleshootingUrl = `https://documentation.wazuh.com/${appInfo['app-version'] .split('.') .slice(0, 2) - .join('.')}/user-manual/kibana-app/troubleshooting.html`; + .join('.')}/user-manual/wazuh-dashboard/troubleshooting.html`; const message: ReactNode = ( <> From 844af8beae919b59369555f55eebe9085a3170b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 23 Feb 2022 08:44:15 +0100 Subject: [PATCH 412/493] fix: Changed revision --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 55e5658735..790da8b4b7 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "wazuh", "version": "4.3.0", - "revision": "4301-0", - "code": "4301-0", + "revision": "4301-1", + "code": "4301-1", "pluginPlatform": { "version": "7.10.2" }, From 082c841f9cf807f39f118d8448ff376f32dcde25 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Wed, 23 Feb 2022 09:18:44 -0300 Subject: [PATCH 413/493] Fixed agents link mobile style in Agents details card (#3860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed agents link style for mobile * Updated CHANGELOG * Moved media query at bottom * changelog: Moved PR link in changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 +- .../agent/components/agents-preview.js | 4 +-- public/styles/common.scss | 36 ++++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a1eea13b..dd62249be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -128,7 +128,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fix updating the aggregation data of Panel section when changing the time filter [#3790](https://github.com/wazuh/wazuh-kibana-app/pull/3790) - Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) -- Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) +- Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index 50534813f8..0fa2e50e99 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -290,7 +290,7 @@ export const AgentsPreview = compose( )} {this.lastAgent && ( - + )} {this.mostActiveAgent && ( - + Date: Wed, 23 Feb 2022 17:21:30 +0100 Subject: [PATCH 414/493] Added parenthesis to search bar query in PDF report --- server/controllers/wazuh-reporting.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/wazuh-reporting.ts b/server/controllers/wazuh-reporting.ts index b18a0e67e9..c29ae9634f 100644 --- a/server/controllers/wazuh-reporting.ts +++ b/server/controllers/wazuh-reporting.ts @@ -89,7 +89,7 @@ export class WazuhReportingCtrl { } if (searchBar) { - str += ' AND ' + searchBar; + str += ` AND (${ searchBar})`; } const agentsFilterStr = agentsFilter.map((filter) => filter.meta.value).join(','); From 0f282dce24714dce1a26cf806d05aa5d8b36f69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 23 Feb 2022 17:23:05 +0100 Subject: [PATCH 415/493] changelog: Add support for the Kibana versions 7.16.3 and 7.17.0 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd62249be8..73c15d47d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ All notable changes to the Wazuh app project will be documented in this file. -## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3 - Revision 4301 +## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4301 ### Added +- Support for Kibana 7.16.3 +- Support for Kibana 7.17.0 - Added ability to filter the results fo the `Network Ports` table in the `Inventory data` section [#3639](https://github.com/wazuh/wazuh-kibana-app/pull/3639) - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Improved the frontend handle errors strategy: UI, Toasts, console log and log in file From 36ffa8013588d94557b0c884f30867f579dfe6e4 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 23 Feb 2022 17:30:46 +0100 Subject: [PATCH 416/493] Added changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd62249be8..cf3c21e747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) +- Fixed search bar query sanitizing in PDF report [#3861](https://github.com/wazuh/wazuh-kibana-app/pull/3861) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 3f872e1f9f5001f8751e62507818b7790c2d9b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 1 Mar 2022 08:57:59 +0100 Subject: [PATCH 417/493] fix: Replaced links related to the plugin platform --- common/constants.ts | 4 ++-- public/utils/check-plugin-version.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index 26981094f0..9e040bc3a6 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -363,8 +363,8 @@ export const PLUGIN_PLATFORM_BASE_REDIRECTION_PATH = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'kibana'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_UPGRADE_PLATFORM = 'https://documentation.wazuh.com/current/upgrade-guide/'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_TROUBLESHOOTING = 'https://documentation.wazuh.com/current/user-manual/wazuh-dashboard/troubleshooting.html'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION = 'https://documentation.wazuh.com/current/user-manual/wazuh-dashboard/reference/config-file.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_TROUBLESHOOTING = 'https://documentation.wazuh.com/current/user-manual/kibana-app/troubleshooting.html'; +export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_APP_CONFIGURATION = 'https://documentation.wazuh.com/current/user-manual/kibana-app/reference/config-file.html'; export const PLUGIN_PLATFORM_URL_GUIDE = 'https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html'; export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'Elastic guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { diff --git a/public/utils/check-plugin-version.tsx b/public/utils/check-plugin-version.tsx index 5c5ac90bdb..e61ad4078a 100644 --- a/public/utils/check-plugin-version.tsx +++ b/public/utils/check-plugin-version.tsx @@ -53,7 +53,7 @@ const checkClientAppVersion = (appInfo: TAppInfo) => { const troubleshootingUrl = `https://documentation.wazuh.com/${appInfo['app-version'] .split('.') .slice(0, 2) - .join('.')}/user-manual/wazuh-dashboard/troubleshooting.html`; + .join('.')}/user-manual/kibana-app/troubleshooting.html`; const message: ReactNode = ( <> From 946196fec5f8832716e651065fcc16eaf9259772 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 2 Mar 2022 11:46:59 +0100 Subject: [PATCH 418/493] Refactored events documents discover links building (#3866) * Refactored events discover links building * fixed base url building * Added changelog --- CHANGELOG.md | 1 + .../angular/doc_table/components/table_row.ts | 29 +++++++------------ .../components/table_row/details.html | 2 +- .../resolves/go-to-plugin-platform.js | 29 ------------------- public/services/resolves/index.js | 2 -- public/services/routes.js | 27 ----------------- 6 files changed, 12 insertions(+), 78 deletions(-) delete mode 100644 public/services/resolves/go-to-plugin-platform.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c15d47d3..3619dbaf30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) +- Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts index f9ee386337..22a70f9700 100644 --- a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts +++ b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts @@ -115,27 +115,18 @@ export function createTableRowDirective($compile: ng.ICompileService) { }; $scope.getContextAppHref = () => { - const globalFilters: any = getServices().filterManager.getGlobalFilters(); - const appFilters: any = getServices().filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns: $scope.columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), - }), - { encode: false, sort: false } - ); - - return `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( - $scope.row._id - )}?${hash}`; + return `${getDiscoverPath()}#/context/${encodeURIComponent($scope.indexPattern.id)}/${ encodeURIComponent($scope.row._id) }?_a=(columns:!(_source),filters:!())` }; + $scope.getDocLink = (id) => { + return `${getDiscoverPath()}#/doc/${$scope.indexPattern.id}/${$scope.row._index}?id=${id}` + }; + + function getDiscoverPath(){ + const pathName = `${location.pathname}#`; + return `${location.origin}${pathName.replace('wazuh#', 'discover')}`; + } + // create a tr element that lists the value for each *column* function createSummaryRow(row: any) { const indexPattern = $scope.indexPattern; diff --git a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html index fd20bea8fb..8fffc2232f 100644 --- a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html +++ b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html @@ -31,7 +31,7 @@ diff --git a/public/services/resolves/go-to-plugin-platform.js b/public/services/resolves/go-to-plugin-platform.js deleted file mode 100644 index c447a7bc18..0000000000 --- a/public/services/resolves/go-to-plugin-platform.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Wazuh app - Module to catch last url - * 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 { PLUGIN_PLATFORM_BASE_REDIRECTION_PATH } from "../../../common/constants"; - -// Manage leaving the app to another Kibana tab -export function goToPluginPlatform($location, $window) { - const url = $location.$$absUrl.substring(0, $location.$$absUrl.indexOf('#')); - const lastSubUrl = $window.sessionStorage.getItem(`lastSubUrl:${url}`) || ''; - if ( - lastSubUrl.includes('/wazuh#/visualize') || - lastSubUrl.includes('/wazuh#/doc') || - lastSubUrl.includes('/wazuh#/context') - ) { - $window.sessionStorage.setItem(`lastSubUrl:${url}`, url); - } - - - $window.location.href = $location.absUrl().replace('/wazuh#', `/${PLUGIN_PLATFORM_BASE_REDIRECTION_PATH}#`); -} diff --git a/public/services/resolves/index.js b/public/services/resolves/index.js index ccbb6e3fcb..2a00125353 100644 --- a/public/services/resolves/index.js +++ b/public/services/resolves/index.js @@ -13,7 +13,6 @@ import { checkTimestamp } from './check-timestamp'; import { healthCheck } from './health-check'; import { settingsWizard } from './settings-wizard'; import { getSavedSearch } from './get-saved-search'; -import { goToPluginPlatform } from './go-to-plugin-platform'; import { getIp } from './get-ip'; import { getWzConfig } from './get-config'; import { apiCount } from './api-count'; @@ -23,7 +22,6 @@ export { healthCheck, settingsWizard, getSavedSearch, - goToPluginPlatform, getIp, getWzConfig, apiCount diff --git a/public/services/routes.js b/public/services/routes.js index 1a153bfba5..b47d6851da 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -18,7 +18,6 @@ import 'angular-route'; import { settingsWizard, getSavedSearch, - goToPluginPlatform, getIp, getWzConfig, apiCount @@ -106,17 +105,6 @@ function wzConfig($q, $rootScope, $location) { return getWzConfig($q, GenericRequest, wazuhConfig); } -function wzKibana($location, $window, $rootScope) { - assignPreviousLocation($rootScope, $location); - if ($location.$$path !== '/visualize/create') { - // Sets ?_a=(columns:!(_source),filters:!()) - $location.search('_a', '(columns:!(_source),filters:!())'); - // Removes ?_g - $location.search('_g', null); - } - return goToPluginPlatform($location, $window); -} - function clearRuleId(commonData) { commonData.removeRuleId(); return Promise.resolve(); @@ -180,21 +168,6 @@ app.config(($routeProvider) => { resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, outerAngularWrapperRoute: true }) - .when('/visualize/create?', { - redirectTo: function () { }, - resolve: { wzConfig, wzKibana }, - outerAngularWrapperRoute: true - }) - .when('/context/:pattern?/:type?/:id?', { - redirectTo: function () { }, - resolve: { wzKibana }, - outerAngularWrapperRoute: true - }) - .when('/doc/:pattern?/:index?/:type?/:id?', { - redirectTo: function () { }, - resolve: { wzKibana }, - outerAngularWrapperRoute: true - }) .when('/wazuh-dev', { template: toolsTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, From 16116bd96ead4934d02700aa45969dcf64b6100f Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 2 Mar 2022 11:46:59 +0100 Subject: [PATCH 419/493] Refactored events documents discover links building (#3866) * Refactored events discover links building * fixed base url building * Added changelog --- CHANGELOG.md | 1 + common/constants.ts | 1 - .../angular/doc_table/components/table_row.ts | 29 +++++++------------ .../components/table_row/details.html | 2 +- .../resolves/go-to-plugin-platform.js | 29 ------------------- public/services/resolves/index.js | 2 -- public/services/routes.js | 27 ----------------- 7 files changed, 12 insertions(+), 79 deletions(-) delete mode 100644 public/services/resolves/go-to-plugin-platform.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c15d47d3..3619dbaf30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,6 +131,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed the button to remove an agent for a group in the agents' table when it is the default group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) +- Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/common/constants.ts b/common/constants.ts index 9e040bc3a6..30c9a8c4f6 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -359,7 +359,6 @@ export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform export const PLUGIN_PLATFORM_NAME = 'Kibana'; export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = '/usr/share/kibana/data/wazuh/'; -export const PLUGIN_PLATFORM_BASE_REDIRECTION_PATH = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'kibana'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'kibana'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_UPGRADE_PLATFORM = 'https://documentation.wazuh.com/current/upgrade-guide/'; diff --git a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts index f9ee386337..22a70f9700 100644 --- a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts +++ b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row.ts @@ -115,27 +115,18 @@ export function createTableRowDirective($compile: ng.ICompileService) { }; $scope.getContextAppHref = () => { - const globalFilters: any = getServices().filterManager.getGlobalFilters(); - const appFilters: any = getServices().filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns: $scope.columns, - filters: (appFilters || []).map(esFilters.disableFilter), - }), - }), - { encode: false, sort: false } - ); - - return `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( - $scope.row._id - )}?${hash}`; + return `${getDiscoverPath()}#/context/${encodeURIComponent($scope.indexPattern.id)}/${ encodeURIComponent($scope.row._id) }?_a=(columns:!(_source),filters:!())` }; + $scope.getDocLink = (id) => { + return `${getDiscoverPath()}#/doc/${$scope.indexPattern.id}/${$scope.row._index}?id=${id}` + }; + + function getDiscoverPath(){ + const pathName = `${location.pathname}#`; + return `${location.origin}${pathName.replace('wazuh#', 'discover')}`; + } + // create a tr element that lists the value for each *column* function createSummaryRow(row: any) { const indexPattern = $scope.indexPattern; diff --git a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html index fd20bea8fb..8fffc2232f 100644 --- a/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html +++ b/public/kibana-integrations/discover/application/angular/doc_table/components/table_row/details.html @@ -31,7 +31,7 @@ diff --git a/public/services/resolves/go-to-plugin-platform.js b/public/services/resolves/go-to-plugin-platform.js deleted file mode 100644 index c447a7bc18..0000000000 --- a/public/services/resolves/go-to-plugin-platform.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Wazuh app - Module to catch last url - * 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 { PLUGIN_PLATFORM_BASE_REDIRECTION_PATH } from "../../../common/constants"; - -// Manage leaving the app to another Kibana tab -export function goToPluginPlatform($location, $window) { - const url = $location.$$absUrl.substring(0, $location.$$absUrl.indexOf('#')); - const lastSubUrl = $window.sessionStorage.getItem(`lastSubUrl:${url}`) || ''; - if ( - lastSubUrl.includes('/wazuh#/visualize') || - lastSubUrl.includes('/wazuh#/doc') || - lastSubUrl.includes('/wazuh#/context') - ) { - $window.sessionStorage.setItem(`lastSubUrl:${url}`, url); - } - - - $window.location.href = $location.absUrl().replace('/wazuh#', `/${PLUGIN_PLATFORM_BASE_REDIRECTION_PATH}#`); -} diff --git a/public/services/resolves/index.js b/public/services/resolves/index.js index ccbb6e3fcb..2a00125353 100644 --- a/public/services/resolves/index.js +++ b/public/services/resolves/index.js @@ -13,7 +13,6 @@ import { checkTimestamp } from './check-timestamp'; import { healthCheck } from './health-check'; import { settingsWizard } from './settings-wizard'; import { getSavedSearch } from './get-saved-search'; -import { goToPluginPlatform } from './go-to-plugin-platform'; import { getIp } from './get-ip'; import { getWzConfig } from './get-config'; import { apiCount } from './api-count'; @@ -23,7 +22,6 @@ export { healthCheck, settingsWizard, getSavedSearch, - goToPluginPlatform, getIp, getWzConfig, apiCount diff --git a/public/services/routes.js b/public/services/routes.js index 1a153bfba5..b47d6851da 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -18,7 +18,6 @@ import 'angular-route'; import { settingsWizard, getSavedSearch, - goToPluginPlatform, getIp, getWzConfig, apiCount @@ -106,17 +105,6 @@ function wzConfig($q, $rootScope, $location) { return getWzConfig($q, GenericRequest, wazuhConfig); } -function wzKibana($location, $window, $rootScope) { - assignPreviousLocation($rootScope, $location); - if ($location.$$path !== '/visualize/create') { - // Sets ?_a=(columns:!(_source),filters:!()) - $location.search('_a', '(columns:!(_source),filters:!())'); - // Removes ?_g - $location.search('_g', null); - } - return goToPluginPlatform($location, $window); -} - function clearRuleId(commonData) { commonData.removeRuleId(); return Promise.resolve(); @@ -180,21 +168,6 @@ app.config(($routeProvider) => { resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, outerAngularWrapperRoute: true }) - .when('/visualize/create?', { - redirectTo: function () { }, - resolve: { wzConfig, wzKibana }, - outerAngularWrapperRoute: true - }) - .when('/context/:pattern?/:type?/:id?', { - redirectTo: function () { }, - resolve: { wzKibana }, - outerAngularWrapperRoute: true - }) - .when('/doc/:pattern?/:index?/:type?/:id?', { - redirectTo: function () { }, - resolve: { wzKibana }, - outerAngularWrapperRoute: true - }) .when('/wazuh-dev', { template: toolsTemplate, resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, From c43c79e5b6765b255132eb87896d03ffa99e3abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Thu, 3 Mar 2022 05:06:30 -0300 Subject: [PATCH 420/493] fix: bug-health-check (#3868) * fix: bug-health-check * docs: add bug [health check] on changelog * docs: add bug [health check] on changelog * docs: add bug [health check] on changelog --- CHANGELOG.md | 1 + .../check-index-pattern/check-index-pattern-object.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3619dbaf30..510b7fa4b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -132,6 +132,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed internal user no longer needs permission to make x-pack detection request [#3831](https://github.com/wazuh/wazuh-kibana-app/pull/3831) - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) +- Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts index a8b0b1a95a..9f9c6ac916 100644 --- a/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts +++ b/public/components/health-check/services/check-index-pattern/check-index-pattern-object.service.ts @@ -72,8 +72,8 @@ export const checkIndexPatternObjectService = async (appConfig, checkLogger: Ch if (AppState.getCurrentPattern() && listValidIndexPatterns.length) { const indexPatternToSelect = listValidIndexPatterns.find(item => item.id === AppState.getCurrentPattern()); if (!indexPatternToSelect){ - AppState.setCurrentPattern(indexPatternToSelect.id); - checkLogger.action(`Set index pattern id in cookie: [${indexPatternToSelect.id}]`); + AppState.setCurrentPattern(listValidIndexPatterns[0].id); + checkLogger.action(`Set index pattern id in cookie: [${listValidIndexPatterns[0].id}]`); } } From 0bfee57e457dfadc419b41a487befdad3ce3b968 Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Fri, 11 Mar 2022 14:26:23 -0300 Subject: [PATCH 421/493] Fixed refreshing agents evolution visualization --- .../agent/components/agents-preview.js | 33 ++++--------------- public/kibana-integrations/kibana-vis.js | 27 ++++++++++++++- public/styles/common.scss | 18 +++++++--- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/public/controllers/agent/components/agents-preview.js b/public/controllers/agent/components/agents-preview.js index 0fa2e50e99..5823b31cf7 100644 --- a/public/controllers/agent/components/agents-preview.js +++ b/public/controllers/agent/components/agents-preview.js @@ -15,18 +15,15 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { EuiPage, - EuiPanel, EuiFlexGroup, EuiFlexItem, EuiStat, EuiLoadingChart, EuiSpacer, - EuiEmptyPrompt, EuiToolTip, EuiCard, } from '@elastic/eui'; import { Pie } from '../../../components/d3/pie'; -import { ProgressChart } from '../../../components/d3/progress'; import { AgentsTable } from './agents-table'; import { WzRequest } from '../../../react-services/wz-request'; import KibanaVis from '../../../kibana-integrations/kibana-vis'; @@ -36,7 +33,6 @@ 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 { WzDatePicker } from '../../../components/wz-date-picker/wz-date-picker'; import { withReduxProvider, withGlobalBreadcrumb, @@ -73,7 +69,7 @@ export const AgentsPreview = compose( data: [], loading: false, showAgentsEvolutionVisualization: false, - agentTableFilters: [], + agentTableFilters: [] }; this.wazuhConfig = new WazuhConfig(); this.agentStatusLabelToIDMap = { @@ -87,7 +83,9 @@ export const AgentsPreview = compose( this._isMount = true; this.getSummary(); if (this.wazuhConfig.getConfig()['wazuh.monitoring.enabled']) { - this._isMount && this.setState({ showAgentsEvolutionVisualization: true }); + this._isMount && this.setState({ + showAgentsEvolutionVisualization: true + }); const tabVisualizations = new TabVisualizations(); tabVisualizations.removeAll(); tabVisualizations.setTab('general'); @@ -96,6 +94,7 @@ export const AgentsPreview = compose( }); const filterHandler = new FilterHandler(AppState.getCurrentPattern()); await VisFactoryHandler.buildOverviewVisualizations(filterHandler, 'general', null); + } } @@ -344,11 +343,11 @@ export const AgentsPreview = compose( description paddingSize="none" betaBadgeLabel="Evolution" - style={{ display: this.props.resultState === 'ready' ? 'block' : 'none' }} + style={{ display: this.props.resultState !== 'loading' ? 'block' : 'none' }} > -
    +
    - - No results found in the selected time range

    } - actions={ {}} />} - /> - )} diff --git a/public/kibana-integrations/kibana-vis.js b/public/kibana-integrations/kibana-vis.js index ea6460099e..30a6102a73 100644 --- a/public/kibana-integrations/kibana-vis.js +++ b/public/kibana-integrations/kibana-vis.js @@ -24,6 +24,7 @@ import store from '../redux/store'; import { updateMetric } from '../redux/actions/visualizationsActions'; import { GenericRequest } from '../react-services/generic-request'; import { createSavedVisLoader } from './visualizations/saved_visualizations'; +import { WzDatePicker } from '../components/wz-date-picker/wz-date-picker'; import { EuiLoadingChart, EuiLoadingSpinner, @@ -31,6 +32,7 @@ import { EuiIcon, EuiFlexItem, EuiFlexGroup, + EuiEmptyPrompt } from '@elastic/eui'; import { getAngularModule, @@ -385,6 +387,21 @@ class KibanaVis extends Component { } }; + showDateRangePicker = () => { + return !this.deadField && !this.state.visRefreshingIndex && this.visID === 'Wazuh-App-Overview-General-Agents-status' + } + + DateRangePickerComponent = () => { + return ( + + {}} + /> + + ) + } + render() { const isLoading = this.props.resultState === 'loading'; return ( @@ -436,10 +453,18 @@ class KibanaVis extends Component {
    + { + !this.isLoading && this.showDateRangePicker() && + this.DateRangePickerComponent() + }
    ) diff --git a/public/styles/common.scss b/public/styles/common.scss index 6ca7bba07e..50642be3ad 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1561,12 +1561,9 @@ div.euiPopover__panel.euiPopover__panel-isOpen.euiPopover__panel--bottom.wz-menu overflow: hidden; } } - } - - + } } - @media (min-width: 1600px) { .agents-evolution-visualization{ width: 35vw; @@ -1764,4 +1761,15 @@ iframe.width-changed { max-width: 100%; } } -} \ No newline at end of file +} + +.agents-evolutions-dpicker { + height: 10%; + position: absolute; + top: 0; + right: 0; + z-index: 999999; + .euiButtonEmpty__text { + font-size: 0.8rem; + } +} \ No newline at end of file From 82e6ec66ade3618c14fc30e3e4c2f8ff5178b70c Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Fri, 11 Mar 2022 14:28:42 -0300 Subject: [PATCH 422/493] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510b7fa4b7..4bbbbda218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) +- Fixed refreshing agents evolution visualization [#3894](https://github.com/wazuh/wazuh-kibana-app/pull/3894) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 From 04ceaf71dad6992d0db03f8a80376afde3a92b6f Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 21 Mar 2022 11:22:32 +0100 Subject: [PATCH 423/493] [FIX] [Vulnerabilities/Inventory] Table not reload when changing the selected agent (#3901) * fix: Fixed Vulnerabilities/Inventory table doesn't reload when change the selected agent - Added the `endpoint` property as dependency and param to redo the request * changelog: Add PR to changelog * fix(component): Reset the page index when the endpoint changes * fix: Updated snapshot --- CHANGELOG.md | 1 + .../__snapshots__/table-wz-api.test.tsx.snap | 1 + public/components/common/tables/table-default.tsx | 14 ++++++++++---- .../common/tables/table-with-search-bar.tsx | 9 ++++++++- public/components/common/tables/table-wz-api.tsx | 6 +++--- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510b7fa4b7..668e26a9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -133,6 +133,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) +- Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 diff --git a/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap index ae152d0c0b..b1ea54681a 100644 --- a/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap @@ -66,6 +66,7 @@ exports[`Table WZ API component renders correctly to match the snapshot 1`] = ` { + // Reset the page index when the endpoint changes. + // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + setPagination({pageIndex: 0, pageSize: pagination.pageSize}); + }, [endpoint]); + useEffect(() => { (async function(){ try{ setLoading(true); - const { items, totalItems } = await onSearch(filters, pagination, sorting); + const { items, totalItems } = await onSearch(endpoint, [], pagination, sorting); setItems(items); setTotalItems(totalItems); }catch(error){ @@ -82,7 +88,7 @@ export function TableDefault({ } setLoading(false); })() - }, [filters, pagination, sorting, reload]); + }, [endpoint, pagination, sorting, reload]); const tablePagination = { ...pagination, diff --git a/public/components/common/tables/table-with-search-bar.tsx b/public/components/common/tables/table-with-search-bar.tsx index fa92fb5433..5bfd358049 100644 --- a/public/components/common/tables/table-with-search-bar.tsx +++ b/public/components/common/tables/table-with-search-bar.tsx @@ -29,6 +29,7 @@ export function TableWithSearchBar({ tableInitialSortingField = '', tableProps = {}, reload, + endpoint, ...rest }) { const [loading, setLoading] = useState(false); @@ -62,11 +63,17 @@ export function TableWithSearchBar({ }); } + useEffect(() => { + // Reset the page index when the endpoint changes. + // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + setPagination({pageIndex: 0, pageSize: pagination.pageSize}); + }, [endpoint]); + useEffect(() => { (async function () { try { setLoading(true); - const { items, totalItems } = await onSearch(filters, pagination, sorting); + const { items, totalItems } = await onSearch(endpoint, filters, pagination, sorting); setItems(items); setTotalItems(totalItems); } catch (error) { diff --git a/public/components/common/tables/table-wz-api.tsx b/public/components/common/tables/table-wz-api.tsx index 93e1c0b1cd..35df090b93 100644 --- a/public/components/common/tables/table-wz-api.tsx +++ b/public/components/common/tables/table-wz-api.tsx @@ -26,14 +26,14 @@ import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/ import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; -export function TableWzAPI({endpoint, ...rest}){ +export function TableWzAPI({...rest}){ 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 onSearch = useCallback(async function(filters, pagination, sorting){ + const onSearch = useCallback(async function(endpoint, filters, pagination, sorting){ try { const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; @@ -79,7 +79,7 @@ export function TableWzAPI({endpoint, ...rest}){ )} - {rest.downloadCsv && } + {rest.downloadCsv && } ) From 0f0e556e4a603f06a4c8d53af32da7c493ffcd3a Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Mon, 21 Mar 2022 11:29:30 +0100 Subject: [PATCH 424/493] Vulnerability inventory rework (#3893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added basic widgets * Added getSummary request * Added getAggregation * Changed module default tab * Merged requests in single file * Added aggregation field change * Added new details fields * feat: Refactorize visualizations - Created charts components: - Donut - Created visulizations - Created visualiation widgets - Simple - Selector: add a switch to choose an option - Replaced visualization of Compliance in Agent overview - Add visualization to Vulnerabilities/Inventory - Update Wazuh API data Update Wazuh API data - Endpoints - Security * Implemented aggregation requests in severity stats * Implemented Severity pie chart * Changed severity pie colors * feat(vulnerabilities): Add documentation to components and functions. Add more fields to summary visualization in Modules/Vulnerabilities/Inventory * Added select label feature * Sort by severity level * fix(visualizations): Removed deprecated code of component * Added filter by severity KPI * Removed Severity field from widget selector * fix(vulnerabilities): Sort results in summary visualization and minor fixes: - Removed console.log in fetching function of compliance visualization - Removed deprecated state in Vulnerability/Inventory * Added loading to severity stats * Changed vulnerabilities table column order * Changed vulnerabilities table column order * Fixed responsive pie chart label * Added tooltip and events status column * fix(vulnerabilities): Align visualizations in Vulnerabilites/Inventory * Changed spacer to xxl * Fixed visualization tooltip * Fixed viz styles * Added agent ID dependency to last scan * Added default values to stats KPI * Added external references to Details * Changed details icons * Fix external reference label * Improved coding style * Deleted obsolete vulnerabilities visualizations * Fixed severity empty results * Fixed buildOverviewVisualizations exception * changelog: Updated changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 + common/api-info/endpoints.json | 76 ++ common/api-info/security-actions.json | 3 +- public/components/agents/vuls/inventory.tsx | 201 ++- .../agents/vuls/inventory/detail.tsx | 63 +- .../agents/vuls/inventory/flyout.tsx | 2 +- .../agents/vuls/inventory/lib/api-requests.ts | 41 + .../vuls/inventory/lib/getFilterValues.ts | 27 - .../agents/vuls/inventory/lib/index.ts | 2 +- .../agents/vuls/inventory/table.tsx | 41 +- .../components/common/charts/charts/donut.tsx | 121 ++ .../hooks/use_chart_dimensions.ts} | 8 +- .../common/charts/visualizations/basic.tsx | 162 +++ .../common/charts/visualizations/legend.scss | 7 + .../common/charts/visualizations/legend.tsx | 32 + public/components/common/hooks/index.ts | 1 + .../hooks/use_async_action_run_on_start.ts | 45 + .../common/modules/events-selected-fields.js | 3 +- .../common/modules/main-overview.tsx | 8 +- .../common/modules/modules-defaults.js | 3 +- .../common/welcome/agents-welcome.js | 8 +- .../requirement_vis/components/index.ts | 15 - .../components/requirement_head/index.ts | 14 - .../requirement_head/requirements_head.tsx | 51 - .../requitement_body/components/index.ts | 17 - .../components/no_alerts_message.tsx | 40 - .../requirements_donnut/hooks/index.ts | 15 - .../components/requirements_donnut/index.ts | 15 - .../requirements_donnut.tsx | 80 -- .../components/requirements_leggend/index.ts | 14 - .../requirements_leggend.scss | 21 - .../requirements_leggend.tsx | 72 - .../components/requitement_body/index.ts | 14 - .../requitement_body/requirements_body.tsx | 67 - .../lib/get_requirement_alerts.ts | 23 +- .../requitement_body => }/lib/index.ts | 0 .../requirement_vis/requirement_vis.tsx | 89 +- .../visualize/agent-visualizations.js | 63 - public/react-services/vis-factory-handler.js | 6 +- public/styles/common.scss | 15 + public/templates/agents/visualizations.js | 48 - .../visualizations/agents/agents-vuls.ts | 1104 --------------- .../visualizations/agents/index.ts | 2 - .../visualizations/overview/index.ts | 2 - .../visualizations/overview/overview-vuls.ts | 1190 ----------------- 45 files changed, 896 insertions(+), 2937 deletions(-) create mode 100644 public/components/agents/vuls/inventory/lib/api-requests.ts delete mode 100644 public/components/agents/vuls/inventory/lib/getFilterValues.ts create mode 100644 public/components/common/charts/charts/donut.tsx rename public/components/common/{welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/use-chart-dimensions.ts => charts/hooks/use_chart_dimensions.ts} (91%) create mode 100644 public/components/common/charts/visualizations/basic.tsx create mode 100644 public/components/common/charts/visualizations/legend.scss create mode 100644 public/components/common/charts/visualizations/legend.tsx create mode 100644 public/components/common/hooks/use_async_action_run_on_start.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requirement_head/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requirement_head/requirements_head.tsx delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/no_alerts_message.tsx delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/requirements_donnut.tsx delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.scss delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.tsx delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/index.ts delete mode 100644 public/components/common/welcome/components/requirement_vis/components/requitement_body/requirements_body.tsx rename public/components/common/welcome/components/requirement_vis/{components/requitement_body => }/lib/get_requirement_alerts.ts (70%) rename public/components/common/welcome/components/requirement_vis/{components/requitement_body => }/lib/index.ts (100%) delete mode 100644 server/integration-files/visualizations/agents/agents-vuls.ts delete mode 100644 server/integration-files/visualizations/overview/overview-vuls.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 668e26a9ba..a63aef054e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Run `template` and `fields` checks in the health check depends on the app configuration [#3783](https://github.com/wazuh/wazuh-kibana-app/pull/3783) - Added a toast message when there is an error creating a new group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Added a step to start the agent to the deploy new Windowns agent guide [#3846](https://github.com/wazuh/wazuh-kibana-app/pull/3846) +- Added 3 new panels to `Vulnerabilities/Inventory` [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) ### Changed @@ -73,6 +74,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed user for sample data management [#3795](https://github.com/wazuh/wazuh-kibana-app/pull/3795) - Changed agent install codeblock copy button and powershell terminal warning [#3792](https://github.com/wazuh/wazuh-kibana-app/pull/3792) - Refactored as the plugin platform name and references is managed [#3811](https://github.com/wazuh/wazuh-kibana-app/pull/3811) +- Removed `Dashboard` tab for the `Vulnerabilities` modules [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) ### Fixed diff --git a/common/api-info/endpoints.json b/common/api-info/endpoints.json index 0009280df5..243d536a0c 100644 --- a/common/api-info/endpoints.json +++ b/common/api-info/endpoints.json @@ -8870,6 +8870,82 @@ } } ] + }, + { + "name": "/vulnerability/:agent_id/summary/:field", + "documentation": "https://documentation.wazuh.com/current/user-manual/api/reference.html#operation/api.controllers.vulnerability_controller.get_vulnerabilities_field_summary", + "description": "Return a summary of the vulnerabilities' field of an agent", + "summary": "Get agent vulnerabilities' field summary", + "tags": [ + "Vulnerability" + ], + "args": [ + { + "name": ":agent_id", + "description": "Agent ID. All possible values from 000 onwards", + "required": true, + "schema": { + "type": "string", + "minLength": 3, + "description": "Agent ID", + "format": "numbers" + } + }, + { + "name": ":field", + "description": "Vulnerability inventory field", + "required": true, + "schema": { + "type": "string", + "enum": [ + "cve", + "name", + "version", + "architecture", + "detection_time", + "severity", + "cvss2_score", + "cvss3_score", + "references", + "type", + "status", + "condition", + "title", + "published", + "updated" + ] + } + } + ], + "query": [ + { + "name": "limit", + "description": "Maximum number of elements to return. Although up to 100.000 can be specified, it is recommended not to exceed 500 elements. Responses may be slower the more this number is exceeded. ", + "schema": { + "type": "integer", + "format": "int32", + "default": 500, + "minimum": 1, + "maximum": 100000 + } + }, + { + "name": "pretty", + "description": "Show results in human-readable format", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "wait_for_complete", + "description": "Disable timeout response", + "schema": { + "type": "boolean", + "default": false + } + } + ] } ] }, diff --git a/common/api-info/security-actions.json b/common/api-info/security-actions.json index 2707a9506b..d0c1fb0d66 100644 --- a/common/api-info/security-actions.json +++ b/common/api-info/security-actions.json @@ -1103,7 +1103,8 @@ }, "related_endpoints": [ "GET /vulnerability/{agent_id}", - "GET /vulnerability/{agent_id}/last_scan" + "GET /vulnerability/{agent_id}/last_scan", + "GET /vulnerability/{agent_id}/summary/{field}" ] } } \ No newline at end of file diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 8895e0a4a4..15670c4c07 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -14,32 +14,71 @@ import React, { Component } from 'react'; import { EuiPanel, EuiPage, + EuiPageBody, EuiSpacer, EuiProgress, EuiFlexGroup, EuiFlexItem, + EuiCard, + EuiStat, + EuiText, + EuiIcon, + EuiToolTip, + euiPaletteColorBlind, } from '@elastic/eui'; +import { EuiPalette } from '@elastic/eui/src/services/color/eui_palettes'; import { InventoryTable, } from './inventory/'; +import { + getLastScan, getAggregation +} from './inventory/lib'; import { ICustomBadges } from '../../wz-search-bar/components'; +import { formatUIDate } from '../../../react-services'; +import { VisualizationBasicWidgetSelector, VisualizationBasicWidget } from '../../common/charts/visualizations/basic'; + +interface Aggregation { title: number, description: string, titleColor: string } +interface pieStats { id: string, label: string, value: number } +interface LastScan { last_full_scan: string, last_partial_scan: string } +interface TitleColors { Critical: string, High: string, Medium: string, Low: string } export class Inventory extends Component { _isMount = false; state: { filters: []; - isLoading: Boolean; + isLoading: boolean; + isLoadingStats: boolean; customBadges: ICustomBadges[]; + stats: Aggregation[], + severityPieStats: pieStats[], + vulnerabilityLastScan: LastScan, }; props: any; + colorsVisualizationVulnerabilitiesSummaryData: EuiPalette; + titleColors: TitleColors = { Critical: '#BD271E', High: '#d5a612', Medium: '#006BB4', Low: '#6a717d' }; constructor(props) { super(props); this.state = { isLoading: true, + isLoadingStats: true, customBadges: [], filters: [], + stats: [ + { title: 0, description: 'Critical', titleColor: this.titleColors.Critical }, + { title: 0, description: 'High', titleColor: this.titleColors.High }, + { title: 0, description: 'Medium', titleColor: this.titleColors.Medium }, + { title: 0, description: 'Low', titleColor: this.titleColors.Low }, + ], + severityPieStats: [], + vulnerabilityLastScan: { + last_full_scan: '', + last_partial_scan: '' + }, } + this.fetchVisualizationVulnerabilitiesSummaryData = this.fetchVisualizationVulnerabilitiesSummaryData.bind(this); + this.fetchVisualizationVulnerabilitiesSeverityData = this.fetchVisualizationVulnerabilitiesSeverityData.bind(this); + this.colorsVisualizationVulnerabilitiesSummaryData = euiPaletteColorBlind(); } async componentDidMount() { @@ -51,9 +90,59 @@ export class Inventory extends Component { this._isMount = false; } + async fetchVisualizationVulnerabilitiesSummaryData(field, agentID) { + const results = await getAggregation(agentID, field, 4); + return Object.entries(results[field]).map(([key, value], index) => ({ + label: key, + value, + color: this.colorsVisualizationVulnerabilitiesSummaryData[index], + onClick: () => this.onFiltersChange(this.buildFilterQuery(field, key)) + })).sort((firstElement, secondElement) => secondElement.value - firstElement.value) + } + + async fetchVisualizationVulnerabilitiesSeverityData(){ + const { id } = this.props.agent; + const FIELD = 'severity'; + const SEVERITY_KEYS = ['Critical', 'High', 'Medium', 'Low']; + this.setState({ isLoadingStats: true }); + + const vulnerabilityLastScan = await getLastScan(id); + const { severity } = await getAggregation(id, FIELD); + + const severityStats = SEVERITY_KEYS.map(key => ({ + titleColor: this.titleColors[key], + description: key, + title: severity[key] ? severity[key] : 0 + })); + + this.setState({ + stats: severityStats, + isLoadingStats: false, + vulnerabilityLastScan + }); + + return Object.keys(severity).length ? SEVERITY_KEYS.map(key => ({ + label: key, + value: severity[key] ? severity[key] : 0, + color: this.titleColors[key], + onClick: () => this.onFiltersChange(this.buildFilterQuery(FIELD, key)) + })) : []; + } + + buildFilterQuery(field = '', selectedItem = '') { + return [ + { + field: 'q', + value: `${field}=${selectedItem}`, + }, + ] + } + async loadAgent() { if (this._isMount) { - this.setState({ isLoading: false }); + this.setState({ + isLoading: false + }); } } @@ -84,18 +173,116 @@ export class Inventory extends Component { ; } + // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates + // when vulnerability module is not configured + // its meant to render nothing when such date is received + beautifyDate(date: string) { + return ['', '1970-01-01T00:00:00Z'].includes(date) ? '-' : formatUIDate(date); + } + + buildTitleFilter({ description, title, titleColor }) { + const { isLoadingStats } = this.state; + return ( + + + this.onFiltersChange(this.buildFilterQuery('severity', description))} + > + {title} + + + )} + description={description} + titleColor={titleColor} + /> + + ) + } render() { - const { isLoading } = this.state; + const { isLoading, stats, vulnerabilityLastScan } = this.state; if (isLoading) { return this.loadingInventory() } + const last_full_scan = this.beautifyDate(vulnerabilityLastScan.last_full_scan); + const last_partial_scan = this.beautifyDate(vulnerabilityLastScan.last_partial_scan); + const table = this.renderTable(); - return - - {table} - + + + + +
    + +
    +
    +
    + + + + + + {stats.map((stat) => this.buildTitleFilter(stat))} + + + + + Last full scan: {last_full_scan} + + + + + Last partial scan: {last_partial_scan} + + + + + + + + + + `No ${optionRequirement.text} results were found.`} + /> + + +
    + + + {table} + +
    } } diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index d980973328..36aa40f956 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -22,7 +22,9 @@ import { EuiSpacer, EuiStat, EuiToolTip, + EuiListGroup, EuiBadge, + EuiText, } from '@elastic/eui'; import { Discover } from '../../../common/modules/discover'; import { ModulesHelper } from '../../../common/modules/modules-helper'; @@ -89,6 +91,12 @@ export class Details extends Component { details() { return [ + { + field: 'title', + name: 'Title', + icon: 'home', + link: false, + }, { field: 'name', name: 'Name', @@ -113,6 +121,12 @@ export class Details extends Component { icon: 'node', link: true, }, + { + field: 'condition', + name: 'Condition', + icon: 'crosshairs', + link: false, + }, { field: 'last_full_scan', name: 'Last Full Scan', @@ -126,7 +140,28 @@ export class Details extends Component { icon: 'clock', link: false, transformValue: formatUIDate - } + }, + { + field: 'published', + name: 'Published', + icon: 'clock', + link: false, + transformValue: formatUIDate + }, + { + field: 'updated', + name: 'Updated', + icon: 'clock', + link: false, + transformValue: formatUIDate + }, + { + field: 'external_references', + name: 'References', + icon: 'link', + link: false, + transformValue: this.renderExternalReferences + }, ]; } @@ -272,6 +307,32 @@ export class Details extends Component { ); } + renderExternalReferences(references) { + return ( + + +

    View external references

    +
    + + }> + ({ label: link, href: link })) + } + /> +
    + ); + } + updateTotalHits = (total) => { this.setState({ totalHits: total }); }; diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 7ee13ca609..1859f1a38e 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -104,7 +104,7 @@ export class FlyoutDetail extends Component { render() { const { currentItem } = this.state; - const title = `${currentItem.cve}`; + const title = `${currentItem.cve || ''}`; const id = title.replace(/ /g, '_'); const filterMap = { name: 'data.vulnerability.package.name', diff --git a/public/components/agents/vuls/inventory/lib/api-requests.ts b/public/components/agents/vuls/inventory/lib/api-requests.ts new file mode 100644 index 0000000000..3edb4c5f16 --- /dev/null +++ b/public/components/agents/vuls/inventory/lib/api-requests.ts @@ -0,0 +1,41 @@ +/* + * Wazuh app - Agent vulnerabilities requests + * 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 { WzRequest } from '../../../../../react-services/wz-request'; + +export async function getAggregation(agentId: string, field: string = 'severity', limit: number | null = null) { + const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}/summary/${field}`, limit ? { params: { limit } } : {}); + return result?.data?.data; +} + +export async function getFilterValues(field, value, agentId, filters = {}, format = (item) => item) { + + const filter = { + ...filters, + distinct: true, + select: field, + limit: 30, + }; + if (value) { + filter['search'] = value; + } + const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter }); + return result?.data?.data?.affected_items?.map((item) => { return format(item[field]) }) || []; +} + +export async function getLastScan(agentId: string = '000') { + const response = await WzRequest.apiReq( + 'GET', + `/vulnerability/${agentId}/last_scan`, + {} + ); + return response?.data?.data?.affected_items[0] || {}; +} diff --git a/public/components/agents/vuls/inventory/lib/getFilterValues.ts b/public/components/agents/vuls/inventory/lib/getFilterValues.ts deleted file mode 100644 index 8317dcac2a..0000000000 --- a/public/components/agents/vuls/inventory/lib/getFilterValues.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Wazuh app - Agent vulnerabilities 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 { WzRequest } from '../../../../../react-services/wz-request'; - -export async function getFilterValues(field, value, agentId, filters={}, format=(item) => item) { - - const filter = { - ...filters, - distinct: true, - select: field, - limit: 30, - }; - if (value) { - filter['search'] = value; - } - const result = await WzRequest.apiReq('GET', `/vulnerability/${agentId}`, { params: filter }); - return (((result || {}).data || {}).data || {}).affected_items.map((item) => {return format(item[field])}); -} \ No newline at end of file diff --git a/public/components/agents/vuls/inventory/lib/index.ts b/public/components/agents/vuls/inventory/lib/index.ts index 7427803104..54579ea0ab 100644 --- a/public/components/agents/vuls/inventory/lib/index.ts +++ b/public/components/agents/vuls/inventory/lib/index.ts @@ -1 +1 @@ -export { getFilterValues } from './getFilterValues'; \ No newline at end of file +export * from './api-requests'; \ No newline at end of file diff --git a/public/components/agents/vuls/inventory/table.tsx b/public/components/agents/vuls/inventory/table.tsx index 151f6b7574..ef95a2466b 100644 --- a/public/components/agents/vuls/inventory/table.tsx +++ b/public/components/agents/vuls/inventory/table.tsx @@ -153,6 +153,12 @@ export class InventoryTable extends Component { ? (width = '60px') : (width = '80px'); return [ + { + field: 'name', + name: 'Name', + sortable: true, + width: '100px', + }, { field: 'version', name: 'Version', @@ -167,22 +173,16 @@ export class InventoryTable extends Component { width: '100px', }, { - field: 'cve', - name: 'CVE', + field: 'severity', + name: 'Severity', sortable: true, - truncateText: true, width: `${width}`, }, { - field: 'name', - name: 'Name', - sortable: true, - width: '100px', - }, - { - field: 'severity', - name: 'Severity', + field: 'cve', + name: 'CVE', sortable: true, + truncateText: true, width: `${width}`, }, { @@ -219,8 +219,22 @@ export class InventoryTable extends Component { const { error } = this.state; const { filters, onFiltersChange } = this.props; const columns = this.columns(); - const selectFields = - 'select=cve,architecture,version,name,severity,cvss2_score,cvss3_score,detection_time'; + const selectFields = `select=${[ + 'cve', + 'architecture', + 'version', + 'name', + 'severity', + 'cvss2_score', + 'cvss3_score', + 'detection_time', + 'title', + 'condition', + 'updated', + 'published', + 'external_references' + ].join(',')}`; + return ( ); } diff --git a/public/components/common/charts/charts/donut.tsx b/public/components/common/charts/charts/donut.tsx new file mode 100644 index 0000000000..877973f01a --- /dev/null +++ b/public/components/common/charts/charts/donut.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useRef} from "react"; +import ReactDOM from "react-dom"; +import d3 from 'd3'; +import { useChartDimensions } from '../hooks/use_chart_dimensions'; + +export type ChartDonutDataEntry = { + color: string + value: number +} + +export type ChartDonutProps = { + data: ChartDonutDataEntry[] +} + +export type ChartDonutWidgetProps = ChartDonutProps & { + fetch: () => ChartDonutDataEntry[] +} + +/** + * Create a donut chart. + */ +export const ChartDonut = (props : ChartDonutProps) => { + const pieRef: null | any = useRef(); + const cache = useRef(props.data); + const [ref, dms] = useChartDimensions({}, pieRef); + + const size = dms.height; + const outerRadius = size / 2; + const innerRadius = outerRadius - Math.min(20, size * 0.2); + let tooltip; + + const createPie = d3.layout + .pie() + .value(d => d.value) + .sort(null); + const createArc = d3.svg + .arc() + .innerRadius(innerRadius) + .outerRadius(outerRadius) + .padAngle(0.015); + const format = d3.format(".2f"); + + const TooltipContent = ({ data }) => { + const borderStyle = { borderLeft: `2px solid ${data.color}` }; + return ( +
    +

    {data.label}: {data.value}

    +
    + ) + } + + // chart mouse events + const onEnter = () => { + tooltip = d3.select("body") + .append("div") + .attr("class", "wz-chart-tooltip visTooltip"); + } + + const onMove = (d) => { + tooltip.style("display", "none"); + tooltip + .style("left", `${(d3.event.pageX + 12)}px`) + .style('top', `${(d3.event.pageY - 10)}px`) + .style("opacity", 1) + .style("padding", '5px 5px 5px 2px') + .style("visibility", 'visible') + .style("position", 'absolute') + .style("display", "block"); + ReactDOM.render(, tooltip[0][0]); + } + + const onOut = () => d3.select("body > div.wz-chart-tooltip").remove() + // end of chart mouse events + + useEffect(() => { + const pieData = createPie(props.data); + const prevData = createPie(cache.current); + const group = d3.select(pieRef.current); + const groupWithData = group.selectAll("g.arc").data(pieData); + + groupWithData.exit().remove(); + + + groupWithData + .enter() + .append("g") + .attr("class", "arc") + .on("mouseenter", () => onEnter()) + .on("mousemove", (d) => onMove(d)) + .on("mouseout", onOut) + + const path = groupWithData.select('path')[0].filter(item => item != null).length ? groupWithData.select("path") : groupWithData.append("path"); + + const arcTween = (d, i) => { + const interpolator = d3.interpolate(prevData[i], d); + return t => createArc(interpolator(t)); + }; + + path + .attr("class", "arc") + .attr("fill", (d, i) => props.data[i].color) + .transition() + .attrTween("d", arcTween); + cache.current = props.data; + return onOut; + }, + [props.data, dms] + ); + + return ( + + + + ); +}; diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/use-chart-dimensions.ts b/public/components/common/charts/hooks/use_chart_dimensions.ts similarity index 91% rename from public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/use-chart-dimensions.ts rename to public/components/common/charts/hooks/use_chart_dimensions.ts index 1d643bf877..54e9785b20 100644 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/use-chart-dimensions.ts +++ b/public/components/common/charts/hooks/use_chart_dimensions.ts @@ -11,7 +11,7 @@ * * Find more information about this on the LICENSE file. */ -import React, { useRef, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; const combineChartDimensions = dimensions => { let parsedDimensions = { @@ -37,14 +37,14 @@ export const useChartDimensions = (passedSettings, ref) => { useEffect(() => { if (dimensions.width && dimensions.height) return - const element = ref.current.parentNode.parentNode.parentNode; + const element = ref.current.parentNode.parentNode; const resizeObserver = new ResizeObserver(entries => { if (!Array.isArray(entries)) return if (!entries.length) return const entry = entries[0] - if (width != entry.contentRect.width) changeWidth(entry.contentRect.width / 2) - if (height != entry.contentRect.height) changeHeight(entry.contentRect.height/2) + if (width != entry.contentRect.width) changeWidth(entry.contentRect.width) + if (height != entry.contentRect.height) changeHeight(entry.contentRect.height) }) resizeObserver.observe(element) diff --git a/public/components/common/charts/visualizations/basic.tsx b/public/components/common/charts/visualizations/basic.tsx new file mode 100644 index 0000000000..c18a479b1d --- /dev/null +++ b/public/components/common/charts/visualizations/basic.tsx @@ -0,0 +1,162 @@ +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"; + +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) +} + +const chartTypes = { + 'donut': ChartDonut +}; + +/** + * Render a visualization. Basic component. It is a controlled component that can render + * loading status, no data, error or the chart. + */ +export const VisualizationBasic = ({ + data, + showLegend, + isLoading, + size, + type, + noDataTitle = 'No data', + noDataMessage, + errorTitle = 'Error', + errorMessage +}: VisualizationBasicProps) => { + const { width, height } = typeof size === 'object' ? size : { width: size, height: size }; + + let visualization = null; + + if(isLoading){ + visualization = ( +
    + +
    + ) + }else if(errorMessage){ + visualization = ( + {errorTitle}} + body={errorMessage} + /> + ) + }else if(!data || (Array.isArray(data) && !data.length)){ + visualization = ( + {noDataTitle}} + 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' }))} + /> + + )} + + ) + } + + return ( +
    + {visualization} +
    + ) + +} + +type VisualizationBasicWidgetProps = VisualizationBasicProps & { + 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); + + return +} + +type VisualizationBasicWidgetSelectorProps = VisualizationBasicWidgetProps & { + 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. + */ +export const VisualizationBasicWidgetSelector = ({selectorOptions, title, onFetchExtraDependencies, ...rest}: VisualizationBasicWidgetSelectorProps) => { + const [selectedOption, setSelectedOption] = useState(selectorOptions[0].value); + + const onChange = useCallback((e) => setSelectedOption(e.target.value)); + + return ( + <> + + + {title && ( +

    +

    {title}

    +

    + )} +
    + + + +
    + + option.value === selectedOption)) + : rest.noDataMessage + } + : {} + )} + onFetchDependencies={[selectedOption,...(onFetchExtraDependencies || [])]} + /> + + ) +} \ No newline at end of file diff --git a/public/components/common/charts/visualizations/legend.scss b/public/components/common/charts/visualizations/legend.scss new file mode 100644 index 0000000000..99e97df2f2 --- /dev/null +++ b/public/components/common/charts/visualizations/legend.scss @@ -0,0 +1,7 @@ +.chart-legend > li { + margin-top: 0px; +} + +.chart-legend > li > * { + padding: 0px; +} diff --git a/public/components/common/charts/visualizations/legend.tsx b/public/components/common/charts/visualizations/legend.tsx new file mode 100644 index 0000000000..38849f7566 --- /dev/null +++ b/public/components/common/charts/visualizations/legend.tsx @@ -0,0 +1,32 @@ +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 + }[] +} + +/** + * 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 + })); + + return ( + + ); +} \ No newline at end of file diff --git a/public/components/common/hooks/index.ts b/public/components/common/hooks/index.ts index 3e0767da3a..944998ea1a 100644 --- a/public/components/common/hooks/index.ts +++ b/public/components/common/hooks/index.ts @@ -24,5 +24,6 @@ export * from './useApiRequest'; export * from './use-app-config'; export * from './useRootScope'; export * from './use_async_action'; +export * from './use_async_action_run_on_start'; export { useEsSearch } from './use-es-search'; export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; diff --git a/public/components/common/hooks/use_async_action_run_on_start.ts b/public/components/common/hooks/use_async_action_run_on_start.ts new file mode 100644 index 0000000000..7aa0d1eb5d --- /dev/null +++ b/public/components/common/hooks/use_async_action_run_on_start.ts @@ -0,0 +1,45 @@ +import { useCallback, useState, useEffect } from 'react'; + +type useAsyncActionRunOnStartAction = (...any) => T +type useAsyncActionRunOnStartDependencies = any[] +type useAsyncActionRunOnStartDependenciesReturns = { + data: T + error: any + running: boolean + run: Promise +} + +/** + * Get data from an asynchronous process. Manage data, error and running states while running the process. It starts with running status activated, so this is + * useful to get data when a component is mounted. The dependencies parameter is passed as parameters to the action function. When the process runs, + * the data and error states are reset. If the action function or dependencies change, it will run the process again. The run function is exposed. + * @param action Define the function to get the data. It reveive the dependencies as params + * @param dependencies Define the dependencies to rerun the process + * @returns It returns data, error, run, running + */ +export function useAsyncActionRunOnStart(action: useAsyncActionRunOnStartAction, dependencies: useAsyncActionRunOnStartDependencies = []): useAsyncActionRunOnStartDependenciesReturns{ + const [running, setRunning] = useState(true); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + const run: Promise = useCallback(async () => { + try{ + setRunning(true); + setError(null); + setData(null); + const data = await action(...dependencies); + setData(data); + }catch(error){ + setError(error); + }finally{ + setRunning(false); + }; + }, [action,...dependencies]); + + useEffect(() => { + run(); + }, [action,...dependencies]); + + + return { data, error, run, running}; +} \ No newline at end of file diff --git a/public/components/common/modules/events-selected-fields.js b/public/components/common/modules/events-selected-fields.js index 02b38eb0d1..ee685c42e7 100644 --- a/public/components/common/modules/events-selected-fields.js +++ b/public/components/common/modules/events-selected-fields.js @@ -87,7 +87,8 @@ export const EventsSelectedFiles = { 'agent.name', 'data.vulnerability.package.name', 'data.vulnerability.cve', - 'data.vulnerability.severity' + 'data.vulnerability.severity', + 'data.vulnerability.status' ], virustotal: [ 'agent.name', diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index e058fa4175..8712704bc5 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -109,12 +109,14 @@ export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverv } async componentDidMount() { + const { module } = this.props; const tabView = AppNavigate.getUrlParameter('tabView') || 'panels'; const tab = AppNavigate.getUrlParameter('tab'); + const tabExceptions = ['sca', 'vuls']; if (tabView && tabView !== this.props.selectView) { - if (tabView === 'panels' && tab === 'sca') { - // SCA initial tab is inventory - this.props.onSelectedTabChanged('inventory'); + if (tabView === 'panels' && tabExceptions.includes(tab)) { + // SCA & Vulnerabilities initial tab is inventory + this.props.onSelectedTabChanged(module.init); } else { this.props.onSelectedTabChanged(tabView); } diff --git a/public/components/common/modules/modules-defaults.js b/public/components/common/modules/modules-defaults.js index 4c334fdf27..0b74074259 100644 --- a/public/components/common/modules/modules-defaults.js +++ b/public/components/common/modules/modules-defaults.js @@ -130,7 +130,7 @@ export const ModulesDefaults = { availableFor: ['manager', 'agent'], }, vuls: { - init: 'dashboard', + init: 'inventory', tabs: [ { id: 'inventory', @@ -138,7 +138,6 @@ export const ModulesDefaults = { buttons: [ButtonModuleExploreAgent], component: MainVuls, }, - DashboardTab, EventsTab, ], buttons: ['settings'], diff --git a/public/components/common/welcome/agents-welcome.js b/public/components/common/welcome/agents-welcome.js index 11d444ae24..5f6284fb01 100644 --- a/public/components/common/welcome/agents-welcome.js +++ b/public/components/common/welcome/agents-welcome.js @@ -51,9 +51,13 @@ import { updateCurrentAgentData } from '../../../redux/actions/appStateActions'; import WzTextWithTooltipIfTruncated from '../wz-text-with-tooltip-if-truncated'; import { getAngularModule } from '../../../kibana-services'; import { hasAgentSupportModule } from '../../../react-services/wz-agents'; -import { withErrorBoundary } from '../hocs'; +import { withErrorBoundary, withReduxProvider } from '../hocs'; +import { compose } from 'redux'; -export const AgentsWelcome = withErrorBoundary (class AgentsWelcome extends Component { +export const AgentsWelcome = compose( + withReduxProvider, + withErrorBoundary)( +class AgentsWelcome extends Component { _isMount = false; constructor(props) { super(props); diff --git a/public/components/common/welcome/components/requirement_vis/components/index.ts b/public/components/common/welcome/components/requirement_vis/components/index.ts deleted file mode 100644 index 0c7169eea2..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { RequirementsHead } from './requirement_head'; -export { RequirementsBody } from './requitement_body'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requirement_head/index.ts b/public/components/common/welcome/components/requirement_vis/components/requirement_head/index.ts deleted file mode 100644 index 9b517ca976..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requirement_head/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { RequirementsHead } from './requirements_head'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requirement_head/requirements_head.tsx b/public/components/common/welcome/components/requirement_vis/components/requirement_head/requirements_head.tsx deleted file mode 100644 index 3ca210ac6d..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requirement_head/requirements_head.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 from 'react' -import { - EuiFlexGroup, - EuiText, - EuiSelect, -} from '@elastic/eui'; - - -export function RequirementsHead({ requirement, setRequirement }) { - return ( - -

    -

    Compliance

    -

    -
    - setRequirement(e.target.value)} - aria-label="Select requirement" - /> -
    -
    - ) -} - -const requirements = [ - { value: 'pci_dss', text: 'PCI DSS' }, - { value: 'gdpr', text: 'GDPR' }, - { value: 'nist_800_53', text: 'NIST 800-53' }, - { value: 'hipaa', text: 'HIPAA' }, - { value: 'gpg13', text: 'GPG13' }, - { value: 'tsc', text: 'TSC' }, -]; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/index.ts b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/index.ts deleted file mode 100644 index effef42cf5..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { RequirementsDonnut } from './requirements_donnut'; -export { Requirements_leggend } from './requirements_leggend'; -export { NoAlertsMessage } from './no_alerts_message'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/no_alerts_message.tsx b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/no_alerts_message.tsx deleted file mode 100644 index 4e6500953f..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/no_alerts_message.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; - - -export function NoAlertsMessage({ requirement }) { - const formatedRequirement = requirements[requirement] - return ( - No results} - body={ -

    - No {formatedRequirement} results were found in the selected time range. -

    - } - /> - ) -} - -const requirements = { - 'pci_dss': 'PCI DSS', - 'gdpr': 'GDPR', - 'nist_800_53': 'NIST 800-53', - 'hipaa': 'HIPAA', - 'gpg13': 'GPG13', - 'tsc': 'TSC', -}; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/index.ts b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/index.ts deleted file mode 100644 index 84a1e26597..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/hooks/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { useChartDimensions } from './use-chart-dimensions'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/index.ts b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/index.ts deleted file mode 100644 index 4dce08cbfa..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { RequirementsDonnut } from './requirements_donnut'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/requirements_donnut.tsx b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/requirements_donnut.tsx deleted file mode 100644 index 3926afd81a..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_donnut/requirements_donnut.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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, { useRef, useEffect } from "react"; -import d3 from 'd3'; -import { useChartDimensions } from './hooks'; - - -export const RequirementsDonnut = props => { - const pieRef: null | any = useRef(); - const cache = useRef(props.data); - const [ref, dms] = useChartDimensions({}, pieRef); - const createPie = d3.layout - .pie() - .value(d => d.doc_count) - .sort(null); - const createArc = d3.svg - .arc() - .innerRadius((dms.width * 0.75) / 2) - .outerRadius(dms.width * 0.95 / 2) - .padAngle(0.015); - const colors = props.colors; - const format = d3.format(".2f"); - - useEffect(() => { - const data = createPie(props.data); - const prevData = createPie(cache.current); - const group = d3.select(pieRef.current); - const groupWithData = group.selectAll("g.arc").data(data); - - groupWithData.exit().remove(); - - groupWithData - .enter() - .append("g") - .attr("class", "arc") - - const path = groupWithData.append("path"); - - const arcTween = (d, i) => { - const interpolator = d3.interpolate(prevData[i], d); - return t => createArc(interpolator(t)); - }; - - path - .attr("class", "arc") - .attr("fill", (d, i) => colors[i]) - .transition() - .attrTween("d", arcTween); - cache.current = props.data; - }, - [props.data, dms] - ); - - - - return ( - - - - ); -}; - diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/index.ts b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/index.ts deleted file mode 100644 index a23b5513c7..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { Requirements_leggend } from './requirements_leggend'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.scss b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.scss deleted file mode 100644 index 24e3cd6185..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.scss +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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. - */ - -.wz-list-group > li { - margin-top: 0px; -} - -.wz-list-group > li > a { - padding: 0px; -} diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.tsx b/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.tsx deleted file mode 100644 index 77d245f68f..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/components/requirements_leggend/requirements_leggend.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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. - */ - -// @ts-ignore -import React from "react"; -import { EuiIcon } from "@elastic/eui"; -import { EuiListGroup } from "@elastic/eui"; -import './requirements_leggend.scss'; -import rison from 'rison-node'; -import { buildPhraseFilter } from '../../../../../../../../../../../../src/plugins/data/common'; -import { getIndexPattern } from '../../../../../../../../overview/mitre/lib'; -import store from '../../../../../../../../../redux/store'; -import { updateCurrentAgentData } from '../../../../../../../../../redux/actions/appStateActions'; -import { getAngularModule } from '../../../../../../../../../kibana-services'; - -export function Requirements_leggend({ data, colors, requirement, agent }) { - const list = data.map((item, idx) => ({ - label: `${item.key} (${item.doc_count})`, - icon: , - onClick: () => (requirement === 'gpg13' ? undefined : goToDashboardWithFilter(requirement, item, agent)), - size: 'xs', - color: 'text', - })); - - return ( - - ); -} - -const goToDashboardWithFilter = (requirement, item, agent) => { - store.dispatch(updateCurrentAgentData(agent)); - const $injector = getAngularModule().$injector; - const route = $injector.get('$route'); - getIndexPattern().then(indexPattern => { - const filters = [{ - ...buildPhraseFilter({ name: `rule.${requirement}`, type: 'text' }, item.key, indexPattern), - "$state": { "isImplicit": false, "store": "appState" }, - }] - const _w = { filters }; - const params = { - tab: tabEquivalence[requirement], - _w: rison.encode(_w) - }; - const url = Object.entries(params).map(e => e.join('=')).join('&'); - window.location.href = `#/overview?${url}`; - route.reload(); - }); -} - -const tabEquivalence = { - 'pci_dss': 'pci', - 'gdpr': 'gdpr', - 'nist_800_53': 'nist', - 'hipaa': 'hipaa', - 'gpg13': '', - 'tsc': 'tsc', -} \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/index.ts b/public/components/common/welcome/components/requirement_vis/components/requitement_body/index.ts deleted file mode 100644 index 4ccc8ea942..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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 { RequirementsBody } from './requirements_body'; \ No newline at end of file diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/requirements_body.tsx b/public/components/common/welcome/components/requirement_vis/components/requitement_body/requirements_body.tsx deleted file mode 100644 index 0823d9b7af..0000000000 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/requirements_body.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Wazuh app - React component building the welcome screen of an agent. - * version, OS, registration date, last keep alive. - * - * 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, { useEffect, useState, Fragment } from 'react' -import { useTimeFilter } from '../../../'; -import { getRequirementAlerts } from './lib'; -import { - euiPaletteColorBlind, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiLoadingChart, -} from '@elastic/eui'; -import { - Requirements_leggend, - RequirementsDonnut, - NoAlertsMessage -} from './components'; - -export function RequirementsBody(props) { - const { requirement, agent } = props; - const colors = euiPaletteColorBlind(); - const [loading, setLoading] = useState(true); - const [data, setData] = useState([]) - const timeFilter = useTimeFilter(); - useEffect(() => { - const { id } = agent; - setLoading(true); - getRequirementAlerts(id, timeFilter, requirement).then(e => { - setData(e.alerts_count); - setTimeout(() => setLoading(false), 700); - }) - }, [requirement, timeFilter]); - if (loading) return ( -
    - -
    - ) - if (!data.length) return (); - return ( - - - - - - - - - - - - - - ); -} - diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/lib/get_requirement_alerts.ts b/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts similarity index 70% rename from public/components/common/welcome/components/requirement_vis/components/requitement_body/lib/get_requirement_alerts.ts rename to public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts index 5688253b4c..b6134b9136 100644 --- a/public/components/common/welcome/components/requirement_vis/components/requitement_body/lib/get_requirement_alerts.ts +++ b/public/components/common/welcome/components/requirement_vis/lib/get_requirement_alerts.ts @@ -12,10 +12,9 @@ * Find more information about this on the LICENSE file. */ -import { IFilterParams, getElasticAlerts, getIndexPattern } from '../../../../../../../overview/mitre/lib'; -import { getWazuhFilter } from '../../../../fim_events_table'; -import { buildPhraseFilter, buildExistsFilter } from '../../../../../../../../../../../src/plugins/data/common'; -import { getToasts } from '../../../../../../../../kibana-services'; +import { IFilterParams, getElasticAlerts, getIndexPattern } from '../../../../../overview/mitre/lib'; +import { getWazuhFilter } from '../../fim_events_table'; +import { buildPhraseFilter, buildExistsFilter } from '../../../../../../../../../src/plugins/data/common'; export async function getRequirementAlerts(agentId, time, requirement) { const indexPattern = await getIndexPattern(); @@ -29,7 +28,7 @@ export async function getRequirementAlerts(agentId, time, requirement) { time }; const aggs = { - alerts_count: { + top_alerts_compliance: { terms: { field: `rule.${requirement}`, size: 5, @@ -38,19 +37,7 @@ export async function getRequirementAlerts(agentId, time, requirement) { } const response = await getElasticAlerts(indexPattern, filterParams, aggs); - const alerts_count = ((((response || {}).data || {}).aggregations || {}).alerts_count || {}).buckets; - if (typeof alerts_count === 'undefined') { - getToasts().add({ - color: 'warning', - title: 'Error getting alerts from compliances', - text: "Your environment may not have any index with Wazuh's alerts." - }) - } - - return { - alerts_count: !!alerts_count ? alerts_count : [], - total_alerts: (((response || {}).data || {}).hits || {}).total - }; + return response?.data?.aggregations?.top_alerts_compliance?.buckets; } function createFilters(agentId, indexPattern) { diff --git a/public/components/common/welcome/components/requirement_vis/components/requitement_body/lib/index.ts b/public/components/common/welcome/components/requirement_vis/lib/index.ts similarity index 100% rename from public/components/common/welcome/components/requirement_vis/components/requitement_body/lib/index.ts rename to public/components/common/welcome/components/requirement_vis/lib/index.ts diff --git a/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx b/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx index 2e192f64c1..0af9aeb0c6 100644 --- a/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx +++ b/public/components/common/welcome/components/requirement_vis/requirement_vis.tsx @@ -12,25 +12,94 @@ * Find more information about this on the LICENSE file. */ -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import { EuiFlexItem, EuiPanel, - EuiSpacer, + euiPaletteColorBlind } from '@elastic/eui'; -import { RequirementsHead, RequirementsBody } from './components'; +import { VisualizationBasicWidgetSelector } from '../../../charts/visualizations/basic'; +import { getRequirementAlerts } from './lib'; +import { useTimeFilter } from '../../../hooks'; +import { useDispatch } from 'react-redux'; +import { updateCurrentAgentData } from '../../../../../redux/actions/appStateActions'; +import { getAngularModule } from '../../../../../kibana-services'; +import { getIndexPattern } from '../../../../overview/mitre/lib'; +import { buildPhraseFilter } from '../../../../../../../../src/plugins/data/common'; +import rison from 'rison-node'; + +const selectionOptionsCompliance = [ + { value: 'pci_dss', text: 'PCI DSS' }, + { value: 'gdpr', text: 'GDPR' }, + { value: 'nist_800_53', text: 'NIST 800-53' }, + { value: 'hipaa', text: 'HIPAA' }, + { value: 'gpg13', text: 'GPG13' }, + { value: 'tsc', text: 'TSC' } +]; + +const requirementNameModuleID = { + 'pci_dss': 'pci', + 'gdpr': 'gdpr', + 'nist_800_53': 'nist', + 'hipaa': 'hipaa', + 'gpg13': '', + 'tsc': 'tsc', +} export function RequirementVis(props) { - const [requirement, setRequirement] = useState('pci_dss'); + const colors = euiPaletteColorBlind(); + const {timeFilter} = useTimeFilter(); + const dispatch = useDispatch(); + + const goToDashboardWithFilter = async (requirement, key, agent) => { + try{ + dispatch(updateCurrentAgentData(agent)); + const $injector = getAngularModule().$injector; + const route = $injector.get('$route'); + const indexPattern = getIndexPattern() + const filters = [{ + ...buildPhraseFilter({ name: `rule.${requirement}`, type: 'text' }, key, indexPattern), + "$state": { "isImplicit": false, "store": "appState" }, + }]; + const _w = { filters }; + const params = { + tab: requirementNameModuleID[requirement], + _w: rison.encode(_w) + }; + const url = Object.entries(params).map(e => e.join('=')).join('&'); + window.location.href = `#/overview?${url}`; + route.reload(); + }catch(error){ + + } + } + + const fetchData = useCallback(async (selectedOptionValue, timeFilter, agent) => { + const buckets = await getRequirementAlerts(agent.id, timeFilter, selectedOptionValue); + return buckets?.length ? buckets.map(({key, doc_count}, index) => ({ + label: key, + value: doc_count, + color: colors[index], + onClick: selectedOptionValue === 'gpg13' ? undefined : (() => goToDashboardWithFilter(selectedOptionValue, key, agent)) + })) : null; + }, []); + return ( - - - - - - + + `No ${optionRequirement.text} results were found in the selected time range.`} + /> ) } + diff --git a/public/components/visualize/agent-visualizations.js b/public/components/visualize/agent-visualizations.js index b72958a01e..13ff661c6b 100644 --- a/public/components/visualize/agent-visualizations.js +++ b/public/components/visualize/agent-visualizations.js @@ -473,69 +473,6 @@ export const agentVisualizations = { } ] }, - vuls: { - rows: [ - { - height: 400, - vis: [ - { - title: 'Alerts severity over time', - id: 'Wazuh-App-Agents-vuls-Alerts-severity-over-time', - width: 50 - }, - { - title: 'Most common rules', - id: 'Wazuh-App-Agents-vuls-Most-common-rules', - width: 50 - } - ] - }, - { - height: 330, - vis: [ - { - title: 'Most common CVEs', - id: 'Wazuh-App-Agents-vuls-Vulnerability-Most-common-CVEs', - width: 25 - }, - { - title: 'Alerts evolution: Commonly affected packages', - id: 'Wazuh-App-Agents-vuls-evolution-affected-packages', - width: 50 - }, - { - title: 'Most common CWEs', - id: 'Wazuh-App-Agents-vuls-Most-common-CWEs', - width: 25 - } - ] - }, - { - height: 330, - vis: [ - { - title: 'Severity distribution', - id: 'Wazuh-App-Agents-vuls-Vulnerability-severity-distribution', - width: 25 - }, - { - title: 'Top affected packages by CVEs', - id: 'Wazuh-App-Agents-vuls-packages-CVEs', - width: 75 - }, - ] - }, - { - hide: true, - vis: [ - { - title: 'Alerts summary', - id: 'Wazuh-App-Agents-vuls-Alerts-summary' - } - ] - } - ] - }, virustotal: { rows: [ { diff --git a/public/react-services/vis-factory-handler.js b/public/react-services/vis-factory-handler.js index 2e8d173c30..cfcde9962a 100644 --- a/public/react-services/vis-factory-handler.js +++ b/public/react-services/vis-factory-handler.js @@ -67,8 +67,9 @@ export class VisFactoryHandler { try { const currentPattern = AppState.getCurrentPattern(); + // TODO change logic to read common/modules/module-defaults.js configuration const data = - tab !== 'sca' + !['sca', 'vuls'].includes(tab) ? await GenericRequest.request( 'GET', `/elastic/visualizations/overview-${tab}/${currentPattern}` @@ -100,8 +101,9 @@ export class VisFactoryHandler { const commonData = $injector.get('commonData'); try { + // TODO change logic to read common/modules/module-defaults.js configuration const data = - (!['sca', 'office'].includes(tab)) + (!['sca', 'office', 'vuls'].includes(tab)) ? await GenericRequest.request( 'GET', `/elastic/visualizations/agents-${tab}/${AppState.getCurrentPattern()}` diff --git a/public/styles/common.scss b/public/styles/common.scss index 6ca7bba07e..cd20327342 100644 --- a/public/styles/common.scss +++ b/public/styles/common.scss @@ -1738,6 +1738,21 @@ iframe.width-changed { width: 100%; } +.wz-user-select-none{ + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.wz-euiCard-no-title{ + .euiCard__title, + .euiCard__description + { + display: none; + } +} + /* Custom Searchbar styles */ .application .filters-search-bar .globalQueryBar, .app-container .filters-search-bar .globalQueryBar{ diff --git a/public/templates/agents/visualizations.js b/public/templates/agents/visualizations.js index 71242a4157..5522c74d65 100644 --- a/public/templates/agents/visualizations.js +++ b/public/templates/agents/visualizations.js @@ -355,54 +355,6 @@ export const visualizations = { } ] }, - vuls: { - rows: [ - { - height: 400, - vis: [ - { - title: 'Alerts severity over time', - id: 'Wazuh-App-Agents-vuls-Alerts-severity-over-time', - width: 50 - }, - { - title: 'Most common rules', - id: 'Wazuh-App-Agents-vuls-Most-common-rules', - width: 50 - } - ] - }, - { - height: 270, - vis: [ - { - title: 'Most common CVEs', - id: 'Wazuh-App-Agents-vuls-Vulnerability-Most-common-CVEs', - width: 33 - }, - { - title: 'Severity distribution', - id: 'Wazuh-App-Agents-vuls-Vulnerability-severity-distribution', - width: 33 - }, - { - title: 'Commonly affected packages', - id: 'Wazuh-App-Agents-vuls-Commonly-affected-packages', - width: 33 - } - ] - }, - { - height: 570, - vis: [ - { - title: 'Alert summary', - id: 'Wazuh-App-Agents-vuls-Alert-summary' - } - ] - } - ] - }, virustotal: { rows: [ { diff --git a/server/integration-files/visualizations/agents/agents-vuls.ts b/server/integration-files/visualizations/agents/agents-vuls.ts deleted file mode 100644 index af482b62e8..0000000000 --- a/server/integration-files/visualizations/agents/agents-vuls.ts +++ /dev/null @@ -1,1104 +0,0 @@ -/* - * Wazuh app - Module for Agents/Vulnerabilities visualizations - * 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 default [ - { - _id: 'Wazuh-App-Agents-vuls-Alerts-severity-over-time', - _type: 'visualization', - _source: { - title: 'Alerts severity over time', - visState: JSON.stringify({ - title: 'Alerts by action over time', - type: 'area', - params: { - type: 'area', - grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'area', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - drawLinesBetweenPoints: true, - showCircles: true, - interpolate: 'cardinal', - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: 'timestamp', - timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, - useNormalizedEsInterval: true, - interval: 'auto', - time_zone: 'Europe/Berlin', - drop_partials: false, - customInterval: '2h', - min_doc_count: 1, - extended_bounds: {}, - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Alert-summary', - _type: 'visualization', - _source: { - title: 'Alerts summary', - visState: JSON.stringify({ - title: 'vulnerability', - type: 'table', - params: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: 4, direction: 'desc' }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'asc', - orderBy: '_key', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Severity', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.title', - size: 5, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Title', - }, - }, - { - id: '6', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.published', - size: 2, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Published', - }, - }, - { - id: '5', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.cve', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'CVE', - }, - }, - ], - }), - uiStateJSON: JSON.stringify({ - vis: { params: { sort: { columnIndex: 4, direction: 'desc' } } }, - }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Commonly-affected-packages', - _type: 'visualization', - _source: { - title: 'Top 5 affected packages', - visState: JSON.stringify({ - title: 'Top 5 affected packages', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.package.name', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Affected package', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Metric-Critical-severity', - _type: 'visualization', - _source: { - title: 'Metric Critical severity', - visState: JSON.stringify({ - title: 'Metric Critical severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Critical severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Critical', - params: { - query: 'Critical', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Critical', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Metric-High-severity', - _type: 'visualization', - _source: { - title: 'Metric High severity', - visState: JSON.stringify({ - title: 'Metric High severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'High severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'High', - params: { - query: 'High', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'High', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Metric-Medium-severity', - _type: 'visualization', - _source: { - title: 'Metric Medium severity', - visState: JSON.stringify({ - title: 'Metric Medium severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Medium severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Medium', - params: { - query: 'Medium', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Medium', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Metric-Low-severity', - _type: 'visualization', - _source: { - title: 'Metric Low severity', - visState: JSON.stringify({ - title: 'Metric Low severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Low severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Low', - params: { - query: 'Low', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Low', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Top-Agents-severity', - _type: 'visualization', - _source: { - title: 'Top Agents severity', - visState: JSON.stringify({ - title: 'Top Agents severity', - type: 'histogram', - params: { - type: 'histogram', - grid: { categoryLines: false, style: { color: '#eee' } }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'agent.name', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Agent name', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Severity', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Most-common-rules', - _type: 'visualization', - _source: { - title: 'Most common rules', - visState: JSON.stringify({ - type: 'table', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'rule.id', - orderBy: '1', - order: 'desc', - size: 20, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Rule ID', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'rule.description', - orderBy: '1', - order: 'desc', - size: 20, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Description', - }, - }, - ], - params: { - perPage: 7, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: null, direction: null }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - percentageCol: '', - }, - title: 'common rules', - }), - uiStateJSON: JSON.stringify({ - vis: { params: { sort: { columnIndex: 2, direction: 'desc' } } }, - }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Vulnerability-severity-distribution', - _type: 'visualization', - _source: { - title: 'Severity distribution', - visState: JSON.stringify({ - title: 'Severity distribution', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Severity', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Vulnerability-Most-common-CVEs', - _type: 'visualization', - _source: { - title: 'Most common CVEs', - visState: JSON.stringify({ - title: 'Most common CVEs', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cve', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'CVE', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-top-CWEs', - _type: 'visualization', - _source: { - title: 'Top CWEs', - visState: JSON.stringify({ - type: 'table', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.cwe_reference', - orderBy: '1', - order: 'desc', - size: 50, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'CWE', - }, - }, - ], - params: { - perPage: 5, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: null, direction: null }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - percentageCol: '', - row: true, - }, - title: 'CWE table', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-evolution-affected-packages', - _type: 'visualization', - _source: { - title: 'Alerts evolution: Commonly affected packages', - visState: JSON.stringify({ - title: 'Alerts evolution: Commonly affected packages', - type: 'histogram', - params: { - type: 'histogram', - grid: { categoryLines: false, style: { color: '#eee' } }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.package.name', - size: 5, - order: 'desc', - orderBy: '1', - }, - }, - { - id: '2', - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: 'timestamp', - interval: 'auto', - customInterval: '2h', - min_doc_count: 1, - extended_bounds: {}, - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-Most-common-CWEs', - _type: 'visualization', - _source: { - title: 'Most common CWEs', - visState: JSON.stringify({ - title: 'Most common CWEs', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cwe_reference', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Severity', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Agents-vuls-packages-CVEs', - _type: 'visualization', - _source: { - title: 'Top affected packages by CVEs', - visState: JSON.stringify({ - type: 'histogram', - mode: 'stacked', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cve', - orderBy: '1', - order: 'desc', - size: 10, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.package.name', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - params: { - type: 'area', - grid: { categoryLines: false }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: true, - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: 'linear', - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, - labels: {}, - }, - title: 'top packages by CVE', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, -]; diff --git a/server/integration-files/visualizations/agents/index.ts b/server/integration-files/visualizations/agents/index.ts index 4527d79700..71a0dd7438 100644 --- a/server/integration-files/visualizations/agents/index.ts +++ b/server/integration-files/visualizations/agents/index.ts @@ -23,7 +23,6 @@ import nist from './agents-nist'; import tsc from './agents-tsc'; import pm from './agents-pm'; import virustotal from './agents-virustotal'; -import vuls from './agents-vuls'; import osquery from './agents-osquery'; import docker from './agents-docker'; import welcome from './agents-welcome'; @@ -44,7 +43,6 @@ export { tsc, pm, virustotal, - vuls, osquery, mitre, docker, diff --git a/server/integration-files/visualizations/overview/index.ts b/server/integration-files/visualizations/overview/index.ts index a5fff0fc65..8f8711b75f 100644 --- a/server/integration-files/visualizations/overview/index.ts +++ b/server/integration-files/visualizations/overview/index.ts @@ -23,7 +23,6 @@ import nist from './overview-nist'; import tsc from './overview-tsc'; import pm from './overview-pm'; import virustotal from './overview-virustotal'; -import vuls from './overview-vuls'; import mitre from './overview-mitre'; import office from './overview-office'; import osquery from './overview-osquery'; @@ -45,7 +44,6 @@ export { tsc, pm, virustotal, - vuls, mitre, office, osquery, diff --git a/server/integration-files/visualizations/overview/overview-vuls.ts b/server/integration-files/visualizations/overview/overview-vuls.ts deleted file mode 100644 index 0d69cc422c..0000000000 --- a/server/integration-files/visualizations/overview/overview-vuls.ts +++ /dev/null @@ -1,1190 +0,0 @@ -/* - * Wazuh app - Module for Overview/Vulnerabilities visualizations - * 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 default [ - { - _id: 'Wazuh-App-Overview-vuls-Alerts-severity', - _type: 'visualization', - _source: { - title: 'Severity count', - visState: JSON.stringify({ - title: 'Alerts by action over time', - type: 'area', - params: { - type: 'area', - grid: { categoryLines: true, style: { color: '#eee' }, valueAxis: 'ValueAxis-1' }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'area', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - drawLinesBetweenPoints: true, - showCircles: true, - interpolate: 'cardinal', - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: 'timestamp', - timeRange: { from: 'now-24h', to: 'now', mode: 'quick' }, - useNormalizedEsInterval: true, - interval: 'auto', - time_zone: 'Europe/Berlin', - drop_partials: false, - customInterval: '2h', - min_doc_count: 1, - extended_bounds: {}, - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Alert-summary', - _type: 'visualization', - _source: { - title: 'Alert summary', - visState: JSON.stringify({ - title: 'vulnerability', - type: 'table', - params: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: 4, direction: 'desc' }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.severity', - size: 10, - order: 'asc', - orderBy: '_key', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Severity', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'agent.id', - size: 100, - order: 'desc', - orderBy: '1', - otherBucket: true, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'ID', - }, - }, - { - id: '4', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.rationale', - size: 10, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Rationale', - }, - }, - { - id: '5', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.title', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Title', - }, - },{ - id: '6', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.cve', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'CVE', - }, - },{ - id: '7', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.package.name', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Name', - }, - },{ - id: '8', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.package.condition', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Condition', - }, - },{ - id: '9', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.references', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'References', - }, - },{ - id: '10', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.package.version', - size: 1, - order: 'desc', - orderBy: '1', - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'Version', - }, - }, - ], - }), - uiStateJSON: JSON.stringify({ - vis: { params: { sort: { columnIndex: 4, direction: 'desc' } } }, - }), - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Commonly-affected-packages', - _type: 'visualization', - _source: { - title: 'Commonly affected packages', - visState: JSON.stringify({ - title: 'Commonly affected packages', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.package.name', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Affected package', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"index":"wazuh-alerts","filter":[],"query":{"query":"","language":"lucene"}}', - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-top-CVEs', - _type: 'visualization', - _source: { - title: 'Top CVEs', - visState: JSON.stringify({ - type: 'table', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.cve', - orderBy: '1', - order: 'desc', - size: 50, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'CVE', - }, - }, - ], - params: { - perPage: 5, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: null, direction: null }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - percentageCol: '', - row: true, - }, - title: 'CVE table', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Most-common-CVEs', - _type: 'visualization', - _source: { - title: 'Most common CVEs', - visState: JSON.stringify({ - title: 'Most common CVEs', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cve', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'CVE', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-packages-CVEs', - _type: 'visualization', - _source: { - title: 'Top affected packages by CVEs', - visState: JSON.stringify({ - type: 'histogram', - mode: 'stacked', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cve', - orderBy: '1', - order: 'desc', - size: 10, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.package.name', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - params: { - type: 'area', - grid: { categoryLines: false }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: true, - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: 'linear', - valueAxis: 'ValueAxis-1', - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - thresholdLine: { show: false, value: 10, width: 1, style: 'full', color: '#E7664C' }, - labels: {}, - }, - title: 'top packages by CVE', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-agents-severities', - _type: 'visualization', - _source: { - title: 'Agents by severity', - visState: JSON.stringify({ - type: 'heatmap', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'agent.name', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: ' ', - }, - }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.severity', - orderBy: '1', - order: 'desc', - size: 5, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - }, - }, - ], - params: { - type: 'heatmap', - addTooltip: true, - addLegend: true, - enableHover: false, - legendPosition: 'right', - times: [], - colorsNumber: 4, - colorSchema: 'Greens', - setColorRange: false, - colorsRange: [], - invertColors: false, - percentageMode: false, - valueAxes: [ - { - show: false, - id: 'ValueAxis-1', - type: 'value', - scale: { type: 'linear', defaultYExtents: false }, - labels: { show: false, rotate: 0, overwriteColor: false, color: 'black' }, - }, - ], - }, - title: 'Agents by severity', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-top-CWEs', - _type: 'visualization', - _source: { - title: 'Top CWEs', - visState: JSON.stringify({ - type: 'table', - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'bucket', - params: { - field: 'data.vulnerability.cwe_reference', - orderBy: '1', - order: 'desc', - size: 50, - otherBucket: false, - otherBucketLabel: 'Other', - missingBucket: false, - missingBucketLabel: 'Missing', - customLabel: 'CWE', - }, - }, - ], - params: { - perPage: 5, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { columnIndex: null, direction: null }, - showTotal: false, - showToolbar: true, - totalFunc: 'sum', - percentageCol: '', - row: true, - }, - title: 'CWE table', - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Most-common-CWEs', - _type: 'visualization', - _source: { - title: 'Most common CWEs', - visState: JSON.stringify({ - title: 'Most common CWEs', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.cwe_reference', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Severity', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Metric-Critical-severity', - _type: 'visualization', - _source: { - title: 'Metric Critical severity', - visState: JSON.stringify({ - title: 'Metric Critical severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Critical severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Critical', - params: { - query: 'Critical', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Critical', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Metric-High-severity', - _type: 'visualization', - _source: { - title: 'Metric High severity', - visState: JSON.stringify({ - title: 'Metric High severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'High severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'High', - params: { - query: 'High', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'High', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Metric-Medium-severity', - _type: 'visualization', - _source: { - title: 'Metric Medium severity', - visState: JSON.stringify({ - title: 'Metric Medium severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Medium severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Medium', - params: { - query: 'Medium', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Medium', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Metric-Low-severity', - _type: 'visualization', - _source: { - title: 'Metric Low severity', - visState: JSON.stringify({ - title: 'Metric Low severity', - type: 'metric', - params: { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [{ from: 0, to: 10000 }], - labels: { show: true }, - invertColors: false, - style: { bgFill: '#000', bgColor: false, labelColor: false, subText: '', fontSize: 20 }, - }, - }, - aggs: [ - { - id: '1', - enabled: true, - type: 'count', - schema: 'metric', - params: { customLabel: 'Low severity alerts' }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [ - { - meta: { - index: 'wazuh-alerts', - negate: false, - disabled: false, - alias: null, - type: 'phrase', - key: 'data.vulnerability.severity', - value: 'Low', - params: { - query: 'Low', - type: 'phrase', - }, - }, - query: { - match: { - 'data.vulnerability.severity': { - query: 'Low', - type: 'phrase', - }, - }, - }, - $state: { - store: 'appState', - }, - }, - ], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Most-affected-agents', - _type: 'visualization', - _source: { - title: 'Most affected agents', - visState: JSON.stringify({ - title: 'Most affected agents', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'agent.name', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Affected agent', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Vulnerability-severity-distribution', - _type: 'visualization', - _source: { - title: 'Severity distribution', - visState: JSON.stringify({ - title: 'Severity distribution', - type: 'pie', - params: { - type: 'pie', - addTooltip: true, - addLegend: true, - legendPosition: 'right', - isDonut: true, - labels: { show: false, values: true, last_level: true, truncate: 100 }, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '2', - enabled: true, - type: 'terms', - schema: 'segment', - params: { - field: 'data.vulnerability.severity', - size: 5, - order: 'desc', - orderBy: '1', - customLabel: 'Severity', - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, - { - _id: 'Wazuh-App-Overview-vuls-Vulnerability-evolution-affected-packages', - _type: 'visualization', - _source: { - title: 'TOP affected packages alerts Evolution', - visState: JSON.stringify({ - title: 'TOP affected packages alerts Evolution', - type: 'histogram', - params: { - type: 'histogram', - grid: { categoryLines: false, style: { color: '#eee' } }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - position: 'bottom', - show: true, - style: {}, - scale: { type: 'linear' }, - labels: { show: true, filter: true, truncate: 100 }, - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - name: 'LeftAxis-1', - type: 'value', - position: 'left', - show: true, - style: {}, - scale: { type: 'linear', mode: 'normal' }, - labels: { show: true, rotate: 0, filter: false, truncate: 100 }, - title: { text: 'Count' }, - }, - ], - seriesParams: [ - { - show: 'true', - type: 'histogram', - mode: 'stacked', - data: { label: 'Count', id: '1' }, - valueAxis: 'ValueAxis-1', - drawLinesBetweenPoints: true, - showCircles: true, - }, - ], - addTooltip: true, - addLegend: true, - legendPosition: 'right', - times: [], - addTimeMarker: false, - }, - aggs: [ - { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, - { - id: '3', - enabled: true, - type: 'terms', - schema: 'group', - params: { - field: 'data.vulnerability.package.name', - size: 5, - order: 'desc', - orderBy: '1', - }, - }, - { - id: '2', - enabled: true, - type: 'date_histogram', - schema: 'segment', - params: { - field: 'timestamp', - interval: 'auto', - customInterval: '2h', - min_doc_count: 1, - extended_bounds: {}, - }, - }, - ], - }), - uiStateJSON: '{}', - description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - index: 'wazuh-alerts', - filter: [], - query: { query: '', language: 'lucene' }, - }), - }, - }, - }, -]; From ba5d5fb4c66f7676850a61c27522baf644eb4f70 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 23 Mar 2022 15:48:01 +0100 Subject: [PATCH 425/493] Enhance rework vulnerabilities inventory flyout (#3908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added all fields prop and changed column width * Fixed coding leftover * show all available fields by default * Added sorting to row details fields * Fixed default value for last scan details * changelog: Add PR to changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 ++ .../components/agents/vuls/inventory/detail.tsx | 16 ++++++++++++---- .../common/modules/discover/discover.tsx | 6 +++++- .../common/modules/discover/row-details.tsx | 15 ++++++++++++++- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a63aef054e..63719e7528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added a toast message when there is an error creating a new group [#3804](https://github.com/wazuh/wazuh-kibana-app/pull/3804) - Added a step to start the agent to the deploy new Windowns agent guide [#3846](https://github.com/wazuh/wazuh-kibana-app/pull/3846) - Added 3 new panels to `Vulnerabilities/Inventory` [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) +- Added new fields of `Vulnerabilities` to the details flyout [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) ### Changed @@ -75,6 +76,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed agent install codeblock copy button and powershell terminal warning [#3792](https://github.com/wazuh/wazuh-kibana-app/pull/3792) - Refactored as the plugin platform name and references is managed [#3811](https://github.com/wazuh/wazuh-kibana-app/pull/3811) - Removed `Dashboard` tab for the `Vulnerabilities` modules [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) +- Display all fields in the `Table` tab when expading an alert row in the alerts tables of flyouts and the `Modules/Security Events/Dashboard` table [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) ### Fixed diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 36aa40f956..1d3700615f 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -129,17 +129,17 @@ export class Details extends Component { }, { field: 'last_full_scan', - name: 'Last Full Scan', + name: 'Last full scan', icon: 'clock', link: false, - transformValue: formatUIDate + transformValue: this.beautifyDate }, { field: 'last_partial_scan', - name: 'Last Partial Scan', + name: 'Last partial scan', icon: 'clock', link: false, - transformValue: formatUIDate + transformValue: this.beautifyDate }, { field: 'published', @@ -165,6 +165,13 @@ export class Details extends Component { ]; } + // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates + // when vulnerability module is not configured + // its meant to render nothing when such date is received + beautifyDate(date: string) { + return ['', '1970-01-01T00:00:00Z'].includes(date) ? '-' : formatUIDate(date); + } + viewInEvents = (ev) => { const { cve } = this.props.currentItem; if (this.props.view === 'extern') { @@ -451,6 +458,7 @@ export class Details extends Component { { field: 'rule.description', label: 'Description' }, { field: 'rule.level', label: 'Level' }, { field: 'rule.id', label: 'Rule ID' }, + { field: 'data.vulnerability.status', label: 'Status', width: '20%' }, ]} includeFilters="vulnerability" implicitFilters={implicitFilters} diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 113281c063..1d0c0e7a4e 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -103,6 +103,7 @@ export const Discover = compose( initialFilters: object[]; query?: { language: 'kuery' | 'lucene'; query: string }; type?: any; + rowDetailsFields?: string[]; updateTotalHits: Function; includeFilters?: string; initialColumns: ColumnDefinition[]; @@ -323,6 +324,8 @@ export const Discover = compose( toggleDetails = (item) => { const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; + const { rowDetailsFields } = this.props; + if (itemIdToExpandedRowMap[item._id]) { delete itemIdToExpandedRowMap[item._id]; this.setState({ itemIdToExpandedRowMap }); @@ -336,6 +339,7 @@ export const Discover = compose( addFilter={(filter) => this.addFilter(filter)} addFilterOut={(filter) => this.addFilterOut(filter)} toggleColumn={(id) => this.addColumn(id)} + rowDetailsFields={rowDetailsFields} />
    ); @@ -474,7 +478,7 @@ export const Discover = compose( const columns = columnsList.map((item) => { if (item === 'icon') { return { - width: '25px', + width: '2.3%', isExpander: true, render: (item) => { return ( diff --git a/public/components/common/modules/discover/row-details.tsx b/public/components/common/modules/discover/row-details.tsx index a6f4dbd1a4..fdc5dc471e 100644 --- a/public/components/common/modules/discover/row-details.tsx +++ b/public/components/common/modules/discover/row-details.tsx @@ -60,6 +60,7 @@ export class RowDetails extends Component { }, syscheck: Object } + rowDetailsFields?: string[] } constructor(props) { @@ -94,6 +95,15 @@ export class RowDetails extends Component { const paths = (obj = {}, head = '') => { return Object.entries(obj) + .sort(([keyA], [keyB]) => { + if (keyA > keyB) { + return 1; + } else if (keyA < keyB) { + return -1; + } else { + return 0; + }; + }) .reduce((product, [key, value]) => { let fullPath = addDelimiter(head, key) return isObject(value) ? @@ -146,7 +156,10 @@ export class RowDetails extends Component { renderRows() { - const fieldsToShow = ['agent', 'cluster', 'manager', 'rule', 'decoder', 'syscheck', 'full_log', 'location']; + // By default show all available fields, otherwise show the fields specified in rowDetailsFields string array + const fieldsToShow = this.props.rowDetailsFields?.length ? + this.props.rowDetailsFields.sort() : Object.keys(this.props.item).sort(); + var rows: any[] = []; const isString = val => typeof val === 'string'; for (var i = 0; i < fieldsToShow.length; i++) { From 405e238238327cab1820e6286b38b8c458461dae Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 23 Mar 2022 18:17:30 +0100 Subject: [PATCH 426/493] Controlled undefined date formating --- public/components/agents/vuls/inventory.tsx | 6 +++--- public/components/agents/vuls/inventory/detail.tsx | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 15670c4c07..1a0786b152 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -173,11 +173,11 @@ export class Inventory extends Component { ; } - // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates + // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates or undefined ones // when vulnerability module is not configured // its meant to render nothing when such date is received - beautifyDate(date: string) { - return ['', '1970-01-01T00:00:00Z'].includes(date) ? '-' : formatUIDate(date); + beautifyDate(date?: string) { + return date && '1970-01-01T00:00:00Z' != date ? formatUIDate(date) : '-'; } buildTitleFilter({ description, title, titleColor }) { diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index 1d3700615f..e71734bcbe 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -146,14 +146,14 @@ export class Details extends Component { name: 'Published', icon: 'clock', link: false, - transformValue: formatUIDate + transformValue: this.beautifyDate }, { field: 'updated', name: 'Updated', icon: 'clock', link: false, - transformValue: formatUIDate + transformValue: this.beautifyDate }, { field: 'external_references', @@ -165,11 +165,11 @@ export class Details extends Component { ]; } - // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates + // This method was created because Wazuh API returns 1970-01-01T00:00:00Z dates or undefined ones // when vulnerability module is not configured // its meant to render nothing when such date is received - beautifyDate(date: string) { - return ['', '1970-01-01T00:00:00Z'].includes(date) ? '-' : formatUIDate(date); + beautifyDate(date?: string) { + return date && '1970-01-01T00:00:00Z' != date ? formatUIDate(date) : '-'; } viewInEvents = (ev) => { From edf3256950e13465ecd4a2ee34be8b85dd0d9e4b Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 23 Mar 2022 18:56:55 +0100 Subject: [PATCH 427/493] Controlled - date formating --- public/components/agents/vuls/inventory.tsx | 2 +- public/components/agents/vuls/inventory/detail.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/agents/vuls/inventory.tsx b/public/components/agents/vuls/inventory.tsx index 1a0786b152..4f310b84c8 100644 --- a/public/components/agents/vuls/inventory.tsx +++ b/public/components/agents/vuls/inventory.tsx @@ -177,7 +177,7 @@ export class Inventory extends Component { // when vulnerability module is not configured // its meant to render nothing when such date is received beautifyDate(date?: string) { - return date && '1970-01-01T00:00:00Z' != date ? formatUIDate(date) : '-'; + return date && !['1970-01-01T00:00:00Z', '-'].includes(date) ? formatUIDate(date) : '-'; } buildTitleFilter({ description, title, titleColor }) { diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index e71734bcbe..c61426941d 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -169,7 +169,7 @@ export class Details extends Component { // when vulnerability module is not configured // its meant to render nothing when such date is received beautifyDate(date?: string) { - return date && '1970-01-01T00:00:00Z' != date ? formatUIDate(date) : '-'; + return date && !['1970-01-01T00:00:00Z', '-'].includes(date) ? formatUIDate(date) : '-'; } viewInEvents = (ev) => { From fc6636afab190dc345b330929671c5b82d15372f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 24 Mar 2022 16:16:12 +0100 Subject: [PATCH 428/493] fix: Open the vulnerabilities references links into a new browser tab --- public/components/agents/vuls/inventory/detail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/agents/vuls/inventory/detail.tsx b/public/components/agents/vuls/inventory/detail.tsx index c61426941d..4a71a2d7c0 100644 --- a/public/components/agents/vuls/inventory/detail.tsx +++ b/public/components/agents/vuls/inventory/detail.tsx @@ -333,7 +333,7 @@ export class Details extends Component { }> ({ label: link, href: link })) + listItems={references.map(link => ({ label: link, href: link, target: '_blank' })) } /> From 471e17191ef8a7b5500be2ed707a9a494031191e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 25 Mar 2022 14:21:15 +0100 Subject: [PATCH 429/493] bump: Bump to Wazuh 4.2.6 --- CHANGELOG.md | 6 ++++++ README.md | 21 ++++++++++++++++----- package.json | 6 +++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e05b39fc..288c7d36d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the Wazuh app project will be documented in this file. +## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 + +### Added + +- Support for Wazuh 4.2.6 + ## Wazuh v4.2.5 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.4, 7.14.2 - Revision 4206 ### Added diff --git a/README.md b/README.md index 17373ca5aa..cda3f46068 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ This plugin for Kibana allows you to visualize and analyze Wazuh alerts stored i ## Requisites -- Wazuh HIDS 4.2.2 +- Wazuh HIDS 4.2.6 - Kibana 7.10.2 - Elasticsearch 7.10.2 @@ -105,7 +105,7 @@ Install the Wazuh app plugin for Kibana ``` cd /usr/share/kibana -sudo -u kibana bin/kibana-plugin install https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.10.2-1.zip +sudo -u kibana bin/kibana-plugin install https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.6_7.10.2-1.zip ``` Restart Kibana @@ -192,7 +192,7 @@ Install the Wazuh app ``` cd /usr/share/kibana/ -sudo -u kibana bin/kibana-plugin install https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.5_7.10.2-1.zip +sudo -u kibana bin/kibana-plugin install https://packages.wazuh.com/4.x/ui/kibana/wazuh_kibana-4.2.6_7.10.2-1.zip ``` Update configuration file permissions. @@ -221,6 +221,17 @@ service kibana restart | Wazuh app | Kibana | Open Distro | Package | | :-------: | :----: | :---------: | :------------------------------------------------------------------------- | +| 4.2.6 | 7.14.2 | | | +| 4.2.6 | 7.14.1 | | | +| 4.2.6 | 7.14.0 | | | +| 4.2.6 | 7.13.4 | | | +| 4.2.6 | 7.13.3 | | | +| 4.2.6 | 7.13.2 | | | +| 4.2.6 | 7.13.1 | | | +| 4.2.6 | 7.13.0 | | | +| 4.2.6 | 7.12.1 | | | +| 4.2.6 | 7.11.2 | | | +| 4.2.6 | 7.10.2 | 1.13.2 | | | 4.2.5 | 7.14.2 | | | | 4.2.5 | 7.14.1 | | | | 4.2.5 | 7.14.0 | | | @@ -231,7 +242,7 @@ service kibana restart | 4.2.5 | 7.13.0 | | | | 4.2.5 | 7.12.1 | | | | 4.2.5 | 7.11.2 | | | -| 4.2.5 | 7.10.2 | 1.13.2 | | +| 4.2.5 | 7.10.2 | 1.13.2 | | | 4.2.4 | 7.12.1 | | | | 4.2.4 | 7.11.2 | | | | 4.2.4 | 7.10.2 | 1.13.2 | | @@ -379,7 +390,7 @@ If you want to contribute to our project please don't hesitate to send a pull re ## Copyright & License -Copyright © 2021 Wazuh, Inc. +Copyright © 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. diff --git a/package.json b/package.json index 3f2348e985..404073d3ba 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "wazuh", - "version": "4.2.5", - "revision": "4206-1", - "code": "4206-1", + "version": "4.2.6", + "revision": "4207-1", + "code": "4207-1", "kibana": { "version": "7.10.2" }, From 0a44d4da69ccc6f6a068735506c5bd6accebd3ff Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Thu, 31 Mar 2022 08:17:25 +0200 Subject: [PATCH 430/493] [Known fields] Add fields to the know fields for the index pattern of alerts (#3924) * fix: Added more fields to the known fields for the index pattern of alerts * changelog: Add PR to the changelog --- CHANGELOG.md | 1 + public/utils/known-fields.js | 400 +++++++++++++++++++++++++++++++++++ 2 files changed, 401 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de218946ca..3d278a8ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added a step to start the agent to the deploy new Windowns agent guide [#3846](https://github.com/wazuh/wazuh-kibana-app/pull/3846) - Added 3 new panels to `Vulnerabilities/Inventory` [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) - Added new fields of `Vulnerabilities` to the details flyout [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) +- Added missing fields used in visualizations to the known fiels related to alerts [#3924](https://github.com/wazuh/wazuh-kibana-app/pull/3924) ### Changed diff --git a/public/utils/known-fields.js b/public/utils/known-fields.js index 51454ade87..fd1c63815d 100644 --- a/public/utils/known-fields.js +++ b/public/utils/known-fields.js @@ -956,6 +956,116 @@ export const KnownFields = [ "aggregatable": true, "readFromDocValues": true }, + { + "name": "data.cis.benchmark", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.error", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.fail", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.group", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.notchecked", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.pass", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.result", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.rule_title", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.score", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.timestamp", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.cis.unknown", + "type": "number", + "esTypes": [ + "long" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, { "name": "data.command", "type": "string", @@ -976,6 +1086,46 @@ export const KnownFields = [ "aggregatable": true, "readFromDocValues": true }, + { + "name": "data.docker.Action", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.docker.Actor.Attributes.image", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.docker.Actor.Attributes.name", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.docker.Type", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, { "name": "data.dstip", "type": "string", @@ -1026,6 +1176,156 @@ export const KnownFields = [ "aggregatable": true, "readFromDocValues": true }, + { + "name": "data.gcp.jsonPayload.authAnswer", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.jsonPayload.queryName", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.jsonPayload.responseCode", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.jsonPayload.vmInstanceId", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.jsonPayload.vmInstanceName", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.resource.labels.location", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.resource.labels.project_id", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.resource.labels.source_type", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.resource.type", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.gcp.severity", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.github.action", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.github.actor", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.github.actor_location.country_code", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.github.org", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.github.repo", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, { "name": "data.hardware.cpu_cores", "type": "number", @@ -1376,6 +1676,66 @@ export const KnownFields = [ "aggregatable": true, "readFromDocValues": true }, + { + "name": "data.office365.Actor.ID", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.office365.ClientIP", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.office365.Operation", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.office365.ResultStatus", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.office365.Subscription", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.office365.UserId", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, { "name": "data.os.architecture", "type": "string", @@ -1656,6 +2016,46 @@ export const KnownFields = [ "aggregatable": true, "readFromDocValues": true }, + { + "name": "data.osquery.action", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.osquery.calendarTime", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.osquery.name", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "name": "data.osquery.pack", + "type": "string", + "esTypes": [ + "keyword" + ], + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, { "name": "data.port.inode", "type": "number", From 5d2e41815588950c3116ba3496fdb565ecbfa1e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 31 Mar 2022 09:47:35 +0200 Subject: [PATCH 431/493] changelog: Replaced entry of changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d278a8ed4..24d146b3f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,6 @@ All notable changes to the Wazuh app project will be documented in this file. [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) -- Added fields status and type in vulnerabilities table [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) [#3726](https://github.com/wazuh/wazuh-kibana-app/pull/3726) - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) @@ -78,6 +77,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored as the plugin platform name and references is managed [#3811](https://github.com/wazuh/wazuh-kibana-app/pull/3811) - Removed `Dashboard` tab for the `Vulnerabilities` modules [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) - Display all fields in the `Table` tab when expading an alert row in the alerts tables of flyouts and the `Modules/Security Events/Dashboard` table [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) +- Refactored the table in `Vulnerabilities/Inventory` [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) ### Fixed From caeaa2c5426de8fd7b26b8835da19e73927aa788 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Mon, 4 Apr 2022 12:51:14 +0200 Subject: [PATCH 432/493] Fixed async problem when checking for unknown fields (#3937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed async problem when checking for unknown fields * Added changelog * remove async from checkUnknownFields * changed component state flags to instance flags * fix: Replace state variables by instance variables to control the flag to release the message about the index pattern fields were refreshed only one time. Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + public/components/common/modules/events.tsx | 36 ++++++++++++--------- public/components/visualize/wz-visualize.js | 14 ++++---- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d146b3f3..e9d9813600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) - Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) +- Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/components/common/modules/events.tsx b/public/components/common/modules/events.tsx index ea38b4efbc..b012f381fb 100644 --- a/public/components/common/modules/events.tsx +++ b/public/components/common/modules/events.tsx @@ -39,20 +39,20 @@ export const Events = compose( class Events extends Component { intervalCheckExistsDiscoverTableTime: number = 200; isMount: boolean; + hasRefreshedKnownFields: boolean; + isRefreshing: boolean; state: { flyout: false | { component: any; props: any }; discoverRowsData: any[]; - hasRefreshedKnownFields: boolean; - isRefreshing: boolean; }; constructor(props) { super(props); this.isMount = true; + this.hasRefreshedKnownFields = false; + this.isRefreshing = false; this.state = { flyout: false, discoverRowsData: [], - hasRefreshedKnownFields: false, - isRefreshing: false, }; } @@ -172,9 +172,12 @@ export const Events = compose( 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((rowDetailField) => { - this.checkUnknownFields(rowDetailField); + 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 && @@ -190,31 +193,34 @@ export const Events = compose( options ); }); + if (hasUnknownFields) { + this.refreshKnownFields(); + } } } - + checkUnknownFields(rowDetailField) { const fieldCell = rowDetailField.parentNode.childNodes && rowDetailField.parentNode.childNodes[2]; - if (fieldCell.querySelector('svg[data-test-subj="noMappingWarning"]')) { - this.refreshKnownFields(); - } + return (fieldCell.querySelector('svg[data-test-subj="noMappingWarning"]')) } refreshKnownFields = async () => { - if (!this.state.hasRefreshedKnownFields) { + if (!this.hasRefreshedKnownFields) { try { - this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); + this.hasRefreshedKnownFields = true; + this.isRefreshing = true; + if (satisfyPluginPlatformVersion('<7.11')) { await PatternHandler.refreshIndexPattern(); } - this.setState({ isRefreshing: false }); + this.isRefreshing = false; this.reloadToast(); } catch (error) { - this.setState({ isRefreshing: false }); + this.isRefreshing = false; throw error; } - } else if (this.state.isRefreshing) { + } else if (this.isRefreshing) { await new Promise((r) => setTimeout(r, 150)); await this.refreshKnownFields(); } diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 2b578f2a98..f37604cbfc 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -56,10 +56,11 @@ export const WzVisualize = compose( this.state = { visualizations: !!props.isAgent ? agentVisualizations : visualizations, expandedVis: false, - hasRefreshedKnownFields: false, refreshingKnownFields: [], refreshingIndex: true, }; + this.hasRefreshedKnownFields = false; + this.isRefreshing = false; this.metricValues = false; this.rawVisualizations = new RawVisualizations(); this.wzReq = WzRequest; @@ -126,18 +127,19 @@ export const WzVisualize = compose( if (newField && newField.name) { this.newFields[newField.name] = newField; } - if (!this.state.hasRefreshedKnownFields) { + if (!this.hasRefreshedKnownFields) { // Known fields are refreshed only once per dashboard loading try { - this.setState({ hasRefreshedKnownFields: true, isRefreshing: true }); + this.hasRefreshedKnownFields = true; + this.isRefreshing = true; if(satisfyPluginPlatformVersion('<7.11')){ await PatternHandler.refreshIndexPattern(this.newFields); }; - this.setState({ isRefreshing: false }); + this.isRefreshing = false; this.reloadToast(); this.newFields = {}; } catch (error) { - this.setState({ isRefreshing: false }); + this.isRefreshing = false; const options = { context: `${WzVisualize.name}.refreshKnownFields`, level: UI_LOGGER_LEVELS.ERROR, @@ -150,7 +152,7 @@ export const WzVisualize = compose( }; getErrorOrchestrator().handleError(options); } - } else if (this.state.isRefreshing) { + } else if (this.isRefreshing) { await new Promise((r) => setTimeout(r, 150)); await this.refreshKnownFields(); } From 700b91aae26b7cb3e5159f43b5307ef4518c994e Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Mon, 4 Apr 2022 14:04:27 +0200 Subject: [PATCH 433/493] Map visualization render problem (#3942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactored kibana-vis render * changelog: Add PR to the changelog Co-authored-by: Antonio David Gutiérrez Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 1 + public/kibana-integrations/kibana-vis.js | 84 ++++++++++-------------- 2 files changed, 36 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d9813600..34e5d48ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) - Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) - Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) +- Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/kibana-integrations/kibana-vis.js b/public/kibana-integrations/kibana-vis.js index ea6460099e..fe59b26199 100644 --- a/public/kibana-integrations/kibana-vis.js +++ b/public/kibana-integrations/kibana-vis.js @@ -389,59 +389,45 @@ class KibanaVis extends Component { const isLoading = this.props.resultState === 'loading'; return ( this.visID && ( - -
    - - - - - - Refreshing Index Pattern. - - - -
    -
    - -
    -
    - No results found   - - No alerts were found with the field: {this.deadField} - - } - > - - -
    +
    - +
    + {(isLoading &&
    ) + || (this.deadField && !isLoading && !this.state.visRefreshingIndex && ( +
    + No results found   + + No alerts were found with the field: {this.deadField} + + } + > + + +
    + )) + || (this.state.visRefreshingIndex && ( + + + + + Refreshing Index Pattern. + + )) + } +
    +
    ) ); } From 2e457ce0a66b2094e763782a0cc6e8d67b26d576 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Tue, 5 Apr 2022 16:28:51 +0200 Subject: [PATCH 434/493] Added troubleshooting link to toast (#3946) * Added troubleshooting link to toast * Added changelog * Optimized app version source --- CHANGELOG.md | 1 + public/components/visualize/wz-visualize.js | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e5d48ad4..58d6c23c1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added 3 new panels to `Vulnerabilities/Inventory` [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) - Added new fields of `Vulnerabilities` to the details flyout [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) - Added missing fields used in visualizations to the known fiels related to alerts [#3924](https://github.com/wazuh/wazuh-kibana-app/pull/3924) +- Added troubleshooting link to "index pattern was refreshed" toast [#3946](https://github.com/wazuh/wazuh-kibana-app/pull/3946) ### Changed diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index f37604cbfc..87b52f3759 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ import React, { Component, Fragment } from 'react'; - +import { version as appVersion} from '../../../package.json'; import { visualizations } from './visualizations'; import { agentVisualizations } from './agent-visualizations'; import KibanaVis from '../../kibana-integrations/kibana-vis'; @@ -23,7 +23,6 @@ import { EuiButtonIcon, EuiDescriptionList, EuiCallOut, - EuiLink, } from '@elastic/eui'; import WzReduxProvider from '../../redux/wz-redux-provider'; import { WazuhConfig } from '../../react-services/wazuh-config'; @@ -159,6 +158,8 @@ export const WzVisualize = compose( }; reloadToast = () => { const toastLifeTimeMs = 300000; + const [mayor, minor] = appVersion.split('.'); + const urlTroubleShootingDocs = `https://documentation.wazuh.com/${mayor}.${minor}/user-manual/kibana-app/troubleshooting.html#no-template-found-for-the-selected-index-pattern`; if(satisfyPluginPlatformVersion('<7.11')){ getToasts().add({ color: 'success', @@ -167,6 +168,13 @@ export const WzVisualize = compose( There were some unknown fields for the current index pattern. You need to refresh the page to apply the changes. + + Troubleshooting + window.location.reload()} size="s">Reload page @@ -182,6 +190,12 @@ export const WzVisualize = compose( There are some unknown fields for the current index pattern. You need to refresh the page to update the fields. + + Troubleshooting + window.location.reload()} size="s">Reload page From a47888b062c19b7c70928c6dd41739a4ce2985d0 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 6 Apr 2022 08:41:02 +0200 Subject: [PATCH 435/493] [BUG][Tools/Api Console] Parse error when using character on the line start (#3877) * fix: Fixed `Parse error` due when using the `#` character when is not at the beginning of line. * changelog: Add PR to changelog --- CHANGELOG.md | 5 +++++ public/controllers/dev-tools/dev-tools.ts | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d6c23c1a..d153c9d8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the Wazuh app project will be documented in this file. +## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4302 + +### Fixed +- Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) + ## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4301 ### Added diff --git a/public/controllers/dev-tools/dev-tools.ts b/public/controllers/dev-tools/dev-tools.ts index be9042444d..4ac7f3fe21 100644 --- a/public/controllers/dev-tools/dev-tools.ts +++ b/public/controllers/dev-tools/dev-tools.ts @@ -158,10 +158,9 @@ export class DevToolsController { try { const currentState = this.apiInputBox.getValue().toString(); AppState.setCurrentDevTools(currentState); - const tmpgroups = []; const splitted = currentState - .split(/[\r\n]+(?=(?:GET|PUT|POST|DELETE|#)\b)/gm) + .split(/[\r\n]+(?=(?:GET|PUT|POST|DELETE)\b)/gm) .filter(item => item.replace(/\s/g, '').length); let start = 0; @@ -170,7 +169,7 @@ export class DevToolsController { const slen = splitted.length; for (let i = 0; i < slen; i++) { let tmp = splitted[i].split('\n'); - if (Array.isArray(tmp)) tmp = tmp.filter(item => !item.includes('#')); + if (Array.isArray(tmp)) tmp = tmp.filter(item => !item.startsWith('#')); const cursor = this.apiInputBox.getSearchCursor(splitted[i], null, { multiline: true }); @@ -205,7 +204,7 @@ export class DevToolsController { const tmplen = tmp.length; for (let j = 1; j < tmplen; ++j) { - if (!!tmp[j] && !tmp[j].includes('#')) { + if (!!tmp[j] && !tmp[j].startsWith('#')) { tmpRequestTextJson += tmp[j]; } } From a62f88bad135158cd88a65f3b35e2c2536e53887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 6 Apr 2022 08:43:37 +0200 Subject: [PATCH 436/493] fix(changelog): Moved changelog entry --- CHANGELOG.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d153c9d8a5..c79110f267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,6 @@ All notable changes to the Wazuh app project will be documented in this file. -## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4302 - -### Fixed -- Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) - ## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4301 ### Added @@ -147,6 +142,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) - Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) - Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) +- Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From 28f64a1f00ef307a07f08c39aa913c09a895aea7 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 6 Apr 2022 09:12:20 +0200 Subject: [PATCH 437/493] Change google group icon (#3949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed Google Groups icon * Added changelog * fix(asset): Fixed the path to Google groups icon in the `Settings/About` section when Kibana is served in a subpath Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + public/assets/images/icons/google_groups.svg | 29 +++++++++++++++++++ .../images/icons/icon_google_groups.svg | 16 ---------- public/controllers/settings/settings.js | 3 ++ public/templates/settings/settings-about.html | 2 +- public/templates/settings/settings.html | 2 +- public/utils/add_help_menu_to_app.tsx | 2 +- 7 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 public/assets/images/icons/google_groups.svg delete mode 100644 public/assets/images/icons/icon_google_groups.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index c79110f267..53579c2413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed `Dashboard` tab for the `Vulnerabilities` modules [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) - Display all fields in the `Table` tab when expading an alert row in the alerts tables of flyouts and the `Modules/Security Events/Dashboard` table [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) - Refactored the table in `Vulnerabilities/Inventory` [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) +- Changed Google Groups app icons [#3949](https://github.com/wazuh/wazuh-kibana-app/pull/3949) ### Fixed diff --git a/public/assets/images/icons/google_groups.svg b/public/assets/images/icons/google_groups.svg new file mode 100644 index 0000000000..934cf1d86d --- /dev/null +++ b/public/assets/images/icons/google_groups.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/public/assets/images/icons/icon_google_groups.svg b/public/assets/images/icons/icon_google_groups.svg deleted file mode 100644 index 292fa411a0..0000000000 --- a/public/assets/images/icons/icon_google_groups.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/public/controllers/settings/settings.js b/public/controllers/settings/settings.js index 31f7be6ce9..403cca6e67 100644 --- a/public/controllers/settings/settings.js +++ b/public/controllers/settings/settings.js @@ -25,6 +25,8 @@ import { updateSelectedSettingsSection } from '../../redux/actions/appStateActio import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; +import { getAssetURL } from '../../utils/assets'; +import { getHttp } from '../../kibana-services'; export class SettingsController { /** @@ -58,6 +60,7 @@ export class SettingsController { this.tabNames = TabNames; this.indexPatterns = []; this.apiEntries = []; + this.$scope.googleGroupsSVG = getHttp().basePath.prepend(getAssetURL('images/icons/google_groups.svg')); } /** diff --git a/public/templates/settings/settings-about.html b/public/templates/settings/settings-about.html index 7a0434501d..83d03a5d60 100644 --- a/public/templates/settings/settings-about.html +++ b/public/templates/settings/settings-about.html @@ -58,7 +58,7 @@

    Community

    + props="{iconType:'googleGroupsSVG', iconSize:'xxl', href: 'https://groups.google.com/forum/#!forum/wazuh', target: '_blank', 'aria-label': 'Wazuh forum'}" > diff --git a/public/templates/settings/settings.html b/public/templates/settings/settings.html index 2b39a0087c..10e56dcf72 100644 --- a/public/templates/settings/settings.html +++ b/public/templates/settings/settings.html @@ -132,7 +132,7 @@

    Community

    Google Group
    + content: Google Group } ] }); From 602302d1b634b096491ea2462b73e43752e2b988 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 6 Apr 2022 09:44:46 +0200 Subject: [PATCH 438/493] GitHub configuration panel text overflow (#3945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added word-break and changed flexgroup column * changelog: Add PRs to the changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 ++ .../modules/panel/components/module_configuration.scss | 10 +++++++--- .../modules/panel/components/module_configuration.tsx | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53579c2413..3ece61e6cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana 7.16.3 - Support for Kibana 7.17.0 +- Added GitHub and Office365 modules [#3557](https://github.com/wazuh/wazuh-kibana-app/pull/3557) +- Added a new `Panel` module tab for GitHub and Office365 modules [#3541](https://github.com/wazuh/wazuh-kibana-app/pull/3541) [#3945](https://github.com/wazuh/wazuh-kibana-app/pull/3945) - Added ability to filter the results fo the `Network Ports` table in the `Inventory data` section [#3639](https://github.com/wazuh/wazuh-kibana-app/pull/3639) - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Improved the frontend handle errors strategy: UI, Toasts, console log and log in file diff --git a/public/components/common/modules/panel/components/module_configuration.scss b/public/components/common/modules/panel/components/module_configuration.scss index cf26973a44..7415f569fb 100644 --- a/public/components/common/modules/panel/components/module_configuration.scss +++ b/public/components/common/modules/panel/components/module_configuration.scss @@ -17,7 +17,11 @@ h5.euiTitle.module-panel-configuration-subtitle{ justify-content: center; } -.module-panel-configuration-list dt.euiDescriptionList__title, .module-panel-configuration-list dd.euiDescriptionList__description { - border-left: 2px solid rgba(0, 0, 0, 0.3); - padding-left: 10px; +.module-panel-configuration-list{ + dt.euiDescriptionList__title, + dd.euiDescriptionList__description { + border-left: 2px solid rgba(0, 0, 0, 0.3); + padding-left: 10px; + word-break: break-word; + } } diff --git a/public/components/common/modules/panel/components/module_configuration.tsx b/public/components/common/modules/panel/components/module_configuration.tsx index 6d424c669d..65bde9575c 100644 --- a/public/components/common/modules/panel/components/module_configuration.tsx +++ b/public/components/common/modules/panel/components/module_configuration.tsx @@ -109,7 +109,7 @@ export const PanelModuleConfiguration : FunctionalComponent<{h: string}> = conne - + From 2af995ca4b2f1cc0d9dc83959e6ed9bc5f28963c Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Wed, 6 Apr 2022 10:00:15 +0200 Subject: [PATCH 439/493] Export a valid json (#3909) * fix(dev-tools.ts): replace deleted * add changelog --- CHANGELOG.md | 1 + public/controllers/dev-tools/dev-tools.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ece61e6cb..be5225566c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) - Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) +- Fixed backslash breaking exported JSON result [#3909](https://github.com/wazuh/wazuh-kibana-app/pull/3909) - Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) - Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) - Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) diff --git a/public/controllers/dev-tools/dev-tools.ts b/public/controllers/dev-tools/dev-tools.ts index 4ac7f3fe21..f00e229007 100644 --- a/public/controllers/dev-tools/dev-tools.ts +++ b/public/controllers/dev-tools/dev-tools.ts @@ -810,10 +810,7 @@ export class DevToolsController { } else { this.apiOutputBox.setValue( - JSON.stringify((output || {}).data || {}, null, 2).replace( - /\\\\/g, - '\\' - ) + JSON.stringify((output || {}).data || {}, null, 2) ); } } From 937cffc286147e78ce1aca4c2b2359bd565ee57b Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 6 Apr 2022 11:36:15 +0200 Subject: [PATCH 440/493] Disable `Agents` and `Configuration checksum` sorting (#3857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Disabled configuration checksum sorting * Added changelog * fix: Removed sorting for `Agents` and `Configuration checksum` column in the table of `Management/Groups` due to this is not supported by the API Co-authored-by: Antonio David Gutiérrez Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 1 + .../components/management/groups/utils/columns-main.js | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be5225566c..4298a9eac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Display all fields in the `Table` tab when expading an alert row in the alerts tables of flyouts and the `Modules/Security Events/Dashboard` table [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) - Refactored the table in `Vulnerabilities/Inventory` [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Changed Google Groups app icons [#3949](https://github.com/wazuh/wazuh-kibana-app/pull/3949) +- Removed sorting for `Agents` or `Configuration checksum` column in the table of `Management/Groups` due to this is not supported by the API [#3857](https://github.com/wazuh/wazuh-kibana-app/pull/3857) ### Fixed diff --git a/public/controllers/management/components/management/groups/utils/columns-main.js b/public/controllers/management/components/management/groups/utils/columns-main.js index 54bcb2b375..bd30750e5d 100644 --- a/public/controllers/management/components/management/groups/utils/columns-main.js +++ b/public/controllers/management/components/management/groups/utils/columns-main.js @@ -19,14 +19,12 @@ export default class GroupsColums { { field: 'count', name: 'Agents', - align: 'left', - sortable: true + align: 'left' }, { field: 'configSum', name: 'Configuration checksum', - align: 'left', - sortable: true + align: 'left' } ]; this.columns.push({ From 08a111103e6faf998f1c888b2a57c7b00e0c4884 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 6 Apr 2022 11:39:46 +0200 Subject: [PATCH 441/493] [FIX][Modules][Events] Fixed cell enhancement when the value is a sub technique (#3944) * fix: Fixed enhancement of button for the `rule.mitre.id` when the value is a subtechnique * changelog: Add PR to the changelog * fix: Updated regex to enhance the `rule.mitre.id` cell in the Events tab of modules --- CHANGELOG.md | 1 + .../components/common/modules/events-enhance-discover-fields.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4298a9eac5..7664400153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) - Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) - Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) +- Fixed the `rule.mitre.id` cell enhancement that doesn't support values with sub techniques [#3944](https://github.com/wazuh/wazuh-kibana-app/pull/3944) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/components/common/modules/events-enhance-discover-fields.ts b/public/components/common/modules/events-enhance-discover-fields.ts index b8f4fb0588..798a6af3a6 100644 --- a/public/components/common/modules/events-enhance-discover-fields.ts +++ b/public/components/common/modules/events-enhance-discover-fields.ts @@ -104,7 +104,7 @@ export const EventsEnhanceDiscoverCell = { }, currentTechnique: content }), { - contentRegex: /(\w+)/g, + contentRegex: /(T\d+\.?(\d+)?)/g, element: 'span' }), 'syscheck.value_name': (content, rowData, element, options) => { From 1d03c23eda9cb690a39352e9eb36e15cc4edc064 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 6 Apr 2022 11:46:05 +0200 Subject: [PATCH 442/493] [BUG][Reports] Generate reports not manage the Wazuh API token expiration (#3881) * fix: Fixed the generating PDF reports doesn't manage the refreshing of token when it expires - Replaced the method to do the request from `GenericRequest.request` to `WzRequest.genericReq` that handles the refreshing of token - Adapted request payload and endpoint expected parameters for ApiID and the index pattern title - Removed not used params of some reports endpoints - Remove message of generating pdf report when the process fails * changelog: Add PR to changelog * changelog: Moved entry --- CHANGELOG.md | 1 + public/react-services/reporting.js | 15 ++++++++++----- server/controllers/wazuh-reporting.ts | 18 +++++++----------- server/routes/wazuh-reporting.ts | 10 ++++++++-- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7664400153..9dffb4f207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agents details card style [#3845](https://github.com/wazuh/wazuh-kibana-app/pull/3845) [#3860](https://github.com/wazuh/wazuh-kibana-app/pull/3860) - Fixed routing redirection in events documents discover links [#3866](https://github.com/wazuh/wazuh-kibana-app/pull/3866) - Fixed health-check [#3868](https://github.com/wazuh/wazuh-kibana-app/pull/3868) +- Fixed an error when generating PDF reports due to Wazuh API token expiration [#3881](https://github.com/wazuh/wazuh-kibana-app/pull/3881) - Fixed the table of Vulnerabilities/Inventory doesn't reload when changing the selected agent [#3901](https://github.com/wazuh/wazuh-kibana-app/pull/3901) - Fixed backslash breaking exported JSON result [#3909](https://github.com/wazuh/wazuh-kibana-app/pull/3909) - Fixed the Events view multiple "The index pattern was refreshed successfully" toast [#3937](https://github.com/wazuh/wazuh-kibana-app/pull/3937) diff --git a/public/react-services/reporting.js b/public/react-services/reporting.js index 44cce0a17a..8a053b9b24 100644 --- a/public/react-services/reporting.js +++ b/public/react-services/reporting.js @@ -13,12 +13,12 @@ import $ from 'jquery'; import moment from 'moment'; import { WazuhConfig } from '../react-services/wazuh-config'; -import { GenericRequest } from '../react-services/generic-request'; +import { AppState } from './app-state'; +import { WzRequest } from './wz-request'; import { Vis2PNG } from '../factories/vis2png'; import { RawVisualizations } from '../factories/raw-visualizations'; import { VisHandlers } from '../factories/vis-handlers'; -import { getToasts } from '../kibana-services'; -import { getAngularModule } from '../kibana-services'; +import { getAngularModule, getDataPlugin, getToasts } from '../kibana-services'; import { UI_LOGGER_LEVELS } from '../../common/constants'; import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; import { getErrorOrchestrator } from './common-services'; @@ -109,11 +109,13 @@ export class ReportingService { section: agents ? 'agents' : 'overview', agents, browserTimezone, + indexPatternTitle: (await getDataPlugin().indexPatterns.get(AppState.getCurrentPattern())).title, + apiId: JSON.parse(AppState.getCurrentAPI()).id }; const apiEndpoint = tab === 'syscollector' ? `/reports/agents/${agents}/inventory` : `/reports/modules/${tab}`; - await GenericRequest.request('POST', apiEndpoint, data); + await WzRequest.genericReq('POST', apiEndpoint, data); this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; @@ -128,6 +130,7 @@ export class ReportingService { } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; + this.$rootScope.$applyAsync(); const options = { context: `${ReportingService.name}.startVis2Png`, level: UI_LOGGER_LEVELS.ERROR, @@ -160,10 +163,11 @@ export class ReportingService { tab: type, browserTimezone, components, + apiId: JSON.parse(AppState.getCurrentAPI()).id }; const apiEndpoint = type === 'agentConfig' ? `/reports/agents/${obj.id}` : `/reports/groups/${obj.name}`; - await GenericRequest.request('POST', apiEndpoint, data); + await WzRequest.genericReq('POST', apiEndpoint, data); this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; @@ -178,6 +182,7 @@ export class ReportingService { } catch (error) { this.$rootScope.reportBusy = false; this.$rootScope.reportStatus = false; + this.$rootScope.$applyAsync(); const options = { context: `${ReportingService.name}.startConfigReport`, level: UI_LOGGER_LEVELS.ERROR, diff --git a/server/controllers/wazuh-reporting.ts b/server/controllers/wazuh-reporting.ts index b18a0e67e9..48e7952f0b 100644 --- a/server/controllers/wazuh-reporting.ts +++ b/server/controllers/wazuh-reporting.ts @@ -1144,9 +1144,10 @@ export class WazuhReportingCtrl { tables, name, section, + indexPatternTitle, + apiId } = request.body; const { moduleID } = request.params; - const { id: apiId, pattern: indexPattern } = request.headers; const { from, to } = time || {}; // Init const printer = new ReportPrinter(); @@ -1176,7 +1177,7 @@ export class WazuhReportingCtrl { new Date(from).getTime(), new Date(to).getTime(), sanitizedFilters, - indexPattern, + indexPatternTitle, agents ); } @@ -1219,10 +1220,8 @@ export class WazuhReportingCtrl { ) { try { log('reporting:createReportsGroups', `Report started`, 'info'); - const { browserTimezone, searchBar, filters, time, name, components } = request.body; + const { name, components, apiId } = request.body; const { groupID } = request.params; - const { id: apiId, pattern: indexPattern } = request.headers; - const { from, to } = time || {}; // Init const printer = new ReportPrinter(); @@ -1493,10 +1492,8 @@ export class WazuhReportingCtrl { ) { try { log('reporting:createReportsAgents', `Report started`, 'info'); - const { browserTimezone, searchBar, filters, time, name, components } = request.body; + const { name, components, apiId } = request.body; const { agentID } = request.params; - const { id: apiId } = request.headers; - const { from, to } = time || {}; const printer = new ReportPrinter(); @@ -1744,9 +1741,8 @@ export class WazuhReportingCtrl { ) { try { log('reporting:createReportsAgentsInventory', `Report started`, 'info'); - const { browserTimezone, searchBar, filters, time, name } = request.body; + const { searchBar, filters, time, name, indexPatternTitle, apiId } = request.body; const { agentID } = request.params; - const { id: apiId, pattern: indexPattern } = request.headers; const { from, to } = time || {}; // Init const printer = new ReportPrinter(); @@ -1940,7 +1936,7 @@ export class WazuhReportingCtrl { from, to, sanitizedFilters + ' AND rule.groups: "vulnerability-detector"', - indexPattern, + indexPatternTitle, agentID ); } diff --git a/server/routes/wazuh-reporting.ts b/server/routes/wazuh-reporting.ts index 21ac359814..d6addf9d1a 100644 --- a/server/routes/wazuh-reporting.ts +++ b/server/routes/wazuh-reporting.ts @@ -34,7 +34,9 @@ export function WazuhReportingRoutes(router: IRouter) { from: schema.string(), to: schema.string() }), schema.string()]), - title: schema.maybe(schema.string()) + title: schema.maybe(schema.string()), + indexPatternTitle: schema.string(), + apiId: schema.string() }), params: schema.object({ moduleID: schema.string() @@ -54,6 +56,7 @@ export function WazuhReportingRoutes(router: IRouter) { name: schema.string(), section: schema.maybe(schema.string()), tab: schema.string(), + apiId: schema.string() }), params: schema.object({ groupID: schema.string() @@ -73,6 +76,7 @@ export function WazuhReportingRoutes(router: IRouter) { name: schema.string(), section: schema.maybe(schema.string()), tab: schema.string(), + apiId: schema.string() }), params: schema.object({ agentID: schema.string() @@ -100,7 +104,9 @@ export function WazuhReportingRoutes(router: IRouter) { from: schema.string(), to: schema.string() }), schema.string()]), - title: schema.maybe(schema.string()) + title: schema.maybe(schema.string()), + indexPatternTitle: schema.string(), + apiId: schema.string() }), params: schema.object({ agentID: schema.string() From f2e7c23fd9b1ac307b819b5a1eaa2b556dc10aaa Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 6 Apr 2022 12:14:07 +0200 Subject: [PATCH 443/493] Fixed index pattern troubleshooting url --- public/components/visualize/wz-visualize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/components/visualize/wz-visualize.js b/public/components/visualize/wz-visualize.js index 87b52f3759..651fb58646 100644 --- a/public/components/visualize/wz-visualize.js +++ b/public/components/visualize/wz-visualize.js @@ -159,7 +159,7 @@ export const WzVisualize = compose( reloadToast = () => { const toastLifeTimeMs = 300000; const [mayor, minor] = appVersion.split('.'); - const urlTroubleShootingDocs = `https://documentation.wazuh.com/${mayor}.${minor}/user-manual/kibana-app/troubleshooting.html#no-template-found-for-the-selected-index-pattern`; + const urlTroubleShootingDocs = `https://documentation.wazuh.com/${mayor}.${minor}/user-manual/elasticsearch/elastic-tuning/troubleshooting.html#index-pattern-was-refreshed-toast-keeps-popping-up`; if(satisfyPluginPlatformVersion('<7.11')){ getToasts().add({ color: 'success', From f81461f8b67bc3d45d49633f47e7630907ba4964 Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:41:47 +0200 Subject: [PATCH 444/493] Details flyout date picker (#3947) * fix(discover.tsx): change in buildFilter() * add changelog Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 1 + public/components/common/modules/discover/discover.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dffb4f207..fdb07f3675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,6 +150,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) - Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) - Fixed the `rule.mitre.id` cell enhancement that doesn't support values with sub techniques [#3944](https://github.com/wazuh/wazuh-kibana-app/pull/3944) +- Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index 1d0c0e7a4e..e66b8c7d58 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -390,8 +390,8 @@ export const Discover = compose( const range = { range: { timestamp: { - gte: dateParse(this.timefilter.getTime().from), - lte: dateParse(this.timefilter.getTime().to), + gte: this.state.dateRange.from, + lte: this.state.dateRange.to, format: 'epoch_millis', }, }, From fb8c1654fbdbbbfaf667128f680139bcb72280e1 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 6 Apr 2022 15:18:04 +0200 Subject: [PATCH 445/493] Changed office365 config description (#3952) * Changed office365 config description * Added changelog --- CHANGELOG.md | 5 ++++- .../components/overview/office-panel/views/office-stats.tsx | 2 +- .../office365/components/general-tab/general-tab.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdb07f3675..d6d5320bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Kibana 7.16.3 - Support for Kibana 7.17.0 - Added GitHub and Office365 modules [#3557](https://github.com/wazuh/wazuh-kibana-app/pull/3557) -- Added a new `Panel` module tab for GitHub and Office365 modules [#3541](https://github.com/wazuh/wazuh-kibana-app/pull/3541) [#3945](https://github.com/wazuh/wazuh-kibana-app/pull/3945) +- Added a new `Panel` module tab for GitHub and Office365 modules + [#3541](https://github.com/wazuh/wazuh-kibana-app/pull/3541) + [#3945](https://github.com/wazuh/wazuh-kibana-app/pull/3945) + [#3952](https://github.com/wazuh/wazuh-kibana-app/pull/3952) - Added ability to filter the results fo the `Network Ports` table in the `Inventory data` section [#3639](https://github.com/wazuh/wazuh-kibana-app/pull/3639) - Added new endpoint service to collect the frontend logs into a file [#3324](https://github.com/wazuh/wazuh-kibana-app/pull/3324) - Improved the frontend handle errors strategy: UI, Toasts, console log and log in file diff --git a/public/components/overview/office-panel/views/office-stats.tsx b/public/components/overview/office-panel/views/office-stats.tsx index a43aca8664..07504763e1 100644 --- a/public/components/overview/office-panel/views/office-stats.tsx +++ b/public/components/overview/office-panel/views/office-stats.tsx @@ -19,7 +19,7 @@ import { renderValueYesThenEnabled } from '../../../../controllers/management/co const settings = [ { field: 'enabled', label: 'Service status', render: renderValueYesThenEnabled }, - { field: 'only_future_events', label: 'Collect events generated since Wazuh agent was started'}, + { field: 'only_future_events', label: 'Collect events generated since Wazuh manager was started'}, { field: 'curl_max_size', label: 'Maximum size allowed for the Office 365 API response'}, { field: 'interval', label: 'Interval between Office 365 wodle executions in seconds'}, { field: 'api_auth', label: 'Credentials', render: (value) => value.map(v => diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx index 56d41dda3f..99ed93a66c 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/general-tab.tsx @@ -25,7 +25,7 @@ const mainSettings = [ { field: 'enabled', label: 'Service status', render: renderValueYesThenEnabled }, { field: 'only_future_events', - label: 'Collect events generated since Wazuh is initialized', + label: 'Collect events generated since Wazuh manager is initialized', }, { field: 'interval', From 06350f873a3e26cce864112b0294b91e7c1bb21c Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Fri, 8 Apr 2022 09:39:13 +0200 Subject: [PATCH 446/493] Change logout path (#3957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(app.js): change logout path * snapshot updated * fix: Added a missing semicolon * changelog: Add PR to changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + public/app.js | 4 ++-- .../__snapshots__/general-tab.test.tsx.snap | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d5320bdd..96579b3116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) - Fixed the `rule.mitre.id` cell enhancement that doesn't support values with sub techniques [#3944](https://github.com/wazuh/wazuh-kibana-app/pull/3944) - Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) +- Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/app.js b/public/app.js index 8c4ed56e77..cfc37c06dc 100644 --- a/public/app.js +++ b/public/app.js @@ -55,7 +55,7 @@ import store from './redux/store'; import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { WzAuthentication, loadAppConfig } from './react-services'; -import { getAngularModule } from './kibana-services'; +import { getAngularModule, getHttp } from './kibana-services'; import { addHelpMenuToAppChrome } from './utils'; const app = getAngularModule(); @@ -112,7 +112,7 @@ app.run(function ($rootElement) { addHelpMenuToAppChrome(); - const urlToLogout = window.location.origin + '/logout'; + const urlToLogout = getHttp().basePath.prepend('/logout'); // Bind deleteExistentToken on Log out component. $('.euiHeaderSectionItem__button, .euiHeaderSectionItemButton').on('mouseleave', function () { diff --git a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap index d201ad64fa..27cf708e99 100644 --- a/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap +++ b/public/controllers/management/components/management/configuration/office365/components/general-tab/__snapshots__/general-tab.test.tsx.snap @@ -432,7 +432,7 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = ` }, Object { "field": "only_future_events", - "label": "Collect events generated since Wazuh is initialized", + "label": "Collect events generated since Wazuh manager is initialized", }, Object { "field": "interval", @@ -586,9 +586,9 @@ exports[`GeneralTab component renders correctly to match the snapshot 1`] = `
    - Collect events generated since Wazuh is initialized + Collect events generated since Wazuh manager is initialized
    From 4126445d3536c34ac5294bce595a30cf561faa3c Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 13 Apr 2022 11:41:36 +0200 Subject: [PATCH 447/493] [Fix] Unhandled promise error when Wazuh API (#3991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed reducing cron-job promises to Promise.all * fix(statistics): Fixed the expected data used to create the document to index * Added changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + server/start/cron-scheduler/scheduler-job.ts | 26 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96579b3116..511c7063ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -155,6 +155,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed the `rule.mitre.id` cell enhancement that doesn't support values with sub techniques [#3944](https://github.com/wazuh/wazuh-kibana-app/pull/3944) - Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) - Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) +- Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/server/start/cron-scheduler/scheduler-job.ts b/server/start/cron-scheduler/scheduler-job.ts index 3941de0b27..f64561dd5d 100644 --- a/server/start/cron-scheduler/scheduler-job.ts +++ b/server/start/cron-scheduler/scheduler-job.ts @@ -27,23 +27,23 @@ export class SchedulerJob { public async run() { const { index, status } = configuredJobs({})[this.jobName]; - if ( !status ) { return; } + if (!status) { return; } try { const hosts = await this.getApiObjects(); - const data = await hosts.reduce(async (acc:Promise, host) => { - const {status} = configuredJobs({host, jobName: this.jobName})[this.jobName]; - if (!status) return acc; - const response = await this.getResponses(host); - const accResolve = await Promise.resolve(acc) - return [ - ...accResolve, - ...response, - ]; - }, Promise.resolve([])); - !!data.length && await this.saveDocument.save(data, index); + const jobPromises = hosts.map(async host => { + try { + const { status } = configuredJobs({ host, jobName: this.jobName })[this.jobName]; + if (!status) return; + return await this.getResponses(host); + } catch (error) { + ErrorHandler(error, this.logger); + } + }); + const data = (await Promise.all(jobPromises)).filter(promise => !!promise).flat(); + Array.isArray(data) && !!data.length && await this.saveDocument.save(data, index); } catch (error) { ErrorHandler(error, this.logger); - } + } } private async getApiObjects() { From 74e66c251a13baacaee25d11a333635104584a90 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Wed, 20 Apr 2022 14:56:39 +0200 Subject: [PATCH 448/493] added red hat option --- .../agent/components/register-agent.js | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index e5e9ca4633..5389e08a63 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -60,7 +60,7 @@ const architectureButtons = [ label: 'aarch64', }, ]; -const architectureCentos5 = [ +const architectureCentos5OrRedHat5 = [ { id: 'i386', label: 'i386', @@ -71,7 +71,7 @@ const architectureCentos5 = [ }, ]; -const versionButtonsCentos = [ +const versionButtonsCentosOrRedHat = [ { id: 'centos5', label: 'CentOS5', @@ -80,6 +80,14 @@ const versionButtonsCentos = [ id: 'centos6', label: 'CentOS6 or higher', }, + { + id: 'redhat5', + label: 'Red Hat 5', + }, + { + id: 'redhat6', + label: 'Red Hat 6 or higher', + }, ]; const osButtons = [ @@ -169,9 +177,9 @@ export const RegisterAgent = withErrorBoundary( serverAddress, needsPassword, hidePasswordInput, - versionButtonsCentos, + versionButtonsCentosOrRedHat, architectureButtons, - architectureCentos5, + architectureCentos5OrRedHat5, wazuhPassword, udpProtocol, wazuhVersion, @@ -324,6 +332,10 @@ export const RegisterAgent = withErrorBoundary( return `https://packages.wazuh.com/4.x/yum5/i386/wazuh-agent-${this.state.wazuhVersion}-1.el5.i386.rpm`; case 'centos5-x86_64': return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.el5.x86_64.rpm`; + case 'redhat5-i386': + return `https://packages.wazuh.com/4.x/yum5/i386/wazuh-agent-${this.state.wazuhVersion}-1.el5.i386.rpm`; + case 'redhat5-x86_64': + return `https://packages.wazuh.com/4.x/yum5/x86_64/wazuh-agent-${this.state.wazuhVersion}-1.el5.x86_64.rpm`; case 'centos6-i386': return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.i386.rpm`; case 'centos6-aarch64': @@ -626,7 +638,7 @@ export const RegisterAgent = withErrorBoundary( this.setVersion(version)} /> @@ -634,7 +646,7 @@ export const RegisterAgent = withErrorBoundary( }, ] : []), - ...(this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos5' + ...(this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos5' || this.state.selectedVersion == 'redhat5' ? [ { title: 'Choose the architecture', @@ -642,7 +654,7 @@ export const RegisterAgent = withErrorBoundary( this.setArchitecture(architecture)} /> @@ -651,7 +663,7 @@ export const RegisterAgent = withErrorBoundary( ] : []), ...(this.state.selectedOS == 'deb' || - (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6') + (this.state.selectedOS == 'rpm' && this.state.selectedVersion == 'centos6' || this.state.selectedVersion == 'redhat6') ? [ { title: 'Choose the architecture', From d28cb115501b1bab5e53537f97e19ff7ed32a743 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Wed, 20 Apr 2022 16:21:03 +0200 Subject: [PATCH 449/493] added case in resolveRPMPackage() --- public/controllers/agent/components/register-agent.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 5389e08a63..e6babb1079 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -344,6 +344,14 @@ export const RegisterAgent = withErrorBoundary( return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; case 'centos6-armhf': return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.armv7hl.rpm`; + case 'redhat6-i386': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.i386.rpm`; + case 'redhat6-aarch64': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.aarch64.rpm`; + case 'redhat6-x86_64': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; + case 'redhat6-armhf': + return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.armv7hl.rpm`; default: return `https://packages.wazuh.com/4.x/yum/wazuh-agent-${this.state.wazuhVersion}-1.x86_64.rpm`; } From 0e6ea633dade8f9da6895638a4ccb385f618e322 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Thu, 21 Apr 2022 16:13:48 +0200 Subject: [PATCH 450/493] cut circular redirections of routes resolvers --- .../health-check/services/check-api.service.ts | 11 +++++++++-- public/react-services/generic-request.js | 4 +++- public/services/resolves/api-count.js | 9 +++------ public/services/resolves/settings-wizard.js | 3 ++- public/services/routes.js | 4 ++-- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/public/components/health-check/services/check-api.service.ts b/public/components/health-check/services/check-api.service.ts index a998d025c4..25a633bef2 100644 --- a/public/components/health-check/services/check-api.service.ts +++ b/public/components/health-check/services/check-api.service.ts @@ -41,6 +41,7 @@ const trySetDefault = async (checkLogger: CheckLogger) => { return Promise.reject('No API available to connect'); } } + return Promise.reject('No API configuration found'); } catch (error) { checkLogger.error(`Error connecting to API: ${error}`); return Promise.reject(`Error connecting to API: ${error}`); @@ -50,7 +51,13 @@ const trySetDefault = async (checkLogger: CheckLogger) => { export const checkApiService = (appInfo: any) => async (checkLogger: CheckLogger) => { let apiChanged = false; try { - const currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); + let currentApi = JSON.parse(AppState.getCurrentAPI() || '{}'); + if(!currentApi.id){ + checkLogger.info(`No current API selected`); + currentApi.id = await trySetDefault(checkLogger); + apiChanged = true; + } + checkLogger.info(`Current API id [${currentApi.id}]`); checkLogger.info(`Checking current API id [${currentApi.id}]...`); const data = await ApiCheck.checkStored(currentApi.id).catch(async (err) => { @@ -68,7 +75,7 @@ export const checkApiService = (appInfo: any) => async (checkLogger: CheckLogger const api = ((data || {}).data || {}).data || {}; const name = (api.cluster_info || {}).manager || false; AppState.setCurrentAPI(JSON.stringify({ name: name, id: api.id })); - checkLogger.info(`Set current API in cookie: id [${api.id}], name [${api.name}]`); + checkLogger.info(`Set current API in cookie: id [${api.id}], name [${name}]`); getToasts().add({ color: 'warning', title: 'Selected Wazuh API has been updated', diff --git a/public/react-services/generic-request.js b/public/react-services/generic-request.js index 07cc31c771..5c3a2d98ac 100644 --- a/public/react-services/generic-request.js +++ b/public/react-services/generic-request.js @@ -100,7 +100,9 @@ export class GenericRequest { const wzMisc = new WzMisc(); wzMisc.setApiIsDown(true); - if (!window.location.hash.includes('#/settings')) { + if (!window.location.hash.includes('#/settings') && + !window.location.hash.includes('#/health-check') && + !window.location.hash.includes('#/blank-screen')) { window.location.href = getHttp().basePath.prepend('/app/wazuh#/health-check'); } } diff --git a/public/services/resolves/api-count.js b/public/services/resolves/api-count.js index 5570c7136f..a9166bc2bc 100644 --- a/public/services/resolves/api-count.js +++ b/public/services/resolves/api-count.js @@ -26,16 +26,13 @@ export function apiCount($q, $location) { GenericRequest.request('GET', '/hosts/apis') .then(async (data) => { - if (!data || !data.data || !data.data.length) throw new Error('No API entries found'); if (!AppState.getCurrentAPI()) { - await tryToSetDefault(data.data, AppState); + await tryToSetDefault(data?.data, AppState); } deferred.resolve(); }) - .catch(() => { - $location.search('_a', null); - $location.search('tab', 'api'); - $location.path('/settings'); + .catch((reject) => { + console.log(reject); deferred.resolve(); }); return deferred.promise; diff --git a/public/services/resolves/settings-wizard.js b/public/services/resolves/settings-wizard.js index 29981e1d30..48949bb406 100644 --- a/public/services/resolves/settings-wizard.js +++ b/public/services/resolves/settings-wizard.js @@ -106,7 +106,8 @@ export function settingsWizard( wzMisc.setWizard(true); if (redirect) { AppState.setCurrentAPI(redirect); - } else if (!$location.path().includes('/settings')) { + } else if (!$location.path().includes('/settings') && + !$location.path().includes('/blank-screen')) { $location.search('_a', null); $location.search('tab', 'api'); $location.path('/settings'); diff --git a/public/services/routes.js b/public/services/routes.js index b47d6851da..33e33615dc 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -125,7 +125,7 @@ app.config(($routeProvider) => { $routeProvider .when('/health-check', { template: healthCheckTemplate, - resolve: { apiCount, wzConfig, ip }, + resolve: { wzConfig, ip }, outerAngularWrapperRoute: true }) .when('/agents/:agent?/:tab?/:tabView?', { @@ -175,7 +175,7 @@ app.config(($routeProvider) => { }) .when('/blank-screen', { template: blankScreenTemplate, - resolve: { enableWzMenu, wzConfig }, + resolve: { enableWzMenu }, outerAngularWrapperRoute: true }) .when('/', { From ed521424bbd1be602150f3f966b8a32188065177 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Thu, 21 Apr 2022 16:28:11 +0200 Subject: [PATCH 451/493] Deprecated api-count --- public/services/resolves/api-count.js | 61 --------------------------- public/services/resolves/index.js | 2 - public/services/routes.js | 1 - 3 files changed, 64 deletions(-) delete mode 100644 public/services/resolves/api-count.js diff --git a/public/services/resolves/api-count.js b/public/services/resolves/api-count.js deleted file mode 100644 index a9166bc2bc..0000000000 --- a/public/services/resolves/api-count.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Wazuh app - Module to fetch API entries - * 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. - */ - -/** - * If there are no API entries it throws an exception, then it redirects to Settings - * for adding a new API entry. - * If there are API entries, then it continues to the health check itself. - * @param {*} $q Promise library for Angular.js resolves. - * @param {*} genericReq Wazuh module for doing generic requests to our backend. - * @param {*} $location Angular.js library for URL and paths manipulation. - */ -import { AppState } from '../../react-services/app-state'; -import { GenericRequest } from '../../react-services/generic-request'; - -export function apiCount($q, $location) { - const deferred = $q.defer(); - - GenericRequest.request('GET', '/hosts/apis') - .then(async (data) => { - if (!AppState.getCurrentAPI()) { - await tryToSetDefault(data?.data, AppState); - } - deferred.resolve(); - }) - .catch((reject) => { - console.log(reject); - deferred.resolve(); - }); - return deferred.promise; -} - -// Iterates the API entries in order to set one as default -function tryToSetDefault(apis, AppState) { - try { - for (let idx in apis) { - const api = apis[idx]; - try { - AppState.setCurrentAPI( - JSON.stringify({ - name: api.cluster_info.manager, - id: api.id, - }) - ); - break; - } catch (error) { - //Do nothing in order to follow the flow of the for - } - } - } catch (error) { - return Promise.reject(error); - } -} diff --git a/public/services/resolves/index.js b/public/services/resolves/index.js index 2a00125353..47c9ab8632 100644 --- a/public/services/resolves/index.js +++ b/public/services/resolves/index.js @@ -15,7 +15,6 @@ import { settingsWizard } from './settings-wizard'; import { getSavedSearch } from './get-saved-search'; import { getIp } from './get-ip'; import { getWzConfig } from './get-config'; -import { apiCount } from './api-count'; export { checkTimestamp, @@ -24,5 +23,4 @@ export { getSavedSearch, getIp, getWzConfig, - apiCount }; diff --git a/public/services/routes.js b/public/services/routes.js index 33e33615dc..6b24043526 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -20,7 +20,6 @@ import { getSavedSearch, getIp, getWzConfig, - apiCount } from './resolves'; // HTML templates From 8dbf7cd3ce29a442ee4aaaaa0585d0ac38cbb23b Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Thu, 21 Apr 2022 18:58:46 +0200 Subject: [PATCH 452/493] Added changelog Signed-off-by: Federico Rodriguez --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb45627196..b5773fc09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) - Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) - Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) +- Fixed circular re-directions when API errors are handled [#4079](https://github.com/wazuh/wazuh-kibana-app/pull/4079) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From bdea573cae3a4a6fe19d8b60e57ee7ff12fea198 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Mon, 25 Apr 2022 10:16:34 +0200 Subject: [PATCH 453/493] feat(github-actions): Add the support to build package for OpenSearch Dashboards using GitHub Actions on dispatch (#4097) --- .github/workflows/create-wazuh-packages.yml | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/create-wazuh-packages.yml b/.github/workflows/create-wazuh-packages.yml index 7e235228a2..3982cc255a 100644 --- a/.github/workflows/create-wazuh-packages.yml +++ b/.github/workflows/create-wazuh-packages.yml @@ -1,18 +1,17 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Create Wazuh Packages - +name: create-wazuh-packages on: workflow_dispatch: inputs: wazuh_kibana_app_branch: - description: 'Branch Wazuh-Kibana-App' + description: 'Branch Kibana App' required: true wazuh_packages_branch: - description: 'Branch Wazuh/Packages' + description: 'Branch Wazuh Packages' required: true - default: 'master' + default: '4.3' jobs: setup-wazuh-kibana-app: name: Run setup environment wazuh kibana app @@ -21,7 +20,7 @@ jobs: - name: Step 01 - Set up environment run: | mkdir packages - - name: Step 02 - Download Project wazuh-packages + - name: Step 02 - Download project wazuh-packages uses: actions/checkout@v2 with: repository: wazuh/wazuh-packages @@ -29,16 +28,17 @@ jobs: path: wazuh-packages - name: Step 03 - Building package run: | - cd $GITHUB_WORKSPACE/wazuh-packages/wazuhapp - echo fixing command... - sed -i -e 's/'\|' cut -d \"\/\" \-f2//g' ./generate_wazuh_app.sh - echo run command... + plugin_platform_directory="kibana" + echo "[info] checking the platform for the plugin..." + response_status=$(curl --silent -o /dev/null -w "%{http_code}" https://raw.githubusercontent.com/wazuh/wazuh-kibana-app/${{ github.event.inputs.wazuh_kibana_app_branch }}/.opensearch_dashboards-plugin-helpers.json) + [ "$response_status" = "200" ] && { plugin_platform_directory="opensearch-dashboards"; } + echo "[info] platform: $plugin_platform_directory" + echo "[info] changing the directory to $GITHUB_WORKSPACE/wazuh-packages/wazuhapp/$plugin_platform_directory" + cd $GITHUB_WORKSPACE/wazuh-packages/wazuhapp/$plugin_platform_directory + echo "[info] running script to generate the plugin package" ./generate_wazuh_app.sh -b ${{ github.event.inputs.wazuh_kibana_app_branch }} -s $GITHUB_WORKSPACE/packages -r 1 - cd $GITHUB_WORKSPACE/packages - ls -a - name: Archive packages uses: actions/upload-artifact@v2 with: name: wazuh-packages - path: packages/* - \ No newline at end of file + path: ${{ github.workspace }}/packages/* From 3de14d8499a06e110117ed8b7945a5eb0d995448 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Tue, 26 Apr 2022 15:48:07 +0200 Subject: [PATCH 454/493] fix(codemirror.js): z-index change in class '.codemirror pre' --- public/utils/codemirror/codemirror.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/utils/codemirror/codemirror.scss b/public/utils/codemirror/codemirror.scss index a31bf26801..93616727b1 100644 --- a/public/utils/codemirror/codemirror.scss +++ b/public/utils/codemirror/codemirror.scss @@ -393,5 +393,5 @@ span.CodeMirror-selectedtext { background: none; } /* Fix the overflow code over the line numbers */ .CodeMirror pre{ - z-index: 0; + z-index: 1; } From f637455d5c1d55c80680e9b38aa6248707553027 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Tue, 26 Apr 2022 16:27:14 +0200 Subject: [PATCH 455/493] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb45627196..4ce0ce6a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) - Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) - Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) +- Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From f175b6786aeb248ccb3c8ad65acb3e8431801ba2 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Wed, 27 Apr 2022 16:09:19 +0200 Subject: [PATCH 456/493] Changed breadcrumb agent push (#4101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed breadcrumb agent push Signed-off-by: Federico Rodriguez * Added changelog Signed-off-by: Federico Rodriguez Co-authored-by: Álex --- CHANGELOG.md | 1 + .../common/globalBreadcrumb/globalBreadcrumb.tsx | 8 +++++++- .../common/modules/agents-current-section.tsx | 2 ++ public/components/common/modules/main-overview.tsx | 10 +--------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce0ce6a15..6bc8b99e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) - Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) - Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) +- Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx index 95ccddb80b..c1d6dedf31 100644 --- a/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx +++ b/public/components/common/globalBreadcrumb/globalBreadcrumb.tsx @@ -31,7 +31,13 @@ class WzGlobalBreadcrumb extends Component { max={6} breadcrumbs={this.props.state.breadcrumb.map(breadcrumb => breadcrumb.agent ? { className: "euiLink euiLink--subdued ", - onClick: (ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": breadcrumb.agent.id }); this.router.reload(); }, + onClick: (ev) => { + ev.stopPropagation(); + AppNavigate.navigateToModule(ev, 'agents', { + "tab": "welcome", "agent": breadcrumb.agent.id + }); + this.router.reload(); + }, id: "breadcrumbNoTitle", truncate: true, text: ( diff --git a/public/components/common/modules/agents-current-section.tsx b/public/components/common/modules/agents-current-section.tsx index 4dbb9ec257..12a519d6cf 100644 --- a/public/components/common/modules/agents-current-section.tsx +++ b/public/components/common/modules/agents-current-section.tsx @@ -21,6 +21,8 @@ import { connect } from 'react-redux'; import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; import { getAngularModule } from '../../../kibana-services'; +// TODO: check if this component is deprecated, if so remove it. +// This component is wrapped by WzCurrentAgentsSectionWrapper class WzCurrentAgentsSection extends Component { constructor(props) { super(props); diff --git a/public/components/common/modules/main-overview.tsx b/public/components/common/modules/main-overview.tsx index 8712704bc5..9d498f3901 100644 --- a/public/components/common/modules/main-overview.tsx +++ b/public/components/common/modules/main-overview.tsx @@ -74,15 +74,7 @@ export const MainModuleOverview = connect(mapStateToProps)(class MainModuleOverv ]; if (currentAgent.id) { breadcrumb.push({ - className: "euiLink euiLink--subdued ", - onClick: (ev) => { ev.stopPropagation(); AppNavigate.navigateToModule(ev, 'agents', { "tab": "welcome", "agent": currentAgent.id }); this.router.reload(); }, - id: "breadcrumbNoTitle", - truncate: true, - text: ( - - {currentAgent.name} - - ), + agent: currentAgent }) } breadcrumb.push({ From 3a08e5833ee8f6c3865bf3d60ecf6bef2365ca93 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 27 Apr 2022 16:11:58 +0200 Subject: [PATCH 457/493] [Jobs] Change the `shards` configuration for the `statistics` indices (#4055) * feat: Changed the default configuration for the `monitoring` and `statistics` indices. - configuration: - shards: 1 - replicas: 0 - affected: - configuration in the creation of indices - initial default plugin configuration - `getWzConfig` route resolver - constant with the default configuration * changelog: Add PR to the changelog --- CHANGELOG.md | 1 + common/constants.ts | 14 ++++++-------- public/services/resolves/get-config.js | 13 +++++++------ server/lib/initial-wazuh-config.ts | 10 +++++----- server/start/cron-scheduler/save-document.ts | 8 ++++---- server/start/monitoring/index.ts | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc8b99e31..0e34a3715c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored the table in `Vulnerabilities/Inventory` [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Changed Google Groups app icons [#3949](https://github.com/wazuh/wazuh-kibana-app/pull/3949) - Removed sorting for `Agents` or `Configuration checksum` column in the table of `Management/Groups` due to this is not supported by the API [#3857](https://github.com/wazuh/wazuh-kibana-app/pull/3857) +- Changed the default `wazuh.statistics.shards` setting from `2` to `1` [#4055](https://github.com/wazuh/wazuh-kibana-app/pull/4055) ### Fixed diff --git a/common/constants.ts b/common/constants.ts index 30c9a8c4f6..5708777e16 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -16,16 +16,13 @@ export const WAZUH_INDEX_TYPE_ALERTS = 'alerts'; export const WAZUH_ALERTS_PREFIX = 'wazuh-alerts-'; export const WAZUH_ALERTS_PATTERN = 'wazuh-alerts-*'; -// Default number of shards and replicas for indices -export const WAZUH_INDEX_SHARDS = 2; -export const WAZUH_INDEX_REPLICAS = 0; - // Job - Wazuh monitoring export const WAZUH_INDEX_TYPE_MONITORING = "monitoring"; export const WAZUH_MONITORING_PREFIX = "wazuh-monitoring-"; export const WAZUH_MONITORING_PATTERN = "wazuh-monitoring-*"; export const WAZUH_MONITORING_TEMPLATE_NAME = "wazuh-agent"; export const WAZUH_MONITORING_DEFAULT_INDICES_SHARDS = 1; +export const WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS = 0; export const WAZUH_MONITORING_DEFAULT_CREATION = 'w'; export const WAZUH_MONITORING_DEFAULT_ENABLED = true; export const WAZUH_MONITORING_DEFAULT_FREQUENCY = 900; @@ -37,7 +34,8 @@ export const WAZUH_STATISTICS_DEFAULT_PREFIX = "wazuh"; export const WAZUH_STATISTICS_DEFAULT_NAME = "statistics"; export const WAZUH_STATISTICS_PATTERN = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}-*`; export const WAZUH_STATISTICS_TEMPLATE_NAME = `${WAZUH_STATISTICS_DEFAULT_PREFIX}-${WAZUH_STATISTICS_DEFAULT_NAME}`; -export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = WAZUH_INDEX_SHARDS; +export const WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS = 1; +export const WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS = 0; export const WAZUH_STATISTICS_DEFAULT_CREATION = 'w'; export const WAZUH_STATISTICS_DEFAULT_STATUS = true; export const WAZUH_STATISTICS_DEFAULT_FREQUENCY = 900; @@ -208,7 +206,7 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'wazuh.monitoring.enabled': WAZUH_MONITORING_DEFAULT_ENABLED, 'wazuh.monitoring.frequency': WAZUH_MONITORING_DEFAULT_FREQUENCY, 'wazuh.monitoring.shards': WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, - 'wazuh.monitoring.replicas': WAZUH_INDEX_REPLICAS, + 'wazuh.monitoring.replicas': WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, 'wazuh.monitoring.creation': WAZUH_MONITORING_DEFAULT_CREATION, 'wazuh.monitoring.pattern': WAZUH_MONITORING_PATTERN, 'cron.prefix': WAZUH_STATISTICS_DEFAULT_PREFIX, @@ -217,8 +215,8 @@ export const WAZUH_DEFAULT_APP_CONFIG = { 'cron.statistics.interval': WAZUH_STATISTICS_DEFAULT_CRON_FREQ, 'cron.statistics.index.name': WAZUH_STATISTICS_DEFAULT_NAME, 'cron.statistics.index.creation': WAZUH_STATISTICS_DEFAULT_CREATION, - 'cron.statistics.index.shards': WAZUH_INDEX_SHARDS, - 'cron.statistics.index.replicas': WAZUH_INDEX_REPLICAS, + 'cron.statistics.index.shards': WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, + 'cron.statistics.index.replicas': WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, 'alerts.sample.prefix': WAZUH_SAMPLE_ALERT_PREFIX, hideManagerAlerts: false, 'logs.level': 'info', diff --git a/public/services/resolves/get-config.js b/public/services/resolves/get-config.js index 960a760ebb..6eeebb287d 100644 --- a/public/services/resolves/get-config.js +++ b/public/services/resolves/get-config.js @@ -12,11 +12,12 @@ import { WAZUH_ALERTS_PATTERN, - WAZUH_INDEX_REPLICAS, - WAZUH_INDEX_SHARDS, WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, + WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, WAZUH_MONITORING_PATTERN, - WAZUH_SAMPLE_ALERT_PREFIX + WAZUH_SAMPLE_ALERT_PREFIX, + WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, + WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, } from "../../../common/constants"; export async function getWzConfig($q, genericReq, wazuhConfig) { @@ -53,7 +54,7 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { 'wazuh.monitoring.enabled': true, 'wazuh.monitoring.frequency': 900, 'wazuh.monitoring.shards': WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, - 'wazuh.monitoring.replicas': WAZUH_INDEX_REPLICAS, + 'wazuh.monitoring.replicas': WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, 'wazuh.monitoring.creation': 'w', 'wazuh.monitoring.pattern': WAZUH_MONITORING_PATTERN, 'cron.prefix': 'wazuh', @@ -62,8 +63,8 @@ export async function getWzConfig($q, genericReq, wazuhConfig) { 'cron.statistics.interval': '0 */5 * * * *', 'cron.statistics.index.name': 'statistics', 'cron.statistics.index.creation': 'w', - 'cron.statistics.index.shards': WAZUH_INDEX_SHARDS, - 'cron.statistics.index.replicas': WAZUH_INDEX_REPLICAS, + 'cron.statistics.index.shards': WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, + 'cron.statistics.index.replicas': WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, 'alerts.sample.prefix': WAZUH_SAMPLE_ALERT_PREFIX, hideManagerAlerts: false, 'logs.level': 'info', diff --git a/server/lib/initial-wazuh-config.ts b/server/lib/initial-wazuh-config.ts index a183c7c7e1..b428cd22f6 100644 --- a/server/lib/initial-wazuh-config.ts +++ b/server/lib/initial-wazuh-config.ts @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ -import { ASSETS_BASE_URL_PREFIX } from "../../common/constants"; +import { ASSETS_BASE_URL_PREFIX, WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS } from "../../common/constants"; export const initialWazuhConfig: string = `--- # @@ -120,8 +120,8 @@ export const initialWazuhConfig: string = `--- #wazuh.monitoring.frequency: 900 # # Configure wazuh-monitoring-* indices shards and replicas. -#wazuh.monitoring.shards: 1 -#wazuh.monitoring.replicas: 0 +#wazuh.monitoring.shards: ${WAZUH_MONITORING_DEFAULT_INDICES_SHARDS} +#wazuh.monitoring.replicas: ${WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS} # # Configure wazuh-monitoring-* indices custom creation interval. # Values: h (hourly), d (daily), w (weekly), m (monthly) @@ -164,8 +164,8 @@ export const initialWazuhConfig: string = `--- #cron.statistics.index.creation: w # # Configure statistics indices shards and replicas. -#cron.statistics.shards: 2 -#cron.statistics.replicas: 0 +#cron.statistics.shards: ${WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS} +#cron.statistics.replicas: ${WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS} # # ------------------------------ wazuh-logo-customization ------------------------------- # diff --git a/server/start/cron-scheduler/save-document.ts b/server/start/cron-scheduler/save-document.ts index 7dbe5ff775..1f0e17c655 100644 --- a/server/start/cron-scheduler/save-document.ts +++ b/server/start/cron-scheduler/save-document.ts @@ -2,8 +2,8 @@ import { BulkIndexDocumentsParams } from 'elasticsearch'; import { getConfiguration } from '../../lib/get-configuration'; import { log } from '../../lib/logger'; import { indexDate } from '../../lib/index-date'; -import { WAZUH_INDEX_SHARDS, WAZUH_INDEX_REPLICAS } from '../../../common/constants' -import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError' +import { WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS } from '../../../common/constants'; +import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; export interface IIndexConfiguration { name: string @@ -53,8 +53,8 @@ export class SaveDocument { body: { settings: { index: { - number_of_shards: shards || WAZUH_INDEX_SHARDS, - number_of_replicas: replicas || WAZUH_INDEX_REPLICAS + number_of_shards: shards ?? WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, + number_of_replicas: replicas ?? WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS } } } diff --git a/server/start/monitoring/index.ts b/server/start/monitoring/index.ts index d287af1c87..e5e43853c8 100644 --- a/server/start/monitoring/index.ts +++ b/server/start/monitoring/index.ts @@ -19,12 +19,12 @@ import { buildIndexSettings } from '../../lib/build-index-settings'; import { WazuhHostsCtrl } from '../../controllers/wazuh-hosts'; import { WAZUH_MONITORING_PATTERN, - WAZUH_INDEX_REPLICAS, WAZUH_MONITORING_TEMPLATE_NAME, - WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, WAZUH_MONITORING_DEFAULT_CREATION, WAZUH_MONITORING_DEFAULT_ENABLED, WAZUH_MONITORING_DEFAULT_FREQUENCY, + WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, + WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, } from '../../../common/constants'; import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; import { delayAsPromise } from '../../../common/utils'; @@ -259,7 +259,7 @@ async function createIndex(context, indexName: string) { settings: { index: { number_of_shards: getAppConfigurationSetting('wazuh.monitoring.shards', appConfig, WAZUH_MONITORING_DEFAULT_INDICES_SHARDS), - number_of_replicas: getAppConfigurationSetting('wazuh.monitoring.replicas', appConfig, WAZUH_INDEX_REPLICAS) + number_of_replicas: getAppConfigurationSetting('wazuh.monitoring.replicas', appConfig, WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS) } } }; From 36f82238c72ecc2a5a56837b9f21b3f00691c7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Wed, 27 Apr 2022 11:22:33 -0300 Subject: [PATCH 458/493] Modules-> Mitre-> Widget: Rows per page (#4066) * Modules-> Mitre-> Widget: Rows per page * Issue 4041 is added to the changelog --- CHANGELOG.md | 1 + .../components/overview/mitre_attack_intelligence/resource.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e34a3715c..b2576900ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new fields of `Vulnerabilities` to the details flyout [#3893](https://github.com/wazuh/wazuh-kibana-app/pull/3893) [#3908](https://github.com/wazuh/wazuh-kibana-app/pull/3908) - Added missing fields used in visualizations to the known fiels related to alerts [#3924](https://github.com/wazuh/wazuh-kibana-app/pull/3924) - Added troubleshooting link to "index pattern was refreshed" toast [#3946](https://github.com/wazuh/wazuh-kibana-app/pull/3946) +- Added more number options to the tables widget in Modules -> "Mitre" [#4041](https://github.com/wazuh/wazuh-kibana-app/pull/4066) ### Changed diff --git a/public/components/overview/mitre_attack_intelligence/resource.tsx b/public/components/overview/mitre_attack_intelligence/resource.tsx index 96cd5cc71a..9dbf394150 100644 --- a/public/components/overview/mitre_attack_intelligence/resource.tsx +++ b/public/components/overview/mitre_attack_intelligence/resource.tsx @@ -81,7 +81,7 @@ export const ModuleMitreAttackIntelligenceResource = ({ searchBarPlaceholder={`Search in ${label}`} searchBarSuggestions={searchBarSuggestions} endpoint={apiEndpoint} - tablePageSizeOptions={[10]} + tablePageSizeOptions={[10, 15, 25, 50, 100]} filters={resourceFilters} /> {details && ( From cb593f8f21fd5956b043250deb4c914092b07ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Ruiz?= Date: Mon, 25 Apr 2022 11:10:11 +0200 Subject: [PATCH 459/493] Bump Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2576900ce..6fbeb7f243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the Wazuh app project will be documented in this file. -## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.3, 7.17.0 - Revision 4301 +## Wazuh v4.3.0 - Kibana 7.10.2 , 7.16.x, 7.17.x - Revision 4301 ### Added From 208d801b21d9b5d864e2f88e8be8d4eb3bc59625 Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Wed, 27 Apr 2022 16:49:23 +0200 Subject: [PATCH 460/493] [Initialize] Remove the migrations tasks related to deprecated `.wazuh` indices (`.wazuh` and `.wazuh-version`) (#4098) * feat(server/initialize): Removed the migration tasks affecting to the deprecated wazuh indices (.wazuh and .wazuh-version) - Note this change avoids the appearance of logs related to indices when the internal user has no permissions on them. * changelog: Add PR to the changelog --- CHANGELOG.md | 1 + common/constants.ts | 2 - server/start/initialize/index.ts | 98 +------------------------------- 3 files changed, 3 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbeb7f243..aaf6d3d56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed Google Groups app icons [#3949](https://github.com/wazuh/wazuh-kibana-app/pull/3949) - Removed sorting for `Agents` or `Configuration checksum` column in the table of `Management/Groups` due to this is not supported by the API [#3857](https://github.com/wazuh/wazuh-kibana-app/pull/3857) - Changed the default `wazuh.statistics.shards` setting from `2` to `1` [#4055](https://github.com/wazuh/wazuh-kibana-app/pull/4055) +- Removed the migration tasks in the `.wazuh` and `.wazuh-version` indices [#4098](https://github.com/wazuh/wazuh-kibana-app/pull/4098) ### Fixed diff --git a/common/constants.ts b/common/constants.ts index 5708777e16..16fd284c77 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -42,8 +42,6 @@ export const WAZUH_STATISTICS_DEFAULT_FREQUENCY = 900; export const WAZUH_STATISTICS_DEFAULT_CRON_FREQ = '0 */5 * * * *'; // Job - Wazuh initialize -export const WAZUH_INDEX = '.wazuh'; -export const WAZUH_VERSION_INDEX = '.wazuh-version'; export const WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME = 'wazuh-kibana'; // Permissions diff --git a/server/start/initialize/index.ts b/server/start/initialize/index.ts index 9264cf9a6c..9b6fc36669 100644 --- a/server/start/initialize/index.ts +++ b/server/start/initialize/index.ts @@ -16,9 +16,8 @@ import { getConfiguration } from '../../lib/get-configuration'; import { totalmem } from 'os'; import fs from 'fs'; import { ManageHosts } from '../../lib/manage-hosts'; -import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_INDEX, WAZUH_VERSION_INDEX, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; +import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; import { createDataDirectoryIfNotExists } from '../../lib/filesystem'; -import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; const manageHosts = new ManageHosts(); @@ -96,75 +95,6 @@ export function jobInitializeRun(context) { } }; - /** - * Checks if the .wazuh index exist in order to migrate to wazuh.yml - */ - const checkWazuhIndex = tryCatchForIndexPermissionError(WAZUH_INDEX)( async () => { - log('initialize:checkWazuhIndex', `Checking ${WAZUH_INDEX} index.`, 'debug'); - const result = await context.core.elasticsearch.client.asInternalUser.indices.exists({ - index: WAZUH_INDEX - }); - if (result.body) { - const data = await context.core.elasticsearch.client.asInternalUser.search({ - index: WAZUH_INDEX, - size: 100 - }); - const apiEntries = (((data || {}).body || {}).hits || {}).hits || []; - await manageHosts.migrateFromIndex(apiEntries); - log( - 'initialize:checkWazuhIndex', - `Index ${WAZUH_INDEX} will be removed and its content will be migrated to wazuh.yml`, - 'debug' - ); - // Check if all APIs entries were migrated properly and delete it from the .wazuh index - await checkProperlyMigrate(); - await context.core.elasticsearch.client.asInternalUser.indices.delete({ - index: WAZUH_INDEX - }); - } - }); - - /** - * Checks if the API entries were properly migrated - * @param {Array} migratedApis - */ - const checkProperlyMigrate = async () => { - try { - let apisIndex = await await context.core.elasticsearch.client.asInternalUser.search({ - index: WAZUH_INDEX, - size: 100 - }); - const hosts = await manageHosts.getHosts(); - apisIndex = ((apisIndex.body || {}).hits || {}).hits || []; - - const apisIndexKeys = apisIndex.map(api => { - return api._id; - }); - const hostsKeys = hosts.map(api => { - return Object.keys(api)[0]; - }); - - // Get into an array the API entries that were not migrated, if the length is 0 then all the API entries were properly migrated. - const rest = apisIndexKeys.filter(k => { - return !hostsKeys.includes(k); - }); - - if (rest.length) { - throw new Error( - `Cannot migrate all API entries, missed entries: (${rest.toString()})` - ); - } - log( - 'initialize:checkProperlyMigrate', - 'The API entries migration was successful', - 'debug' - ); - } catch (error) { - log('initialize:checkProperlyMigrate', `${error}`, 'error'); - return Promise.reject(error); - } - }; - /** * Checks if the .wazuh-version exists, in this case it will be deleted and the wazuh-registry.json will be created */ @@ -175,27 +105,6 @@ export function jobInitializeRun(context) { 'Checking wazuh-version registry.', 'debug' ); - try { - const exists = await context.core.elasticsearch.client.asInternalUser.indices.exists({ - index: WAZUH_VERSION_INDEX - }); - if (exists.body){ - await context.core.elasticsearch.client.asInternalUser.indices.delete({ - index: WAZUH_VERSION_INDEX - }); - log( - 'initialize[checkwazuhRegistry]', - `Successfully deleted old ${WAZUH_VERSION_INDEX} index.`, - 'debug' - ); - }; - } catch (error) { - log( - 'initialize[checkwazuhRegistry]', - `No need to delete old ${WAZUH_VERSION_INDEX} index`, - 'debug' - ); - } if(!fs.existsSync(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH)){ throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); @@ -235,10 +144,7 @@ export function jobInitializeRun(context) { // Init function. Check for "wazuh-version" document existance. const init = async () => { - await Promise.all([ - checkWazuhIndex(), - checkWazuhRegistry() - ]); + await checkWazuhRegistry(); }; const createKibanaTemplate = () => { From 3c8e72f582eea1a276f321960f35c4cf366476aa Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Wed, 27 Apr 2022 17:19:10 +0200 Subject: [PATCH 461/493] fix(activeNoImplicitsFilters()): change on check if the filter is already used --- .../common/modules/modules-helper.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/public/components/common/modules/modules-helper.js b/public/components/common/modules/modules-helper.js index 2f4949f6de..8210e1b7c7 100644 --- a/public/components/common/modules/modules-helper.js +++ b/public/components/common/modules/modules-helper.js @@ -51,16 +51,18 @@ export class ModulesHelper { } const filters = $(`.globalFilterItem .euiBadge__childButton`); for (let i = 0; i < filters.length; i++) { + const data = filters[i].attributes[3]; let found = false; (implicitFilters || []).forEach(x => { - const objKey = x.query && x.query.match ? Object.keys(x.query.match)[0] : x.meta.key; - const key = `filter-key-${objKey}`; - const value = x.query && x.query.match - ? `filter-value-${x.query.match[objKey].query}` - : `filter-value-${x.meta.value}`; - const data = filters[i].attributes[3]; - if (data.value.includes(key) && data.value.includes(value)) { - found = true; + if(!x.used){ + const objKey = x.query && x.query.match ? Object.keys(x.query.match)[0] : x.meta.key; + const objValue = x.query && x.query.match ? x.query.match[objKey].query : x.meta.value; + const key = `filter-key-${objKey}`; + const value = `filter-value-${objValue}`; + if (data.value.includes(key) && data.value.includes(value) && !data.value.includes('filter-pinned')) { + found = true; + x.used = true; + } } }); if (!found) { From ed589c107c7068e5d5ccababffebf445012297b2 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Wed, 27 Apr 2022 17:29:07 +0200 Subject: [PATCH 462/493] add comments --- public/components/common/modules/modules-helper.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/components/common/modules/modules-helper.js b/public/components/common/modules/modules-helper.js index 8210e1b7c7..4673e3f7f1 100644 --- a/public/components/common/modules/modules-helper.js +++ b/public/components/common/modules/modules-helper.js @@ -54,11 +54,14 @@ export class ModulesHelper { const data = filters[i].attributes[3]; let found = false; (implicitFilters || []).forEach(x => { + // The IF: checks if the filter is already in use + // Check which of the filters are from the view and which are not pinned filters if(!x.used){ const objKey = x.query && x.query.match ? Object.keys(x.query.match)[0] : x.meta.key; const objValue = x.query && x.query.match ? x.query.match[objKey].query : x.meta.value; const key = `filter-key-${objKey}`; const value = `filter-value-${objValue}`; + // if (data.value.includes(key) && data.value.includes(value) && !data.value.includes('filter-pinned')) { found = true; x.used = true; From a57be8230980e77b16d745c8d3d151cde8cdb132 Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Thu, 28 Apr 2022 08:32:53 +0200 Subject: [PATCH 463/493] Improve messages in the agent installation (#4040) * improve messages in the agent installation * add changelog * add target=_blank * Updated proposal * Updated proposal * change place of admin message Co-authored-by: Antonio <34042064+Desvelao@users.noreply.github.com> --- CHANGELOG.md | 1 + .../controllers/agent/components/register-agent.js | 12 +++--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf6d3d56a..9fdd645c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Refactored the table in `Vulnerabilities/Inventory` [#3196](https://github.com/wazuh/wazuh-kibana-app/pull/3196) - Changed Google Groups app icons [#3949](https://github.com/wazuh/wazuh-kibana-app/pull/3949) - Removed sorting for `Agents` or `Configuration checksum` column in the table of `Management/Groups` due to this is not supported by the API [#3857](https://github.com/wazuh/wazuh-kibana-app/pull/3857) +- Changed messages in the agent installation guide [#4040](https://github.com/wazuh/wazuh-kibana-app/pull/4040) - Changed the default `wazuh.statistics.shards` setting from `2` to `1` [#4055](https://github.com/wazuh/wazuh-kibana-app/pull/4055) - Removed the migration tasks in the `.wazuh` and `.wazuh-version` indices [#4098](https://github.com/wazuh/wazuh-kibana-app/pull/4098) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index e6babb1079..728ac01999 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -425,8 +425,7 @@ export const RegisterAgent = withErrorBoundary( const ipInput = (

    - You can predefine the Wazuh server address with the enrollment.dns{' '} - Wazuh app setting. + This is the address the agent uses to communicate with the Wazuh server. It can be an IP address or a fully qualified domain name (FQDN).

    - Running this command on a host with an agent already installed upgrades the - agent package without enrolling the agent. To enroll it, see the{' '} - - Wazuh documentation - - . + If the installer finds another Wazuh agent in the system, it will upgrade it preserving the configuration. } iconType="iInCircle" /> + {windowsAdvice}
    {this.state.wazuhPassword && !this.state.showPassword ? this.obfuscatePassword(text) : text} @@ -566,7 +561,6 @@ export const RegisterAgent = withErrorBoundary( /> )} - {windowsAdvice} )}
    From a3fb0add258e4bf26499fbe646605567ace078a6 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Thu, 28 Apr 2022 10:18:57 +0200 Subject: [PATCH 464/493] suggested improvement --- .../common/modules/modules-helper.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/public/components/common/modules/modules-helper.js b/public/components/common/modules/modules-helper.js index 4673e3f7f1..4883ab0933 100644 --- a/public/components/common/modules/modules-helper.js +++ b/public/components/common/modules/modules-helper.js @@ -49,31 +49,35 @@ export class ModulesHelper { this.activeNoImplicitsFilters(); }, 100); } - const filters = $(`.globalFilterItem .euiBadge__childButton`); - for (let i = 0; i < filters.length; i++) { - const data = filters[i].attributes[3]; + // With the filter classes decide if they are from the module view or not + const allFilters = $(`.globalFilterItem .euiBadge__childButton`); + for (let i = 0; i < allFilters.length; i++) { + const data = allFilters[i].attributes['data-test-subj']; let found = false; - (implicitFilters || []).forEach(x => { - // The IF: checks if the filter is already in use - // Check which of the filters are from the view and which are not pinned filters - if(!x.used){ - const objKey = x.query && x.query.match ? Object.keys(x.query.match)[0] : x.meta.key; - const objValue = x.query && x.query.match ? x.query.match[objKey].query : x.meta.value; + (implicitFilters || []).forEach(mooduleFilter => { + // Checks if the filter is already in use + // Check which of the filters are from the module view and which are not pinned filters + if(!mooduleFilter.used){ + const objKey = mooduleFilter.query?.match ? Object.keys(mooduleFilter.query.match)[0] : mooduleFilter.meta.key; + const objValue = mooduleFilter.query?.match ? mooduleFilter.query.match[objKey].query : mooduleFilter.meta.value; const key = `filter-key-${objKey}`; const value = `filter-value-${objValue}`; - // - if (data.value.includes(key) && data.value.includes(value) && !data.value.includes('filter-pinned')) { + + const noExcludedValues = !data.value.includes('filter-pinned') && !data.value.includes('filter-negated') + const acceptedValues = data.value.includes(key) && data.value.includes(value) + + if ( acceptedValues && noExcludedValues) { found = true; - x.used = true; + mooduleFilter.used = true; } } }); if (!found) { - $(filters[i]).siblings('.euiBadge__iconButton').removeClass('hide-close-button'); - $(filters[i]).off('click'); + $(allFilters[i]).siblings('.euiBadge__iconButton').removeClass('hide-close-button'); + $(allFilters[i]).off('click'); } else { - $(filters[i]).siblings('.euiBadge__iconButton').addClass('hide-close-button'); - $(filters[i]).on('click', ev => { + $(allFilters[i]).siblings('.euiBadge__iconButton').addClass('hide-close-button'); + $(allFilters[i]).on('click', ev => { ev.stopPropagation(); }); } From 7ef3676c66192cfffce8a7b288b2238d4a7738be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex?= Date: Thu, 28 Apr 2022 11:11:02 +0200 Subject: [PATCH 465/493] Fix the `Missing parameters` error on the Manager Logs view (#4110) * Fix the missing parameters error on the manager logs controller * Changelog --- CHANGELOG.md | 1 + .../components/management/mg-logs/logs.js | 27 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fdd645c35..47ba518de6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) +- Fixed the 'missing parameters' error on the Manager Logs [#4110](https://github.com/wazuh/wazuh-kibana-app/pull/4110) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index d598492a51..f2d7332c87 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -43,11 +43,7 @@ import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchest import { getErrorOrchestrator } from '../../../../../react-services/common-services'; export default compose( - withGlobalBreadcrumb([ - { text: '' }, - { text: 'Management', href: '#/manager' }, - { text: 'Logs' } - ]), + withGlobalBreadcrumb([{ text: '' }, { text: 'Management', href: '#/manager' }, { text: 'Logs' }]), withUserAuthorizationPrompt([ { action: 'cluster:status', resource: '*:*:*' }, { action: 'cluster:read', resource: 'node:id:*' }, @@ -139,23 +135,20 @@ export default compose( } async initDaemonsList(logsPath) { - const daemonsNotIncluded = [ - 'wazuh-modulesd:task-manager', - 'wazuh-modulesd:agent-upgrade' - ] + const daemonsNotIncluded = ['wazuh-modulesd:task-manager', 'wazuh-modulesd:agent-upgrade']; try { const path = logsPath + '/summary'; const data = await WzRequest.apiReq('GET', path, {}); const formattedData = (((data || {}).data || {}).data || {}).affected_items; const daemonsList = [...['all']]; for (const daemon of formattedData) { - if(!daemonsNotIncluded.includes(Object.keys(daemon)[0])){ + if (!daemonsNotIncluded.includes(Object.keys(daemon)[0])) { daemonsList.push(Object.keys(daemon)[0]); } } this.setState({ daemonsList }); } catch (error) { - throw new Error(error); + throw new Error('Error fetching daemons list: ' + error); } } @@ -192,7 +185,13 @@ export default compose( const { logsPath } = this.state; let result = ''; let totalItems = 0; - + + // Avoid attempts to send invalid requests if the logsPath variable + // hasn't been intialized yet (caused by the onSearchBarSearch event). + if (logsPath === '') { + return result; + } + try { const tmpResult = await WzRequest.apiReq('GET', logsPath, { params: this.buildFilters(customOffset), @@ -201,7 +200,7 @@ export default compose( totalItems = ((tmpResult || {}).data.data || {}).total_affected_items; result = this.parseLogsToText(resultItems) || ''; } catch (error) { - throw new Error(error); + throw new Error('Error fetching logs: ' + error); } this.setState({ totalItems }); @@ -258,7 +257,7 @@ export default compose( return { nodeList: '', logsPath: '/manager/logs', selectedNode: '' }; } catch (error) { - throw new Error(error); + throw new Error('Error building logs path: ' + error); } } From f183abb2e3c72a6d6f1caab6a6702e0f11d50260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Ruiz?= Date: Thu, 28 Apr 2022 12:13:01 +0200 Subject: [PATCH 466/493] Fix typo and apply prettier/linter --- .../common/modules/modules-helper.js | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/public/components/common/modules/modules-helper.js b/public/components/common/modules/modules-helper.js index 4883ab0933..4482664952 100644 --- a/public/components/common/modules/modules-helper.js +++ b/public/components/common/modules/modules-helper.js @@ -5,7 +5,7 @@ export class ModulesHelper { const $injector = getAngularModule().$injector; const location = $injector.get('$location'); const initialTab = location.search().tab; - return new Promise(resolve => { + return new Promise((resolve) => { const checkExist = setInterval(() => { const app = getAngularModule(); if (app.discoverScope) { @@ -21,11 +21,9 @@ export class ModulesHelper { } static async cleanAvailableFields() { - const fields = document.querySelectorAll( - `.dscFieldChooser .dscFieldList--unpopular li` - ); + const fields = document.querySelectorAll(`.dscFieldChooser .dscFieldList--unpopular li`); if (fields.length) { - fields.forEach(field => { + fields.forEach((field) => { const attr = field.getAttribute('data-attr-field'); if (attr.startsWith('_')) { field.style.display = 'none'; @@ -35,15 +33,14 @@ export class ModulesHelper { } static hideCloseButtons = () => { - this.activeNoImplicitsFilters() + this.activeNoImplicitsFilters(); }; static activeNoImplicitsFilters() { const { filterManager } = getDataPlugin().query; const implicitFilters = filterManager.getFilters().filter((x) => { - return x.$state.isImplicit - } - ); + return x.$state.isImplicit; + }); if (!(implicitFilters || []).length) { setTimeout(() => { this.activeNoImplicitsFilters(); @@ -54,30 +51,35 @@ export class ModulesHelper { for (let i = 0; i < allFilters.length; i++) { const data = allFilters[i].attributes['data-test-subj']; let found = false; - (implicitFilters || []).forEach(mooduleFilter => { + (implicitFilters || []).forEach((moduleFilter) => { // Checks if the filter is already in use // Check which of the filters are from the module view and which are not pinned filters - if(!mooduleFilter.used){ - const objKey = mooduleFilter.query?.match ? Object.keys(mooduleFilter.query.match)[0] : mooduleFilter.meta.key; - const objValue = mooduleFilter.query?.match ? mooduleFilter.query.match[objKey].query : mooduleFilter.meta.value; + if (!moduleFilter.used) { + const objKey = moduleFilter.query?.match + ? Object.keys(moduleFilter.query.match)[0] + : moduleFilter.meta.key; + const objValue = moduleFilter.query?.match + ? moduleFilter.query.match[objKey].query + : moduleFilter.meta.value; const key = `filter-key-${objKey}`; const value = `filter-value-${objValue}`; - const noExcludedValues = !data.value.includes('filter-pinned') && !data.value.includes('filter-negated') - const acceptedValues = data.value.includes(key) && data.value.includes(value) + const noExcludedValues = + !data.value.includes('filter-pinned') && !data.value.includes('filter-negated'); + const acceptedValues = data.value.includes(key) && data.value.includes(value); - if ( acceptedValues && noExcludedValues) { + if (acceptedValues && noExcludedValues) { found = true; - mooduleFilter.used = true; + moduleFilter.used = true; } } }); if (!found) { - $(allFilters[i]).siblings('.euiBadge__iconButton').removeClass('hide-close-button'); - $(allFilters[i]).off('click'); + $(allFilters[i]).siblings('.euiBadge__iconButton').removeClass('hide-close-button'); + $(allFilters[i]).off('click'); } else { $(allFilters[i]).siblings('.euiBadge__iconButton').addClass('hide-close-button'); - $(allFilters[i]).on('click', ev => { + $(allFilters[i]).on('click', (ev) => { ev.stopPropagation(); }); } From 517ab5f7ddf15cdddebe092175e6be36edf8ad66 Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Thu, 28 Apr 2022 16:20:51 -0300 Subject: [PATCH 467/493] Ensured all promise reject return error --- public/react-services/wz-request.ts | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 4ead68ee7f..ea6853421f 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -88,13 +88,13 @@ export class WzRequest { return this.genericReq(method, path, payload, customTimeout, false); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(error.data.message) - : Promise.reject(error.message || error); + ? Promise.reject(this.customErroMessage(error, error.data.message)) + : Promise.reject(this.customErroMessage(error, error.message)); } } return errorMessage - ? Promise.reject(errorMessage) - : Promise.reject(error || 'Server did not respond'); + ? Promise.reject(this.customErroMessage(error, errorMessage)) + : Promise.reject(this.customErroMessage(error,'Server did not respond')); } } @@ -121,18 +121,14 @@ export class WzRequest { const failed_ids = ((((response.data || {}).data || {}).failed_items || [])[0] || {}).id || {}; const message = (response.data || {}).message || 'Unexpected error'; - - return Promise.reject( - `${message} (${error.code}) - ${error.message} ${ - failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : '' - }` - ); + const errorMessage = `${message} (${error.code}) - ${error.message} ${failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : ''}` + return Promise.reject(this.customErroMessage(null, errorMessage)); } return Promise.resolve(response); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(error.data.message) - : Promise.reject(error.message || error); + ? Promise.reject(this.customErroMessage(error, error.data.message)) + : Promise.reject(this.customErroMessage(error, error.message)); } } @@ -156,4 +152,18 @@ export class WzRequest { : Promise.reject(error.message || error); } } + + /** + * Customize message and return an error object + * @param error + * @param message + * @returns error + */ + static customErroMessage(error, message){ + if(!error || typeof error === 'string'){ + return new Error(message || error); + } + error.message = message + return error + } } From f2b8f02cab8dce92e3bfa617eeafae0f12352296 Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Thu, 28 Apr 2022 16:29:24 -0300 Subject: [PATCH 468/493] Reverted last commit - wz-request changes --- public/react-services/wz-request.ts | 36 +++++++++++------------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index ea6853421f..69565ad959 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -88,13 +88,13 @@ export class WzRequest { return this.genericReq(method, path, payload, customTimeout, false); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(this.customErroMessage(error, error.data.message)) - : Promise.reject(this.customErroMessage(error, error.message)); + ? Promise.reject(error.data.message) + : Promise.reject(error.message || error); } } return errorMessage - ? Promise.reject(this.customErroMessage(error, errorMessage)) - : Promise.reject(this.customErroMessage(error,'Server did not respond')); + ? Promise.reject(errorMessage) + : Promise.reject(error || 'Server did not respond'); } } @@ -121,14 +121,18 @@ export class WzRequest { const failed_ids = ((((response.data || {}).data || {}).failed_items || [])[0] || {}).id || {}; const message = (response.data || {}).message || 'Unexpected error'; - const errorMessage = `${message} (${error.code}) - ${error.message} ${failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : ''}` - return Promise.reject(this.customErroMessage(null, errorMessage)); + + return Promise.reject( + `${message} (${error.code}) - ${error.message} ${ + failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : '' + }` + ); } return Promise.resolve(response); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(this.customErroMessage(error, error.data.message)) - : Promise.reject(this.customErroMessage(error, error.message)); + ? Promise.reject(error.data.message) + : Promise.reject(error.message || error); } } @@ -152,18 +156,4 @@ export class WzRequest { : Promise.reject(error.message || error); } } - - /** - * Customize message and return an error object - * @param error - * @param message - * @returns error - */ - static customErroMessage(error, message){ - if(!error || typeof error === 'string'){ - return new Error(message || error); - } - error.message = message - return error - } -} +} \ No newline at end of file From 0f9ae311e5f0e8d7e742ef4b42f044453ce0f57c Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Thu, 28 Apr 2022 16:35:21 -0300 Subject: [PATCH 469/493] Fixed promise reject on wz-request --- public/react-services/wz-request.ts | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index 69565ad959..ea6853421f 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -88,13 +88,13 @@ export class WzRequest { return this.genericReq(method, path, payload, customTimeout, false); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(error.data.message) - : Promise.reject(error.message || error); + ? Promise.reject(this.customErroMessage(error, error.data.message)) + : Promise.reject(this.customErroMessage(error, error.message)); } } return errorMessage - ? Promise.reject(errorMessage) - : Promise.reject(error || 'Server did not respond'); + ? Promise.reject(this.customErroMessage(error, errorMessage)) + : Promise.reject(this.customErroMessage(error,'Server did not respond')); } } @@ -121,18 +121,14 @@ export class WzRequest { const failed_ids = ((((response.data || {}).data || {}).failed_items || [])[0] || {}).id || {}; const message = (response.data || {}).message || 'Unexpected error'; - - return Promise.reject( - `${message} (${error.code}) - ${error.message} ${ - failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : '' - }` - ); + const errorMessage = `${message} (${error.code}) - ${error.message} ${failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : ''}` + return Promise.reject(this.customErroMessage(null, errorMessage)); } return Promise.resolve(response); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(error.data.message) - : Promise.reject(error.message || error); + ? Promise.reject(this.customErroMessage(error, error.data.message)) + : Promise.reject(this.customErroMessage(error, error.message)); } } @@ -156,4 +152,18 @@ export class WzRequest { : Promise.reject(error.message || error); } } -} \ No newline at end of file + + /** + * Customize message and return an error object + * @param error + * @param message + * @returns error + */ + static customErroMessage(error, message){ + if(!error || typeof error === 'string'){ + return new Error(message || error); + } + error.message = message + return error + } +} From 83f6a8702a00961d9d2b0dea021976bd0e9f958f Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Thu, 28 Apr 2022 16:38:02 -0300 Subject: [PATCH 470/493] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ba518de6..104cfe4e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) - Fixed the 'missing parameters' error on the Manager Logs [#4110](https://github.com/wazuh/wazuh-kibana-app/pull/4110) +- Fixed "See full error" on error toast [#4119](https://github.com/wazuh/wazuh-kibana-app/pull/4119) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From 26448cf9cd363f4ea54f76353e75b71f0c96b6bd Mon Sep 17 00:00:00 2001 From: Matias Ezequiel Moreno Date: Fri, 29 Apr 2022 03:47:48 -0300 Subject: [PATCH 471/493] Fix error when selecting date range in discover (#4115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed issue when selecting a time range * changelog * changelog: Moved PR to another entry Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 +- public/components/common/modules/discover/discover.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ba518de6..d163fb4ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,7 +158,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed a rendering problem in the map visualizations [#3942](https://github.com/wazuh/wazuh-kibana-app/pull/3942) - Parse error when using `#` character not at the beginning of the line [#3877](https://github.com/wazuh/wazuh-kibana-app/pull/3877) - Fixed the `rule.mitre.id` cell enhancement that doesn't support values with sub techniques [#3944](https://github.com/wazuh/wazuh-kibana-app/pull/3944) -- Fixed error not working the alerts displayed when changin the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) +- Fixed error not working the alerts displayed when changing the selected time in some flyouts [#3947](https://github.com/wazuh/wazuh-kibana-app/pull/3947) [#4115](https://github.com/wazuh/wazuh-kibana-app/pull/4115) - Fixed the user can not logout when the Kibana server has a basepath configurated [#3957](https://github.com/wazuh/wazuh-kibana-app/pull/3957) - Fixed fatal cron-job error when Wazuh API is down [#3991](https://github.com/wazuh/wazuh-kibana-app/pull/3991) - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) diff --git a/public/components/common/modules/discover/discover.tsx b/public/components/common/modules/discover/discover.tsx index e66b8c7d58..84b2cadfbd 100644 --- a/public/components/common/modules/discover/discover.tsx +++ b/public/components/common/modules/discover/discover.tsx @@ -390,8 +390,8 @@ export const Discover = compose( const range = { range: { timestamp: { - gte: this.state.dateRange.from, - lte: this.state.dateRange.to, + gte: dateParse(this.state.dateRange.from), + lte: dateParse(this.state.dateRange.to), format: 'epoch_millis', }, }, From 888389d135ff0282fb2462781938d260ee15fecf Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Fri, 29 Apr 2022 09:50:39 +0200 Subject: [PATCH 472/493] fix: add the spinner --- .../management/components/management/mg-logs/logs.js | 10 ++++++++-- .../components/management/ruleset/actions-buttons.js | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index f2d7332c87..30ef6cb322 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -90,6 +90,7 @@ export default compose( this.setState( { + generatingCsv: false, selectedNode, selectedDaemon: 'all', logLevelSelect: 'all', @@ -382,6 +383,7 @@ export default compose( exportFormatted = async () => { try { + this.setState({generatingCsv: true}) this.showToast('success', 'Your download should begin automatically...', 3000); const filters = this.buildFilters(); await exportCsv( @@ -401,9 +403,10 @@ export default compose( title: error.name, }, }; - + this.setState({generatingCsv: false}) getErrorOrchestrator().handleError(options); } + this.setState({generatingCsv: false}) }; header() { @@ -422,7 +425,10 @@ export default compose( - + Export formatted diff --git a/public/controllers/management/components/management/ruleset/actions-buttons.js b/public/controllers/management/components/management/ruleset/actions-buttons.js index 32db7bc546..a0296fc170 100644 --- a/public/controllers/management/components/management/ruleset/actions-buttons.js +++ b/public/controllers/management/components/management/ruleset/actions-buttons.js @@ -210,6 +210,7 @@ class WzRulesetActionButtons extends Component { iconType="exportAction" iconSide="left" onClick={async () => await this.generateCsv()} + isLoading={this.state.generatingCsv} > Export formatted From 809527293e2b170e5f643bc66ec4827dc4af3ece Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Fri, 29 Apr 2022 10:33:48 +0200 Subject: [PATCH 473/493] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ba518de6..b58bb01ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) - Fixed the 'missing parameters' error on the Manager Logs [#4110](https://github.com/wazuh/wazuh-kibana-app/pull/4110) +- Fixed not being able to remove custom filters. [#4112](https://github.com/wazuh/wazuh-kibana-app/pull/4112) +- Fixed spinner not showing when export button is clicked in management views [#4120](https://github.com/wazuh/wazuh-kibana-app/pull/4120) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From b4a4c9081919412bb6b8cc61970501017b6ce4a2 Mon Sep 17 00:00:00 2001 From: Ian Yenien Serrano <63758389+yenienserrano@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:09:21 +0200 Subject: [PATCH 474/493] The icon correct groups files (#4114) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(columns-file.js): separate show function from edit function * add changelog * suggested changes * changelog: Moved the PR entry Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + .../management/groups/utils/columns-files.js | 59 ++++++++++++------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d163fb4ecc..9eae8b7563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed messages in the agent installation guide [#4040](https://github.com/wazuh/wazuh-kibana-app/pull/4040) - Changed the default `wazuh.statistics.shards` setting from `2` to `1` [#4055](https://github.com/wazuh/wazuh-kibana-app/pull/4055) - Removed the migration tasks in the `.wazuh` and `.wazuh-version` indices [#4098](https://github.com/wazuh/wazuh-kibana-app/pull/4098) +- Separated the actions of viewing and editing the `agent.conf` group file [#4114](https://github.com/wazuh/wazuh-kibana-app/pull/4114) ### Fixed diff --git a/public/controllers/management/components/management/groups/utils/columns-files.js b/public/controllers/management/components/management/groups/utils/columns-files.js index f8e05aa625..46bedb5a08 100644 --- a/public/controllers/management/components/management/groups/utils/columns-files.js +++ b/public/controllers/management/components/management/groups/utils/columns-files.js @@ -2,6 +2,7 @@ import React from 'react'; import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; import GroupsHandler from '../utils/groups-handler'; import beautifier from '../../../../../../utils/json-beautifier'; +import { WzButtonPermissions } from '../../../../../../components/common/permissions/button'; export default class GroupsFilesColumns { constructor(tableProps) { @@ -10,6 +11,31 @@ export default class GroupsFilesColumns { const { itemDetail } = this.tableProps.state; this.groupsHandler = GroupsHandler; + this.actionFile = async (item, edit) => { + let result = await this.groupsHandler.getFileContent( + `/groups/${itemDetail.name}/files/${item.filename}/xml` + ); + + if (Object.keys(result).length == 0) { + result = ''; + } + + const data = edit + ? this.autoFormat(result) + : typeof result === 'object' + ? JSON.stringify(result, null, 2) + : result.toString(); + + const file = { + name: item.filename, + content: data, + isEditable: edit, + groupName: itemDetail.name, + }; + + this.tableProps.updateFileContent(file); + }; + this.buildColumns = () => { this.columns = [ { @@ -35,30 +61,21 @@ export default class GroupsFilesColumns { { - let result = await this.groupsHandler.getFileContent( - `/groups/${itemDetail.name}/files/${item.filename}/xml` - ); - if (Object.keys(result).length == 0) { - result = ''; - } - const isEditable = item.filename === 'agent.conf'; - const data = !isEditable - ? typeof result === 'object' - ? JSON.stringify(result, null, 2) - : result.toString() - : this.autoFormat(result); - const file = { - name: item.filename, - content: data, - isEditable: isEditable, - groupName: itemDetail.name - }; - this.tableProps.updateFileContent(file); - }} + onClick={() => this.actionFile(item, false)} color="primary" /> + {item.filename === 'agent.conf' && ( + this.actionFile(item, true)} + color="primary" + /> + )}
    ); } From 3bc4dbb9ae13687b27fbb45ba8ffcbe761cd8465 Mon Sep 17 00:00:00 2001 From: yenienserrano Date: Fri, 29 Apr 2022 17:39:18 +0200 Subject: [PATCH 475/493] suggested changes --- .../controllers/management/components/management/mg-logs/logs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/controllers/management/components/management/mg-logs/logs.js b/public/controllers/management/components/management/mg-logs/logs.js index 30ef6cb322..23c9e0a630 100644 --- a/public/controllers/management/components/management/mg-logs/logs.js +++ b/public/controllers/management/components/management/mg-logs/logs.js @@ -403,7 +403,6 @@ export default compose( title: error.name, }, }; - this.setState({generatingCsv: false}) getErrorOrchestrator().handleError(options); } this.setState({generatingCsv: false}) From f288e24d156b4bb286d4de9e65d85a892fa1092c Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Fri, 29 Apr 2022 13:59:30 -0300 Subject: [PATCH 476/493] Fixed not found FIM file toast error --- public/components/agents/fim/inventory/flyout.tsx | 2 +- public/components/agents/vuls/inventory/flyout.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/components/agents/fim/inventory/flyout.tsx b/public/components/agents/fim/inventory/flyout.tsx index 46dd758b74..8d85cbbaf6 100644 --- a/public/components/agents/fim/inventory/flyout.tsx +++ b/public/components/agents/fim/inventory/flyout.tsx @@ -91,7 +91,7 @@ export class FlyoutDetail extends Component { currentFile = ((((data || {}).data || {}).data || {}).affected_items || [])[0]; } if (!currentFile) { - throw false; + throw new Error('File not found'); } this.setState({ currentFile, type: currentFile.type, isLoading: false }); } catch (error) { diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 1859f1a38e..4687b08a3a 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -74,8 +74,8 @@ export class FlyoutDetail extends Component { this.setState({ clusterFilter }); const currentItem = this.props.item; - if (!currentItem) { - throw false; + if (!currentFile) { + throw new Error('File not found'); } const lastScan = await this.getLastScan(); From 4734571cb8db0749a0d867bd6ad0ee82098143c1 Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Fri, 29 Apr 2022 14:02:40 -0300 Subject: [PATCH 477/493] Updated CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22b7f69fb..71938b6372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,7 +166,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) - Fixed the 'missing parameters' error on the Manager Logs [#4110](https://github.com/wazuh/wazuh-kibana-app/pull/4110) - +- Fixed not found FIM file toast error #4124 [#4124](https://github.com/wazuh/wazuh-kibana-app/pull/4124) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 ### Added From 809aef48f66e4caddf93ad9fa289488a82347898 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Fri, 29 Apr 2022 19:14:08 +0200 Subject: [PATCH 478/493] Added pre-existing inputRef to constructor Signed-off-by: Federico Rodriguez --- public/components/wz-search-bar/lib/suggest-handler.ts | 4 +++- public/components/wz-search-bar/wz-search-bar.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/components/wz-search-bar/lib/suggest-handler.ts b/public/components/wz-search-bar/lib/suggest-handler.ts index 8fd9f4ba05..7b9b986e8e 100644 --- a/public/components/wz-search-bar/lib/suggest-handler.ts +++ b/public/components/wz-search-bar/lib/suggest-handler.ts @@ -41,7 +41,7 @@ export class SuggestHandler extends BaseHandler { ':': 'Equals', }; - constructor(props, setInputValue) { + constructor(props, setInputValue, inputRef) { super(); this.props = props; this.filters = props.filters; @@ -50,6 +50,8 @@ export class SuggestHandler extends BaseHandler { this.suggestItems = props.suggestions; this.searchType = 'search'; this.lastCall = 0; + if (inputRef) + this.inputRef = inputRef; } combine = (...args) => (input) => args.reduceRight((acc, arg) => (acc = arg(acc)), input); diff --git a/public/components/wz-search-bar/wz-search-bar.tsx b/public/components/wz-search-bar/wz-search-bar.tsx index 85c3190924..d922f9486d 100644 --- a/public/components/wz-search-bar/wz-search-bar.tsx +++ b/public/components/wz-search-bar/wz-search-bar.tsx @@ -101,7 +101,7 @@ function useSuggestHandler( useEffect(() => { setHandler( - new SuggestHandler({ ...props, status, setStatus, setInvalid, setIsOpen }, setInputValue) + new SuggestHandler({ ...props, status, setStatus, setInvalid, setIsOpen }, setInputValue, inputRef) ); !props.noDeleteFiltersOnUpdateSuggests && props.onFiltersChange([]); }, [props.suggestions]); From 5f658af21d7dff24db2c25fc0c6a83b32db3e786 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Fri, 29 Apr 2022 15:25:47 -0300 Subject: [PATCH 479/493] management -> groups -> agent: selectors appear when there are more than 3 options --- package.json | 2 -- .../agent/components/export-configuration.js | 13 ++++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 790da8b4b7..f3f2395d66 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "angular-animate": "1.7.8", "angular-chart.js": "1.1.1", "angular-material": "1.1.18", - "axios": "^0.21.1", "install": "^0.10.1", "js2xmlparser": "^3.0.0", "json2csv": "^4.1.2", @@ -52,7 +51,6 @@ "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", - "pdfmake": "0.1.65", "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", "react-codemirror": "^1.0.0", diff --git a/public/controllers/agent/components/export-configuration.js b/public/controllers/agent/components/export-configuration.js index bc0eb63ae5..0c3ee40f33 100644 --- a/public/controllers/agent/components/export-configuration.js +++ b/public/controllers/agent/components/export-configuration.js @@ -143,13 +143,12 @@ export const ExportConfiguration = withErrorBoundary (class ExportConfiguration compressed /> - this.selectAll(true)}> - Select all - - - this.selectAll(false)}> - Unselect all - + {this.options.length > 3 && + <> this.selectAll(true)}> + Select all + this.selectAll(false)}> + Unselect all + } Date: Fri, 29 Apr 2022 15:31:20 -0300 Subject: [PATCH 480/493] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 477fc2794c..49767d09e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added missing fields used in visualizations to the known fiels related to alerts [#3924](https://github.com/wazuh/wazuh-kibana-app/pull/3924) - Added troubleshooting link to "index pattern was refreshed" toast [#3946](https://github.com/wazuh/wazuh-kibana-app/pull/3946) - Added more number options to the tables widget in Modules -> "Mitre" [#4041](https://github.com/wazuh/wazuh-kibana-app/pull/4066) - +- Management -> groups -> agent: Selectors appear when there are more than 3 options [#4126](https://github.com/wazuh/wazuh-kibana-app/pull/4126) ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) From bfdea9810ecdbb8027d0f5f7b2e68411ec16f491 Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Fri, 29 Apr 2022 20:45:04 +0200 Subject: [PATCH 481/493] Added changelog Signed-off-by: Federico Rodriguez --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d22b7f69fb..1d741cfdb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,6 +166,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed agent breadcrumb routing minor error [#4101](https://github.com/wazuh/wazuh-kibana-app/pull/4101) - Fixed selected text not visible in API Console [#4102](https://github.com/wazuh/wazuh-kibana-app/pull/4102) - Fixed the 'missing parameters' error on the Manager Logs [#4110](https://github.com/wazuh/wazuh-kibana-app/pull/4110) +- Fixed undefined input reference when switching between rule set view and rule files view [#4125](https://github.com/wazuh/wazuh-kibana-app/pull/4125) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 From 03c64430ceba1d27f0d62016681732f0c4cbbeba Mon Sep 17 00:00:00 2001 From: Machi3mfl Date: Fri, 29 Apr 2022 15:57:19 -0300 Subject: [PATCH 482/493] Renamed function --- public/react-services/wz-request.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/public/react-services/wz-request.ts b/public/react-services/wz-request.ts index ea6853421f..a435736180 100644 --- a/public/react-services/wz-request.ts +++ b/public/react-services/wz-request.ts @@ -88,13 +88,13 @@ export class WzRequest { return this.genericReq(method, path, payload, customTimeout, false); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(this.customErroMessage(error, error.data.message)) - : Promise.reject(this.customErroMessage(error, error.message)); + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); } } return errorMessage - ? Promise.reject(this.customErroMessage(error, errorMessage)) - : Promise.reject(this.customErroMessage(error,'Server did not respond')); + ? Promise.reject(this.returnErrorInstance(error, errorMessage)) + : Promise.reject(this.returnErrorInstance(error,'Server did not respond')); } } @@ -122,13 +122,13 @@ export class WzRequest { ((((response.data || {}).data || {}).failed_items || [])[0] || {}).id || {}; const message = (response.data || {}).message || 'Unexpected error'; const errorMessage = `${message} (${error.code}) - ${error.message} ${failed_ids && failed_ids.length > 1 ? ` Affected ids: ${failed_ids} ` : ''}` - return Promise.reject(this.customErroMessage(null, errorMessage)); + return Promise.reject(this.returnErrorInstance(null, errorMessage)); } return Promise.resolve(response); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(this.customErroMessage(error, error.data.message)) - : Promise.reject(this.customErroMessage(error, error.message)); + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); } } @@ -148,8 +148,8 @@ export class WzRequest { return Promise.resolve(data); } catch (error) { return ((error || {}).data || {}).message || false - ? Promise.reject(error.data.message) - : Promise.reject(error.message || error); + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); } } @@ -159,7 +159,7 @@ export class WzRequest { * @param message * @returns error */ - static customErroMessage(error, message){ + static returnErrorInstance(error, message){ if(!error || typeof error === 'string'){ return new Error(message || error); } From 6fc840acced7fd1e80c1d0a555e0e8a7ddfcad00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:35:11 -0300 Subject: [PATCH 483/493] package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index f3f2395d66..1e800e42f3 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "angular-animate": "1.7.8", "angular-chart.js": "1.1.1", "angular-material": "1.1.18", + "axios": "^0.21.1", "install": "^0.10.1", "js2xmlparser": "^3.0.0", "json2csv": "^4.1.2", From c9821d573e0613d106e7169b8e13609cc17a8826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:38:06 -0300 Subject: [PATCH 484/493] package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1e800e42f3..1e6ad8dc09 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", + "pdfmake" : " 0.1.65 " , "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", "react-codemirror": "^1.0.0", From c26c0731f5fc3ce1b776b5d8c5630f09dcc1f503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:39:00 -0300 Subject: [PATCH 485/493] package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e6ad8dc09..bb6b78e552 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", - "pdfmake" : " 0.1.65 " , + "pdfmake" : "0.1.65" , "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", "react-codemirror": "^1.0.0", From 7641649a0bb5badfac5d22071fc001b924b1d546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:39:29 -0300 Subject: [PATCH 486/493] package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb6b78e552..8badee2ea1 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", - "pdfmake" : "0.1.65" , + "pdfmake" : "0.1.65", "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", "react-codemirror": "^1.0.0", From b210e13b0679b298c5f67eb40d264241a34eecee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:39:58 -0300 Subject: [PATCH 487/493] package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8badee2ea1..790da8b4b7 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "markdown-it-link-attributes": "^3.0.0", "needle": "^2.0.1", "node-cron": "^1.1.2", - "pdfmake" : "0.1.65", + "pdfmake": "0.1.65", "pug-loader": "^2.4.0", "querystring-browser": "1.0.4", "react-codemirror": "^1.0.0", From 4fb96799af32fec17d0e528ce0f4968e6ff75154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Tue, 3 May 2022 04:46:47 -0300 Subject: [PATCH 488/493] 4044 grammatical error and absence of capitalization in the state field (#4127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * status with the first letter capitalized * Correction of field name in the section: last registered agent * Changelog * Caps resolution done with textTransform * changelog: Moved PR entry Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 2 ++ .../components/management/status/status-agent-info.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1991af679..63eb3c74c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added troubleshooting link to "index pattern was refreshed" toast [#3946](https://github.com/wazuh/wazuh-kibana-app/pull/3946) - Added more number options to the tables widget in Modules -> "Mitre" [#4041](https://github.com/wazuh/wazuh-kibana-app/pull/4066) - Management -> groups -> agent: Selectors appear when there are more than 3 options [#4126](https://github.com/wazuh/wazuh-kibana-app/pull/4126) + ### Changed - Changed ossec to wazuh in sample-data [#3121](https://github.com/wazuh/wazuh-kibana-app/pull/3121) @@ -172,6 +173,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed "See full error" on error toast [#4119](https://github.com/wazuh/wazuh-kibana-app/pull/4119) - Fixed not being able to remove custom filters. [#4112](https://github.com/wazuh/wazuh-kibana-app/pull/4112) - Fixed spinner not showing when export button is clicked in management views [#4120](https://github.com/wazuh/wazuh-kibana-app/pull/4120) +- Correction of field and value in the section: last registered agent [#4127](https://github.com/wazuh/wazuh-kibana-app/pull/4127) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207 diff --git a/public/controllers/management/components/management/status/status-agent-info.js b/public/controllers/management/components/management/status/status-agent-info.js index 32f6b69c76..3171d6def0 100644 --- a/public/controllers/management/components/management/status/status-agent-info.js +++ b/public/controllers/management/components/management/status/status-agent-info.js @@ -75,14 +75,14 @@ export class WzStatusAgentInfo extends Component { Status - {agentInfo.status} + {agentInfo.status} IP Address {agentInfo.ip} - Date add + Date added {formatUIDate(agentInfo.dateAdd)} {status !== 'never_connected' && ( From d1ece911cf3bd1d54584366bf67256950e4931ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex?= Date: Tue, 3 May 2022 11:14:52 +0200 Subject: [PATCH 489/493] Improve the error handler on the configuration save (#4116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve error handler for the configuration save * Apply ESlint fixes * fix: Replace error message when saving the manager configuration file * changelog: Add PR entry to the changelog Co-authored-by: Antonio David Gutiérrez --- CHANGELOG.md | 1 + .../edit-configuration/edit-configuration.js | 40 +++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63eb3c74c7..8cfe3e02a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ All notable changes to the Wazuh app project will be documented in this file. [#3465](https://github.com/wazuh/wazuh-kibana-app/pull/3465) [#3464](https://github.com/wazuh/wazuh-kibana-app/pull/3464) [#3478](https://github.com/wazuh/wazuh-kibana-app/pull/3478) + [#4116](https://github.com/wazuh/wazuh-kibana-app/pull/4116) - Added Intelligence tab to Mitre Att&ck module [#3368](https://github.com/wazuh/wazuh-kibana-app/pull/3368) [#3344](https://github.com/wazuh/wazuh-kibana-app/pull/3344) [#3726](https://github.com/wazuh/wazuh-kibana-app/pull/3726) - Added sample data for office365 events [#3424](https://github.com/wazuh/wazuh-kibana-app/pull/3424) - Created a separate component to check for sample data [#3475](https://github.com/wazuh/wazuh-kibana-app/pull/3475) diff --git a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js index 6b214d00bd..ca4d2dc39f 100644 --- a/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js +++ b/public/controllers/management/components/management/configuration/edit-configuration/edit-configuration.js @@ -16,11 +16,9 @@ import PropTypes from 'prop-types'; import { EuiButton, EuiIcon, - EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, - EuiLoadingSpinner, EuiOverlayMask, EuiConfirmModal, EuiCallOut, @@ -50,8 +48,6 @@ import { getToasts } from '../../../../../..//kibana-services'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { AppState } from '../../../../../../react-services/app-state'; -import { ApiCheck } from '../../../../../../react-services/wz-api-check'; import { UI_LOGGER_LEVELS } from '../../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../../react-services/error-orchestrator/types'; @@ -97,10 +93,10 @@ class WzEditConfiguration extends Component { }); } catch (error) { let errorMessage; - if (error.details) { - errorMessage = `File ossec.conf saved, but there were found several error while validating the configuration. ${error.details}`; - } else { - errorMessage = 'Error saving configuration'; + if (error instanceof Error) { + errorMessage = error.details + ? `Configuration saved, but some validation errors were found.\n${error.details}` + : String(error); } this.setState({ saving: false, infoChangesAfterRestart: false }); const options = { @@ -109,8 +105,8 @@ class WzEditConfiguration extends Component { severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: errorMessage || error, - title: `${error.name}: Mitre alerts could not be fetched`, + message: errorMessage || error, + title: "Error found saving the file.", }, }; getErrorOrchestrator().handleError(options); @@ -122,19 +118,19 @@ class WzEditConfiguration extends Component { refresh() { try { this.checkIfClusterOrManager(); - }catch(error){ + } catch (error) { const options = { context: `${WzEditConfiguration.name}.refresh`, level: UI_LOGGER_LEVELS.ERROR, severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: error.message || error, - title: error.name || error + message: error.message || error, + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); - } + } } toggleRestart() { this.setState({ restart: !this.state.restart }); @@ -190,8 +186,8 @@ class WzEditConfiguration extends Component { severity: UI_ERROR_SEVERITIES.BUSINESS, error: { error: error, - message: error.message || error, - title: error.name || error + message: error.message || error, + title: error.name || error, }, }; getErrorOrchestrator().handleError(options); @@ -227,16 +223,12 @@ class WzEditConfiguration extends Component { // do nothing if it isn't a cluster this.props.updateClusterNodes(false); this.props.updateClusterNodeSelected(false); - this.props.updateConfigurationSection( - 'edit-configuration', - 'Manager configuration' - ); + this.props.updateConfigurationSection('edit-configuration', 'Manager configuration'); throw error; } } render() { const { restart, restarting, saving, editorValue, disableSaveRestartButtons } = this.state; - const initialValue = editorValue; const { clusterNodeSelected, agent } = this.props; const xmlError = editorValue && validateXML(editorValue); return ( @@ -311,7 +303,7 @@ class WzEditConfiguration extends Component { cancelButtonText="Cancel" confirmButtonText="Confirm" defaultFocusedButton="cancel" - > + /> )}
    @@ -368,8 +360,8 @@ const WzEditorConfiguration = compose( props.refreshTime !== prevProps.refreshTime ) )( + // eslint-disable-next-line react/no-multi-comp class WzEditorConfiguration extends Component { - constructor(props) { super(props); } @@ -429,5 +421,3 @@ const WzEditorConfiguration = compose( } } ); - - From ade4c5115d97db202813e3654f32e34e6a3d9e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 3 May 2022 11:31:30 +0200 Subject: [PATCH 490/493] fix: Changed the supported Kibana versions for the release of 4.3.0 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cfe3e02a4..d4dde2ee70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added -- Support for Kibana 7.16.3 -- Support for Kibana 7.17.0 +- Support for Kibana 7.16.x +- Support for Kibana 7.17.x - Added GitHub and Office365 modules [#3557](https://github.com/wazuh/wazuh-kibana-app/pull/3557) - Added a new `Panel` module tab for GitHub and Office365 modules [#3541](https://github.com/wazuh/wazuh-kibana-app/pull/3541) From eb27d0969c79b322723e49a688f44e9962206a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 4 May 2022 14:02:30 +0200 Subject: [PATCH 491/493] fix: Fixes an undefined variable when opening the flyout of vulnerability detail --- public/components/agents/vuls/inventory/flyout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/components/agents/vuls/inventory/flyout.tsx b/public/components/agents/vuls/inventory/flyout.tsx index 4687b08a3a..fc9aa97993 100644 --- a/public/components/agents/vuls/inventory/flyout.tsx +++ b/public/components/agents/vuls/inventory/flyout.tsx @@ -74,8 +74,8 @@ export class FlyoutDetail extends Component { this.setState({ clusterFilter }); const currentItem = this.props.item; - if (!currentFile) { - throw new Error('File not found'); + if (!currentItem) { + throw new Error('Vulnerability not found'); } const lastScan = await this.getLastScan(); From 5b5de9473223e08b9f96d87fce11796ba4d27dd6 Mon Sep 17 00:00:00 2001 From: MauGaP Date: Wed, 4 May 2022 19:28:30 -0300 Subject: [PATCH 492/493] modified the windows agent download command --- public/controllers/agent/components/register-agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js index 728ac01999..39347da2d4 100644 --- a/public/controllers/agent/components/register-agent.js +++ b/public/controllers/agent/components/register-agent.js @@ -485,7 +485,7 @@ export const RegisterAgent = withErrorBoundary( }-1.pkg && sudo launchctl setenv ${this.optionalDeploymentVariables()}&& sudo installer -pkg ./wazuh-agent-${this.state.wazuhVersion }.pkg -target /`, winText: `Invoke-WebRequest -Uri https://packages.wazuh.com/4.x/windows/wazuh-agent-${this.state.wazuhVersion - }-1.msi -OutFile wazuh-agent-${this.state.wazuhVersion}.msi; ./wazuh-agent-${this.state.wazuhVersion + }-1.msi -OutFile \${env:tmp}\\wazuh-agent-${this.state.wazuhVersion}.msi; msiexec.exe /i \${env:tmp}\\wazuh-agent-${this.state.wazuhVersion }.msi /q ${this.optionalDeploymentVariables()}`, }; From c99f608ca6e585b4cc4b43fc43454ed1eb86258b Mon Sep 17 00:00:00 2001 From: MauGaP Date: Wed, 4 May 2022 19:34:57 -0300 Subject: [PATCH 493/493] modified Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4dde2ee70..964619b2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,6 +175,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed not being able to remove custom filters. [#4112](https://github.com/wazuh/wazuh-kibana-app/pull/4112) - Fixed spinner not showing when export button is clicked in management views [#4120](https://github.com/wazuh/wazuh-kibana-app/pull/4120) - Correction of field and value in the section: last registered agent [#4127](https://github.com/wazuh/wazuh-kibana-app/pull/4127) +- Fixed the download agent installer command [#4132] (https://github.com/wazuh/wazuh-kibana-app/pull/4132) ## Wazuh v4.2.6 - Kibana 7.10.2, 7.11.2, 7.12.1, 7.13.0, 7.13.1, 7.13.2, 7.13.3, 7.13.4, 7.14.0, 7.14.1, 7.14.2 - Revision 4207