From 20e61a79bc7644d3bf91eac4ba25e13a2ac1d9af Mon Sep 17 00:00:00 2001 From: Luciano Gorza <103193307+lucianogorza@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:27:26 -0300 Subject: [PATCH] Create plugin wazuh check updates (#5897) * Add wazuh_check_updates plugin Signed-off-by: Luciano Gorza * Manage saved object for plugin configurations Signed-off-by: Luciano Gorza * New componente UpToDateStatus and improvements Signed-off-by: Luciano Gorza * Current update component and improvements Signed-off-by: Luciano Gorza * CurrentUpdateDetails improvements and others Signed-off-by: Luciano Gorza * Improvements with unit tests Signed-off-by: Luciano Gorza * Complete public unit tests Signed-off-by: Luciano Gorza * Unit test updatesNotification component Signed-off-by: Luciano Gorza * Backend unit tests Signed-off-by: Luciano Gorza * Add current update details to currentUpdateDetails component Signed-off-by: Luciano Gorza * Add translation to show details message Signed-off-by: Luciano Gorza * Add DismissNotificationCheck component Signed-off-by: Luciano Gorza * Modify styles Signed-off-by: Luciano Gorza * Expose new component Signed-off-by: Luciano Gorza * Update DismissNotificationCheck component Signed-off-by: Luciano Gorza * Add getCurrentUser implementation Signed-off-by: Luciano Gorza * Fix plugin start types Signed-off-by: Luciano Gorza * Fix checks ids Signed-off-by: Luciano Gorza * Delete console.log in component Signed-off-by: Luciano Gorza * Fixes in PR Signed-off-by: Luciano Gorza * Modify README Signed-off-by: Luciano Gorza * Delete unused files Signed-off-by: Luciano Gorza * Delete commented code Signed-off-by: Luciano Gorza * Removing unnecessary packages Signed-off-by: Luciano Gorza * Clear available updates value on error Signed-off-by: Luciano Gorza * Change constant name Signed-off-by: Luciano Gorza * Change message on new update Signed-off-by: Luciano Gorza * Change camelCase to kebab-case Signed-off-by: Luciano Gorza * Change plugin name using kebab-case Signed-off-by: Luciano Gorza * Add useEffect to component UpToDateStatus Signed-off-by: Luciano Gorza * Improve return errors in routes Signed-off-by: Luciano Gorza * Show update description in lines Signed-off-by: Luciano Gorza * Add Logger Signed-off-by: Luciano Gorza * Improve constant name Signed-off-by: Luciano Gorza * Improve check texts and links icons Signed-off-by: Luciano Gorza * Fix if statements Signed-off-by: Luciano Gorza * Change folder name in dev.yml using kebab-case Signed-off-by: Luciano Gorza * Update test snapshots Signed-off-by: Luciano Gorza * Add I18nProvider wrapper to components Signed-off-by: Luciano Gorza * Unit test for routes and kebab case fixes Signed-off-by: Luciano Gorza * Add toast when on get updates error Signed-off-by: Luciano Gorza * Omit username property on GET user preferences Signed-off-by: Luciano Gorza * Improve try catch finally Signed-off-by: Luciano Gorza * Add mock html_id_generator to component unit tests Signed-off-by: Luciano Gorza * Fix unit test get-user-preferences Signed-off-by: Luciano Gorza * Fix route unit tests port by adding a random port Signed-off-by: Luciano Gorza * Improved user preferences saved object Signed-off-by: Luciano Gorza * Fix toast message Signed-off-by: Luciano Gorza * Change port in route unit tests Signed-off-by: Luciano Gorza --------- Signed-off-by: Luciano Gorza --- docker/osd-dev/dev.yml | 59 +-- plugins/wazuh-check-updates/.i18nrc.json | 7 + plugins/wazuh-check-updates/README.md | 24 ++ .../wazuh-check-updates/common/constants.ts | 47 +++ plugins/wazuh-check-updates/common/types.ts | 29 ++ .../opensearch_dashboards.json | 9 + plugins/wazuh-check-updates/package.json | 28 ++ .../current-update-details.test.tsx.snap | 193 ++++++++++ .../dismiss-notification-check.test.tsx.snap | 26 ++ .../up-to-date-status.test.tsx.snap | 315 ++++++++++++++++ .../updates-notification.test.tsx.snap | 13 + .../current-update-details.test.tsx | 56 +++ .../components/current-update-details.tsx | 97 +++++ .../dismiss-notification-check.test.tsx | 45 +++ .../components/dismiss-notification-check.tsx | 42 +++ .../components/up-to-date-status.test.tsx | 202 +++++++++++ .../public/components/up-to-date-status.tsx | 144 ++++++++ .../components/updates-notification.test.tsx | 255 +++++++++++++ .../components/updates-notification.tsx | 134 +++++++ .../public/hooks/available-updates.test.ts | 113 ++++++ .../public/hooks/available-updates.ts | 50 +++ .../wazuh-check-updates/public/hooks/index.ts | 2 + .../public/hooks/user-preferences.test.ts | 94 +++++ .../public/hooks/user-preferences.ts | 50 +++ plugins/wazuh-check-updates/public/index.ts | 8 + .../public/plugin-services.ts | 5 + plugins/wazuh-check-updates/public/plugin.ts | 28 ++ plugins/wazuh-check-updates/public/types.ts | 16 + .../get-current-available-update.test.ts | 76 ++++ .../utils/get-current-available-update.ts | 18 + .../wazuh-check-updates/public/utils/index.ts | 2 + .../wazuh-check-updates/public/utils/time.ts | 15 + plugins/wazuh-check-updates/scripts/jest.js | 19 + .../server/cronjob/index.ts | 1 + .../server/cronjob/job-scheduler-run.test.ts | 45 +++ .../server/cronjob/job-scheduler-run.ts | 22 ++ plugins/wazuh-check-updates/server/index.ts | 11 + .../server/lib/base-logger.ts | 245 +++++++++++++ .../server/lib/filesystem.ts | 28 ++ .../server/lib/get-configuration.ts | 66 ++++ .../wazuh-check-updates/server/lib/logger.ts | 11 + .../factories/default-factory.ts | 21 ++ .../lib/security-factory/factories/index.ts | 2 + .../opensearch-dashboards-security-factory.ts | 29 ++ .../server/lib/security-factory/index.ts | 1 + .../lib/security-factory/security-factory.ts | 20 ++ .../server/plugin-services.ts | 7 + plugins/wazuh-check-updates/server/plugin.ts | 76 ++++ .../server/routes/index.ts | 8 + .../server/routes/updates/get-updates.test.ts | 131 +++++++ .../server/routes/updates/get-updates.ts | 59 +++ .../server/routes/updates/index.ts | 6 + .../get-user-preferences.test.ts | 85 +++++ .../user-preferences/get-user-preferences.ts | 42 +++ .../server/routes/user-preferences/index.ts | 8 + .../update-user-preferences.test.ts | 85 +++++ .../update-user-preferences.ts | 53 +++ .../saved-object/get-saved-object.test.ts | 41 +++ .../services/saved-object/get-saved-object.ts | 26 ++ .../server/services/saved-object/index.ts | 2 + .../saved-object/set-saved-object.test.ts | 31 ++ .../services/saved-object/set-saved-object.ts | 30 ++ .../saved-object/types/available-updates.ts | 51 +++ .../services/saved-object/types/index.ts | 3 + .../services/saved-object/types/settings.ts | 16 + .../saved-object/types/user-preferences.ts | 19 + .../services/settings/get-settings.test.ts | 51 +++ .../server/services/settings/get-settings.ts | 29 ++ .../server/services/settings/index.ts | 2 + .../services/settings/update-settings.test.ts | 39 ++ .../services/settings/update-settings.ts | 31 ++ .../server/services/updates/get-updates.ts | 36 ++ .../server/services/updates/index.ts | 1 + .../server/services/updates/mocks.ts | 52 +++ .../get-user-preferences.test.ts | 40 +++ .../user-preferences/get-user-preferences.ts | 27 ++ .../server/services/user-preferences/index.ts | 2 + .../update-user-preferences.test.ts | 51 +++ .../update-user-preferences.ts | 29 ++ plugins/wazuh-check-updates/server/types.ts | 8 + .../wazuh-check-updates/test/jest/config.js | 41 +++ .../translations/en-US.json | 95 +++++ plugins/wazuh-check-updates/tsconfig.json | 17 + plugins/wazuh-check-updates/yarn.lock | 336 ++++++++++++++++++ 84 files changed, 4360 insertions(+), 29 deletions(-) create mode 100644 plugins/wazuh-check-updates/.i18nrc.json create mode 100755 plugins/wazuh-check-updates/README.md create mode 100644 plugins/wazuh-check-updates/common/constants.ts create mode 100644 plugins/wazuh-check-updates/common/types.ts create mode 100644 plugins/wazuh-check-updates/opensearch_dashboards.json create mode 100644 plugins/wazuh-check-updates/package.json create mode 100644 plugins/wazuh-check-updates/public/components/__snapshots__/current-update-details.test.tsx.snap create mode 100644 plugins/wazuh-check-updates/public/components/__snapshots__/dismiss-notification-check.test.tsx.snap create mode 100644 plugins/wazuh-check-updates/public/components/__snapshots__/up-to-date-status.test.tsx.snap create mode 100644 plugins/wazuh-check-updates/public/components/__snapshots__/updates-notification.test.tsx.snap create mode 100644 plugins/wazuh-check-updates/public/components/current-update-details.test.tsx create mode 100644 plugins/wazuh-check-updates/public/components/current-update-details.tsx create mode 100644 plugins/wazuh-check-updates/public/components/dismiss-notification-check.test.tsx create mode 100644 plugins/wazuh-check-updates/public/components/dismiss-notification-check.tsx create mode 100644 plugins/wazuh-check-updates/public/components/up-to-date-status.test.tsx create mode 100644 plugins/wazuh-check-updates/public/components/up-to-date-status.tsx create mode 100644 plugins/wazuh-check-updates/public/components/updates-notification.test.tsx create mode 100644 plugins/wazuh-check-updates/public/components/updates-notification.tsx create mode 100644 plugins/wazuh-check-updates/public/hooks/available-updates.test.ts create mode 100644 plugins/wazuh-check-updates/public/hooks/available-updates.ts create mode 100644 plugins/wazuh-check-updates/public/hooks/index.ts create mode 100644 plugins/wazuh-check-updates/public/hooks/user-preferences.test.ts create mode 100644 plugins/wazuh-check-updates/public/hooks/user-preferences.ts create mode 100644 plugins/wazuh-check-updates/public/index.ts create mode 100644 plugins/wazuh-check-updates/public/plugin-services.ts create mode 100644 plugins/wazuh-check-updates/public/plugin.ts create mode 100644 plugins/wazuh-check-updates/public/types.ts create mode 100644 plugins/wazuh-check-updates/public/utils/get-current-available-update.test.ts create mode 100644 plugins/wazuh-check-updates/public/utils/get-current-available-update.ts create mode 100644 plugins/wazuh-check-updates/public/utils/index.ts create mode 100644 plugins/wazuh-check-updates/public/utils/time.ts create mode 100644 plugins/wazuh-check-updates/scripts/jest.js create mode 100644 plugins/wazuh-check-updates/server/cronjob/index.ts create mode 100644 plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.test.ts create mode 100644 plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.ts create mode 100644 plugins/wazuh-check-updates/server/index.ts create mode 100644 plugins/wazuh-check-updates/server/lib/base-logger.ts create mode 100644 plugins/wazuh-check-updates/server/lib/filesystem.ts create mode 100644 plugins/wazuh-check-updates/server/lib/get-configuration.ts create mode 100644 plugins/wazuh-check-updates/server/lib/logger.ts create mode 100644 plugins/wazuh-check-updates/server/lib/security-factory/factories/default-factory.ts create mode 100644 plugins/wazuh-check-updates/server/lib/security-factory/factories/index.ts create mode 100644 plugins/wazuh-check-updates/server/lib/security-factory/factories/opensearch-dashboards-security-factory.ts create mode 100644 plugins/wazuh-check-updates/server/lib/security-factory/index.ts create mode 100644 plugins/wazuh-check-updates/server/lib/security-factory/security-factory.ts create mode 100644 plugins/wazuh-check-updates/server/plugin-services.ts create mode 100644 plugins/wazuh-check-updates/server/plugin.ts create mode 100644 plugins/wazuh-check-updates/server/routes/index.ts create mode 100644 plugins/wazuh-check-updates/server/routes/updates/get-updates.test.ts create mode 100644 plugins/wazuh-check-updates/server/routes/updates/get-updates.ts create mode 100644 plugins/wazuh-check-updates/server/routes/updates/index.ts create mode 100644 plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.test.ts create mode 100644 plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.ts create mode 100644 plugins/wazuh-check-updates/server/routes/user-preferences/index.ts create mode 100644 plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.test.ts create mode 100644 plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/index.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/types/available-updates.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/types/index.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/types/settings.ts create mode 100644 plugins/wazuh-check-updates/server/services/saved-object/types/user-preferences.ts create mode 100644 plugins/wazuh-check-updates/server/services/settings/get-settings.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/settings/get-settings.ts create mode 100644 plugins/wazuh-check-updates/server/services/settings/index.ts create mode 100644 plugins/wazuh-check-updates/server/services/settings/update-settings.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/settings/update-settings.ts create mode 100644 plugins/wazuh-check-updates/server/services/updates/get-updates.ts create mode 100644 plugins/wazuh-check-updates/server/services/updates/index.ts create mode 100644 plugins/wazuh-check-updates/server/services/updates/mocks.ts create mode 100644 plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.ts create mode 100644 plugins/wazuh-check-updates/server/services/user-preferences/index.ts create mode 100644 plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.test.ts create mode 100644 plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.ts create mode 100644 plugins/wazuh-check-updates/server/types.ts create mode 100644 plugins/wazuh-check-updates/test/jest/config.js create mode 100644 plugins/wazuh-check-updates/translations/en-US.json create mode 100644 plugins/wazuh-check-updates/tsconfig.json create mode 100644 plugins/wazuh-check-updates/yarn.lock diff --git a/docker/osd-dev/dev.yml b/docker/osd-dev/dev.yml index 173b0bbed4..544b395d68 100755 --- a/docker/osd-dev/dev.yml +++ b/docker/osd-dev/dev.yml @@ -1,10 +1,10 @@ -version: "2.2" +version: '2.2' x-logging: &logging logging: driver: loki options: - loki-url: "http://host.docker.internal:3100/loki/api/v1/push" + loki-url: 'http://host.docker.internal:3100/loki/api/v1/push' services: exporter: @@ -12,15 +12,15 @@ services: <<: *logging hostname: exporter-osd-${OS_VERSION} profiles: - - "saml" - - "standard" + - 'saml' + - 'standard' networks: - os-dev - mon command: - - "--es.uri=https://admin:${PASSWORD}@os1:9200" - - "--es.ssl-skip-verify" - - "--es.all" + - '--es.uri=https://admin:${PASSWORD}@os1:9200' + - '--es.ssl-skip-verify' + - '--es.all' imposter: image: outofcoffee/imposter @@ -39,8 +39,8 @@ services: image: cfssl/cfssl <<: *logging profiles: - - "saml" - - "standard" + - 'saml' + - 'standard' volumes: - wi_certs:/certs/wi - wd_certs:/certs/wd @@ -117,7 +117,7 @@ services: sleep 300 ' healthcheck: - test: ["CMD-SHELL", "[ -r /certs/wi/os1.pem ]"] + test: ['CMD-SHELL', '[ -r /certs/wi/os1.pem ]'] interval: 2s timeout: 5s retries: 10 @@ -129,15 +129,15 @@ services: image: opensearchproject/opensearch:${OS_VERSION} <<: *logging profiles: - - "saml" - - "standard" + - 'saml' + - 'standard' environment: - cluster.name=os-dev-cluster - node.name=os1 - discovery.seed_hosts=os1 - cluster.initial_master_nodes=os1 - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping - - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM + - 'OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m' # minimum and maximum Java heap size, recommend setting both to 50% of system RAM - OPENSEARCH_PATH_CONF=/usr/share/opensearch/config/ ulimits: memlock: @@ -162,7 +162,7 @@ services: healthcheck: test: [ - "CMD-SHELL", + 'CMD-SHELL', "curl -v --cacert config/certs/ca.pem https://os1:9200 2>&1 | grep -q '401 Unauthorized'", ] interval: 1s @@ -175,17 +175,17 @@ services: condition: service_healthy image: elastic/filebeat:7.10.2 profiles: - - "saml" - - "standard" + - 'saml' + - 'standard' hostname: filebeat - user: "0:0" + user: '0:0' networks: - os-dev - mon <<: *logging # restart: always entrypoint: - - "/bin/bash" + - '/bin/bash' command: > -c ' mkdir -p /etc/filebeat @@ -211,23 +211,24 @@ services: condition: service_healthy image: quay.io/wazuh/osd-dev:${OSD_VERSION} profiles: - - "saml" - - "standard" + - 'saml' + - 'standard' hostname: osd networks: - os-dev - devel - mon - user: "1000:1000" + user: '1000:1000' <<: *logging ports: - ${OSD_PORT}:5601 environment: - - "LOGS=/proc/1/fd/1" - entrypoint: ["tail", "-f", "/dev/null"] + - 'LOGS=/proc/1/fd/1' + entrypoint: ['tail', '-f', '/dev/null'] volumes: - osd_cache:/home/node/.cache - - "${SRC}:/home/node/kbn/plugins/wazuh" + - '${SRC}/main:/home/node/kbn/plugins/wazuh' + - '${SRC}/wazuh-check-updates:/home/node/kbn/plugins/wazuh-check-updates' - wd_certs:/home/node/kbn/certs/ - ${WAZUH_DASHBOARD_CONF}:/home/node/kbn/config/opensearch_dashboards.yml - ./config/${OSD_MAJOR}/osd/wazuh.yml:/home/node/kbn/data/wazuh/config/wazuh.yml @@ -238,7 +239,7 @@ services: generator: condition: service_healthy profiles: - - "saml" + - 'saml' volumes: - wi_certs:/certs/wi - wd_certs:/certs/wd @@ -258,7 +259,7 @@ services: sleep 300 ' healthcheck: - test: ["CMD-SHELL", "[ -r /certs/idp/truststore.jks ]"] + test: ['CMD-SHELL', '[ -r /certs/idp/truststore.jks ]'] interval: 2s timeout: 5s retries: 10 @@ -269,14 +270,14 @@ services: idpsec: condition: service_healthy profiles: - - "saml" + - 'saml' hostname: idp <<: *logging networks: - os-dev - mon ports: - - "8080:8080" + - '8080:8080' environment: - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin @@ -298,7 +299,7 @@ services: idp: condition: service_healthy profiles: - - "saml" + - 'saml' hostname: idpsetup <<: *logging networks: diff --git a/plugins/wazuh-check-updates/.i18nrc.json b/plugins/wazuh-check-updates/.i18nrc.json new file mode 100644 index 0000000000..cd6b285378 --- /dev/null +++ b/plugins/wazuh-check-updates/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "wazuhCheckUpdates", + "paths": { + "wazuhCheckUpdates": "." + }, + "translations": ["translations/en-US.json"] +} diff --git a/plugins/wazuh-check-updates/README.md b/plugins/wazuh-check-updates/README.md new file mode 100755 index 0000000000..ca3bea0876 --- /dev/null +++ b/plugins/wazuh-check-updates/README.md @@ -0,0 +1,24 @@ +# Wazuh Check Updates Plugin + +The **Wazuh Check Updates Plugin** is a extension for Wazuh that allows you to seamlessly query an external service to retrieve information about the latest available updates and their corresponding features. This dedicated plugin has been designed to work in conjunction with the primary Wazuh plugin, enhancing its capabilities to notify users whenever new updates become accessible. With a focus on modularity, the Check Updates Plugin provides various components to manage updates and notification preferences. + +## Features + +### 1. Notification of New Updates + +The core functionality of the plugin is to notify users about the availability of new updates. It continuously queries an external service to check for updates and sends notifications to users when new updates are detected. + +### 2. Deployment Status + +Stay informed about the deployment status of updates. The plugin offers a component that allows you to easily view the status of updates. + +### 3. Update Details + +Get detailed information about the latest update, including links to release notes and updagrade guide, as well as the update description. + +## Software and libraries used + +- [OpenSearch](https://opensearch.org/) +- [Elastic UI Framework](https://eui.elastic.co/) +- [Node.js](https://nodejs.org) +- [React](https://reactjs.org) diff --git a/plugins/wazuh-check-updates/common/constants.ts b/plugins/wazuh-check-updates/common/constants.ts new file mode 100644 index 0000000000..29c6695ffd --- /dev/null +++ b/plugins/wazuh-check-updates/common/constants.ts @@ -0,0 +1,47 @@ +import path from 'path'; + +export const PLUGIN_ID = 'wazuhCheckUpdates'; +export const PLUGIN_NAME = 'wazuh_check_updates'; + +export const SAVED_OBJECT_UPDATES = 'wazuh-check-updates-available-updates'; +export const SAVED_OBJECT_SETTINGS = 'wazuh-check-updates-settings'; +export const SAVED_OBJECT_USER_PREFERENCES = 'wazuh-check-updates-user-preferences'; + +export const DEFAULT_SCHEDULE = '* */12 * * *'; + +export enum routes { + checkUpdates = '/api/wazuh-check-updates/updates', + userPreferences = '/api/wazuh-check-updates/user-preferences', +} + +// Security +export const WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY = + 'OpenSearch Dashboards Security'; + +// Default Elasticsearch user name context +export const ELASTIC_NAME = 'elastic'; + +// Wazuh data path +const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; +export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( + __dirname, + '../../../', + WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH +); +export const WAZUH_DATA_ABSOLUTE_PATH = path.join( + WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, + 'wazuh' +); + +// Wazuh data path - config +export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join(WAZUH_DATA_ABSOLUTE_PATH, 'config'); +export const WAZUH_DATA_CONFIG_APP_PATH = path.join(WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh.yml'); + +// 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_RAW_FILENAME = 'wazuhapp.log'; + +// App configuration +export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; diff --git a/plugins/wazuh-check-updates/common/types.ts b/plugins/wazuh-check-updates/common/types.ts new file mode 100644 index 0000000000..9723d7ea3f --- /dev/null +++ b/plugins/wazuh-check-updates/common/types.ts @@ -0,0 +1,29 @@ +export interface AvailableUpdates { + mayor: Update[]; + minor: Update[]; + patch: Update[]; + last_check?: Date | string | undefined; +} + +export interface Update { + description: string; + published_date: string; + semver: { + mayor: number; + minor: number; + patch: number; + }; + tag: string; + title: string; +} + +export interface UserPreferences { + last_dismissed_update?: string; + hide_update_notifications?: boolean; +} + +export interface CheckUpdatesSettings { + schedule?: string; +} + +export type savedObjectType = AvailableUpdates | UserPreferences | CheckUpdatesSettings; diff --git a/plugins/wazuh-check-updates/opensearch_dashboards.json b/plugins/wazuh-check-updates/opensearch_dashboards.json new file mode 100644 index 0000000000..1590d742da --- /dev/null +++ b/plugins/wazuh-check-updates/opensearch_dashboards.json @@ -0,0 +1,9 @@ +{ + "id": "wazuhCheckUpdates", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": ["navigation", "opensearchDashboardsUtils"], + "optionalPlugins": ["securityDashboards"] +} diff --git a/plugins/wazuh-check-updates/package.json b/plugins/wazuh-check-updates/package.json new file mode 100644 index 0000000000..a56bd77879 --- /dev/null +++ b/plugins/wazuh-check-updates/package.json @@ -0,0 +1,28 @@ +{ + "name": "wazuh-check-updates", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build", + "plugin-helpers": "node ../../scripts/plugin_helpers", + "osd": "node ../../scripts/osd", + "test:ui:runner": "node ../../scripts/functional_test_runner.js", + "test:server": "plugin-helpers test:server", + "test:browser": "plugin-helpers test:browser", + "test:jest": "node scripts/jest", + "test:jest:runner": "node scripts/runner test" + }, + "dependencies": { + "axios": "^1.5.0", + "axios-mock-adapter": "^1.21.5", + "md5": "^2.3.0", + "node-cron": "^3.0.2", + "winston": "^3.10.0" + }, + "devDependencies": { + "@testing-library/user-event": "^14.5.0", + "@types/": "testing-library/user-event", + "@types/md5": "^2.3.2", + "@types/node-cron": "^3.0.8" + } +} diff --git a/plugins/wazuh-check-updates/public/components/__snapshots__/current-update-details.test.tsx.snap b/plugins/wazuh-check-updates/public/components/__snapshots__/current-update-details.test.tsx.snap new file mode 100644 index 0000000000..ca5d07a489 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/__snapshots__/current-update-details.test.tsx.snap @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CurrentUpdateDetails component should render the current update tag and links to the Relese Notes and the Upgrade Guide 1`] = ` +
+
+
+ + +
+
+ Wazuh new release is available now! +
+
+ + + + 4.2.6 + + + +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+

+ Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements. +

+
+
+
+
+
+
+
+
+
+`; + +exports[`CurrentUpdateDetails component should return null when there is no current update 1`] = `
`; diff --git a/plugins/wazuh-check-updates/public/components/__snapshots__/dismiss-notification-check.test.tsx.snap b/plugins/wazuh-check-updates/public/components/__snapshots__/dismiss-notification-check.test.tsx.snap new file mode 100644 index 0000000000..ef72b03992 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/__snapshots__/dismiss-notification-check.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DismissNotificationCheck component should render the check 1`] = ` +
+
+ +
+ +
+
+`; + +exports[`DismissNotificationCheck component should return null when there is an error 1`] = `
`; diff --git a/plugins/wazuh-check-updates/public/components/__snapshots__/up-to-date-status.test.tsx.snap b/plugins/wazuh-check-updates/public/components/__snapshots__/up-to-date-status.test.tsx.snap new file mode 100644 index 0000000000..14cc6eca30 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/__snapshots__/up-to-date-status.test.tsx.snap @@ -0,0 +1,315 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpToDateStatus component should render a initial state with a loader and a loader button 1`] = ` +
+
+
+ +
+
+ +
+
+
+
+`; + +exports[`UpToDateStatus component should render a initial state with an error 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+ Error trying to get available updates +
+
+
+
+
+ + + + + +
+
+
+
+ +
+
+
+
+`; + +exports[`UpToDateStatus component should render the available updates status with a tooltip and a button to check updates without loaders 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+ Available updates +
+
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+`; + +exports[`UpToDateStatus component should retrieve available updates when click the button 1`] = ` +
+
+
+ +
+
+ +
+
+
+
+`; diff --git a/plugins/wazuh-check-updates/public/components/__snapshots__/updates-notification.test.tsx.snap b/plugins/wazuh-check-updates/public/components/__snapshots__/updates-notification.test.tsx.snap new file mode 100644 index 0000000000..5e205ef07e --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/__snapshots__/updates-notification.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UpdatesNotification component should return null when is loading 1`] = `
`; + +exports[`UpdatesNotification component should return null when there are no available updates 1`] = `
`; + +exports[`UpdatesNotification component should return null when user already dismissed the notifications for current update 1`] = `
`; + +exports[`UpdatesNotification component should return null when user close notification 1`] = `
`; + +exports[`UpdatesNotification component should return null when user dismissed notifications for future 1`] = `
`; + +exports[`UpdatesNotification component should return the nofication component 1`] = `
`; diff --git a/plugins/wazuh-check-updates/public/components/current-update-details.test.tsx b/plugins/wazuh-check-updates/public/components/current-update-details.test.tsx new file mode 100644 index 0000000000..865bd1bc1c --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/current-update-details.test.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { CurrentUpdateDetails } from './current-update-details'; + +jest.mock( + '../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +describe('CurrentUpdateDetails component', () => { + test('should render the current update tag and links to the Relese Notes and the Upgrade Guide', () => { + const { container, getByText, getByRole } = render( + // + + // + ); + + expect(container).toMatchSnapshot(); + + const elementWithTag = getByText('4.2.6'); + expect(elementWithTag).toBeInTheDocument(); + + const releaseNotesUrl = 'https://documentation.wazuh.com/4.2/release-notes/release-4-2-6.html'; + const releaseNotesLink = getByRole('link', { name: 'Release notes' }); + expect(releaseNotesLink).toHaveAttribute('href', releaseNotesUrl); + + const upgradeGuideUrl = `https://documentation.wazuh.com/4.2/upgrade-guide/index.html`; + const upgradeGuideLink = getByRole('link', { name: 'Upgrade guide' }); + expect(upgradeGuideLink).toHaveAttribute('href', upgradeGuideUrl); + }); + + test('should return null when there is no current update', () => { + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/components/current-update-details.tsx b/plugins/wazuh-check-updates/public/components/current-update-details.tsx new file mode 100644 index 0000000000..b004603773 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/current-update-details.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { + EuiAccordion, + EuiBadge, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiHeaderLink, + EuiText, + EuiSpacer, +} from '@elastic/eui'; +import { Update } from '../../common/types'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; + +export interface CurrentUpdateDetailsProps { + currentUpdate?: Update; +} + +export const CurrentUpdateDetails = ({ currentUpdate }: CurrentUpdateDetailsProps) => { + if (!currentUpdate) { + return null; + } + + const currentRelease = `${currentUpdate?.semver.mayor}.${currentUpdate?.semver.minor}`; + const releaseNotesUrl = `https://documentation.wazuh.com/${currentRelease}/release-notes/release-${currentUpdate.semver.mayor}-${currentUpdate.semver.minor}-${currentUpdate.semver.patch}.html`; + const upgradeGuideUrl = `https://documentation.wazuh.com/${currentRelease}/upgrade-guide/index.html`; + + return ( + + + + + + + {currentUpdate.tag} + + + } + color="warning" + iconType="bell" + > + + + + + + + + + + + + + + + } + paddingSize="m" + > + + {currentUpdate.description.split('\r\n').map((line, index) => ( +

{line}

+ ))} +
+
+
+
+ ); +}; diff --git a/plugins/wazuh-check-updates/public/components/dismiss-notification-check.test.tsx b/plugins/wazuh-check-updates/public/components/dismiss-notification-check.test.tsx new file mode 100644 index 0000000000..976bfe97b3 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/dismiss-notification-check.test.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { CurrentUpdateDetails } from './current-update-details'; +import { DismissNotificationCheck } from './dismiss-notification-check'; +import { useUserPreferences } from '../hooks'; + +jest.mock( + '../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const mockedUseUserPreferences = useUserPreferences as jest.Mock; +jest.mock('../hooks/user-preferences'); + +describe('DismissNotificationCheck component', () => { + test('should render the check', () => { + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.1' }, + })); + + const { container, getByText } = render(); + + expect(container).toMatchSnapshot(); + + const elementWithTag = getByText('Disable updates notifications'); + expect(elementWithTag).toBeInTheDocument(); + }); + + test('should return null when there is an error', () => { + mockedUseUserPreferences.mockImplementation(() => ({ + error: 'Error', + })); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/components/dismiss-notification-check.tsx b/plugins/wazuh-check-updates/public/components/dismiss-notification-check.tsx new file mode 100644 index 0000000000..9a54f64fe4 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/dismiss-notification-check.tsx @@ -0,0 +1,42 @@ +import { EuiCheckbox } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { useUserPreferences } from '../hooks'; + +export const DismissNotificationCheck = () => { + const [dismissFutureUpdates, setDismissFutureUpdates] = useState(); + + const { userPreferences, error, isLoading, updateUserPreferences } = useUserPreferences(); + + useEffect(() => { + if (isLoading) { + return; + } + setDismissFutureUpdates(userPreferences?.hide_update_notifications); + }, [userPreferences, isLoading]); + + if (error) { + return null; + } + + const handleOnChange = (checked: boolean) => { + updateUserPreferences({ hide_update_notifications: checked }); + }; + + return ( + + + } + checked={dismissFutureUpdates} + onChange={(e) => handleOnChange(e.target.checked)} + disabled={isLoading} + /> + + ); +}; diff --git a/plugins/wazuh-check-updates/public/components/up-to-date-status.test.tsx b/plugins/wazuh-check-updates/public/components/up-to-date-status.test.tsx new file mode 100644 index 0000000000..839c4267e0 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/up-to-date-status.test.tsx @@ -0,0 +1,202 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { UpToDateStatus } from './up-to-date-status'; +import { useAvailableUpdates } from '../hooks'; + +jest.mock( + '../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const mockedUseAvailabeUpdates = useAvailableUpdates as jest.Mock; +jest.mock('../hooks/available-updates'); + +jest.mock('../utils/get-current-available-update', () => ({ + getCurrentAvailableUpdate: jest.fn().mockReturnValue({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }), +})); + +jest.mock('../utils/time', () => ({ + formatUIDate: jest.fn().mockReturnValue('2023-09-18T14:00:00.000Z'), +})); + +describe('UpToDateStatus component', () => { + test('should render a initial state with a loader and a loader button', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ isLoading: true })); + + const { container, getByRole } = render( + ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + })} + /> + ); + + expect(container).toMatchSnapshot(); + + const loaders = container.getElementsByClassName('euiLoadingSpinner'); + expect(loaders.length).toBe(2); + + const checkUpdatesButton = getByRole('button', { name: 'Check updates' }); + expect(checkUpdatesButton).toBeInTheDocument(); + }); + + test('should render the available updates status with a tooltip and a button to check updates without loaders', async () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + availableUpdates: { + last_check: '2023-09-18T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }, + isLoading: false, + refreshAvailableUpdates: jest.fn().mockResolvedValue({}), + })); + + const { container, getByRole, getByText } = render( + ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + })} + /> + ); + + expect(container).toMatchSnapshot(); + + const checkUpdatesButton = getByRole('button', { name: 'Check updates' }); + expect(checkUpdatesButton).toBeInTheDocument(); + + const availableUpdates = getByText('Available updates'); + expect(availableUpdates).toBeInTheDocument(); + + const helpIcon = container.getElementsByClassName('euiToolTipAnchor'); + + await userEvent.hover(helpIcon[0]); + waitFor(() => { + expect(getByText('Last check')).toBeInTheDocument(); + expect(getByText('2023-09-18T14:00:00.000Z')).toBeInTheDocument(); + }); + + const loaders = container.getElementsByClassName('euiLoadingSpinner'); + expect(loaders.length).toBe(0); + }); + + test('should retrieve available updates when click the button', async () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ isLoading: true })); + + const { container, getByRole, getByText } = render( + ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + })} + /> + ); + + expect(container).toMatchSnapshot(); + + const checkUpdatesButton = getByRole('button', { name: 'Check updates' }); + expect(checkUpdatesButton).toBeInTheDocument(); + await userEvent.click(checkUpdatesButton); + waitFor(async () => { + const availableUpdates = getByText('Available updates'); + expect(availableUpdates).toBeInTheDocument(); + + const helpIcon = container.getElementsByClassName('euiToolTipAnchor'); + + await userEvent.hover(helpIcon[0]); + waitFor(() => { + expect(getByText('Last check')).toBeInTheDocument(); + expect(getByText('2023-09-18T14:00:00.000Z')).toBeInTheDocument(); + }); + + const loaders = container.getElementsByClassName('euiLoadingSpinner'); + expect(loaders.length).toBe(0); + }); + }); + + test('should render a initial state with an error', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + isLoading: false, + error: 'This is an error', + })); + + const { container, getByText } = render( + ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + })} + /> + ); + + expect(container).toMatchSnapshot(); + + const loaders = container.getElementsByClassName('euiLoadingSpinner'); + expect(loaders.length).toBe(0); + + const availableUpdates = getByText('Error trying to get available updates'); + expect(availableUpdates).toBeInTheDocument(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/components/up-to-date-status.tsx b/plugins/wazuh-check-updates/public/components/up-to-date-status.tsx new file mode 100644 index 0000000000..fa8b823abe --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/up-to-date-status.tsx @@ -0,0 +1,144 @@ +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastList, + EuiHealth, + EuiIconTip, + EuiLoadingSpinner, +} from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { useAvailableUpdates } from '../hooks'; +import { formatUIDate, getCurrentAvailableUpdate } from '../utils'; +import { Update } from '../../common/types'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; + +export interface UpToDateStatusProps { + setCurrentUpdate: (currentUpdate?: Update) => void; +} + +let toastId = 0; + +export const UpToDateStatus = ({ setCurrentUpdate }: UpToDateStatusProps) => { + const [toasts, setToasts] = useState([]); + + const addToastHandler = (error: any) => { + const toast = { + id: `${toastId++}`, + title: ( + + ), + color: 'danger', + iconType: 'alert', + text: error?.body?.message, + } as Toast; + setToasts(toasts.concat(toast)); + }; + + const removeToast = (removedToast: Toast) => { + setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); + }; + + const { availableUpdates, isLoading, refreshAvailableUpdates, error } = useAvailableUpdates(); + + const handleOnClick = async () => { + const response = await refreshAvailableUpdates(true, true); + if (response instanceof Error) { + addToastHandler(response); + } + }; + + useEffect(() => { + setCurrentUpdate(getCurrentAvailableUpdate(availableUpdates)); + }, [availableUpdates]); + + const currentUpdate = getCurrentAvailableUpdate(availableUpdates); + + const isUpToDate = !currentUpdate; + + const getI18nMessageId = () => { + if (error) { + return 'getAvailableUpdatesError'; + } + if (isUpToDate) { + return 'upToDate'; + } + return 'availableUpdates'; + }; + + const getDefaultMessage = () => { + if (error) { + return 'Error trying to get available updates'; + } + if (isUpToDate) { + return 'Up to date'; + } + return 'Available updates'; + }; + + const getColor = () => { + if (error) { + return 'danger'; + } + if (isUpToDate) { + return 'success'; + } + return 'warning'; + }; + + return ( + + + + {!isLoading ? ( + + + + + + + + + } + content={ + availableUpdates?.last_check + ? formatUIDate(new Date(availableUpdates.last_check)) + : '-' + } + iconProps={{ + className: 'eui-alignTop', + }} + /> + + + ) : ( + + )} + + + + + + + + + + ); +}; diff --git a/plugins/wazuh-check-updates/public/components/updates-notification.test.tsx b/plugins/wazuh-check-updates/public/components/updates-notification.test.tsx new file mode 100644 index 0000000000..01c8804391 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/updates-notification.test.tsx @@ -0,0 +1,255 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { useAvailableUpdates, useUserPreferences } from '../hooks'; +import { getCurrentAvailableUpdate } from '../utils'; +import { UpdatesNotification } from './updates-notification'; +import userEvent from '@testing-library/user-event'; + +jest.mock( + '../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', + () => ({ + htmlIdGenerator: () => () => 'htmlId', + }) +); + +const mockedUseAvailabeUpdates = useAvailableUpdates as jest.Mock; +jest.mock('../hooks/available-updates'); + +const mockedUseUserPreferences = useUserPreferences as jest.Mock; +jest.mock('../hooks/user-preferences'); + +const mockedGetCurrentAvailableUpdate = getCurrentAvailableUpdate as jest.Mock; +jest.mock('../utils/get-current-available-update'); + +describe('UpdatesNotification component', () => { + test('should return the nofication component', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + isLoading: false, + availableUpdates: { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + }, + ], + minor: [], + patch: [], + }, + })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.1' }, + })); + mockedGetCurrentAvailableUpdate.mockImplementation(() => ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + })); + + const { container, getByText, getByRole } = render(); + + expect(container).toMatchSnapshot(); + + const message = getByText('Wazuh new release is available now!'); + expect(message).toBeInTheDocument(); + + const elementWithTag = getByText('v4.2.6'); + expect(elementWithTag).toBeInTheDocument(); + + const releaseNotesUrl = 'https://documentation.wazuh.com/4.2/release-notes/release-4-2-6.html'; + const releaseNotesLink = getByRole('link', { name: 'Go to the release notes for details' }); + expect(releaseNotesLink).toHaveAttribute('href', releaseNotesUrl); + + const dismissCheck = getByText('Disable updates notifications'); + expect(dismissCheck).toBeInTheDocument(); + + const checkUpdatesButton = getByRole('button', { name: 'Close' }); + expect(checkUpdatesButton).toBeInTheDocument(); + }); + + test('should return null when user close notification', async () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + isLoading: false, + availableUpdates: { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + }, + ], + minor: [], + patch: [], + }, + })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.1' }, + updateUserPreferences: () => {}, + })); + mockedGetCurrentAvailableUpdate.mockImplementation(() => ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + })); + + const { container, getByRole } = render(); + + const closeButton = getByRole('button', { name: 'Close' }); + expect(closeButton).toBeInTheDocument(); + await userEvent.click(closeButton); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); + + test('should return null when is loading', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ isLoading: true })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: true, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.1' }, + })); + mockedGetCurrentAvailableUpdate.mockImplementation(() => undefined); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); + + test('should return null when there are no available updates', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ isLoading: false })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.1' }, + })); + mockedGetCurrentAvailableUpdate.mockImplementation(() => undefined); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); + + test('should return null when user dismissed notifications for future', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + isLoading: false, + availableUpdates: { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + }, + ], + minor: [], + patch: [], + }, + })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: true, last_dismissed_update: 'v4.2.1' }, + })); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); + + test('should return null when user already dismissed the notifications for current update', () => { + mockedUseAvailabeUpdates.mockImplementation(() => ({ + isLoading: false, + availableUpdates: { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + }, + ], + minor: [], + patch: [], + }, + })); + mockedUseUserPreferences.mockImplementation(() => ({ + isLoading: false, + userPreferences: { hide_update_notifications: false, last_dismissed_update: 'v4.2.6' }, + })); + mockedGetCurrentAvailableUpdate.mockImplementation(() => ({ + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: 'v4.2.6', + })); + + const { container } = render(); + + expect(container).toMatchSnapshot(); + + const firstChild = container.firstChild; + expect(firstChild).toBeNull(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/components/updates-notification.tsx b/plugins/wazuh-check-updates/public/components/updates-notification.tsx new file mode 100644 index 0000000000..021b3ecb83 --- /dev/null +++ b/plugins/wazuh-check-updates/public/components/updates-notification.tsx @@ -0,0 +1,134 @@ +import { + EuiBadge, + EuiBottomBar, + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiHeaderLink, + EuiText, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; +import { useAvailableUpdates, useUserPreferences } from '../hooks'; +import { getCurrentAvailableUpdate } from '../utils'; + +export const UpdatesNotification = () => { + const [isDismissed, setIsDismissed] = useState(false); + const [dismissFutureUpdates, setDismissFutureUpdates] = useState(false); + + const { + userPreferences, + error: userPreferencesError, + isLoading: isLoadingUserPreferences, + updateUserPreferences, + } = useUserPreferences(); + + const { + availableUpdates, + error: getAvailableUpdatesError, + isLoading: isLoadingAvailableUpdates, + } = useAvailableUpdates(); + + if (userPreferencesError) { + return null; + } + + if (getAvailableUpdatesError) { + return null; + } + + if (isLoadingAvailableUpdates || isLoadingUserPreferences) { + return null; + } + + const currentUpdate = getCurrentAvailableUpdate(availableUpdates); + + const hideNotification = + userPreferences?.hide_update_notifications || + userPreferences?.last_dismissed_update === currentUpdate?.tag; + + if (hideNotification) { + return null; + } + + const releaseNotesUrl = `https://documentation.wazuh.com/${currentUpdate?.semver.mayor}.${currentUpdate?.semver.minor}/release-notes/release-${currentUpdate?.semver.mayor}-${currentUpdate?.semver.minor}-${currentUpdate?.semver.patch}.html`; + const isVisible = !isDismissed && !!currentUpdate; + + const handleOnChangeDismiss = (checked: boolean) => { + setDismissFutureUpdates(checked); + }; + + const handleOnClose = () => { + updateUserPreferences({ + last_dismissed_update: currentUpdate?.tag, + ...(dismissFutureUpdates ? { hide_update_notifications: true } : {}), + }); + setIsDismissed(true); + }; + + return isVisible ? ( + + + + + + + + + + + + {currentUpdate.tag} + + + + { + + } + + + + + + + + + } + checked={dismissFutureUpdates} + onChange={(e) => handleOnChangeDismiss(e.target.checked)} + /> + + + handleOnClose()}> + + + + + + + + + ) : null; +}; diff --git a/plugins/wazuh-check-updates/public/hooks/available-updates.test.ts b/plugins/wazuh-check-updates/public/hooks/available-updates.test.ts new file mode 100644 index 0000000000..f39de11ffb --- /dev/null +++ b/plugins/wazuh-check-updates/public/hooks/available-updates.test.ts @@ -0,0 +1,113 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useAvailableUpdates } from './available-updates'; +import { AvailableUpdates } from '../../common/types'; +import { getCore } from '../plugin-services'; + +jest.mock('../plugin-services', () => ({ + getCore: jest.fn().mockReturnValue({ + http: { + get: jest.fn().mockResolvedValue({ + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }), + }, + }), +})); + +describe('useAvailableUpdates hook', () => { + test('should fetch initial data without any error', async () => { + const mockAvailableUpdates: AvailableUpdates = { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }; + + const { result, waitForNextUpdate } = renderHook(() => useAvailableUpdates()); + + expect(result.current.isLoading).toBeTruthy(); + + await waitForNextUpdate(); + expect(result.current.availableUpdates).toEqual(mockAvailableUpdates); + expect(result.current.isLoading).toBeFalsy(); + }); + + test('should update availableUpdates', async () => { + const mockAvailableUpdates: AvailableUpdates = { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }; + const { result, waitForNextUpdate } = renderHook(() => useAvailableUpdates()); + + await waitForNextUpdate(); + expect(result.current.isLoading).toBeFalsy(); + + act(() => { + result.current.refreshAvailableUpdates(true); + }); + + expect(result.current.isLoading).toBeTruthy(); + + await waitForNextUpdate(); + expect(result.current.availableUpdates).toEqual(mockAvailableUpdates); + expect(result.current.isLoading).toBeFalsy(); + }); + + test('should handle error while fetching data', async () => { + jest.setTimeout(30000); + const mockErrorMessage = 'Some error occurred'; + const core = getCore(); + core.http.get = jest.fn().mockRejectedValue(mockErrorMessage); + + const { result, waitForNextUpdate } = renderHook(() => useAvailableUpdates()); + + expect(result.current.isLoading).toBeTruthy(); + + await waitForNextUpdate(); + expect(result.current.error).toBe(mockErrorMessage); + expect(result.current.isLoading).toBeFalsy(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/hooks/available-updates.ts b/plugins/wazuh-check-updates/public/hooks/available-updates.ts new file mode 100644 index 0000000000..9d85fda2c3 --- /dev/null +++ b/plugins/wazuh-check-updates/public/hooks/available-updates.ts @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react'; +import { AvailableUpdates } from '../../common/types'; +import { routes } from '../../common/constants'; +import { getCore } from '../plugin-services'; + +export const useAvailableUpdates = () => { + const defaultAvailableUpdates = { + mayor: [], + minor: [], + patch: [], + }; + + const [availableUpdates, setAvailableUpdates] = useState( + defaultAvailableUpdates + ); + + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + const refreshAvailableUpdates = async (forceUpdate = false, returnError = false) => { + try { + setIsLoading(true); + const response = await getCore().http.get(`${routes.checkUpdates}`, { + query: { + checkAvailableUpdates: forceUpdate, + }, + }); + setAvailableUpdates(response); + setError(undefined); + } catch (error: any) { + setAvailableUpdates(defaultAvailableUpdates); + setError(error); + if (returnError) { + return error instanceof Error + ? error + : typeof error === 'string' + ? new Error(error) + : new Error('Error trying to get available updates'); + } + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + refreshAvailableUpdates(); + }, []); + + return { isLoading, availableUpdates, refreshAvailableUpdates, error }; +}; diff --git a/plugins/wazuh-check-updates/public/hooks/index.ts b/plugins/wazuh-check-updates/public/hooks/index.ts new file mode 100644 index 0000000000..025af1fddb --- /dev/null +++ b/plugins/wazuh-check-updates/public/hooks/index.ts @@ -0,0 +1,2 @@ +export { useAvailableUpdates } from './available-updates'; +export { useUserPreferences } from './user-preferences'; diff --git a/plugins/wazuh-check-updates/public/hooks/user-preferences.test.ts b/plugins/wazuh-check-updates/public/hooks/user-preferences.test.ts new file mode 100644 index 0000000000..75801e3156 --- /dev/null +++ b/plugins/wazuh-check-updates/public/hooks/user-preferences.test.ts @@ -0,0 +1,94 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUserPreferences } from './user-preferences'; +import { UserPreferences } from '../../common/types'; +import { getCore } from '../plugin-services'; + +jest.mock('../plugin-services', () => ({ + getCore: jest.fn().mockReturnValue({ + http: { + get: jest + .fn() + .mockResolvedValue({ last_dismissed_update: 'v4.3.1', hide_update_notifications: false }), + patch: jest + .fn() + .mockResolvedValue({ last_dismissed_update: 'v4.3.1', hide_update_notifications: false }), + }, + }), +})); + +describe('useUserPreferences hook', () => { + it('should fetch initial data without any error', async () => { + const mockUserPreferences: UserPreferences = { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }; + const { result, waitForNextUpdate } = renderHook(() => useUserPreferences()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.userPreferences).toEqual(mockUserPreferences); + expect(result.current.isLoading).toBeFalsy(); + }); + + it('should update user preferences', async () => { + const mockUserPreferences: UserPreferences = { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }; + const { result, waitForNextUpdate } = renderHook(() => useUserPreferences()); + + await waitForNextUpdate(); + expect(result.current.isLoading).toBeFalsy(); + + act(() => { + result.current.updateUserPreferences(mockUserPreferences); + }); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.userPreferences).toEqual(mockUserPreferences); + expect(result.current.isLoading).toBeFalsy(); + }); + + it('should handle error while fetching data', async () => { + jest.setTimeout(30000); + const mockErrorMessage = 'Some error occurred'; + const core = getCore(); + core.http.get = jest.fn().mockRejectedValue(mockErrorMessage); + + const { result, waitForNextUpdate } = renderHook(() => useUserPreferences()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.error).toBe(mockErrorMessage); + expect(result.current.isLoading).toBeFalsy(); + }); + + it('should handle error while updating user preferences', async () => { + const mockUserPreferences: UserPreferences = { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }; + const mockErrorMessage = 'Some error occurred'; + const mockPatch = jest + .spyOn(getCore().http, 'patch') + .mockImplementation(() => Promise.reject(mockErrorMessage)); + + const { result, waitForNextUpdate } = renderHook(() => useUserPreferences()); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.isLoading).toBeFalsy(); + + act(() => { + result.current.updateUserPreferences(mockUserPreferences); + }); + + expect(result.current.isLoading).toBeTruthy(); + await waitForNextUpdate(); + expect(result.current.error).toBe(mockErrorMessage); + expect(result.current.isLoading).toBeFalsy(); + + mockPatch.mockRestore(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/hooks/user-preferences.ts b/plugins/wazuh-check-updates/public/hooks/user-preferences.ts new file mode 100644 index 0000000000..227816c84c --- /dev/null +++ b/plugins/wazuh-check-updates/public/hooks/user-preferences.ts @@ -0,0 +1,50 @@ +import { useState, useEffect } from 'react'; +import { UserPreferences } from '../../common/types'; +import { getCore } from '../plugin-services'; +import { routes } from '../../common/constants'; + +export const useUserPreferences = () => { + const [userPreferences, setUserPreferences] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + const refreshUserPreferences = () => { + (async () => { + try { + setIsLoading(true); + const response = await getCore().http.get(routes.userPreferences); + setUserPreferences(response); + setError(undefined); + } catch (error: any) { + setError(error); + } finally { + setIsLoading(false); + } + })(); + }; + + useEffect(() => { + refreshUserPreferences(); + }, []); + + const updateUserPreferences = async (userPreferences: UserPreferences) => { + try { + setIsLoading(true); + await getCore().http.patch(routes.userPreferences, { + body: JSON.stringify(userPreferences), + }); + setUserPreferences(userPreferences); + } catch (error: any) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + return { + isLoading, + userPreferences, + updateUserPreferences, + error, + }; +}; diff --git a/plugins/wazuh-check-updates/public/index.ts b/plugins/wazuh-check-updates/public/index.ts new file mode 100644 index 0000000000..d1c86dcea6 --- /dev/null +++ b/plugins/wazuh-check-updates/public/index.ts @@ -0,0 +1,8 @@ +import { WazuhCheckUpdatesPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export function plugin() { + return new WazuhCheckUpdatesPlugin(); +} +export { WazuhCheckUpdatesPluginSetup, WazuhCheckUpdatesPluginStart } from './types'; diff --git a/plugins/wazuh-check-updates/public/plugin-services.ts b/plugins/wazuh-check-updates/public/plugin-services.ts new file mode 100644 index 0000000000..de618c9e18 --- /dev/null +++ b/plugins/wazuh-check-updates/public/plugin-services.ts @@ -0,0 +1,5 @@ +import { CoreStart, IUiSettingsClient } from 'opensearch-dashboards/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; + +export const [getCore, setCore] = createGetterSetter('Core'); +export const [getUiSettings, setUiSettings] = createGetterSetter('UiSettings'); diff --git a/plugins/wazuh-check-updates/public/plugin.ts b/plugins/wazuh-check-updates/public/plugin.ts new file mode 100644 index 0000000000..f19f8eaa7d --- /dev/null +++ b/plugins/wazuh-check-updates/public/plugin.ts @@ -0,0 +1,28 @@ +import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; +import { WazuhCheckUpdatesPluginSetup, WazuhCheckUpdatesPluginStart } from './types'; +import { UpdatesNotification } from './components/updates-notification'; +import { UpToDateStatus } from './components/up-to-date-status'; +import { setCore, setUiSettings } from './plugin-services'; +import { CurrentUpdateDetails } from './components/current-update-details'; +import { DismissNotificationCheck } from './components/dismiss-notification-check'; + +export class WazuhCheckUpdatesPlugin + implements Plugin { + public setup(core: CoreSetup): WazuhCheckUpdatesPluginSetup { + return {}; + } + + public start(core: CoreStart): WazuhCheckUpdatesPluginStart { + setCore(core); + setUiSettings(core.uiSettings); + + return { + UpdatesNotification, + UpToDateStatus, + CurrentUpdateDetails, + DismissNotificationCheck, + }; + } + + public stop() {} +} diff --git a/plugins/wazuh-check-updates/public/types.ts b/plugins/wazuh-check-updates/public/types.ts new file mode 100644 index 0000000000..75c61366fb --- /dev/null +++ b/plugins/wazuh-check-updates/public/types.ts @@ -0,0 +1,16 @@ +import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { CurrentUpdateDetailsProps } from './components/current-update-details'; +import { UpToDateStatusProps } from './components/up-to-date-status'; + +export interface WazuhCheckUpdatesPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhCheckUpdatesPluginStart { + UpdatesNotification: () => JSX.Element | null; + UpToDateStatus: (props: UpToDateStatusProps) => JSX.Element | null; + CurrentUpdateDetails: (props: CurrentUpdateDetailsProps) => JSX.Element | null; + DismissNotificationCheck: () => JSX.Element | null; +} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart; +} diff --git a/plugins/wazuh-check-updates/public/utils/get-current-available-update.test.ts b/plugins/wazuh-check-updates/public/utils/get-current-available-update.test.ts new file mode 100644 index 0000000000..6b33e78f4a --- /dev/null +++ b/plugins/wazuh-check-updates/public/utils/get-current-available-update.test.ts @@ -0,0 +1,76 @@ +import { getCurrentAvailableUpdate } from './get-current-available-update'; + +describe('getCurrentAvailableUpdate function', () => { + test('should return an available update', () => { + const mockAvailabeUpdates = { + mayor: [], + minor: [ + { + description: + '## Manager\r\n\r\n### Added\r\n\r\n- Added support for Arch Linux OS in Vulnerability Detector...', + published_date: '2022-05-05T16:06:52Z', + semver: { + mayor: 4, + minor: 3, + patch: 0, + }, + tag: 'v4.3.0', + title: 'Wazuh v4.3.0', + }, + { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + mayor: 4, + minor: 3, + patch: 1, + }, + tag: 'v4.3.1', + title: 'Wazuh v4.3.1', + }, + { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-06-20T13:20:11Z', + semver: { + mayor: 4, + minor: 3, + patch: 2, + }, + tag: 'v4.3.2', + title: 'Wazuh v4.3.2', + }, + ], + patch: [], + }; + + const currentUpdate = getCurrentAvailableUpdate(mockAvailabeUpdates); + expect(currentUpdate).toBeDefined(); + expect(currentUpdate).toEqual({ + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-06-20T13:20:11Z', + semver: { + mayor: 4, + minor: 3, + patch: 2, + }, + tag: 'v4.3.2', + title: 'Wazuh v4.3.2', + }); + }); + test('should return undefined', () => { + const mockNoAvailableUpdates = { + mayor: [], + minor: [], + patch: [], + }; + const currentUpdate = getCurrentAvailableUpdate(mockNoAvailableUpdates); + expect(currentUpdate).toBeUndefined(); + }); + test('should return undefined', () => { + const currentUpdate = getCurrentAvailableUpdate(); + expect(currentUpdate).toBeUndefined(); + }); +}); diff --git a/plugins/wazuh-check-updates/public/utils/get-current-available-update.ts b/plugins/wazuh-check-updates/public/utils/get-current-available-update.ts new file mode 100644 index 0000000000..730f05eb29 --- /dev/null +++ b/plugins/wazuh-check-updates/public/utils/get-current-available-update.ts @@ -0,0 +1,18 @@ +import { AvailableUpdates } from '../../common/types'; + +export const getCurrentAvailableUpdate = (availableUpdates: Partial = {}) => { + const { patch, minor, mayor } = availableUpdates; + + //TODO: Check real service to determinate the current update + + if (patch?.length) { + return patch[patch.length - 1]; + } + if (minor?.length) { + return minor[minor.length - 1]; + } + if (mayor?.length) { + return mayor[mayor.length - 1]; + } + return undefined; +}; diff --git a/plugins/wazuh-check-updates/public/utils/index.ts b/plugins/wazuh-check-updates/public/utils/index.ts new file mode 100644 index 0000000000..7036afbe8f --- /dev/null +++ b/plugins/wazuh-check-updates/public/utils/index.ts @@ -0,0 +1,2 @@ +export { getCurrentAvailableUpdate } from './get-current-available-update'; +export { formatUIDate } from './time'; diff --git a/plugins/wazuh-check-updates/public/utils/time.ts b/plugins/wazuh-check-updates/public/utils/time.ts new file mode 100644 index 0000000000..3ddf9b9880 --- /dev/null +++ b/plugins/wazuh-check-updates/public/utils/time.ts @@ -0,0 +1,15 @@ +import moment from 'moment-timezone'; +import { getUiSettings } from '../plugin-services'; + +export const formatUIDate = (date: Date) => { + const dateFormat = getUiSettings().get('dateFormat'); + const timezone = getTimeZone(); + const momentDate = moment(date); + momentDate.tz(timezone); + return momentDate.format(dateFormat); +}; +const getTimeZone = () => { + const dateFormatTZ = getUiSettings().get('dateFormat:tz'); + const detectedTimezone = moment.tz.guess(); + return dateFormatTZ === 'Browser' ? detectedTimezone : dateFormatTZ; +}; diff --git a/plugins/wazuh-check-updates/scripts/jest.js b/plugins/wazuh-check-updates/scripts/jest.js new file mode 100644 index 0000000000..cb58c54ec0 --- /dev/null +++ b/plugins/wazuh-check-updates/scripts/jest.js @@ -0,0 +1,19 @@ +// # Run Jest tests +// +// All args will be forwarded directly to Jest, e.g. to watch tests run: +// +// node scripts/jest --watch +// +// or to build code coverage: +// +// node scripts/jest --coverage +// +// 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)); diff --git a/plugins/wazuh-check-updates/server/cronjob/index.ts b/plugins/wazuh-check-updates/server/cronjob/index.ts new file mode 100644 index 0000000000..8cfcd84597 --- /dev/null +++ b/plugins/wazuh-check-updates/server/cronjob/index.ts @@ -0,0 +1 @@ +export { jobSchedulerRun } from './job-scheduler-run'; diff --git a/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.test.ts b/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.test.ts new file mode 100644 index 0000000000..5f7018860f --- /dev/null +++ b/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.test.ts @@ -0,0 +1,45 @@ +import cron from 'node-cron'; +import { getSettings } from '../services/settings'; +import { getUpdates } from '../services/updates'; +import { jobSchedulerRun } from './job-scheduler-run'; + +const mockedCron = cron.schedule as jest.Mock; +jest.mock('node-cron'); + +const mockedGetSettings = getSettings as jest.Mock; +jest.mock('../services/settings'); + +const mockedGetUpdates = getUpdates as jest.Mock; +jest.mock('../services/updates'); + +describe('jobSchedulerRun function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('schedule job to check updates', async () => { + mockedCron.mockImplementation(() => {}); + mockedGetSettings.mockImplementation(() => ({ schedule: '* * * * *' })); + mockedGetUpdates.mockImplementation(() => ({})); + + const response = await jobSchedulerRun(); + + expect(getSettings).toHaveBeenCalledTimes(1); + expect(getSettings).toHaveBeenCalledWith(); + + expect(cron.schedule).toHaveBeenCalledTimes(1); + expect(cron.schedule).toHaveBeenCalledWith('* * * * *', expect.any(Function)); + + expect(response).toBeUndefined(); + }); + + test('should return an error', async () => { + mockedGetSettings.mockRejectedValue(new Error('getSettings error')); + + const promise = jobSchedulerRun(); + + expect(getSettings).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSettings error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.ts b/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.ts new file mode 100644 index 0000000000..caa337adf7 --- /dev/null +++ b/plugins/wazuh-check-updates/server/cronjob/job-scheduler-run.ts @@ -0,0 +1,22 @@ +import cron from 'node-cron'; +import { DEFAULT_SCHEDULE } from '../../common/constants'; +import { getSettings } from '../services/settings'; +import { getUpdates } from '../services/updates'; +import { log } from '../lib/logger'; + +export const jobSchedulerRun = async () => { + try { + const settings = await getSettings(); + + cron.schedule(settings?.schedule || DEFAULT_SCHEDULE, () => getUpdates()); + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to schedule a cron job to get updates'; + log('wazuh-check-updates:jobSchedulerRun', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/index.ts b/plugins/wazuh-check-updates/server/index.ts new file mode 100644 index 0000000000..d667fc4c27 --- /dev/null +++ b/plugins/wazuh-check-updates/server/index.ts @@ -0,0 +1,11 @@ +import { PluginInitializerContext } from '../../../src/core/server'; +import { WazuhCheckUpdatesPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WazuhCheckUpdatesPlugin(initializerContext); +} + +export { WazuhCheckUpdatesPluginSetup, WazuhCheckUpdatesPluginStart } from './types'; diff --git a/plugins/wazuh-check-updates/server/lib/base-logger.ts b/plugins/wazuh-check-updates/server/lib/base-logger.ts new file mode 100644 index 0000000000..55f95e9f09 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/base-logger.ts @@ -0,0 +1,245 @@ +import winston, { LogEntry } from 'winston'; +import fs from 'fs'; +import path from 'path'; +import { getConfiguration } from './get-configuration'; +import { createDataDirectoryIfNotExists, createLogFileIfNotExists } from './filesystem'; + +import { WAZUH_DATA_LOGS_DIRECTORY_PATH, MAX_MB_LOG_FILES } from '../../common/constants'; + +export interface IUIPlainLoggerSettings { + level: string; + message?: string; + data?: any; +} + +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 = ''; + PLAIN_LOGS_FILE_NAME: string = ''; + RAW_LOGS_PATH: string = ''; + RAW_LOGS_FILE_NAME: string = ''; + + 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; + } + + /** + * 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: string) => { + 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: string) => { + 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. + */ + private checkFiles = () => { + createLogFileIfNotExists(this.RAW_LOGS_PATH); + createLogFileIfNotExists(this.PLAIN_LOGS_PATH); + if (this.allowed) { + // check raw log file + 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 + this.rotateFiles(this.PLAIN_LOGS_FILE_NAME, this.PLAIN_LOGS_PATH); + } + }; + + /** + * 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}`; + }; + + /** + * 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 {*} data Message or object to log + * @param {*} level Optional, default is 'error' + */ + async log(location: string, data: any, level?: string) { + const parsedData = this.parseData(data); + return this.initDirectory() + .then(() => { + if (this.allowed) { + this.checkFiles(); + const plainLogData: IUIPlainLoggerSettings = { + level: level || 'error', + message: `${this.yyyymmdd()}: ${location || 'Unknown origin'}: ${ + parsedData.toString() || 'An error occurred' + }`, + }; + + this.wazuhPlainLogger?.log(plainLogData as LogEntry); + + const logData: IUILoggerSettings = { + date: new Date(), + level: level || 'error', + location: location || 'Unknown origin', + data: parsedData || 'An error occurred', + }; + + if (typeof data == 'string') { + logData.message = parsedData; + delete logData.data; + } + + this.wazuhLogger?.log(logData as LogEntry); + } + }) + .catch((error) => { + console.error(`Cannot create the logs directory due to:\n${error.message || error}`); + throw error; + }); + } +} diff --git a/plugins/wazuh-check-updates/server/lib/filesystem.ts b/plugins/wazuh-check-updates/server/lib/filesystem.ts new file mode 100644 index 0000000000..ab102da9a7 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/filesystem.ts @@ -0,0 +1,28 @@ +import fs from 'fs'; +import path from 'path'; +import { WAZUH_DATA_ABSOLUTE_PATH } from '../../common/constants'; + +export const createDirectoryIfNotExists = (directory: string): void => { + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } +}; + +export const createLogFileIfNotExists = (filePath: string): void => { + if (!fs.existsSync(filePath)) { + 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; + if (!fs.existsSync(absoluteRoute)) { + fs.mkdirSync(absoluteRoute, { recursive: true }); + } +}; + +export const getDataDirectoryRelative = (directory: string) => { + return path.join(WAZUH_DATA_ABSOLUTE_PATH, directory); +}; diff --git a/plugins/wazuh-check-updates/server/lib/get-configuration.ts b/plugins/wazuh-check-updates/server/lib/get-configuration.ts new file mode 100644 index 0000000000..b13d3b243f --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/get-configuration.ts @@ -0,0 +1,66 @@ +import fs from 'fs'; +import yml from 'js-yaml'; +import { WAZUH_DATA_CONFIG_APP_PATH, WAZUH_CONFIGURATION_CACHE_TIME } from '../../common/constants'; + +let cachedConfiguration: any = null; +let lastAssign: number = new Date().getTime(); + +/** + * Get the plugin configuration and cache it. + * @param options.force Force to read the configuration and no use the cache . + * @returns plugin configuration in JSON + */ +export function getConfiguration(options: { force?: boolean } = {}) { + try { + const now = new Date().getTime(); + const dateDiffer = now - lastAssign; + if (!cachedConfiguration || dateDiffer >= WAZUH_CONFIGURATION_CACHE_TIME || options?.force) { + cachedConfiguration = obfuscateHostsConfiguration( + readPluginConfigurationFile(WAZUH_DATA_CONFIG_APP_PATH), + ['password'] + ); + + lastAssign = now; + } + return cachedConfiguration; + } catch (error) { + return false; + } +} + +/** + * Read the configuration file and transform to JSON. + * @param path File path of the plugin configuration file. + * @returns Configuration as JSON. + */ +function readPluginConfigurationFile(filepath: string) { + const content = fs.readFileSync(filepath, { encoding: 'utf-8' }); + return yml.load(content); +} + +/** + * Obfuscate fields of the hosts configuration. + * @param configuration Plugin configuration as JSON. + * @param obfuscateHostConfigurationKeys Keys to obfuscate its value in the hosts configuration. + * @returns + */ +function obfuscateHostsConfiguration(configuration: any, obfuscateHostConfigurationKeys: string[]) { + if (configuration.hosts) { + configuration.hosts = configuration.hosts.map((host: { [hostID: string]: any }) => { + const hostID = Object.keys(host)[0]; + return { + [hostID]: { + ...host[hostID], + ...obfuscateHostConfigurationKeys.reduce( + (accumObfuscateHostConfigurationKeys, obfuscateHostConfigurationKey) => ({ + ...accumObfuscateHostConfigurationKeys, + [obfuscateHostConfigurationKey]: '*****', + }), + {} + ), + }, + }; + }); + } + return configuration; +} diff --git a/plugins/wazuh-check-updates/server/lib/logger.ts b/plugins/wazuh-check-updates/server/lib/logger.ts new file mode 100644 index 0000000000..1f0ffb0856 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/logger.ts @@ -0,0 +1,11 @@ +import { BaseLogger } from './base-logger'; +import { + WAZUH_DATA_LOGS_PLAIN_FILENAME, + WAZUH_DATA_LOGS_RAW_FILENAME, +} from '../../common/constants'; + +const logger = new BaseLogger(WAZUH_DATA_LOGS_PLAIN_FILENAME, WAZUH_DATA_LOGS_RAW_FILENAME); + +export const log = (location: string, message: string, level?: string) => { + logger.log(location, message, level); +}; diff --git a/plugins/wazuh-check-updates/server/lib/security-factory/factories/default-factory.ts b/plugins/wazuh-check-updates/server/lib/security-factory/factories/default-factory.ts new file mode 100644 index 0000000000..60359470f3 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/security-factory/factories/default-factory.ts @@ -0,0 +1,21 @@ +import { ISecurityFactory } from '..'; +import { + OpenSearchDashboardsRequest, + RequestHandlerContext, +} from 'src/core/server'; +import { ELASTIC_NAME } from '../../../../common/constants'; +import md5 from 'md5'; + +export class DefaultFactory implements ISecurityFactory { + platform: string = ''; + async getCurrentUser( + request: OpenSearchDashboardsRequest, + context?: RequestHandlerContext, + ) { + return { + username: ELASTIC_NAME, + authContext: { username: ELASTIC_NAME }, + hashUsername: md5(ELASTIC_NAME), + }; + } +} diff --git a/plugins/wazuh-check-updates/server/lib/security-factory/factories/index.ts b/plugins/wazuh-check-updates/server/lib/security-factory/factories/index.ts new file mode 100644 index 0000000000..b02efdd30a --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/security-factory/factories/index.ts @@ -0,0 +1,2 @@ +export { OpenSearchDashboardsSecurityFactory } from './opensearch-dashboards-security-factory'; +export { DefaultFactory } from './default-factory'; \ No newline at end of file diff --git a/plugins/wazuh-check-updates/server/lib/security-factory/factories/opensearch-dashboards-security-factory.ts b/plugins/wazuh-check-updates/server/lib/security-factory/factories/opensearch-dashboards-security-factory.ts new file mode 100644 index 0000000000..ba290fbba3 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/security-factory/factories/opensearch-dashboards-security-factory.ts @@ -0,0 +1,29 @@ +import { ISecurityFactory } from '..'; +import { OpenSearchDashboardsRequest, RequestHandlerContext } from 'opensearch-dashboards/server'; +import md5 from 'md5'; +import { WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY } from '../../../../common/constants'; + +export class OpenSearchDashboardsSecurityFactory implements ISecurityFactory { + platform: string = WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY; + + async getCurrentUser(request: OpenSearchDashboardsRequest, context: RequestHandlerContext) { + try { + const params = { + path: `/_opendistro/_security/api/account`, + method: 'GET', + }; + + const { + body: authContext, + } = await context.core.opensearch.client.asCurrentUser.transport.request(params); + const username = this.getUserName(authContext); + return { username, authContext, hashUsername: md5(username) }; + } catch (error) { + throw error; + } + } + + getUserName(authContext: any) { + return authContext['user_name']; + } +} diff --git a/plugins/wazuh-check-updates/server/lib/security-factory/index.ts b/plugins/wazuh-check-updates/server/lib/security-factory/index.ts new file mode 100644 index 0000000000..629d004a60 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/security-factory/index.ts @@ -0,0 +1 @@ +export { ISecurityFactory, SecurityObj} from './security-factory'; \ No newline at end of file diff --git a/plugins/wazuh-check-updates/server/lib/security-factory/security-factory.ts b/plugins/wazuh-check-updates/server/lib/security-factory/security-factory.ts new file mode 100644 index 0000000000..f3168bb1c4 --- /dev/null +++ b/plugins/wazuh-check-updates/server/lib/security-factory/security-factory.ts @@ -0,0 +1,20 @@ +import { OpenSearchDashboardsSecurityFactory, DefaultFactory } from './factories'; +import { OpenSearchDashboardsRequest, RequestHandlerContext } from 'src/core/server'; +import { PluginSetup } from '../../types'; + +type CurrentUser = { + username?: string; + authContext: { [key: string]: any }; +}; + +export interface ISecurityFactory { + platform?: string; + getCurrentUser( + request: OpenSearchDashboardsRequest, + context?: RequestHandlerContext + ): Promise; +} + +export async function SecurityObj({ securityDashboards }: PluginSetup): Promise { + return !!securityDashboards ? new OpenSearchDashboardsSecurityFactory() : new DefaultFactory(); +} diff --git a/plugins/wazuh-check-updates/server/plugin-services.ts b/plugins/wazuh-check-updates/server/plugin-services.ts new file mode 100644 index 0000000000..137b6aa172 --- /dev/null +++ b/plugins/wazuh-check-updates/server/plugin-services.ts @@ -0,0 +1,7 @@ +import { CoreStart, ISavedObjectsRepository } from 'opensearch-dashboards/server'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; + +export const [getInternalSavedObjectsClient, setInternalSavedObjectsClient] = createGetterSetter< + ISavedObjectsRepository +>('SavedObjectsRepository'); +export const [getCore, setCore] = createGetterSetter('Core'); diff --git a/plugins/wazuh-check-updates/server/plugin.ts b/plugins/wazuh-check-updates/server/plugin.ts new file mode 100644 index 0000000000..0e909384a4 --- /dev/null +++ b/plugins/wazuh-check-updates/server/plugin.ts @@ -0,0 +1,76 @@ +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from 'opensearch-dashboards/server'; + +import { PluginSetup, WazuhCheckUpdatesPluginSetup, WazuhCheckUpdatesPluginStart } from './types'; +import { defineRoutes } from './routes'; +import { + availableUpdatesObject, + settingsObject, + userPreferencesObject, +} from './services/saved-object/types'; +import { setCore, setInternalSavedObjectsClient } from './plugin-services'; +import { jobSchedulerRun } from './cronjob'; +import { ISecurityFactory, SecurityObj } from './lib/security-factory'; + +declare module 'opensearch-dashboards/server' { + interface RequestHandlerContext { + wazuh_check_updates: { + logger: Logger; + security: ISecurityFactory; + }; + } +} + +export class WazuhCheckUpdatesPlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: PluginSetup) { + this.logger.debug('wazuh_check_updates: Setup'); + + const wazuhSecurity = await SecurityObj(plugins); + + core.http.registerRouteHandlerContext('wazuh_check_updates', () => { + return { + logger: this.logger, + security: wazuhSecurity, + }; + }); + + const router = core.http.createRouter(); + + // Register saved objects types + core.savedObjects.registerType(availableUpdatesObject); + core.savedObjects.registerType(settingsObject); + core.savedObjects.registerType(userPreferencesObject); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart): WazuhCheckUpdatesPluginStart { + this.logger.debug('wazuhCheckUpdates: Started'); + + const internalSavedObjectsClient = core.savedObjects.createInternalRepository(); + setCore(core); + setInternalSavedObjectsClient(internalSavedObjectsClient); + + // Scheduler + jobSchedulerRun(); + + return {}; + } + + public stop() {} +} diff --git a/plugins/wazuh-check-updates/server/routes/index.ts b/plugins/wazuh-check-updates/server/routes/index.ts new file mode 100644 index 0000000000..06a81ced57 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/index.ts @@ -0,0 +1,8 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { updatesRoutes } from './updates'; +import { userPreferencesRoutes } from './user-preferences'; + +export function defineRoutes(router: IRouter) { + updatesRoutes(router); + userPreferencesRoutes(router); +} diff --git a/plugins/wazuh-check-updates/server/routes/updates/get-updates.test.ts b/plugins/wazuh-check-updates/server/routes/updates/get-updates.test.ts new file mode 100644 index 0000000000..9c2d6c8e37 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/updates/get-updates.test.ts @@ -0,0 +1,131 @@ +import { Router } from '../../../../../src/core/server/http/router/router'; +import { HttpServer } from '../../../../../src/core/server/http/http_server'; +import { loggingSystemMock } from '../../../../../src/core/server/logging/logging_system.mock'; +import { ByteSizeValue } from '@osd/config-schema'; +import { getUpdates } from '../../services/updates'; +import { getSavedObject } from '../../services/saved-object'; +import { routes } from '../../../common/constants'; +import { getUpdatesRoute } from './get-updates'; +import axios from 'axios'; + +const serverAddress = '127.0.0.1'; +const port = 10002; //assign a different port in each unit test +axios.defaults.baseURL = `http://${serverAddress}:${port}`; + +const mockedGetUpdates = getUpdates as jest.Mock; +jest.mock('../../services/updates'); + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../../services/saved-object'); + +const loggingService = loggingSystemMock.create(); +const logger = loggingService.get(); +const context = { + wazuh: { + security: { + getCurrentUser: () => { + return { username: 'admin' }; + }, + }, + }, +}; +const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); +let server: HttpServer, innerServer: any; + +beforeAll(async () => { + // Create server + const config = { + name: 'plugin_platform', + host: serverAddress, + maxPayload: new ByteSizeValue(1024), + port, + ssl: { enabled: false }, + compression: { enabled: true }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any; + + server = new HttpServer(loggingService, 'tests'); + const router = new Router('', logger, enhanceWithContext); + const { registerRouter, server: innerServerTest } = await server.setup(config); + innerServer = innerServerTest; + + // Register routes + getUpdatesRoute(router); + + // Register router + registerRouter(router); + + // start server + await server.start(); +}); + +afterAll(async () => { + // Stop server + await server.stop(); + + // Clear all mocks + jest.clearAllMocks(); +}); + +describe(`[endpoint] GET ${routes.checkUpdates}`, () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('get available updates from saved object', async () => { + const mockResponse = { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }; + + mockedGetSavedObject.mockImplementation(() => mockResponse); + const response = await axios.get(routes.checkUpdates); + + expect(response.data).toEqual(mockResponse); + }); + + test('get available updates from the external service', async () => { + const mockResponse = { + last_check: '2021-09-30T14:00:00.000Z', + mayor: [ + { + title: 'Wazuh 4.2.6', + description: + 'Wazuh 4.2.6 is now available. This version includes several bug fixes and improvements.', + published_date: '2021-09-30T14:00:00.000Z', + semver: { + mayor: 4, + minor: 2, + patch: 6, + }, + tag: '4.2.6', + }, + ], + minor: [], + patch: [], + }; + + mockedGetUpdates.mockImplementation(() => mockResponse); + const response = await axios.get(`${routes.checkUpdates}?checkAvailableUpdates=true`); + + expect(response.data).toEqual(mockResponse); + }); +}); diff --git a/plugins/wazuh-check-updates/server/routes/updates/get-updates.ts b/plugins/wazuh-check-updates/server/routes/updates/get-updates.ts new file mode 100644 index 0000000000..1e593a1d07 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/updates/get-updates.ts @@ -0,0 +1,59 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { schema } from '@osd/config-schema'; +import { routes, SAVED_OBJECT_UPDATES } from '../../../common/constants'; +import { AvailableUpdates } from '../../../common/types'; +import { getUpdates } from '../../services/updates'; +import { getSavedObject } from '../../services/saved-object'; + +export const getUpdatesRoute = (router: IRouter) => { + router.get( + { + path: routes.checkUpdates, + validate: { + query: schema.object({ + checkAvailableUpdates: schema.maybe(schema.string()), + }), + }, + }, + async (context, request, response) => { + try { + const defaultValues = { + mayor: [], + minor: [], + patch: [], + }; + + if (request.query.checkAvailableUpdates === 'true') { + const updates = await getUpdates(); + return response.ok({ + body: { + ...defaultValues, + ...updates, + }, + }); + } + + const result = (await getSavedObject(SAVED_OBJECT_UPDATES)) as AvailableUpdates; + + return response.ok({ + body: { + ...defaultValues, + ...result, + }, + }); + } catch (error) { + const finalError = + error instanceof Error + ? error + : typeof error === 'string' + ? new Error(error) + : new Error('Error trying to get available updates'); + + return response.customError({ + statusCode: 503, + body: finalError, + }); + } + } + ); +}; diff --git a/plugins/wazuh-check-updates/server/routes/updates/index.ts b/plugins/wazuh-check-updates/server/routes/updates/index.ts new file mode 100644 index 0000000000..2f1b289981 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/updates/index.ts @@ -0,0 +1,6 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { getUpdatesRoute } from './get-updates'; + +export function updatesRoutes(router: IRouter) { + getUpdatesRoute(router); +} diff --git a/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.test.ts b/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.test.ts new file mode 100644 index 0000000000..c2d75354e5 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.test.ts @@ -0,0 +1,85 @@ +import { Router } from '../../../../../src/core/server/http/router/router'; +import { HttpServer } from '../../../../../src/core/server/http/http_server'; +import { loggingSystemMock } from '../../../../../src/core/server/logging/logging_system.mock'; +import { ByteSizeValue } from '@osd/config-schema'; +import { routes } from '../../../common/constants'; +import axios from 'axios'; +import { getUserPreferences } from '../../services/user-preferences'; +import { getUserPreferencesRoutes } from './get-user-preferences'; + +const serverAddress = '127.0.0.1'; +const port = 10003; //assign a different port in each unit test +axios.defaults.baseURL = `http://${serverAddress}:${port}`; + +const mockedGetUserPreferences = getUserPreferences as jest.Mock; +jest.mock('../../services/user-preferences'); + +const loggingService = loggingSystemMock.create(); +const logger = loggingService.get(); +const context = { + wazuh_check_updates: { + security: { + getCurrentUser: () => { + return { username: 'admin' }; + }, + }, + }, +}; +const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); +let server: HttpServer, innerServer: any; + +beforeAll(async () => { + // Create server + const config = { + name: 'plugin_platform', + host: serverAddress, + maxPayload: new ByteSizeValue(1024), + port, + ssl: { enabled: false }, + compression: { enabled: true }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any; + + server = new HttpServer(loggingService, 'tests'); + const router = new Router('', logger, enhanceWithContext); + const { registerRouter, server: innerServerTest } = await server.setup(config); + innerServer = innerServerTest; + + // Register routes + getUserPreferencesRoutes(router); + + // Register router + registerRouter(router); + + // start server + await server.start(); +}); + +afterAll(async () => { + // Stop server + await server.stop(); + + // Clear all mocks + jest.clearAllMocks(); +}); + +describe(`[endpoint] GET ${routes.userPreferences}`, () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('get user preferences', async () => { + const mockResponse = { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }; + + mockedGetUserPreferences.mockImplementation(() => mockResponse); + const response = await axios.get(routes.userPreferences); + + expect(response.data).toEqual(mockResponse); + }); +}); diff --git a/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.ts b/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.ts new file mode 100644 index 0000000000..dbc712ec3b --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/user-preferences/get-user-preferences.ts @@ -0,0 +1,42 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { routes } from '../../../common/constants'; +import { getUserPreferences } from '../../services/user-preferences'; + +export const getUserPreferencesRoutes = (router: IRouter) => { + router.get( + { + path: routes.userPreferences, + validate: false, + }, + async (context, request, response) => { + try { + const user = await context['wazuh_check_updates'].security.getCurrentUser(request, context); + + if (!user?.username) { + return response.customError({ + statusCode: 503, + body: new Error('Error trying to get username'), + }); + } + + const userPreferences = await getUserPreferences(user.username); + + return response.ok({ + body: userPreferences, + }); + } catch (error) { + const finalError = + error instanceof Error + ? error + : typeof error === 'string' + ? new Error(error) + : new Error('Error trying to get user preferences'); + + return response.customError({ + statusCode: 503, + body: finalError, + }); + } + } + ); +}; diff --git a/plugins/wazuh-check-updates/server/routes/user-preferences/index.ts b/plugins/wazuh-check-updates/server/routes/user-preferences/index.ts new file mode 100644 index 0000000000..f02df9c128 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/user-preferences/index.ts @@ -0,0 +1,8 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { getUserPreferencesRoutes } from './get-user-preferences'; +import { updateUserPreferencesRoutes } from './update-user-preferences'; + +export function userPreferencesRoutes(router: IRouter) { + getUserPreferencesRoutes(router); + updateUserPreferencesRoutes(router); +} diff --git a/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.test.ts b/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.test.ts new file mode 100644 index 0000000000..abdc80e74a --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.test.ts @@ -0,0 +1,85 @@ +import { Router } from '../../../../../src/core/server/http/router/router'; +import { HttpServer } from '../../../../../src/core/server/http/http_server'; +import { loggingSystemMock } from '../../../../../src/core/server/logging/logging_system.mock'; +import { ByteSizeValue } from '@osd/config-schema'; +import { routes } from '../../../common/constants'; +import axios from 'axios'; +import { updateUserPreferences } from '../../services/user-preferences'; +import { updateUserPreferencesRoutes } from './update-user-preferences'; + +const serverAddress = '127.0.0.1'; +const port = 10004; //assign a different port in each unit test +axios.defaults.baseURL = `http://${serverAddress}:${port}`; + +const mockedUpdateUserPreferences = updateUserPreferences as jest.Mock; +jest.mock('../../services/user-preferences'); + +const loggingService = loggingSystemMock.create(); +const logger = loggingService.get(); +const context = { + wazuh_check_updates: { + security: { + getCurrentUser: () => { + return { username: 'admin' }; + }, + }, + }, +}; +const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, context); +let server: HttpServer, innerServer: any; + +beforeAll(async () => { + // Create server + const config = { + name: 'plugin_platform', + host: serverAddress, + maxPayload: new ByteSizeValue(1024), + port, + ssl: { enabled: false }, + compression: { enabled: true }, + requestId: { + allowFromAnyIp: true, + ipAllowlist: [], + }, + } as any; + + server = new HttpServer(loggingService, 'tests'); + const router = new Router('', logger, enhanceWithContext); + const { registerRouter, server: innerServerTest } = await server.setup(config); + innerServer = innerServerTest; + + // Register routes + updateUserPreferencesRoutes(router); + + // Register router + registerRouter(router); + + // start server + await server.start(); +}); + +afterAll(async () => { + // Stop server + await server.stop(); + + // Clear all mocks + jest.clearAllMocks(); +}); + +describe(`[endpoint] PATCH ${routes.userPreferences}`, () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('update user preferences', async () => { + const mockResponse = { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }; + + mockedUpdateUserPreferences.mockImplementation(() => mockResponse); + const response = await axios.patch(routes.userPreferences); + + expect(response.data).toEqual(mockResponse); + }); +}); diff --git a/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.ts b/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.ts new file mode 100644 index 0000000000..3abc6a72c0 --- /dev/null +++ b/plugins/wazuh-check-updates/server/routes/user-preferences/update-user-preferences.ts @@ -0,0 +1,53 @@ +import { IRouter } from 'opensearch-dashboards/server'; +import { schema } from '@osd/config-schema'; +import { routes } from '../../../common/constants'; +import { updateUserPreferences } from '../../services/user-preferences'; + +export const updateUserPreferencesRoutes = (router: IRouter) => { + router.patch( + { + path: routes.userPreferences, + validate: { + body: schema.object({ + last_dismissed_update: schema.maybe(schema.string()), + hide_update_notifications: schema.maybe(schema.boolean()), + }), + }, + options: { + body: { + parse: true, + }, + }, + }, + async (context, request, response) => { + try { + const user = await context['wazuh_check_updates'].security.getCurrentUser(request, context); + + if (!user?.username) { + return response.customError({ + statusCode: 503, + body: new Error('Error trying to get username'), + }); + } + + const userPreferences = await updateUserPreferences(user.username, request.body); + + return response.ok({ + body: userPreferences, + }); + } catch (error) { + const finalError = + error instanceof Error + ? error + : typeof error === 'string' + ? new Error(error) + : new Error('Error trying to update user preferences'); + + return response.customError({ + statusCode: 503, + body: finalError, + }); + } + } + ); +}; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.test.ts b/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.test.ts new file mode 100644 index 0000000000..4c955569e1 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.test.ts @@ -0,0 +1,41 @@ +import { getInternalSavedObjectsClient } from '../../plugin-services'; +import { getSavedObject } from './get-saved-object'; + +const mockedGetInternalObjectsClient = getInternalSavedObjectsClient as jest.Mock; +jest.mock('../../plugin-services'); + +describe('getSavedObject function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return saved object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: () => ({ attributes: 'value' }), + })); + + const response = await getSavedObject('type'); + + expect(response).toEqual('value'); + }); + + test('should return an empty object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: jest.fn().mockRejectedValue({ output: { statusCode: 404 } }), + })); + + const response = await getSavedObject('type'); + + expect(response).toEqual({}); + }); + + test('should return an error', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: jest.fn().mockRejectedValue(new Error('getSavedObject error')), + })); + + const promise = getSavedObject('type'); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.ts b/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.ts new file mode 100644 index 0000000000..57c542e8ae --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/get-saved-object.ts @@ -0,0 +1,26 @@ +import { getInternalSavedObjectsClient } from '../../plugin-services'; +import { savedObjectType } from '../../../common/types'; +import { log } from '../../lib/logger'; + +export const getSavedObject = async (type: string, id?: string): Promise => { + try { + const client = getInternalSavedObjectsClient(); + + const responseGet = await client.get(type, id || type); + + const result = (responseGet?.attributes || {}) as savedObjectType; + return result; + } catch (error: any) { + if (error?.output?.statusCode === 404) { + return {}; + } + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get saved object'; + log('wazuh-check-updates:getSavedObject', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/index.ts b/plugins/wazuh-check-updates/server/services/saved-object/index.ts new file mode 100644 index 0000000000..cca5f45685 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/index.ts @@ -0,0 +1,2 @@ +export { getSavedObject } from './get-saved-object'; +export { setSavedObject } from './set-saved-object'; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.test.ts b/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.test.ts new file mode 100644 index 0000000000..cf1464fb5e --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.test.ts @@ -0,0 +1,31 @@ +import { getInternalSavedObjectsClient } from '../../plugin-services'; +import { setSavedObject } from './set-saved-object'; + +const mockedGetInternalObjectsClient = getInternalSavedObjectsClient as jest.Mock; +jest.mock('../../plugin-services'); + +describe('setSavedObject function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return saved object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + create: () => ({ attributes: { schedule: '* * * * *' } }), + })); + + const response = await setSavedObject('settings', { schedule: '* * * * *' }); + + expect(response).toEqual({ schedule: '* * * * *' }); + }); + + test('should return an error', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + create: jest.fn().mockRejectedValue(new Error('setSavedObject error')), + })); + + const promise = setSavedObject('settings', { schedule: '* * * * *' }); + + await expect(promise).rejects.toThrow('setSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.ts b/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.ts new file mode 100644 index 0000000000..20b1586d1c --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/set-saved-object.ts @@ -0,0 +1,30 @@ +import { savedObjectType } from '../../../common/types'; +import { log } from '../../lib/logger'; +import { getInternalSavedObjectsClient } from '../../plugin-services'; + +export const setSavedObject = async ( + type: string, + value: savedObjectType, + id?: string +): Promise => { + try { + const client = getInternalSavedObjectsClient(); + + const responseCreate = await client.create(type, value, { + id: id || type, + overwrite: true, + refresh: true, + }); + + return responseCreate?.attributes; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to update saved object'; + log('wazuh-check-updates:setSavedObject', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/types/available-updates.ts b/plugins/wazuh-check-updates/server/services/saved-object/types/available-updates.ts new file mode 100644 index 0000000000..4f1692d252 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/types/available-updates.ts @@ -0,0 +1,51 @@ +import { SavedObjectsFieldMapping, SavedObjectsType } from 'opensearch-dashboards/server'; +import { SAVED_OBJECT_UPDATES } from '../../../../common/constants'; + +const updateObjectType: SavedObjectsFieldMapping = { + type: 'nested', + properties: { + description: { + type: 'text', + }, + published_date: { + type: 'date', + }, + semver: { + type: 'nested', + properties: { + mayor: { + type: 'integer', + }, + minor: { + type: 'integer', + }, + patch: { + type: 'integer', + }, + }, + }, + tag: { + type: 'text', + }, + title: { + type: 'text', + }, + }, +}; + +export const availableUpdatesObject: SavedObjectsType = { + name: SAVED_OBJECT_UPDATES, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + last_check: { + type: 'date', + }, + mayor: updateObjectType, + minor: updateObjectType, + patch: updateObjectType, + }, + }, + migrations: {}, +}; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/types/index.ts b/plugins/wazuh-check-updates/server/services/saved-object/types/index.ts new file mode 100644 index 0000000000..062424fd05 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/types/index.ts @@ -0,0 +1,3 @@ +export { availableUpdatesObject } from './available-updates'; +export { settingsObject } from './settings'; +export { userPreferencesObject } from './user-preferences'; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/types/settings.ts b/plugins/wazuh-check-updates/server/services/saved-object/types/settings.ts new file mode 100644 index 0000000000..401f7ccb4a --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/types/settings.ts @@ -0,0 +1,16 @@ +import { SavedObjectsType } from 'opensearch-dashboards/server'; +import { SAVED_OBJECT_SETTINGS } from '../../../../common/constants'; + +export const settingsObject: SavedObjectsType = { + name: SAVED_OBJECT_SETTINGS, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + schedule: { + type: 'text', + }, + }, + }, + migrations: {}, +}; diff --git a/plugins/wazuh-check-updates/server/services/saved-object/types/user-preferences.ts b/plugins/wazuh-check-updates/server/services/saved-object/types/user-preferences.ts new file mode 100644 index 0000000000..128bacc7c7 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/saved-object/types/user-preferences.ts @@ -0,0 +1,19 @@ +import { SavedObjectsType } from 'opensearch-dashboards/server'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../../common/constants'; + +export const userPreferencesObject: SavedObjectsType = { + name: SAVED_OBJECT_USER_PREFERENCES, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + last_dismissed_update: { + type: 'text', + }, + hide_update_notifications: { + type: 'boolean', + }, + }, + }, + migrations: {}, +}; diff --git a/plugins/wazuh-check-updates/server/services/settings/get-settings.test.ts b/plugins/wazuh-check-updates/server/services/settings/get-settings.test.ts new file mode 100644 index 0000000000..5d50451145 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/settings/get-settings.test.ts @@ -0,0 +1,51 @@ +import { getSavedObject } from '../saved-object/get-saved-object'; +import { setSavedObject } from '../saved-object/set-saved-object'; +import { getSettings } from './get-settings'; +import { DEFAULT_SCHEDULE, SAVED_OBJECT_SETTINGS } from '../../../common/constants'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedSetSavedObject = setSavedObject as jest.Mock; +jest.mock('../saved-object/set-saved-object'); + +describe('getSettings function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return settings', async () => { + mockedGetSavedObject.mockImplementation(() => ({ schedule: '* * * * *' })); + + const response = await getSettings(); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_SETTINGS); + + expect(response).toEqual({ schedule: '* * * * *' }); + }); + + test('should return default settings', async () => { + mockedGetSavedObject.mockImplementation(() => ({})); + + mockedSetSavedObject.mockImplementation(() => {}); + + const response = await getSettings(); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_SETTINGS); + + expect(response).toEqual({ schedule: DEFAULT_SCHEDULE }); + }); + + test('should return an error', async () => { + mockedGetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + mockedSetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = getSettings(); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/settings/get-settings.ts b/plugins/wazuh-check-updates/server/services/settings/get-settings.ts new file mode 100644 index 0000000000..953a37a827 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/settings/get-settings.ts @@ -0,0 +1,29 @@ +import { DEFAULT_SCHEDULE, SAVED_OBJECT_SETTINGS } from '../../../common/constants'; +import { CheckUpdatesSettings } from '../../../common/types'; +import { log } from '../../lib/logger'; +import { getSavedObject, setSavedObject } from '../saved-object'; + +export const getSettings = async (): Promise => { + try { + const settings = (await getSavedObject(SAVED_OBJECT_SETTINGS)) as CheckUpdatesSettings; + + if (!settings?.schedule) { + const defaultSettings = { + schedule: DEFAULT_SCHEDULE, + }; + await setSavedObject(SAVED_OBJECT_SETTINGS, defaultSettings); + return defaultSettings; + } + + return settings; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get settings'; + log('wazuh-check-updates:getSettings', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/settings/index.ts b/plugins/wazuh-check-updates/server/services/settings/index.ts new file mode 100644 index 0000000000..a8a60dd1b4 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/settings/index.ts @@ -0,0 +1,2 @@ +export { getSettings } from './get-settings'; +export { updateSettings } from './update-settings'; diff --git a/plugins/wazuh-check-updates/server/services/settings/update-settings.test.ts b/plugins/wazuh-check-updates/server/services/settings/update-settings.test.ts new file mode 100644 index 0000000000..ef6bf3340b --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/settings/update-settings.test.ts @@ -0,0 +1,39 @@ +import { updateSettings } from '.'; +import { getSavedObject } from '../saved-object/get-saved-object'; +import { setSavedObject } from '../saved-object/set-saved-object'; +import { SAVED_OBJECT_SETTINGS } from '../../../common/constants'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedSetSavedObject = setSavedObject as jest.Mock; +jest.mock('../saved-object/set-saved-object'); + +describe('updateSettings function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return user preferences', async () => { + mockedGetSavedObject.mockImplementation(() => ({ schedule: '* * * * *' })); + + mockedSetSavedObject.mockImplementation(() => {}); + + const response = await updateSettings({ schedule: '* * * * *' }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_SETTINGS); + + expect(response).toEqual({ schedule: '* * * * *' }); + }); + + test('should return an error', async () => { + mockedSetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = updateSettings({ schedule: '* * * * *' }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/settings/update-settings.ts b/plugins/wazuh-check-updates/server/services/settings/update-settings.ts new file mode 100644 index 0000000000..cbbc0107a7 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/settings/update-settings.ts @@ -0,0 +1,31 @@ +import { getSettings } from '.'; +import { SAVED_OBJECT_SETTINGS } from '../../../common/constants'; +import { CheckUpdatesSettings } from '../../../common/types'; +import { log } from '../../lib/logger'; +import { setSavedObject } from '../saved-object'; + +export const updateSettings = async ( + settings: Partial +): Promise => { + try { + const savedSettings = await getSettings(); + + const newSettings = { + ...savedSettings, + ...settings, + }; + + await setSavedObject(SAVED_OBJECT_SETTINGS, newSettings); + + return newSettings; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to update settings'; + log('wazuh-check-updates:updateSettings', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/updates/get-updates.ts b/plugins/wazuh-check-updates/server/services/updates/get-updates.ts new file mode 100644 index 0000000000..3a3ba248df --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/updates/get-updates.ts @@ -0,0 +1,36 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { mockSuccessResponse } from './mocks'; +import { AvailableUpdates } from '../../../common/types'; +import { SAVED_OBJECT_UPDATES } from '../../../common/constants'; +import { setSavedObject } from '../saved-object'; +import { log } from '../../lib/logger'; + +export const getUpdates = async (): Promise => { + const mock = new MockAdapter(axios); + + try { + const updatesServiceUrl = `/api/updates`; + + mock.onGet(updatesServiceUrl).reply(200, mockSuccessResponse); + + const updatesResponse = await axios.get(updatesServiceUrl); + + const updates = updatesResponse?.data?.data || {}; + + const updatesToSave = { ...updates, last_check: new Date() }; + + await setSavedObject(SAVED_OBJECT_UPDATES, updatesToSave); + + return updatesToSave; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get available updates'; + log('wazuh-check-updates:getUpdates', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/updates/index.ts b/plugins/wazuh-check-updates/server/services/updates/index.ts new file mode 100644 index 0000000000..cecc732c86 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/updates/index.ts @@ -0,0 +1 @@ +export { getUpdates } from './get-updates'; diff --git a/plugins/wazuh-check-updates/server/services/updates/mocks.ts b/plugins/wazuh-check-updates/server/services/updates/mocks.ts new file mode 100644 index 0000000000..778d488476 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/updates/mocks.ts @@ -0,0 +1,52 @@ +import { AvailableUpdates } from '../../../common/types'; + +export const mockSuccessResponse: { data: Omit } = { + data: { + mayor: [], + minor: [ + { + description: + '## Manager\r\n\r\n### Added\r\n\r\n- Added support for Arch Linux OS in Vulnerability Detector...', + published_date: '2022-05-05T16:06:52Z', + semver: { + mayor: 4, + minor: 3, + patch: 0, + }, + tag: 'v4.3.0', + title: 'Wazuh v4.3.0', + }, + { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + mayor: 4, + minor: 3, + patch: 1, + }, + tag: 'v4.3.1', + title: 'Wazuh v4.3.1', + }, + { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-06-20T13:20:11Z', + semver: { + mayor: 4, + minor: 3, + patch: 2, + }, + tag: 'v4.3.2', + title: 'Wazuh v4.3.2', + }, + ], + patch: [], + }, +}; + +export const mockErrorResponse = { + errors: { + tag: ['is invalid'], + }, +}; diff --git a/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.test.ts b/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.test.ts new file mode 100644 index 0000000000..c404786f31 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.test.ts @@ -0,0 +1,40 @@ +import { getSavedObject } from '../saved-object/get-saved-object'; +import { getUserPreferences } from './get-user-preferences'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +describe('getUserPreferences function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return user preferences', async () => { + mockedGetSavedObject.mockImplementation(() => ({ + username: 'admin', + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + })); + + const response = await getUserPreferences('admin'); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_USER_PREFERENCES, 'admin'); + + expect(response).toEqual({ + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }); + }); + + test('should return an error', async () => { + mockedGetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = getUserPreferences('admin'); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.ts b/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.ts new file mode 100644 index 0000000000..2cd76766f4 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/user-preferences/get-user-preferences.ts @@ -0,0 +1,27 @@ +import _ from 'lodash'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { UserPreferences } from '../../../common/types'; +import { log } from '../../lib/logger'; +import { getSavedObject } from '../saved-object'; + +export const getUserPreferences = async (username: string): Promise => { + try { + const userPreferences = (await getSavedObject( + SAVED_OBJECT_USER_PREFERENCES, + username + )) as UserPreferences; + + const userPreferencesWithoutUsername = _.omit(userPreferences, 'username'); + + return userPreferencesWithoutUsername; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get user preferences'; + log('wazuh-check-updates:getUserPreferences', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/services/user-preferences/index.ts b/plugins/wazuh-check-updates/server/services/user-preferences/index.ts new file mode 100644 index 0000000000..b0011fdc48 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/user-preferences/index.ts @@ -0,0 +1,2 @@ +export { updateUserPreferences } from './update-user-preferences'; +export { getUserPreferences } from './get-user-preferences'; diff --git a/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.test.ts b/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.test.ts new file mode 100644 index 0000000000..05b40d0d05 --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.test.ts @@ -0,0 +1,51 @@ +import { updateUserPreferences } from '.'; +import { getSavedObject } from '../saved-object/get-saved-object'; +import { setSavedObject } from '../saved-object/set-saved-object'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedSetSavedObject = setSavedObject as jest.Mock; +jest.mock('../saved-object/set-saved-object'); + +describe('updateUserPreferences function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return user preferences', async () => { + mockedGetSavedObject.mockImplementation(() => ({ + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + })); + + mockedSetSavedObject.mockImplementation(() => {}); + + const response = await updateUserPreferences('admin', { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_USER_PREFERENCES, 'admin'); + + expect(response).toEqual({ + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }); + }); + + test('should return an error', async () => { + mockedSetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = updateUserPreferences('admin', { + last_dismissed_update: 'v4.3.1', + hide_update_notifications: false, + }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.ts b/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.ts new file mode 100644 index 0000000000..a2ddd7ae8f --- /dev/null +++ b/plugins/wazuh-check-updates/server/services/user-preferences/update-user-preferences.ts @@ -0,0 +1,29 @@ +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { UserPreferences } from '../../../common/types'; +import { log } from '../../lib/logger'; +import { getSavedObject, setSavedObject } from '../saved-object'; + +export const updateUserPreferences = async ( + username: string, + preferences: UserPreferences +): Promise => { + try { + const userPreferences = + ((await getSavedObject(SAVED_OBJECT_USER_PREFERENCES, username)) as UserPreferences) || {}; + + const newUserPreferences = { ...userPreferences, ...preferences }; + + await setSavedObject(SAVED_OBJECT_USER_PREFERENCES, newUserPreferences, username); + + return newUserPreferences; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to update user preferences'; + log('wazuh-check-updates:getUserPreferences', message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-check-updates/server/types.ts b/plugins/wazuh-check-updates/server/types.ts new file mode 100644 index 0000000000..b708280374 --- /dev/null +++ b/plugins/wazuh-check-updates/server/types.ts @@ -0,0 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhCheckUpdatesPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhCheckUpdatesPluginStart {} + +export type PluginSetup = { + securityDashboards?: {}; // TODO: Add OpenSearch Dashboards Security interface +}; diff --git a/plugins/wazuh-check-updates/test/jest/config.js b/plugins/wazuh-check-updates/test/jest/config.js new file mode 100644 index 0000000000..c49cd92aa0 --- /dev/null +++ b/plugins/wazuh-check-updates/test/jest/config.js @@ -0,0 +1,41 @@ +import path from 'path'; + +const kbnDir = path.resolve(__dirname, '../../../../'); + +export default { + rootDir: path.resolve(__dirname, '../..'), + roots: ['/public', '/server', '/common'], + modulePaths: [`${kbnDir}/node_modules`], + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', './!**/node_modules/**'], + moduleNameMapper: { + '^ui/(.*)': `${kbnDir}/src/ui/public/$1`, + // eslint-disable-next-line max-len + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kbnDir}/src/dev/jest/mocks/file_mock.js`, + '\\.(css|less|scss)$': `${kbnDir}/src/dev/jest/mocks/style_mock.js`, + axios: 'axios/dist/node/axios.cjs', + }, + setupFiles: [ + `${kbnDir}/src/dev/jest/setup/babel_polyfill.js`, + `${kbnDir}/src/dev/jest/setup/enzyme.js`, + ], + collectCoverage: true, + coverageDirectory: './target/test-coverage', + coverageReporters: ['html', 'text-summary', 'json-summary'], + globals: { + 'ts-jest': { + skipBabel: true, + }, + }, + moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'html'], + modulePathIgnorePatterns: ['__fixtures__/', 'target/'], + testMatch: ['**/*.test.{js,ts,tsx}'], + 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$'], + snapshotSerializers: [`${kbnDir}/node_modules/enzyme-to-json/serializer`], + testEnvironment: 'jest-environment-jsdom', + reporters: ['default', `${kbnDir}/src/dev/jest/junit_reporter.js`], +}; diff --git a/plugins/wazuh-check-updates/translations/en-US.json b/plugins/wazuh-check-updates/translations/en-US.json new file mode 100644 index 0000000000..75cab75567 --- /dev/null +++ b/plugins/wazuh-check-updates/translations/en-US.json @@ -0,0 +1,95 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": { + "wazuhCheckUpdates.updatesNotification.message": "Wazuh new release is available now!", + "wazuhCheckUpdates.updatesNotification.linkText": "Go to the release notes for details", + "wazuhCheckUpdates.updatesNotification.dismissCheckText": "Disable updates notifications", + "wazuhCheckUpdates.updatesNotification.closeButtonText": "Close", + "wazuhCheckUpdates.upToDateStatus.upToDate": "Up to date", + "wazuhCheckUpdates.upToDateStatus.availableUpdates": "Available updates", + "wazuhCheckUpdates.upToDateStatus.getAvailableUpdatesError": "Error trying to get available updates", + "wazuhCheckUpdates.upToDateStatus.lastCheck": "Last check", + "wazuhCheckUpdates.upToDateStatus.buttonText": "Check updates", + "wazuhCheckUpdates.upToDateStatus.onClickButtonError": "Error trying to get updates", + "wazuhCheckUpdates.currentUpdateDetails.releaseNotesLink": "Release notes", + "wazuhCheckUpdates.currentUpdateDetails.title": "Wazuh new release is available now!", + "wazuhCheckUpdates.currentUpdateDetails.upgradeGuideLink": "Upgrade guide", + "wazuhCheckUpdates.currentUpdateDetails.showDetails": "Show details", + "wazuhCheckUpdates.dismissNotificationCheck.checkText": "Disable updates notifications" + } +} diff --git a/plugins/wazuh-check-updates/tsconfig.json b/plugins/wazuh-check-updates/tsconfig.json new file mode 100644 index 0000000000..d3b63f9aee --- /dev/null +++ b/plugins/wazuh-check-updates/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + "public/hooks" + ], + "exclude": [] +} \ No newline at end of file diff --git a/plugins/wazuh-check-updates/yarn.lock b/plugins/wazuh-check-updates/yarn.lock new file mode 100644 index 0000000000..eed40ab640 --- /dev/null +++ b/plugins/wazuh-check-updates/yarn.lock @@ -0,0 +1,336 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@testing-library/user-event@^14.5.0": + version "14.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.0.tgz#4036add379525b635a64bce4d727820d4ba516a7" + integrity sha512-nQRCteEZvULJJrlcGQuNhwGekz25TOUILA+sTWI9PB/vNKKivS+7K7XRTwoikw/2fmJPaM4pPKy+hLWEGg9+JA== + +"@types/@testing-library/user-event": + version "0.0.0-semantically-released" + resolved "https://codeload.github.com/testing-library/user-event/tar.gz/4be87b3452f524bcc256d43cfb891ba1f0e236d6" + +"@types/md5@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.2.tgz#529bb3f8a7e9e9f621094eb76a443f585d882528" + integrity sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og== + +"@types/node-cron@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.8.tgz#c4d774b86bf8250d1e9046e08b17875c21ae64eb" + integrity sha512-+z5VrCvLwiJUohbRSgHdyZnHzAaLuD/E2bBANw+NQ1l05Crj8dIxb/kKK+OEqRitV2Wr/LYLuEBenGDsHZVV5Q== + +"@types/triple-beam@^1.3.2": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.3.tgz#726ae98a5f6418c8f24f9b0f2a9f81a8664876ae" + integrity sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g== + +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios-mock-adapter@^1.21.5: + version "1.21.5" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.21.5.tgz#dd85081717a759f88509c20515082dc09c1cedd7" + integrity sha512-5NI1V/VK+8+JeTF8niqOowuysA4b8mGzdlMN/QnTnoXbYh4HZSNiopsDclN2g/m85+G++IrEtUdZaQ3GnaMsSA== + dependencies: + fast-deep-equal "^3.1.3" + is-buffer "^2.0.5" + +axios@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.0.tgz#f02e4af823e2e46a9768cfc74691fdd0517ea267" + integrity sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +logform@^2.3.2, logform@^2.4.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.5.1.tgz#44c77c34becd71b3a42a3970c77929e52c6ed48b" + integrity sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg== + dependencies: + "@colors/colors" "1.5.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +node-cron@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.2.tgz#bb0681342bd2dfb568f28e464031280e7f06bd01" + integrity sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ== + dependencies: + uuid "8.3.2" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.10.0.tgz#d033cb7bd3ced026fed13bf9d92c55b903116803" + integrity sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g== + dependencies: + "@colors/colors" "1.5.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0"