diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d61dfa8fe..b31ba95654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh 4.9.0 - Added AngularJS dependencies [#6145](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6145) +- Added a migration task to setup the configuration using a configuration file [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) +- Added the ability to manage the API hosts from the Server APIs [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) - Added edit groups action to Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) - Added global actions add agents to groups and remove agents from groups to Endpoints Summary [#6274](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6274) - Added propagation of updates from the table to dashboard visualizations in Endpoints summary [#6460](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6460) @@ -17,6 +19,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120) [#6235](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6235) [#6254](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6254) [#6285](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6285) [#6288](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6288) [#6290](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6290) [#6289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6289) [#6286](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6286) [#6275](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6275) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6297](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6297) [#6287](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6291](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6287) [#6459](https://github.com/wazuh/wazuh-dashboard-plugins/pull/#6459) - Develop logic of a new index for the fim module [#6227](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6227) - Allow editing groups for an agent from Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) +- Changed as the configuration is defined and stored [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) +- Change the view of API is down and check connection to Server APIs application [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) - Changed the usage of the endpoint GET /groups/{group_id}/files/{file_name} [#6385](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6385) - Refactoring and redesign endpoints summary visualizations [#6268](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6268) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 0d30df7489..fdd4e7631b 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -11,8 +11,6 @@ */ import path from 'path'; import { version } from '../package.json'; -import { validate as validateNodeCronInterval } from 'node-cron'; -import { SettingsValidator } from '../common/services/settings-validator'; // Plugin export const PLUGIN_VERSION = version; @@ -62,10 +60,6 @@ export const WAZUH_FIM_PATTERN = 'wazuh-states-fim'; // Job - Wazuh initialize export const WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME = 'wazuh-kibana'; -// Permissions -export const WAZUH_ROLE_ADMINISTRATOR_ID = 1; -export const WAZUH_ROLE_ADMINISTRATOR_NAME = 'administrator'; - // Sample data export const WAZUH_SAMPLE_ALERT_PREFIX = 'wazuh-alerts-4.x-'; export const WAZUH_SAMPLE_ALERTS_INDEX_SHARDS = 1; @@ -135,10 +129,6 @@ export const WAZUH_DATA_CONFIG_DIRECTORY_PATH = path.join( WAZUH_DATA_ABSOLUTE_PATH, 'config', ); -export const WAZUH_DATA_CONFIG_APP_PATH = path.join( - WAZUH_DATA_CONFIG_DIRECTORY_PATH, - 'wazuh.yml', -); export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( WAZUH_DATA_CONFIG_DIRECTORY_PATH, 'wazuh-registry.json', @@ -392,73 +382,6 @@ export const NOT_TIME_FIELD_NAME_INDEX_PATTERN = // Customization export const CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES = 1048576; -// Plugin settings -export enum SettingCategory { - GENERAL, - HEALTH_CHECK, - MONITORING, - STATISTICS, - VULNERABILITIES, - SECURITY, - CUSTOMIZATION, -} - -type TPluginSettingOptionsTextArea = { - maxRows?: number; - minRows?: number; - maxLength?: number; -}; - -type TPluginSettingOptionsSelect = { - select: { text: string; value: any }[]; -}; - -type TPluginSettingOptionsEditor = { - editor: { - language: string; - }; -}; - -type TPluginSettingOptionsFile = { - file: { - type: 'image'; - extensions?: string[]; - size?: { - maxBytes?: number; - minBytes?: number; - }; - recommended?: { - dimensions?: { - width: number; - height: number; - unit: string; - }; - }; - store?: { - relativePathFileSystem: string; - filename: string; - resolveStaticURL: (filename: string) => string; - }; - }; -}; - -type TPluginSettingOptionsNumber = { - number: { - min?: number; - max?: number; - integer?: boolean; - }; -}; - -type TPluginSettingOptionsSwitch = { - switch: { - values: { - disabled: { label?: string; value: any }; - enabled: { label?: string; value: any }; - }; - }; -}; - export enum EpluginSettingType { text = 'text', textarea = 'textarea', @@ -467,1255 +390,11 @@ export enum EpluginSettingType { editor = 'editor', select = 'select', filepicker = 'filepicker', + password = 'password', + arrayOf = 'arrayOf', + custom = 'custom', } -export type TPluginSetting = { - // Define the text displayed in the UI. - title: string; - // Description. - description: string; - // Category. - category: SettingCategory; - // Type. - type: EpluginSettingType; - // Default value. - defaultValue: any; - // Default value if it is not set. It has preference over `default`. - defaultValueIfNotSet?: any; - // Configurable from the configuration file. - isConfigurableFromFile: boolean; - // Configurable from the UI (Settings/Configuration). - isConfigurableFromUI: boolean; - // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean; - // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean; - // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean; - // Define options related to the `type`. - options?: - | TPluginSettingOptionsEditor - | TPluginSettingOptionsFile - | TPluginSettingOptionsNumber - | TPluginSettingOptionsSelect - | TPluginSettingOptionsSwitch - | TPluginSettingOptionsTextArea; - // Transform the input value. The result is saved in the form global state of Settings/Configuration - uiFormTransformChangedInputValue?: (value: any) => any; - // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any; - // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any; - // Validate the value in the form of Settings/Configuration. It returns a string if there is some validation error. - validate?: (value: any) => string | undefined; - // Validate function creator to validate the setting in the backend. It uses `schema` of the `@kbn/config-schema` package. - validateBackend?: (schema: any) => (value: unknown) => string | undefined; -}; - -export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; -export type TPluginSettingCategory = { - title: string; - description?: string; - documentationLink?: string; - renderOrder?: number; -}; - -export const PLUGIN_SETTINGS_CATEGORIES: { - [category: number]: TPluginSettingCategory; -} = { - [SettingCategory.HEALTH_CHECK]: { - title: 'Health check', - description: "Checks will be executed by the app's Healthcheck.", - renderOrder: SettingCategory.HEALTH_CHECK, - }, - [SettingCategory.GENERAL]: { - title: 'General', - description: - 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', - renderOrder: SettingCategory.GENERAL, - }, - [SettingCategory.SECURITY]: { - title: 'Security', - description: 'Application security options such as unauthorized roles.', - renderOrder: SettingCategory.SECURITY, - }, - [SettingCategory.MONITORING]: { - title: 'Task:Monitoring', - description: - 'Options related to the agent status monitoring job and its storage in indexes.', - renderOrder: SettingCategory.MONITORING, - }, - [SettingCategory.STATISTICS]: { - title: 'Task:Statistics', - description: - 'Options related to the daemons manager monitoring job and their storage in indexes.', - renderOrder: SettingCategory.STATISTICS, - }, - [SettingCategory.VULNERABILITIES]: { - title: 'Vulnerabilities', - description: - 'Options related to the agent vulnerabilities monitoring job and its storage in indexes.', - renderOrder: SettingCategory.VULNERABILITIES, - }, - [SettingCategory.CUSTOMIZATION]: { - title: 'Custom branding', - description: - 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', - documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', - renderOrder: SettingCategory.CUSTOMIZATION, - }, -}; - -export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { - 'alerts.sample.prefix': { - title: 'Sample alerts prefix', - description: - 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: WAZUH_SAMPLE_ALERT_PREFIX, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - '*', - ), - ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - 'checks.api': { - title: 'API connection', - description: 'Enable or disable the API health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.fields': { - title: 'Known fields', - description: - 'Enable or disable the known fields health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.maxBuckets': { - title: 'Set max buckets to 200000', - description: - 'Change the default value of the plugin platform max buckets configuration.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.metaFields': { - title: 'Remove meta fields', - description: - 'Change the default value of the plugin platform metaField configuration.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.pattern': { - title: 'Index pattern', - description: - 'Enable or disable the index pattern health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.setup': { - title: 'API version', - description: - 'Enable or disable the setup health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.template': { - title: 'Index template', - description: - 'Enable or disable the template health check when opening the app.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'checks.timeFilter': { - title: 'Set time filter to 24h', - description: - 'Change the default value of the plugin platform timeFilter configuration.', - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'cron.prefix': { - title: 'Cron prefix', - description: 'Define the index prefix of predefined jobs.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - '*', - ), - ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - 'cron.statistics.apis': { - title: 'Includes APIs', - description: - 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.editor, - defaultValue: [], - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - editor: { - language: 'json', - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: any): any { - return JSON.stringify(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): any { - try { - return JSON.parse(value); - } catch (error) { - return value; - } - }, - validate: SettingsValidator.json( - SettingsValidator.compose( - SettingsValidator.array( - SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - ), - ), - ), - ), - validateBackend: function (schema) { - return schema.arrayOf( - schema.string({ - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - ), - }), - ); - }, - }, - 'cron.statistics.index.creation': { - title: 'Index creation', - description: 'Define the interval in which a new index will be created.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.select, - options: { - select: [ - { - text: 'Hourly', - value: 'h', - }, - { - text: 'Daily', - value: 'd', - }, - { - text: 'Weekly', - value: 'w', - }, - { - text: 'Monthly', - value: 'm', - }, - ], - }, - defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - validate: function (value) { - return SettingsValidator.literal( - this.options.select.map(({ value }) => value), - )(value); - }, - validateBackend: function (schema) { - return schema.oneOf( - this.options.select.map(({ value }) => schema.literal(value)), - ); - }, - }, - 'cron.statistics.index.name': { - title: 'Index name', - description: - 'Define the name of the index in which the documents will be saved.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.text, - defaultValue: WAZUH_STATISTICS_DEFAULT_NAME, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - '*', - ), - ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - 'cron.statistics.index.replicas': { - title: 'Index replicas', - description: - 'Define the number of replicas to use for the statistics indices.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.number, - defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 0, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function ( - value: number, - ): string { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'cron.statistics.index.shards': { - title: 'Index shards', - description: - 'Define the number of shards to use for the statistics indices.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.number, - defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 1, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'cron.statistics.interval': { - title: 'Interval', - description: - 'Define the frequency of task execution using cron schedule expressions.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.text, - defaultValue: WAZUH_STATISTICS_DEFAULT_CRON_FREQ, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRestartingPluginPlatform: true, - validate: function (value: string) { - return validateNodeCronInterval(value) - ? undefined - : 'Interval is not valid.'; - }, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - 'cron.statistics.status': { - title: 'Status', - description: 'Enable or disable the statistics tasks.', - category: SettingCategory.STATISTICS, - type: EpluginSettingType.switch, - defaultValue: WAZUH_STATISTICS_DEFAULT_STATUS, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'customization.enabled': { - title: 'Status', - description: 'Enable or disable the customization.', - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'customization.logo.app': { - title: 'App main logo', - description: `This logo is used as loading indicator while the user is logging into Wazuh API.`, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.app', - resolveStaticURL: (filename: string) => - `custom/images/${filename}?v=${Date.now()}`, - // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - }, - }, - }, - validate: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...this.options.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, - ), - )(value); - }, - }, - 'customization.logo.healthcheck': { - title: 'Healthcheck logo', - description: `This logo is displayed during the Healthcheck routine of the app.`, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.healthcheck', - resolveStaticURL: (filename: string) => - `custom/images/${filename}?v=${Date.now()}`, - // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - }, - }, - }, - validate: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...this.options.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, - ), - )(value); - }, - }, - 'customization.logo.reports': { - title: 'PDF reports logo', - description: `This logo is used in the PDF reports generated by the app. It's placed at the top left corner of every page of the PDF.`, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - defaultValueIfNotSet: REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 190, - height: 40, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.reports', - resolveStaticURL: (filename: string) => `custom/images/${filename}`, - }, - }, - }, - validate: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...this.options.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, - ), - )(value); - }, - }, - 'customization.reports.footer': { - title: 'Reports footer', - description: 'Set the footer of the reports.', - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: '', - defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { maxRows: 2, maxLength: 50 }, - validate: function (value) { - return SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, - })(value); - }, - validateBackend: function (schema) { - return schema.string({ validate: this.validate.bind(this) }); - }, - }, - 'customization.reports.header': { - title: 'Reports header', - description: 'Set the header of the reports.', - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: '', - defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { maxRows: 3, maxLength: 40 }, - validate: function (value) { - return SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, - })(value); - }, - validateBackend: function (schema) { - return schema.string({ validate: this.validate.bind(this) }); - }, - }, - 'enrollment.dns': { - title: 'Enrollment DNS', - description: - 'Specifies the Wazuh registration server, used for the agent enrollment.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: '', - isConfigurableFromFile: true, - isConfigurableFromUI: true, - validate: SettingsValidator.hasNoSpaces, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - 'enrollment.password': { - title: 'Enrollment password', - description: - 'Specifies the password used to authenticate during the agent enrollment.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: '', - isConfigurableFromFile: true, - isConfigurableFromUI: false, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - hideManagerAlerts: { - title: 'Hide manager alerts', - description: 'Hide the alerts of the manager in every dashboard.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.switch, - defaultValue: false, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'ip.ignore': { - title: 'Index pattern ignore', - description: - 'Disable certain index pattern names from being available in index pattern selector.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.editor, - defaultValue: [], - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - editor: { - language: 'json', - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: any): any { - return JSON.stringify(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): any { - try { - return JSON.parse(value); - } catch (error) { - return value; - } - }, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.json( - SettingsValidator.compose( - SettingsValidator.array( - SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - ), - ), - ), - validateBackend: function (schema) { - return schema.arrayOf( - schema.string({ - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - }), - ); - }, - }, - 'ip.selector': { - title: 'IP selector', - description: - 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromFile: true, - isConfigurableFromUI: false, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - pattern: { - title: 'Index pattern', - description: - "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: WAZUH_ALERTS_PATTERN, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, - }, - timeout: { - title: 'Request timeout', - description: - 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', - category: SettingCategory.GENERAL, - type: EpluginSettingType.number, - defaultValue: 20000, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - options: { - number: { - min: 1500, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'wazuh.monitoring.creation': { - title: 'Index creation', - description: - 'Define the interval in which a new wazuh-monitoring index will be created.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.select, - options: { - select: [ - { - text: 'Hourly', - value: 'h', - }, - { - text: 'Daily', - value: 'd', - }, - { - text: 'Weekly', - value: 'w', - }, - { - text: 'Monthly', - value: 'm', - }, - ], - }, - defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - validate: function (value) { - return SettingsValidator.literal( - this.options.select.map(({ value }) => value), - )(value); - }, - validateBackend: function (schema) { - return schema.oneOf( - this.options.select.map(({ value }) => schema.literal(value)), - ); - }, - }, - 'wazuh.monitoring.enabled': { - title: 'Status', - description: - 'Enable or disable the wazuh-monitoring index creation and/or visualization.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.switch, - defaultValue: WAZUH_MONITORING_DEFAULT_ENABLED, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRestartingPluginPlatform: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); - }, - }, - 'wazuh.monitoring.frequency': { - title: 'Frequency', - description: - 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_FREQUENCY, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRestartingPluginPlatform: true, - options: { - number: { - min: 60, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'wazuh.monitoring.pattern': { - title: 'Index pattern', - description: 'Default index pattern to use for Wazuh monitoring.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.text, - defaultValue: WAZUH_MONITORING_PATTERN, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - validateBackend: function (schema) { - return schema.string({ minLength: 1, validate: this.validate }); - }, - }, - 'wazuh.monitoring.replicas': { - title: 'Index replicas', - description: - 'Define the number of replicas to use for the wazuh-monitoring-* indices.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 0, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'wazuh.monitoring.shards': { - title: 'Index shards', - description: - 'Define the number of shards to use for the wazuh-monitoring-* indices.', - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 1, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, - }, - 'vulnerabilities.pattern': { - title: 'Index pattern', - description: 'Default index pattern to use for vulnerabilities.', - category: SettingCategory.VULNERABILITIES, - type: EpluginSettingType.text, - defaultValue: WAZUH_VULNERABILITIES_PATTERN, - isConfigurableFromFile: true, - isConfigurableFromUI: true, - requiresRunningHealthCheck: false, - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - validateBackend: function (schema) { - return schema.string({ minLength: 1, validate: this.validate }); - }, - }, -}; - -export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS; - export enum HTTP_STATUS_CODES { CONTINUE = 100, SWITCHING_PROTOCOLS = 101, diff --git a/plugins/main/common/plugin.ts b/plugins/main/common/plugin.ts deleted file mode 100644 index edb5c76d0f..0000000000 --- a/plugins/main/common/plugin.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PLUGIN_PLATFORM_BASE_INSTALLATION_PATH } from "./constants"; - -/** - * - * @param path Path to file or directory - * @returns Absolute path to the file or directory with the prefix path of app data path - */ -export const getPluginDataPath = (path: string = ''): string => `${PLUGIN_PLATFORM_BASE_INSTALLATION_PATH}${path}`; \ No newline at end of file diff --git a/plugins/main/common/services/settings-validator.ts b/plugins/main/common/services/settings-validator.ts deleted file mode 100644 index b62675f0f9..0000000000 --- a/plugins/main/common/services/settings-validator.ts +++ /dev/null @@ -1,235 +0,0 @@ -import path from 'path'; -import { formatBytes } from './file-size'; - -export class SettingsValidator { - /** - * Create a function that is a composition of the input validations - * @param functions SettingsValidator functions to compose - * @returns composed validation - */ - static compose(...functions) { - return function composedValidation(value) { - for (const fn of functions) { - const result = fn(value); - if (typeof result === 'string' && result.length > 0) { - return result; - }; - }; - }; - }; - - /** - * Check the value is a string - * @param value - * @returns - */ - static isString(value: unknown): string | undefined { - return typeof value === 'string' ? undefined : "Value is not a string."; - }; - - /** - * Check the string has no spaces - * @param value - * @returns - */ - static hasNoSpaces(value: string): string | undefined { - return /^\S*$/.test(value) ? undefined : "No whitespaces allowed."; - }; - - /** - * Check the string has no empty - * @param value - * @returns - */ - static isNotEmptyString(value: string): string | undefined { - if (typeof value === 'string') { - if (value.length === 0) { - return "Value can not be empty." - } else { - return undefined; - } - }; - }; - - /** - * Check the number of string lines is limited - * @param options - * @returns - */ - static multipleLinesString(options: { minRows?: number, maxRows?: number, maxLength?: number } = {}) { - return function (value: number) { - const lines = value.split(/\r\n|\r|\n/).length; - if (typeof options.maxLength !== 'undefined' && value.split('\n').some(line => line.length > options.maxLength)) { - return `The maximum length of a line is ${options.maxLength} characters.`; - }; - if (typeof options.minRows !== 'undefined' && lines < options.minRows) { - return `The string should have more or ${options.minRows} line/s.`; - }; - if (typeof options.maxRows !== 'undefined' && lines > options.maxRows) { - return `The string should have less or equal to ${options.maxRows} line/s.`; - }; - } - }; - - /** - * Creates a function that checks the string does not contain some characters - * @param invalidCharacters - * @returns - */ - static hasNotInvalidCharacters(...invalidCharacters: string[]) { - return function (value: string): string | undefined { - return invalidCharacters.some(invalidCharacter => value.includes(invalidCharacter)) - ? `It can't contain invalid characters: ${invalidCharacters.join(', ')}.` - : undefined; - }; - }; - - /** - * Creates a function that checks the string does not start with a substring - * @param invalidStartingCharacters - * @returns - */ - static noStartsWithString(...invalidStartingCharacters: string[]) { - return function (value: string): string | undefined { - return invalidStartingCharacters.some(invalidStartingCharacter => value.startsWith(invalidStartingCharacter)) - ? `It can't start with: ${invalidStartingCharacters.join(', ')}.` - : undefined; - }; - }; - - /** - * Creates a function that checks the string is not equals to some values - * @param invalidLiterals - * @returns - */ - static noLiteralString(...invalidLiterals: string[]) { - return function (value: string): string | undefined { - return invalidLiterals.some(invalidLiteral => value === invalidLiteral) - ? `It can't be: ${invalidLiterals.join(', ')}.` - : undefined; - }; - }; - - /** - * Check the value is a boolean - * @param value - * @returns - */ - static isBoolean(value: string): string | undefined { - return typeof value === 'boolean' - ? undefined - : "It should be a boolean. Allowed values: true or false."; - }; - - /** - * Check the value is a number between some optional limits - * @param options - * @returns - */ - static number(options: { min?: number, max?: number, integer?: boolean } = {}) { - return function (value: number) { - if (options.integer - && ( - (typeof value === 'string' ? ['.', ','].some(character => value.includes(character)) : false) - || !Number.isInteger(Number(value)) - ) - ) { - return 'Number should be an integer.' - }; - - const valueNumber = typeof value === 'string' ? Number(value) : value; - - if (typeof options.min !== 'undefined' && valueNumber < options.min) { - return `Value should be greater or equal than ${options.min}.`; - }; - if (typeof options.max !== 'undefined' && valueNumber > options.max) { - return `Value should be lower or equal than ${options.max}.`; - }; - }; - }; - - /** - * Creates a function that checks if the value is a json - * @param validateParsed Optional parameter to validate the parsed object - * @returns - */ - static json(validateParsed: (object: any) => string | undefined) { - return function (value: string) { - let jsonObject; - // Try to parse the string as JSON - try { - jsonObject = JSON.parse(value); - } catch (error) { - return "Value can't be parsed. There is some error."; - }; - - return validateParsed ? validateParsed(jsonObject) : undefined; - }; - }; - - /** - * Creates a function that checks is the value is an array and optionally validates each element - * @param validationElement Optional function to validate each element of the array - * @returns - */ - static array(validationElement: (json: any) => string | undefined) { - return function (value: unknown[]) { - // Check the JSON is an array - if (!Array.isArray(value)) { - return 'Value is not a valid list.'; - }; - - return validationElement - ? value.reduce((accum, elementValue) => { - if (accum) { - return accum; - }; - - const resultValidationElement = validationElement(elementValue); - if (resultValidationElement) { - return resultValidationElement; - }; - - return accum; - }, undefined) - : undefined; - }; - }; - - /** - * Creates a function that checks if the value is equal to list of values - * @param literals Array of values to compare - * @returns - */ - static literal(literals: unknown[]) { - return function (value: any): string | undefined { - return literals.includes(value) ? undefined : `Invalid value. Allowed values: ${literals.map(String).join(', ')}.`; - }; - }; - - // FilePicker - static filePickerSupportedExtensions = (extensions: string[]) => (options: { name: string }) => { - if (typeof options === 'undefined' || typeof options.name === 'undefined') { - return; - } - if (!extensions.includes(path.extname(options.name))) { - return `File extension is invalid. Allowed file extensions: ${extensions.join(', ')}.`; - }; - }; - - /** - * filePickerFileSize - * @param options - */ - static filePickerFileSize = (options: { maxBytes?: number, minBytes?: number, meaningfulUnit?: boolean }) => (value: { size: number }) => { - if (typeof value === 'undefined' || typeof value.size === 'undefined') { - return; - }; - if (typeof options.minBytes !== 'undefined' && value.size <= options.minBytes) { - return `File size should be greater or equal than ${options.meaningfulUnit ? formatBytes(options.minBytes) : `${options.minBytes} bytes`}.`; - }; - if (typeof options.maxBytes !== 'undefined' && value.size >= options.maxBytes) { - return `File size should be lower or equal than ${options.meaningfulUnit ? formatBytes(options.maxBytes) : `${options.maxBytes} bytes`}.`; - }; - }; -}; diff --git a/plugins/main/common/services/settings.test.ts b/plugins/main/common/services/settings.test.ts deleted file mode 100644 index 21efe9e414..0000000000 --- a/plugins/main/common/services/settings.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - formatLabelValuePair, - formatSettingValueToFile, - getCustomizationSetting, -} from './settings'; - -describe('[settings] Methods', () => { - describe('formatLabelValuePair: Format the label-value pairs used to display the allowed values', () => { - it.each` - label | value | expected - ${'TestLabel'} | ${true} | ${'true (TestLabel)'} - ${'true'} | ${true} | ${'true'} - `( - `label: $label | value: $value | expected: $expected`, - ({ label, expected, value }) => { - expect(formatLabelValuePair(label, value)).toBe(expected); - }, - ); - }); - - describe('formatSettingValueToFile: Format setting values to save in the configuration file', () => { - it.each` - input | expected - ${'test'} | ${'"test"'} - ${'test space'} | ${'"test space"'} - ${'test\nnew line'} | ${'"test\\nnew line"'} - ${''} | ${'""'} - ${1} | ${1} - ${true} | ${true} - ${false} | ${false} - ${['test1']} | ${'["test1"]'} - ${['test1', 'test2']} | ${'["test1","test2"]'} - `(`input: $input | expected: $expected`, ({ input, expected }) => { - expect(formatSettingValueToFile(input)).toBe(expected); - }); - }); - - describe('getCustomizationSetting: Get the value for the "customization." settings depending on the "customization.enabled" setting', () => { - it.each` - customizationEnabled | settingKey | configValue | expected - ${true} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${'custom-image-app.png'} - ${true} | ${'customization.logo.app'} | ${''} | ${''} - ${false} | ${'customization.logo.app'} | ${'custom-image-app.png'} | ${''} - ${false} | ${'customization.logo.app'} | ${''} | ${''} - ${true} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Custom footer'} - ${true} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${'Custom footer'} | ${'Copyright © 2023 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} - ${false} | ${'customization.reports.footer'} | ${''} | ${'Copyright © 2023 Wazuh, Inc.'} - ${true} | ${'customization.reports.header'} | ${'Custom header'} | ${'Custom header'} - ${true} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} - ${false} | ${'customization.reports.header'} | ${'Custom header'} | ${'info@wazuh.com\nhttps://wazuh.com'} - ${false} | ${'customization.reports.header'} | ${''} | ${'info@wazuh.com\nhttps://wazuh.com'} - `( - `customizationEnabled: $customizationEnabled | settingKey: $settingKey | configValue: $configValue | expected: $expected`, - ({ configValue, customizationEnabled, expected, settingKey }) => { - const configuration = { - 'customization.enabled': customizationEnabled, - [settingKey]: configValue, - }; - expect(getCustomizationSetting(configuration, settingKey)).toBe( - expected, - ); - }, - ); - }); -}); diff --git a/plugins/main/common/services/settings.ts b/plugins/main/common/services/settings.ts deleted file mode 100644 index 868f54c984..0000000000 --- a/plugins/main/common/services/settings.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - PLUGIN_SETTINGS, - PLUGIN_SETTINGS_CATEGORIES, - TPluginSetting, - TPluginSettingKey, - TPluginSettingWithKey -} from '../constants'; -import { formatBytes } from './file-size'; - -/** - * Look for a configuration category setting by its name - * @param categoryTitle - * @returns category settings - */ -export function getCategorySettingByTitle(categoryTitle: string): any { - return Object.entries(PLUGIN_SETTINGS_CATEGORIES).find(([key, category]) => category?.title == categoryTitle)?.[1]; -} - -/** - * Get the default value of the plugin setting. - * @param setting setting key - * @returns setting default value. It returns `defaultValueIfNotSet` or `defaultValue`. - */ -export function getSettingDefaultValue(settingKey: string): any { - return typeof PLUGIN_SETTINGS[settingKey].defaultValueIfNotSet !== 'undefined' - ? PLUGIN_SETTINGS[settingKey].defaultValueIfNotSet - : PLUGIN_SETTINGS[settingKey].defaultValue; -}; - -/** - * Get the default settings configuration. key-value pair - * @returns an object with key-value pairs whose value is the default one - */ -export function getSettingsDefault() : {[key in TPluginSettingKey]: unknown} { - return Object.entries(PLUGIN_SETTINGS).reduce((accum, [pluginSettingID, pluginSettingConfiguration]) => ({ - ...accum, - [pluginSettingID]: pluginSettingConfiguration.defaultValue - }), {}); -}; - -/** - * Get the settings grouped by category - * @returns an object whose keys are the categories and its value is an array of setting of that category - */ -export function getSettingsByCategories() : {[key: string]: TPluginSetting[]} { - return Object.entries(PLUGIN_SETTINGS).reduce((accum, [pluginSettingID, pluginSettingConfiguration]) => ({ - ...accum, - [pluginSettingConfiguration.category]: [...(accum[pluginSettingConfiguration.category] || []), { ...pluginSettingConfiguration, key: pluginSettingID }] - }), {}); -}; - -/** - * Get the plugin settings as an array - * @returns an array of plugin setting denifitions including the key - */ -export function getSettingsDefaultList(): TPluginSettingWithKey[] { - return Object.entries(PLUGIN_SETTINGS).reduce((accum, [pluginSettingID, pluginSettingConfiguration]) => ([ - ...accum, - { ...pluginSettingConfiguration, key: pluginSettingID } - ]), []); -}; - -/** - * Format the plugin setting value received in the backend to store in the plugin configuration file (.yml). - * @param value plugin setting value sent to the endpoint - * @returns valid value to .yml - */ -export function formatSettingValueToFile(value: any) { - const formatter = formatSettingValueToFileType[typeof value] || formatSettingValueToFileType.default; - return formatter(value); -}; - -const formatSettingValueToFileType = { - string: (value: string): string => `"${value.replace(/"/,'\\"').replace(/\n/g,'\\n')}"`, // Escape the " character and new line - object: (value: any): string => JSON.stringify(value), - default: (value: any): any => value -}; - -/** - * Group the settings by category - * @param settings - * @returns - */ -export function groupSettingsByCategory(settings: TPluginSettingWithKey[]){ - const settingsSortedByCategories = settings - .sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)) - .reduce((accum, pluginSettingConfiguration) => ({ - ...accum, - [pluginSettingConfiguration.category]: [ - ...(accum[pluginSettingConfiguration.category] || []), - { ...pluginSettingConfiguration } - ] - }), {}); - - return Object.entries(settingsSortedByCategories) - .map(([category, settings]) => ({ category, settings })) - .filter(categoryEntry => categoryEntry.settings.length); -}; - -/** - * Get the plugin setting description composed. - * @param options - * @returns - */ - export function getPluginSettingDescription({description, options}: TPluginSetting): string{ - return [ - description, - ...(options?.select ? [`Allowed values: ${options.select.map(({text, value}) => formatLabelValuePair(text, value)).join(', ')}.`] : []), - ...(options?.switch ? [`Allowed values: ${['enabled', 'disabled'].map(s => formatLabelValuePair(options.switch.values[s].label, options.switch.values[s].value)).join(', ')}.`] : []), - ...(options?.number && 'min' in options.number ? [`Minimum value: ${options.number.min}.`] : []), - ...(options?.number && 'max' in options.number ? [`Maximum value: ${options.number.max}.`] : []), - // File extensions - ...(options?.file?.extensions ? [`Supported extensions: ${options.file.extensions.join(', ')}.`] : []), - // File recommended dimensions - ...(options?.file?.recommended?.dimensions ? [`Recommended dimensions: ${options.file.recommended.dimensions.width}x${options.file.recommended.dimensions.height}${options.file.recommended.dimensions.unit || ''}.`] : []), - // File size - ...((options?.file?.size && typeof options.file.size.minBytes !== 'undefined') ? [`Minimum file size: ${formatBytes(options.file.size.minBytes)}.`] : []), - ...((options?.file?.size && typeof options.file.size.maxBytes !== 'undefined') ? [`Maximum file size: ${formatBytes(options.file.size.maxBytes)}.`] : []), - // Multi line text - ...((options?.maxRows && typeof options.maxRows !== 'undefined' ? [`Maximum amount of lines: ${options.maxRows}.`] : [])), - ...((options?.minRows && typeof options.minRows !== 'undefined' ? [`Minimum amount of lines: ${options.minRows}.`] : [])), - ...((options?.maxLength && typeof options.maxLength !== 'undefined' ? [`Maximum lines length is ${options.maxLength} characters.`] : [])), - ].join(' '); -}; - -/** - * Format the pair value-label to display the pair. If label and the string of value are equals, only displays the value, if not, displays both. - * @param value - * @param label - * @returns - */ -export function formatLabelValuePair(label, value){ - return label !== `${value}` - ? `${value} (${label})` - : `${value}` -}; - -/** - * Get the configuration value if the customization is enabled. - * @param configuration JSON object from `wazuh.yml` - * @param settingKey key of the setting - * @returns - */ -export function getCustomizationSetting(configuration: {[key: string]: any }, settingKey: string): any { - const isCustomizationEnabled = typeof configuration['customization.enabled'] === 'undefined' - ? getSettingDefaultValue('customization.enabled') - : configuration['customization.enabled']; - const defaultValue = getSettingDefaultValue(settingKey); - - if ( isCustomizationEnabled && settingKey.startsWith('customization') && settingKey !== 'customization.enabled'){ - return (typeof configuration[settingKey] !== 'undefined' ? resolveEmptySetting(settingKey, configuration[settingKey]) : defaultValue); - }else{ - return defaultValue; - }; -}; - -/** - * Returns the default value if not set when the setting is an empty string - * @param settingKey plugin setting - * @param value value of the plugin setting - * @returns - */ -function resolveEmptySetting(settingKey: string, value : unknown){ - return typeof value === 'string' && value.length === 0 && PLUGIN_SETTINGS[settingKey].defaultValueIfNotSet - ? getSettingDefaultValue(settingKey) - : value; -}; diff --git a/plugins/main/public/app.js b/plugins/main/public/app.js index f8e23658ea..bca52fa9ec 100644 --- a/plugins/main/public/app.js +++ b/plugins/main/public/app.js @@ -42,12 +42,14 @@ import './controllers'; import './factories'; // Imports to update currentPlatform when app starts -import { checkCurrentSecurityPlatform } from './controllers/management/components/management/configuration/utils/wz-fetch'; import store from './redux/store'; -import { updateCurrentPlatform } from './redux/actions/appStateActions'; +import { + updateCurrentPlatform, + updateUserAccount, +} from './redux/actions/appStateActions'; import { WzAuthentication, loadAppConfig } from './react-services'; -import { getAngularModule, getHttp } from './kibana-services'; +import { getAngularModule, getWazuhCorePlugin } from './kibana-services'; const app = getAngularModule(); @@ -73,11 +75,20 @@ app.run([ app.$injector = _$injector; // Set currentSecurity platform in Redux when app starts. - checkCurrentSecurityPlatform() + getWazuhCorePlugin() + .dashboardSecurity.fetchCurrentPlatform() .then(item => { store.dispatch(updateCurrentPlatform(item)); }) - .catch(() => { }); + .catch(() => {}); + + // Set user account data in Redux when app starts. + getWazuhCorePlugin() + .dashboardSecurity.fetchAccount() + .then(response => { + store.dispatch(updateUserAccount(response)); + }) + .catch(() => {}); // Init the process of refreshing the user's token when app start. checkPluginVersion().finally(WzAuthentication.refresh); @@ -101,7 +112,6 @@ app.run(function ($rootElement) { `); - // Bind deleteExistentToken on Log out component. $('.euiHeaderSectionItem__button, .euiHeaderSectionItemButton').on( 'mouseleave', diff --git a/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js b/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js index 3bf1213a52..c7a0e79e52 100644 --- a/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js +++ b/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js @@ -29,7 +29,6 @@ import { withReduxProvider, } from '../../components/common/hocs'; import { compose } from 'redux'; -import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; export class WzSampleDataProvider extends Component { constructor(props) { @@ -68,5 +67,5 @@ export class WzSampleDataProvider extends Component { export const WzSampleDataWrapper = compose( withErrorBoundary, withReduxProvider, - withUserAuthorizationPrompt(null, [WAZUH_ROLE_ADMINISTRATOR_NAME]), + withUserAuthorizationPrompt(null, { isAdmininistrator: true }), )(WzSampleDataProvider); diff --git a/plugins/main/public/components/add-modules-data/sample-data.tsx b/plugins/main/public/components/add-modules-data/sample-data.tsx index 80989e10e1..ee7ab442ca 100644 --- a/plugins/main/public/components/add-modules-data/sample-data.tsx +++ b/plugins/main/public/components/add-modules-data/sample-data.tsx @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { WzButtonPermissions } from '../../components/common/permissions/button'; import { @@ -25,8 +25,6 @@ import { import { getToasts } from '../../kibana-services'; import { WzRequest } from '../../react-services/wz-request'; import { AppState } from '../../react-services/app-state'; -import { WAZUH_ROLE_ADMINISTRATOR_NAME } from '../../../common/constants'; - import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { getErrorOrchestrator } from '../../react-services/common-services'; @@ -286,7 +284,7 @@ export default class WzSampleData extends Component { {(exists && ( this.removeSampleData(category)} > {(removeDataLoading && 'Removing data') || 'Remove data'} @@ -294,7 +292,7 @@ export default class WzSampleData extends Component { )) || ( this.addSampleData(category)} > {(addDataLoading && 'Adding data') || 'Add data'} diff --git a/plugins/main/public/components/common/buttons/flyout.tsx b/plugins/main/public/components/common/buttons/flyout.tsx new file mode 100644 index 0000000000..7e22970bda --- /dev/null +++ b/plugins/main/public/components/common/buttons/flyout.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { + WzButtonOpenOnClick, + WzButtonPermissionsOpenOnClick, +} from './modal-confirm'; +import { WzFlyout } from '../flyouts'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiConfirmModal, + EuiOverlayMask, +} from '@elastic/eui'; + +function RenderFlyout({ flyoutTitle, flyoutProps, flyoutBody, onClose }) { + const [canClose, setCanClose] = useState(true); + const [canNotCloseIsOpen, setCanNotCloseIsOpen] = useState(false); + const onFlyoutClose = function () { + if (!canClose) { + setCanNotCloseIsOpen(true); + return; + } + onClose(); + }; + + return ( + <> + + + +

{flyoutTitle}

+
+
+ + {typeof flyoutBody === 'function' + ? flyoutBody({ + onClose, + onUpdateCanClose: setCanClose, + }) + : flyoutBody} + +
+ {canNotCloseIsOpen && ( + + setCanNotCloseIsOpen(false)} + cancelButtonText="No, don't do it" + confirmButtonText='Yes, do it' + > +

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

+
+
+ )} + + ); +} + +export const WzButtonOpenFlyout: React.FunctionComponent = ({ + flyoutTitle, + flyoutProps = {}, + flyoutBody = null, + buttonProps = {}, + ...rest +}) => ( + ( + + )} + /> +); + +export const WzButtonPermissionsOpenFlyout: React.FunctionComponent = ({ + flyoutTitle, + flyoutProps = {}, + flyoutBody = null, + buttonProps = {}, + ...rest +}) => ( + ( + + )} + /> +); diff --git a/plugins/main/public/components/common/buttons/index.ts b/plugins/main/public/components/common/buttons/index.ts index 5aef0f94e0..da7fa9034f 100644 --- a/plugins/main/public/components/common/buttons/index.ts +++ b/plugins/main/public/components/common/buttons/index.ts @@ -11,4 +11,8 @@ */ export { WzButton } from './button'; -export { WzButtonModalConfirm, WzButtonPermissionsModalConfirm } from './modal-confirm'; \ No newline at end of file +export { + WzButtonModalConfirm, + WzButtonPermissionsModalConfirm, +} from './modal-confirm'; +export * from './flyout'; diff --git a/plugins/main/public/components/common/form/hooks.test.tsx b/plugins/main/public/components/common/form/hooks.test.tsx index 283c4809bf..e0d8eee60c 100644 --- a/plugins/main/public/components/common/form/hooks.test.tsx +++ b/plugins/main/public/components/common/form/hooks.test.tsx @@ -2,9 +2,257 @@ import { fireEvent, render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { renderHook, act } from '@testing-library/react-hooks'; import React, { useState } from 'react'; -import { useForm } from './hooks'; +import { + enhanceFormFields, + getFormFields, + mapFormFields, + useForm, +} from './hooks'; import { FormConfiguration, IInputForm } from './types'; +function inspect(obj) { + return console.log( + require('util').inspect(obj, false, null, true /* enable colors */), + ); +} + +describe('[hook] useForm utils', () => { + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: '', + }, + }); + expect(result.text1.currentValue).toBe(''); + expect(result.text1.initialValue).toBe(''); + }); + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: 'text1', + }, + number1: { + type: 'number', + initialValue: 1, + }, + }); + expect(result.text1.currentValue).toBe('text1'); + expect(result.text1.initialValue).toBe('text1'); + expect(result.number1.currentValue).toBe(1); + expect(result.number1.initialValue).toBe(1); + }); + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: 'text1', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }, + }, + }); + expect(result.text1.currentValue).toBe('text1'); + expect(result.text1.initialValue).toBe('text1'); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].currentValue).toBe( + 'text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].initialValue).toBe( + 'text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].currentValue).toBe(10); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].initialValue).toBe(10); + }); + it.only('[utils] mapFormFields', () => { + const result = mapFormFields( + { + formDefinition: { + text1: { + type: 'text', + initialValue: 'text1', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }, + }, + }, + formState: { + text1: { + currentValue: 'changed1', + initialValue: 'text1', + }, + arrayOf1: { + fields: [ + { + 'arrayOf1.text1': { + currentValue: 'arrayOf1.text1.changed1', + initialValue: 'arrayOf1.text1', + }, + 'arrayOf1.number1': { + currentValue: 10, + initialValue: 0, + }, + }, + ], + }, + }, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, currentValue: state.initialValue }), + ); + expect(result.text1.currentValue).toBe('text1'); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].currentValue).toBe( + 'arrayOf1.text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].currentValue).toBe(0); + }); +}); + +describe('[hook] useForm', () => { + it('[hook] enhanceFormFields', () => { + let state; + const setState = updateState => (state = updateState); + const references = { + current: {}, + }; + + const fields = { + text1: { + type: 'text', + initialValue: '', + }, + }; + + const formFields = getFormFields(fields); + const enhancedFields = enhanceFormFields(formFields, { + fields, + references, + setState, + }); + expect(enhancedFields.text1).toBeDefined(); + expect(enhancedFields.text1.type).toBe('text'); + expect(enhancedFields.text1.initialValue).toBe(''); + expect(enhancedFields.text1.value).toBe(''); + expect(enhancedFields.text1.changed).toBeDefined(); + expect(enhancedFields.text1.error).toBeUndefined(); + expect(enhancedFields.text1.setInputRef).toBeDefined(); + expect(enhancedFields.text1.inputRef).toBeUndefined(); + expect(enhancedFields.text1.onChange).toBeDefined(); + }); + + it('[hook] enhanceFormFields', () => { + let state; + const setState = updateState => (state = updateState); + const references = { + current: {}, + }; + + const arrayOfFields = { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }; + const fields = { + text1: { + type: 'text', + initialValue: '', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: arrayOfFields, + }, + }; + + const formFields = getFormFields(fields); + const enhancedFields = enhanceFormFields(formFields, { + fields, + references, + setState, + }); + expect(enhancedFields.text1).toBeDefined(); + expect(enhancedFields.text1.type).toBe('text'); + expect(enhancedFields.text1.initialValue).toBe(''); + expect(enhancedFields.text1.value).toBe(''); + expect(enhancedFields.text1.changed).toBeDefined(); + expect(enhancedFields.text1.error).toBeUndefined(); + expect(enhancedFields.text1.setInputRef).toBeDefined(); + expect(enhancedFields.text1.inputRef).toBeUndefined(); + expect(enhancedFields.text1.onChange).toBeDefined(); + expect(enhancedFields.arrayOf1).toBeDefined(); + expect(enhancedFields.arrayOf1.fields).toBeDefined(); + expect(enhancedFields.arrayOf1.fields).toHaveLength(1); + expect(enhancedFields.arrayOf1.fields[0]).toBeDefined(); + expect(enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].type).toBe( + 'text', + ); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + expect(enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].value).toBe( + 'text1', + ); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBeDefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].setInputRef, + ).toBeDefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].inputRef, + ).toBeUndefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].onChange, + ).toBeDefined(); + }); +}); + describe('[hook] useForm', () => { it(`[hook] useForm. Verify the initial state`, async () => { const initialFields: FormConfiguration = { @@ -176,6 +424,173 @@ describe('[hook] useForm', () => { expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); }); + it.only(`[hook] useForm. ArrayOf.`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + options: { + min: 0, + max: 10, + integer: true, + }, + }, + }, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + + expect(result.current.fields.arrayOf1).toBeDefined(); + expect(result.current.fields.arrayOf1.fields).toBeDefined(); + expect(result.current.fields.arrayOf1.fields).toHaveLength(1); + expect(result.current.fields.arrayOf1.fields[0]).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].type, + ).toBe('text'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].setInputRef, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].inputRef, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].onChange, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].type, + ).toBe('number'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].initialValue, + ).toBe(10); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].value, + ).toBe(10); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].changed, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].setInputRef, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].inputRef, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].onChange, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options.min, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options.max, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options + .integer, + ).toBeDefined(); + + // change the input + const changedValue = 'changed_text'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.initialValue).toBe(''); + + // change arrayOf input + const changedArrayOfValue = 'changed_arrayOf_field'; + act(() => { + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].onChange({ + target: { + value: changedArrayOfValue, + }, + }); + }); + + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBe(true); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].type, + ).toBe('text'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe(changedArrayOfValue); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + + // Undo changes + act(() => { + result.current.undoChanges(); + }); + + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.changed).toBe(false); + + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBe(false); + }); + it('[hook] useForm. Verify the hook behavior when receives a custom field type', async () => { const CustomComponent = (props: any) => { const { onChange, field, initialValue } = props; diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index 63ff8fdb72..a4f61cd573 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -1,9 +1,8 @@ import { useState, useRef } from 'react'; -import { isEqual } from 'lodash'; +import { isEqual, get, set, cloneDeep } from 'lodash'; import { EpluginSettingType } from '../../../../common/constants'; import { CustomSettingType, - EnhancedFields, FormConfiguration, SettingTypes, UseFormReturn, @@ -36,108 +35,265 @@ const getValueFromEventType: IgetValueFromEventType = { [EpluginSettingType.text]: (event: any) => event.target.value, [EpluginSettingType.textarea]: (event: any) => event.target.value, [EpluginSettingType.number]: (event: any) => event.target.value, + [EpluginSettingType.password]: (event: any) => event.target.value, custom: (event: any) => event.target.value, default: (event: any) => event.target.value, }; -export const useForm = (fields: FormConfiguration): UseFormReturn => { - const [formFields, setFormFields] = useState<{ - [key: string]: { currentValue: any; initialValue: any }; - }>( - Object.entries(fields).reduce( - (accum, [fieldKey, fieldConfiguration]) => ({ +export function getFormFields(fields) { + return Object.entries(fields).reduce( + (accum, [fieldKey, fieldConfiguration]) => { + return { ...accum, [fieldKey]: { - currentValue: fieldConfiguration.initialValue, - initialValue: fieldConfiguration.initialValue, + ...(fieldConfiguration.type === 'arrayOf' + ? { + fields: fieldConfiguration.initialValue.map(item => + getFormFields( + Object.entries(fieldConfiguration.fields).reduce( + (accum, [key]) => ({ + ...accum, + [key]: { + initialValue: item[key], + currentValue: item[key], + defaultValue: fieldConfiguration?.defaultValue, + }, + }), + {}, + ), + ), + ), + } + : { + currentValue: fieldConfiguration.initialValue, + initialValue: fieldConfiguration.initialValue, + defaultValue: fieldConfiguration?.defaultValue, + }), }, - }), - {}, - ), + }; + }, + {}, ); +} - const fieldRefs = useRef<{ [key: string]: any }>({}); +export function enhanceFormFields( + formFields, + { + fields, + references, + setState, + pathFieldParent = [], + pathFormFieldParent = [], + }, +) { + return Object.entries(formFields).reduce( + (accum, [fieldKey, { currentValue: value, ...restFieldState }]) => { + // Define the path to fields object + const pathField = [...pathFieldParent, fieldKey]; + // Define the path to the form fields object + const pathFormField = [...pathFormFieldParent, fieldKey]; + // Get the field form the fields + const field = get(fields, pathField); - const enhanceFields = Object.entries(formFields).reduce( - (accum, [fieldKey, { currentValue: value, ...restFieldState }]) => ({ - ...accum, - [fieldKey]: { - ...fields[fieldKey], - ...restFieldState, - type: fields[fieldKey].type, - value, - changed: !isEqual(restFieldState.initialValue, value), - error: fields[fieldKey]?.validate?.(value), - setInputRef: (reference: any) => { - fieldRefs.current[fieldKey] = reference; - }, - inputRef: fieldRefs.current[fieldKey], - onChange: (event: any) => { - const inputValue = getValueFromEvent(event, fields[fieldKey].type); - const currentValue = - fields[fieldKey]?.transformChangedInputValue?.(inputValue) ?? - inputValue; - setFormFields(state => ({ - ...state, - [fieldKey]: { - ...state[fieldKey], - currentValue, - }, - })); + return { + ...accum, + [fieldKey]: { + ...(field.type === 'arrayOf' + ? { + type: field.type, + fields: (() => { + return restFieldState.fields.map((fieldState, index) => + enhanceFormFields(fieldState, { + fields, + references, + setState, + pathFieldParent: [...pathField, 'fields'], + pathFormFieldParent: [...pathFormField, 'fields', index], + }), + ); + })(), + addNewItem: () => { + setState(state => { + const _state = get(state, [...pathField, 'fields']); + const newstate = set( + state, + [...pathField, 'fields', _state.length], + Object.entries(field.fields).reduce( + (accum, [key, { defaultValue }]) => ({ + ...accum, + [key]: { + currentValue: cloneDeep(defaultValue), + initialValue: cloneDeep(defaultValue), + defaultValue: cloneDeep(defaultValue), + }, + }), + {}, + ), + ); + return cloneDeep(newstate); + }); + }, + } + : { + ...field, + ...restFieldState, + type: field.type, + value, + changed: !isEqual(restFieldState.initialValue, value), + error: field?.validate?.(value), + setInputRef: (reference: any) => { + set(references, pathFormField, reference); + }, + inputRef: get(references, pathFormField), + onChange: (event: any) => { + const inputValue = getValueFromEvent(event, field.type); + const currentValue = + field?.transformChangedInputValue?.(inputValue) ?? + inputValue; + setState(state => { + const newState = set( + cloneDeep(state), + [...pathFormField, 'currentValue'], + currentValue, + ); + return newState; + }); + }, + }), }, - }, - }), + }; + }, {}, ); +} - const changed = Object.fromEntries( - Object.entries(enhanceFields as EnhancedFields) - .filter(([, { changed }]) => changed) - .map(([fieldKey, { value }]) => [ - fieldKey, - fields[fieldKey]?.transformChangedOutputValue?.(value) ?? value, - ]), - ); +export function mapFormFields( + { + formDefinition, + formState, + pathFieldFormDefinition = [], + pathFormState = [], + }, + callbackFormField, +) { + return Object.entries(formState).reduce((accum, [key, value]) => { + const pathField = [...pathFieldFormDefinition, key]; + const fieldDefinition = get(formDefinition, pathField); + return { + ...accum, + [key]: + fieldDefinition.type === 'arrayOf' + ? { + fields: value.fields.map((valueField, index) => + mapFormFields( + { + formDefinition, + formState: valueField, + pathFieldFormDefinition: [...pathField, 'fields'], + pathFormState: [ + ...[...pathFormState, key], + 'fields', + index, + ], + }, + callbackFormField, + ), + ), + } + : callbackFormField?.(value, key, { + formDefinition, + formState, + pathFieldFormDefinition, + pathFormState: [...pathFormState, key], + fieldDefinition, + }), + }; + }, {}); +} - const errors = Object.fromEntries( - Object.entries(enhanceFields as EnhancedFields) - .filter(([, { error }]) => error) - .map(([fieldKey, { error }]) => [fieldKey, error]), - ); +export const useForm = (fields: FormConfiguration): UseFormReturn => { + const [formFields, setFormFields] = useState<{ + [key: string]: { currentValue: any; initialValue: any }; + }>(getFormFields(fields)); + + const fieldRefs = useRef<{ [key: string]: any }>({}); + + const enhanceFields = enhanceFormFields(formFields, { + fields, + references: fieldRefs.current, + setState: setFormFields, + pathFieldParent: [], + pathFormFieldParent: [], + }); + + const { changed, errors } = (() => { + const result = { + changed: {}, + errors: {}, + }; + mapFormFields( + { + formDefinition: fields, + formState: enhanceFields, + pathFieldFormDefinition: [], + pathFormState: [], + }, + ({ changed, error, value }, _, { pathFormState, fieldDefinition }) => { + changed && + (result.changed[pathFormState] = + fieldDefinition?.transformChangedOutputValue?.(value) ?? value); + error && (result.errors[pathFormState] = error); + }, + ); + return result; + })(); function undoChanges() { setFormFields(state => - Object.fromEntries( - Object.entries(state).map(([fieldKey, fieldConfiguration]) => [ - fieldKey, - { - ...fieldConfiguration, - currentValue: fieldConfiguration.initialValue, - }, - ]), + mapFormFields( + { + formDefinition: fields, + formState: state, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, currentValue: state.initialValue }), ), ); } function doChanges() { setFormFields(state => - Object.fromEntries( - Object.entries(state).map(([fieldKey, fieldConfiguration]) => [ - fieldKey, - { - ...fieldConfiguration, - initialValue: fieldConfiguration.currentValue, - }, - ]), + mapFormFields( + { + formDefinition: fields, + formState: state, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, initialValue: state.currentValue }), ), ); } + function forEach(callback) { + return mapFormFields( + { + formDefinition: fields, + formState: enhanceFields, + pathFieldFormDefinition: [], + pathFormState: [], + }, + callback, + ); + } + return { fields: enhanceFields, changed, errors, undoChanges, doChanges, + forEach, }; }; diff --git a/plugins/main/public/components/common/form/index.tsx b/plugins/main/public/components/common/form/index.tsx index d10797ca0d..08ef95f94d 100644 --- a/plugins/main/public/components/common/form/index.tsx +++ b/plugins/main/public/components/common/form/index.tsx @@ -8,6 +8,7 @@ import { InputFormFilePicker } from './input_filepicker'; import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { SettingTypes } from './types'; +import { InputFormPassword } from './input-password'; export interface InputFormProps { type: SettingTypes; @@ -93,5 +94,6 @@ const Input = { select: InputFormSelect, text: InputFormText, textarea: InputFormTextArea, + password: InputFormPassword, custom: ({ component, ...rest }) => component(rest), }; diff --git a/plugins/main/public/components/common/form/input-password.tsx b/plugins/main/public/components/common/form/input-password.tsx new file mode 100644 index 0000000000..eebae6e517 --- /dev/null +++ b/plugins/main/public/components/common/form/input-password.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { EuiFieldPassword } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormPassword = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, + options, +}: IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/main/public/components/common/form/types.ts b/plugins/main/public/components/common/form/types.ts index 762f73962f..09846abd86 100644 --- a/plugins/main/public/components/common/form/types.ts +++ b/plugins/main/public/components/common/form/types.ts @@ -1,4 +1,4 @@ -import { TPluginSettingWithKey } from '../../../../common/constants'; +import { TPluginSettingWithKey } from '../../../../../wazuh-core/common/constants'; export interface IInputFormType { field: TPluginSettingWithKey; @@ -46,8 +46,18 @@ interface CustomFieldConfiguration extends FieldConfiguration { component: (props: any) => JSX.Element; } +interface ArrayOfFieldConfiguration extends FieldConfiguration { + type: 'arrayOf'; + fields: { + [key: string]: any; // TODO: enhance this type + }; +} + export interface FormConfiguration { - [key: string]: DefaultFieldConfiguration | CustomFieldConfiguration; + [key: string]: + | DefaultFieldConfiguration + | CustomFieldConfiguration + | ArrayOfFieldConfiguration; } interface EnhancedField { @@ -70,7 +80,9 @@ interface EnhancedCustomField extends EnhancedField { component: (props: any) => JSX.Element; } -export type EnhancedFieldConfiguration = EnhancedDefaultField | EnhancedCustomField; +export type EnhancedFieldConfiguration = + | EnhancedDefaultField + | EnhancedCustomField; export interface EnhancedFields { [key: string]: EnhancedFieldConfiguration; } @@ -81,4 +93,15 @@ export interface UseFormReturn { errors: { [key: string]: string }; undoChanges: () => void; doChanges: () => void; + forEach: ( + value: any, + key: string, + form: { + formDefinition: any; + formState: any; + pathFieldFormDefinition: string[]; + pathFormState: string[]; + fieldDefinition: FormConfiguration; + }, + ) => { [key: string]: any }; } diff --git a/plugins/main/public/components/common/hocs/index.ts b/plugins/main/public/components/common/hocs/index.ts index 0bcc4cc726..03f496774c 100644 --- a/plugins/main/public/components/common/hocs/index.ts +++ b/plugins/main/public/components/common/hocs/index.ts @@ -12,7 +12,6 @@ export * from './withWindowSize'; export * from './withPluginPlatformContext'; export * from './withUserPermissions'; -export * from './withUserRoles'; export * from './withUserAuthorization'; export * from './withGlobalBreadcrumb'; export * from './withReduxProvider'; diff --git a/plugins/main/public/components/common/hocs/withUserAuthorization.tsx b/plugins/main/public/components/common/hocs/withUserAuthorization.tsx index fd4ce0bf81..aea01ef14a 100644 --- a/plugins/main/public/components/common/hocs/withUserAuthorization.tsx +++ b/plugins/main/public/components/common/hocs/withUserAuthorization.tsx @@ -10,21 +10,43 @@ * Find more information about this on the LICENSE file. */ -import React from "react"; -import { useUserPermissions, useUserPermissionsRequirements, useUserPermissionsPrivate } from '../hooks/useUserPermissions'; -import { useUserRoles, useUserRolesRequirements, useUserRolesPrivate } from '../hooks/useUserRoles'; +import React from 'react'; +import { useUserPermissionsRequirements } from '../hooks/useUserPermissions'; +import { useUserPermissionsIsAdminRequirements } from '../hooks/use-user-is-admin'; import { WzEmptyPromptNoPermissions } from '../permissions/prompt'; import { compose } from 'redux'; -import { withUserLogged } from './withUserLogged' - // - const withUserAuthorizationPromptChanged = (permissions = null, roles = null) => WrappedComponent => props => { - const [userPermissionRequirements, userPermissions] = useUserPermissionsRequirements(typeof permissions === 'function' ? permissions(props) : permissions); - const [userRolesRequirements, userRoles] = useUserRolesRequirements(typeof roles === 'function' ? roles(props) : roles); +import { withUserLogged } from './withUserLogged'; +// +const withUserAuthorizationPromptChanged = + (permissions = null, othersPermissions = { isAdmininistrator: null }) => + WrappedComponent => + props => { + const [userPermissionRequirements, userPermissions] = + useUserPermissionsRequirements( + typeof permissions === 'function' ? permissions(props) : permissions, + ); + const [_userPermissionIsAdminRequirements] = + useUserPermissionsIsAdminRequirements(); - return (userPermissionRequirements || userRolesRequirements) ? : ; -} + const userPermissionIsAdminRequirements = + othersPermissions?.isAdmininistrator + ? _userPermissionIsAdminRequirements + : null; -export const withUserAuthorizationPrompt = (permissions = null, roles = null) => WrappedComponent => compose( - withUserLogged, - withUserAuthorizationPromptChanged(permissions,roles) - )(WrappedComponent) + return userPermissionRequirements || userPermissionIsAdminRequirements ? ( + + ) : ( + + ); + }; + +export const withUserAuthorizationPrompt = + (permissions = null, othersPermissions = { isAdmininistrator: null }) => + WrappedComponent => + compose( + withUserLogged, + withUserAuthorizationPromptChanged(permissions, othersPermissions), + )(WrappedComponent); diff --git a/plugins/main/public/components/common/hocs/withUserRoles.tsx b/plugins/main/public/components/common/hocs/withUserRoles.tsx deleted file mode 100644 index d6823980d9..0000000000 --- a/plugins/main/public/components/common/hocs/withUserRoles.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Wazuh app - React HOCs to manage user role requirements - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import React from "react"; -import { useUserRoles, useUserRolesRequirements, useUserRolesPrivate } from '../hooks/useUserRoles'; - -// This HOC passes rolesValidation to wrapped component -export const withUserRoles = WrappedComponent => props => { - const userRoles = useUserRoles(); - return ; -} - -// This HOC hides the wrapped component if user has not permissions -export const withUserRolesRequirements = requiredUserRoles => WrappedComponent => props => { - const [userRolesRequirements, userRoles] = useUserRolesRequirements(typeof requiredUserRoles === 'function' ? requiredUserRoles(props) : requiredUserRoles); - return ; -} - -// This HOC redirects to redirectURL if user has not permissions -export const withUserRolesPrivate = (requiredUserRoles, redirectURL) => WrappedComponent => props => { - const [userRolesRequirements, userRoles] = useUserRolesPrivate(requiredUserRoles, redirectURL); - return userRolesRequirements ? : null; -} - diff --git a/plugins/main/public/components/common/hooks/index.ts b/plugins/main/public/components/common/hooks/index.ts index e3ce7584c7..f8e404da53 100644 --- a/plugins/main/public/components/common/hooks/index.ts +++ b/plugins/main/public/components/common/hooks/index.ts @@ -17,7 +17,7 @@ export * from './use-query'; export * from './use-time-filter'; export * from './useWindowSize'; export * from './useUserPermissions'; -export * from './useUserRoles'; +export * from './use-user-is-admin'; export * from './useResfreshAngularDiscover'; export * from './useAllowedAgents'; export * from './useApiRequest'; diff --git a/plugins/main/public/components/common/hooks/use-user-is-admin.ts b/plugins/main/public/components/common/hooks/use-user-is-admin.ts new file mode 100644 index 0000000000..65c8e98102 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-user-is-admin.ts @@ -0,0 +1,7 @@ +import { useSelector } from 'react-redux'; + +// It retuns user requirements if is is not admin +export const useUserPermissionsIsAdminRequirements = () => { + const account = useSelector(state => state.appStateReducers.userAccount); + return [account.administrator_error_message, account]; +}; diff --git a/plugins/main/public/components/common/hooks/useUserRoles.ts b/plugins/main/public/components/common/hooks/useUserRoles.ts deleted file mode 100644 index f986eee57f..0000000000 --- a/plugins/main/public/components/common/hooks/useUserRoles.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Wazuh app - React hooks to manage user role requirements - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import { useSelector } from 'react-redux'; -import { WzUserPermissions } from '../../../react-services/wz-user-permissions'; - -// It retuns user Roles -export const useUserRoles = () => { - const userRoles = useSelector(state => state.appStateReducers.userRoles); - return userRoles; -} - -// It returns user roles validation and user roles -export const useUserRolesRequirements = (requiredRoles) => { - const userRoles = useUserRoles(); - if(requiredRoles === null){ - return [false, userRoles] - } - const requiredRolesArray = typeof requiredRoles === 'function' ? requiredRoles() : requiredRoles; - return [WzUserPermissions.checkMissingUserRoles(requiredRolesArray, userRoles), userRoles]; -} - -// It redirects to other URL if user roles are not valid -export const useUserRolesPrivate = (requiredRoles, redirectURL) => { - const [userRolesValidation, userRoles] = useUserRolesRequirements(requiredRoles); - if(userRolesValidation){ - window.location.href = redirectURL; - } - return [userRolesValidation, userRoles]; -} \ No newline at end of file diff --git a/plugins/main/public/components/common/permissions/button.tsx b/plugins/main/public/components/common/permissions/button.tsx index 799f5539ae..419477fc2e 100644 --- a/plugins/main/public/components/common/permissions/button.tsx +++ b/plugins/main/public/components/common/permissions/button.tsx @@ -34,7 +34,7 @@ interface IWzButtonPermissionsProps export const WzButtonPermissions = ({ buttonType = 'default', permissions, - roles, + administrator, tooltip, ...rest }: IWzButtonPermissionsProps) => { @@ -52,7 +52,7 @@ export const WzButtonPermissions = ({ return ( { const additionalProps = { diff --git a/plugins/main/public/components/common/permissions/element.tsx b/plugins/main/public/components/common/permissions/element.tsx index 216a9f7d44..d957a82482 100644 --- a/plugins/main/public/components/common/permissions/element.tsx +++ b/plugins/main/public/components/common/permissions/element.tsx @@ -12,11 +12,9 @@ import React, { Fragment } from 'react'; import { useUserPermissionsRequirements } from '../hooks/useUserPermissions'; -import { useUserRolesRequirements } from '../hooks/useUserRoles'; - import { EuiToolTip, EuiSpacer } from '@elastic/eui'; - import { WzPermissionsFormatted } from './format'; +import { useUserPermissionsIsAdminRequirements } from '../hooks/use-user-is-admin'; export interface IUserPermissionsObject { action: string; @@ -25,11 +23,12 @@ export interface IUserPermissionsObject { export type TUserPermissionsFunction = (props: any) => TUserPermissions; export type TUserPermissions = (string | IUserPermissionsObject)[] | null; export type TUserRoles = string[] | null; +export type TUserIsAdministrator = string | null; export type TUserRolesFunction = (props: any) => TUserRoles; export interface IWzElementPermissionsProps { permissions?: TUserPermissions | TUserPermissionsFunction; - roles?: TUserRoles | TUserRolesFunction; + administrator?: boolean; tooltip?: any; children: React.ReactElement; getAdditionalProps?: (disabled: boolean) => { @@ -40,7 +39,7 @@ export interface IWzElementPermissionsProps { export const WzElementPermissions = ({ children, permissions = null, - roles = null, + administrator = false, getAdditionalProps, tooltip, ...rest @@ -48,15 +47,16 @@ export const WzElementPermissions = ({ const [userPermissionRequirements] = useUserPermissionsRequirements( typeof permissions === 'function' ? permissions(rest) : permissions, ); - const [userRolesRequirements] = useUserRolesRequirements( - typeof roles === 'function' ? roles(rest) : roles, - ); - const isDisabledByRolesOrPermissions = - userRolesRequirements || userPermissionRequirements; + const [userRequireAdministratorRequirements] = + useUserPermissionsIsAdminRequirements(); + + const isDisabledByPermissions = + userPermissionRequirements || + (administrator && userRequireAdministratorRequirements); const disabled = Boolean( - isDisabledByRolesOrPermissions || rest?.isDisabled || rest?.disabled, + isDisabledByPermissions || rest?.isDisabled || rest?.disabled, ); const additionalProps = getAdditionalProps @@ -67,7 +67,7 @@ export const WzElementPermissions = ({ ...additionalProps, }); - const contentTextRequirements = isDisabledByRolesOrPermissions && ( + const contentTextRequirements = isDisabledByPermissions && ( {userPermissionRequirements && (
@@ -81,23 +81,18 @@ export const WzElementPermissions = ({ {WzPermissionsFormatted(userPermissionRequirements)}
)} - {userPermissionRequirements && userRolesRequirements && ( + {userPermissionRequirements && userRequireAdministratorRequirements && ( )} - {userRolesRequirements && ( + {userRequireAdministratorRequirements && (
- Require{' '} - {userRolesRequirements - .map(role => ( - {role} - )) - .reduce((prev, cur) => [prev, ', ', cur])}{' '} - {userRolesRequirements.length > 1 ? 'roles' : 'role'} + Require administrator privilegies:{' '} + {userRequireAdministratorRequirements}
)}
); - return isDisabledByRolesOrPermissions ? ( + return isDisabledByPermissions ? ( {childrenWithAdditionalProps} diff --git a/plugins/main/public/components/common/permissions/prompt.tsx b/plugins/main/public/components/common/permissions/prompt.tsx index ae7817dd5a..962cdcd491 100644 --- a/plugins/main/public/components/common/permissions/prompt.tsx +++ b/plugins/main/public/components/common/permissions/prompt.tsx @@ -11,44 +11,38 @@ */ import React, { Fragment } from 'react'; -import { useUserPermissionsRequirements, useUserRolesRequirements } from '../hooks'; import { EuiEmptyPrompt, EuiSpacer } from '@elastic/eui'; -import { - TUserPermissions, - TUserPermissionsFunction, - TUserRoles, - TUserRolesFunction, -} from '../permissions/element'; +import { TUserPermissions, TUserIsAdministrator } from '../permissions/element'; import { WzPermissionsFormatted } from './format'; import { withErrorBoundary } from '../hocs/error-boundary/with-error-boundary'; interface IEmptyPromptNoPermissions { permissions?: TUserPermissions; - roles?: TUserRoles; + administrator?: TUserIsAdministrator; actions?: React.ReactNode; } export const WzEmptyPromptNoPermissions = withErrorBoundary( - ({ permissions, roles, actions }: IEmptyPromptNoPermissions) => { + ({ permissions, administrator, actions }: IEmptyPromptNoPermissions) => { return ( You have no permissions} body={ {permissions && (
- This section requires the {permissions.length > 1 ? 'permissions' : 'permission'}: + This section requires the{' '} + {permissions.length > 1 ? 'permissions' : 'permission'}: {WzPermissionsFormatted(permissions)}
)} - {permissions && roles && } - {roles && ( + {permissions && administrator && } + {administrator && (
- This section requires{' '} - {roles - .map((role) => {role}) - .reduce((accum, cur) => [accum, ', ', cur])}{' '} - {roles.length > 1 ? 'roles' : 'role'} + This section requires administrator privilegies:{' '} + + {administrator} +
)}
@@ -56,34 +50,5 @@ export const WzEmptyPromptNoPermissions = withErrorBoundary( actions={actions} /> ); - } + }, ); -interface IPromptNoPermissions { - permissions?: TUserPermissions | TUserPermissionsFunction; - roles?: TUserRoles | TUserRolesFunction; - children?: React.ReactNode; - rest?: any; -} - -export const WzPromptPermissions = ({ - permissions = null, - roles = null, - children, - ...rest -}: IPromptNoPermissions) => { - const [userPermissionRequirements, userPermissions] = useUserPermissionsRequirements( - typeof permissions === 'function' ? permissions(rest) : permissions - ); - const [userRolesRequirements, userRoles] = useUserRolesRequirements( - typeof roles === 'function' ? roles(rest) : roles - ); - - return userPermissionRequirements || userRolesRequirements ? ( - - ) : ( - children - ); -}; diff --git a/plugins/main/public/components/common/search-bar/use-search-bar.ts b/plugins/main/public/components/common/search-bar/use-search-bar.ts index 5f88d36919..e5cf2b1999 100644 --- a/plugins/main/public/components/common/search-bar/use-search-bar.ts +++ b/plugins/main/public/components/common/search-bar/use-search-bar.ts @@ -8,7 +8,7 @@ import { IIndexPattern, IndexPatternsContract, } from '../../../../../../src/plugins/data/public'; -import { getDataPlugin } from '../../../kibana-services'; +import { getDataPlugin, getWazuhCorePlugin } from '../../../kibana-services'; import { useFilterManager, useQueryManager, useTimeFilter } from '../hooks'; import { AUTHORIZED_AGENTS, diff --git a/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx b/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx index 02b9cc8d1f..785851b767 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/components/server-address/server-address.tsx @@ -16,6 +16,7 @@ import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; import '../group-input/group-input.scss'; import { WzRequest } from '../../../../../react-services'; import { ErrorHandler } from '../../../../../react-services/error-management/error-handler/error-handler'; +import { WzButtonPermissions } from '../../../../common/permissions/button'; interface ServerAddressInputProps { formField: EnhancedFieldConfiguration; @@ -147,7 +148,9 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { - ({ const permissionsStore = { appStateReducers: { + userAccount: { + administrator: true, + }, withUserLogged: true, - userRoles: ['administrator'], userPermissions: { 'agent:create': { '*:*:*': 'allow' }, rbac_mode: 'black', diff --git a/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index b75770a669..b9fef190f6 100644 --- a/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -29,8 +29,10 @@ import { import { AppNavigate } from '../../../../../react-services/app-navigate'; import { AppState } from '../../../../../react-services/app-state'; import { RequirementFlyout } from '../requirement-flyout/requirement-flyout'; -import { getDataPlugin } from '../../../../../kibana-services'; -import { getSettingDefaultValue } from '../../../../../../common/services/settings'; +import { + getDataPlugin, + getWazuhCorePlugin, +} from '../../../../../kibana-services'; export class ComplianceSubrequirements extends Component { _isMount = false; @@ -50,7 +52,7 @@ export class ComplianceSubrequirements extends Component { this.setState({ hideAlerts: !this.state.hideAlerts }); } - onSearchValueChange = (e) => { + onSearchValueChange = e => { this.setState({ searchValue: e.target.value }); }; @@ -69,7 +71,9 @@ export class ComplianceSubrequirements extends Component { params: { query: filter.value }, type: 'phrase', negate: filter.negate || false, - index: AppState.getCurrentPattern() || getSettingDefaultValue('pattern'), + index: + AppState.getCurrentPattern() || + getWazuhCorePlugin().configuration.getSettingValue('pattern'), }, query: { match_phrase: matchPhrase }, $state: { store: 'appState' }, @@ -97,12 +101,20 @@ export class ComplianceSubrequirements extends Component { } openDashboardCurrentWindow(requirementId) { - this.addFilter({ key: this.getRequirementKey(), value: requirementId, negate: false }); + this.addFilter({ + key: this.getRequirementKey(), + value: requirementId, + negate: false, + }); this.props.onSelectedTabChanged('dashboard'); } openDiscoverCurrentWindow(requirementId) { - this.addFilter({ key: this.getRequirementKey(), value: requirementId, negate: false }); + this.addFilter({ + key: this.getRequirementKey(), + value: requirementId, + negate: false, + }); this.props.onSelectedTabChanged('events'); } @@ -113,7 +125,7 @@ export class ComplianceSubrequirements extends Component { e, 'overview', { tab: this.props.section, tabView: 'discover', filters }, - () => this.openDiscoverCurrentWindow(requirementId) + () => this.openDiscoverCurrentWindow(requirementId), ); } @@ -124,7 +136,7 @@ export class ComplianceSubrequirements extends Component { e, 'overview', { tab: this.props.section, tabView: 'panels', filters }, - () => this.openDashboardCurrentWindow(requirementId) + () => this.openDashboardCurrentWindow(requirementId), ); } @@ -140,14 +152,20 @@ export class ComplianceSubrequirements extends Component { currentTechniques.forEach((technique, idx) => { if ( !showTechniques[technique] && - (technique.toLowerCase().includes(this.state.searchValue.toLowerCase()) || + (technique + .toLowerCase() + .includes(this.state.searchValue.toLowerCase()) || this.props.descriptions[technique] .toLowerCase() .includes(this.state.searchValue.toLowerCase())) ) { const quantity = - (requirementsCount.find((item) => item.key === technique) || {}).doc_count || 0; - if (!this.state.hideAlerts || (this.state.hideAlerts && quantity > 0)) { + (requirementsCount.find(item => item.key === technique) || {}) + .doc_count || 0; + if ( + !this.state.hideAlerts || + (this.state.hideAlerts && quantity > 0) + ) { showTechniques[technique] = true; tacticsToRender.push({ id: technique, @@ -165,17 +183,22 @@ export class ComplianceSubrequirements extends Component { .map((item, idx) => { const tooltipContent = `View details of ${item.id}`; const toolTipAnchorClass = - 'wz-display-inline-grid' + (this.state.hover === item.id ? ' wz-mitre-width' : ' '); + 'wz-display-inline-grid' + + (this.state.hover === item.id ? ' wz-mitre-width' : ' '); return ( this.setState({ hover: item.id })} onMouseLeave={() => this.setState({ hover: '' })} key={idx} - style={{ border: '1px solid #8080804a', maxWidth: 'calc(25% - 8px)', maxHeight: 41 }} + style={{ + border: '1px solid #8080804a', + maxWidth: 'calc(25% - 8px)', + maxHeight: 41, + }} > @@ -209,25 +232,31 @@ export class ComplianceSubrequirements extends Component { {this.state.hover === item.id && ( - + { + onMouseDown={e => { this.openDashboard(e, item.id); e.stopPropagation(); }} - color="primary" - type="visualizeApp" + color='primary' + type='visualizeApp' > {' '}   - + { + onMouseDown={e => { this.openDiscover(e, item.id); e.stopPropagation(); }} - color="primary" - type="discoverApp" + color='primary' + type='discoverApp' > @@ -236,9 +265,9 @@ export class ComplianceSubrequirements extends Component { } isOpen={this.state.actionsOpen === item.id} closePopover={() => {}} - panelPaddingSize="none" + panelPaddingSize='none' style={{ width: '100%' }} - anchorPosition="downLeft" + anchorPosition='downLeft' > xxx @@ -249,7 +278,7 @@ export class ComplianceSubrequirements extends Component { return ( + ); } } - onChangeFlyout = (flyoutOn) => { + onChangeFlyout = flyoutOn => { this.setState({ flyoutOn }); }; @@ -288,7 +321,7 @@ export class ComplianceSubrequirements extends Component {
- +

Requirements

@@ -299,32 +332,34 @@ export class ComplianceSubrequirements extends Component { Hide requirements with no alerts   this.hideAlerts()} + onChange={e => this.hideAlerts()} />
- + this.onSearchValueChange(e)} + onChange={e => this.onSearchValueChange(e)} isClearable={true} - aria-label="Use aria labels when no actual label is in use" + aria-label='Use aria labels when no actual label is in use' /> - +
{this.props.loadingAlerts ? ( - + {this.state.flyoutOn && ( - { - return this.getRequirementKey(); - }} - openDashboard={(e, itemId) => this.openDashboard(e, itemId)} - openDiscover={(e, itemId) => this.openDiscover(e, itemId)} - /> + { + return this.getRequirementKey(); + }} + openDashboard={(e, itemId) => this.openDashboard(e, itemId)} + openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + /> )}
); diff --git a/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx b/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx index 2daf6834cb..2e9b2ac120 100644 --- a/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx +++ b/plugins/main/public/components/overview/mitre/components/techniques/techniques.tsx @@ -33,11 +33,14 @@ import { withWindowSize } from '../../../../../components/common/hocs/withWindow import { WzRequest } from '../../../../../react-services/wz-request'; import { AppState } from '../../../../../react-services/app-state'; import { WzFieldSearchDelay } from '../../../../common/search'; -import { getDataPlugin, getToasts } from '../../../../../kibana-services'; +import { + getDataPlugin, + getToasts, + getWazuhCorePlugin, +} from '../../../../../kibana-services'; import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; -import { getSettingDefaultValue } from '../../../../../../common/services/settings'; const MITRE_ATTACK = 'mitre-attack'; @@ -516,7 +519,8 @@ export const Techniques = withWindowSize( type: 'phrase', negate: filter.negate || false, index: - AppState.getCurrentPattern() || getSettingDefaultValue('pattern'), + AppState.getCurrentPattern() || + getWazuhCorePlugin().configuration.getSettingValue('pattern'), }, query: { match_phrase: matchPhrase }, $state: { store: 'appState' }, diff --git a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx index a7979eabb0..94cda9b639 100644 --- a/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx +++ b/plugins/main/public/components/overview/mitre_attack_intelligence/intelligence.test.tsx @@ -22,7 +22,7 @@ jest.mock( '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'htmlId', - }) + }), ); jest.mock('../../../react-services', () => ({ @@ -51,8 +51,10 @@ describe('Module Mitre Att&ck intelligence container', () => { it('should render the component if has permissions', () => { const store = mockStore({ appStateReducers: { + userAccount: { + administrator: true, + }, withUserLogged: true, - userRoles: ['administrator'], userPermissions: { 'mitre:read': { '*:*:*': 'allow' }, }, @@ -61,7 +63,7 @@ describe('Module Mitre Att&ck intelligence container', () => { const component = render( - + , ); expect(component).toMatchSnapshot(); }); @@ -69,8 +71,10 @@ describe('Module Mitre Att&ck intelligence container', () => { it('should render permissions prompt when no has permissions', () => { const store = mockStore({ appStateReducers: { + userAccount: { + administrator: true, + }, withUserLogged: true, - userRoles: ['administrator'], userPermissions: { 'mitre:read': { '*:*:*': 'deny' }, }, @@ -79,7 +83,7 @@ describe('Module Mitre Att&ck intelligence container', () => { const component = render( - + , ); expect(component).toMatchSnapshot(); }); diff --git a/plugins/main/public/components/security/main.tsx b/plugins/main/public/components/security/main.tsx index b29170a303..ceb728331c 100644 --- a/plugins/main/public/components/security/main.tsx +++ b/plugins/main/public/components/security/main.tsx @@ -27,7 +27,6 @@ import { import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; -import { getPluginDataPath } from '../../../common/plugin'; import { security } from '../../utils/applications'; import { getWazuhCorePlugin } from '../../kibana-services'; @@ -129,22 +128,18 @@ export const WzSecurity = compose( let runAsWarningTxt = ''; switch (allowRunAs) { case getWazuhCorePlugin().API_USER_STATUS_RUN_AS.HOST_DISABLED: - runAsWarningTxt = `For the role mapping to take effect, enable run_as in ${getPluginDataPath( - 'config/wazuh.yml', - )} configuration file, restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; + runAsWarningTxt = `For the role mapping to take effect, enable run_as in the API host configuration, restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; break; case getWazuhCorePlugin().API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED: runAsWarningTxt = - 'The role mapping has no effect because the current Wazuh API user has allow_run_as disabled.'; + 'The role mapping has no effect because the current API user has allow_run_as disabled.'; break; case getWazuhCorePlugin().API_USER_STATUS_RUN_AS.ALL_DISABLED: - runAsWarningTxt = `For the role mapping to take effect, enable run_as in ${getPluginDataPath( - 'config/wazuh.yml', - )} configuration file and set the current Wazuh API user allow_run_as to true. Restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; + runAsWarningTxt = `For the role mapping to take effect, enable run_as in the API host configuration and set the current API user allow_run_as to true. Restart the ${PLUGIN_PLATFORM_NAME} service and clear your browser cache and cookies.`; break; default: runAsWarningTxt = - 'The role mapping has no effect because the current Wazuh API user has run_as disabled.'; + 'The role mapping has no effect because the current API user has run_as disabled.'; break; } diff --git a/plugins/main/public/components/settings/api/add-api.js b/plugins/main/public/components/settings/api/add-api.js deleted file mode 100644 index fd977dfc4f..0000000000 --- a/plugins/main/public/components/settings/api/add-api.js +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Wazuh app - React component for the adding an API entry form. - * - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiCodeBlock, - EuiText, - EuiSpacer, - EuiCode, - EuiButton, - EuiButtonEmpty, - EuiSteps, - EuiCallOut, - EuiPanel -} from '@elastic/eui'; -import { withErrorBoundary } from '../../common/hocs'; -import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { getPluginDataPath } from '../../../../common/plugin'; - -export const AddApi = withErrorBoundary (class AddApi extends Component { - constructor(props) { - super(props); - this.state = { - status: 'incomplete', - fetchingData: false, - blockClose: false - }; - } - - componentDidMount() { - this.setState({ enableClose: this.props.enableClose }); - this.checkErrorsAtInit(); - } - - /** - * Checks if the component was initialized with some error in order to show it - */ - checkErrorsAtInit() { - if (this.props.errorsAtInit) { - const error = this.props.errorsAtInit; - this.setState({ - status: error.type || 'danger', - blockClose: true, - message: - (error.data || error).message || - 'Wazuh API not reachable, please review your configuration', - fetchingData: false - }); - } - } - - /** - * Check the APIs connections - */ - async checkConnection() { - //TODO handle this - try { - this.setState({ - status: 'incomplete', - fetchingData: true, - blockClose: false - }); - - await this.props.checkForNewApis(); - - this.setState({ - status: 'complete', - fetchingData: false, - closedEnabled: true - }); - } catch (error) { - const close = - error.data && error.data.code && error.data.code === 2001 - ? false - : error.closedEnabled || false; - this.setState({ - status: error.type || 'danger', - closedEnabled: close, - blockClose: !close, - enableClose: false, - message: - (error.data || error).message || error || - 'Wazuh API not reachable, please review your configuration', - fetchingData: false - }); - - const options = { - context: `${AddApi.name}.checkConnection`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - store: true, - error: { - error: error, - message: error.message || error, - title: `Wazuh API not reachable, please review your configuration: ${error.message || error}`, - }, - }; - - getErrorOrchestrator().handleError(options); - } - } - - render() { - const apiExample = `hosts: - - : - url: - port: - username: - password: - run_as: `; - - const checkConnectionChildren = ( -
- {(this.state.status === 'warning' || - this.state.status === 'danger') && ( - - )} - {(this.state.status === 'warning' || - this.state.status === 'danger') && } - - Check that the {PLUGIN_PLATFORM_NAME} server can reach the configured Wazuh API(s). - - - await this.checkConnection()} - isLoading={this.state.fetchingData} - > - Check connection - - {(this.state.closedEnabled || this.state.enableClose) && - !this.state.blockClose && ( - this.props.closeAddApi()}> - Close - - )} -
- ); - - const editConfigChildren = ( -
- - Modify{' '} - {getPluginDataPath('config/wazuh.yml')}{' '} - to set the connection information. - - - {apiExample} - - - Where {''} is an arbitrary ID,{' '} - {''} is the URL of the Wazuh API,{' '} - {''} is the port,{' '} - {''} and{' '} - {''} are the credentials to - authenticate,{' '} - {''} defines if the app user's permissions depends on the authentication context ({'true'} / {'false'}). - -
- ); - - const steps = [ - { - title: 'Edit the configuration', - children: editConfigChildren - }, - { - title: 'Test the configuration', - children: checkConnectionChildren, - status: this.state.status - } - ]; - - const view = ( - - - - - - - -

Getting started

-
-
- - - {this.state.enableClose && !this.state.blockClose && ( - this.props.closeAddApi()} - iconType="cross" - > - close - - )} - -
- - -
-
- -
- ); - - return view; - } -}) - -AddApi.propTypes = { - checkForNewApis: PropTypes.func, - closeAddApi: PropTypes.func, - enableClose: PropTypes.bool -}; diff --git a/plugins/main/public/components/settings/api/add-api.tsx b/plugins/main/public/components/settings/api/add-api.tsx new file mode 100644 index 0000000000..3344d168f8 --- /dev/null +++ b/plugins/main/public/components/settings/api/add-api.tsx @@ -0,0 +1,211 @@ +/* + * Wazuh app - React component for the adding an API entry form. + * + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import React, { useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiSpacer, + EuiButton, +} from '@elastic/eui'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { getWazuhCorePlugin } from '../../../kibana-services'; +import { useForm } from '../../common/form/hooks'; +import { InputForm } from '../../common/form'; +import { ErrorHandler, GenericRequest } from '../../../react-services'; + +const transformPluginSettingsToFormFields = (configuration, pluginSettings) => { + return Object.entries(pluginSettings).reduce( + ( + accum, + [ + key, + { + type, + validate, + defaultValue: initialValue, + uiFormTransformChangedInputValue, + uiFormTransformConfigurationValueToInputValue, + uiFormTransformInputValueToConfigurationValue, + ...rest + }, + ], + ) => { + return { + ...accum, + [key]: { + _meta: rest, + type, + validate: validate?.bind?.(rest), + transformChangedInputValue: + uiFormTransformChangedInputValue?.bind?.(rest), + transformChangedOutputValue: + uiFormTransformInputValueToConfigurationValue?.bind?.(rest), + initialValue: uiFormTransformConfigurationValueToInputValue + ? uiFormTransformConfigurationValueToInputValue.bind(rest)( + configuration?.[key] ?? initialValue, + ) + : configuration?.[key] ?? initialValue, + defaultValue: uiFormTransformConfigurationValueToInputValue + ? uiFormTransformConfigurationValueToInputValue.bind(rest)( + configuration?.[key] ?? initialValue, + ) + : configuration?.[key] ?? initialValue, + options: rest.options, + }, + }; + }, + {}, + ); +}; + +interface IPropsAddAPIHostForm { + initialValue?: { + id?: string; + url?: string; + port?: number; + username?: string; + password?: string; + run_as?: string; + }; + apiId: string; + mode: 'CREATE' | 'EDIT'; + onSave: () => void; + onUpdateCanClose: (boolean) => void; +} + +export const AddAPIHostForm = ({ + initialValue = {}, + apiId = '', + mode = 'CREATE', + onSave: onSaveProp, + onUpdateCanClose, +}: IPropsAddAPIHostForm) => { + const { fields, changed, errors } = useForm( + transformPluginSettingsToFormFields(initialValue, { + ...Array.from( + getWazuhCorePlugin().configuration._settings.entries(), + ).find(([key]) => key === 'hosts')[1].options.arrayOf, + // Add an input to confirm the password + password_confirm: { + ...Array.from( + getWazuhCorePlugin().configuration._settings.entries(), + ).find(([key]) => key === 'hosts')[1].options.arrayOf.password, + title: 'Confirm password', + }, + }), + ); + + useEffect(() => { + onUpdateCanClose?.(!Boolean(Object.keys(changed).length)); + }, [changed]); + + const onSave = async () => { + try { + const apiHostId = mode === 'CREATE' ? fields.id.value : apiId; + const saveFields = + mode === 'CREATE' + ? fields + : Object.fromEntries( + Object.keys(changed).map(key => [key, fields[key]]), + ); + const { password_confirm, ...rest } = saveFields; + + const response = await GenericRequest.request( + mode === 'CREATE' ? 'POST' : 'PUT', + `/hosts/apis/${apiHostId}`, + Object.entries(rest).reduce( + (accum, [key, { value, transformChangedOutputValue }]) => ({ + ...accum, + [key]: transformChangedOutputValue?.(value) ?? value, + }), + {}, + ), + ); + ErrorHandler.info(response.data.message); + onSaveProp && (await onSaveProp()); + } catch (error) { + const options = { + context: 'AddAPIHostForm.onSave', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + display: true, + store: false, + error: { + error: error, + message: error.message || error, + title: `API host could not be ${ + mode === 'CREATE' ? 'created' : 'updated' + } due to ${error.message}`, + }, + }; + + getErrorOrchestrator().handleError(options); + } + }; + + const passwordNotMatch = + fields.password.value !== fields.password_confirm.value; + + const disableApplyButton = + mode === 'EDIT' + ? Object.values(fields).some(({ changed, error }) => changed && error) || + passwordNotMatch + : Boolean(Object.keys(errors).length) || passwordNotMatch; + + return ( +
+ {[ + 'id', + 'url', + 'port', + 'username', + 'password', + 'password_confirm', + 'run_as', + ].map(key => { + const { _meta, ...field } = fields[key]; + return ( + + ); + })} + + {passwordNotMatch && ( + + Password must match. + + )} + + + + Apply + + + +
+ ); +}; diff --git a/plugins/main/public/components/settings/api/api-is-down.js b/plugins/main/public/components/settings/api/api-is-down.js deleted file mode 100644 index 9fd899aca6..0000000000 --- a/plugins/main/public/components/settings/api/api-is-down.js +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Wazuh app - React component for the adding an API entry form. - * - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiCodeBlock, - EuiText, - EuiSpacer, - EuiCode, - EuiButton, - EuiButtonEmpty, - EuiSteps, - EuiBasicTable, - EuiHealth, - EuiCallOut, - EuiLoadingSpinner, - EuiToolTip, - EuiButtonIcon, - EuiPanel -} from '@elastic/eui'; -import { withErrorBoundary } from '../../common/hocs'; -import { - UI_ERROR_SEVERITIES, -} from '../../../react-services/error-orchestrator/types'; -import { UI_LOGGER_LEVELS, PLUGIN_PLATFORM_NAME } from '../../../../common/constants'; -import { getErrorOrchestrator } from '../../../react-services/common-services'; -import { getPluginDataPath } from '../../../../common/plugin'; - -export const ApiIsDown = withErrorBoundary (class ApiIsDown extends Component { - constructor(props) { - super(props); - this.state = { - status: 'incomplete', - fetchingData: false, - apiEntries: [], - refreshingEntries: false - }; - } - - componentDidMount() { - this.setState({ - apiEntries: [...this.props.apiEntries] - }); - } - - /** - * Checks again the connection in order to know the state of the API entries - */ - async checkConnection() { - try { - let status = 'complete'; - this.setState({ error: false }); - const hosts = await this.props.getHosts(); - this.setState({ - fetchingData: true, - refreshingEntries: true, - apiEntries: hosts - }); - const entries = this.state.apiEntries; - let numErr = 0; - for (let idx in entries) { - const entry = entries[idx]; - try { - const data = await this.props.testApi(entry, true); // token refresh is forced - const clusterInfo = data.data || {}; - const id = entries[idx].id; - entries[idx].status = 'online'; - entries[idx].cluster_info = clusterInfo; - //Updates the cluster info in the registry - await this.props.updateClusterInfoInRegistry(id, clusterInfo); - this.props.setDefault(entry); - } catch (error) { - numErr = numErr + 1; - const code = ((error || {}).data || {}).code; - const downReason = typeof error === 'string' ? error : - (error || {}).message || ((error || {}).data || {}).message || 'Wazuh is not reachable'; - const status = code === 3099 ? 'down' : 'unknown'; - entries[idx].status = { status, downReason }; - } - } - if (numErr) { - status = numErr >= entries.length ? 'danger' : 'warning'; - } - this.setState({ - apiEntries: entries, - fetchingData: false, - status: status, - refreshingEntries: false - }); - } catch (error) { - if ( - error && - error.data && - error.data.message && - error.data.code === 2001 - ) { - this.setState({ error: error.data.message, status: 'danger' }); - } - - const options = { - context: `${ApiIsDown.name}.checkConnection`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - store: true, - error: { - error: error, - message: error.message || error, - title: error.message || error, - }, - }; - - getErrorOrchestrator().handleError(options); - } - } - - render() { - const apiExample = `# Example Wazuh API configuration -hosts: - - production: - url: https://172.16.1.2 - port: 55000 - username: wazuh-wui - password: wazuh-wui - run_as: false -`; - - const checkConnectionChildren = ( -
- - Check that the {PLUGIN_PLATFORM_NAME} server can reach the configured Wazuh API(s). - - - await this.checkConnection()} - isLoading={this.state.fetchingData} - > - Check connection - - {this.state.status !== 'danger' && - this.state.status !== 'incomplete' && ( - this.props.closeApiIsDown()}> - Close - - )} - - Already configured Wazuh API(s) - - {(!this.state.error && ( - { - if (item) { - return item === 'online' ? ( - Online - ) : item.status === 'down' ? ( - - Warning - - - this.props.copyToClipBoard(item.downReason) - } - /> - - - ) : ( - - Offline - - - this.props.copyToClipBoard(item.downReason) - } - /> - - - ); - } else { - return ( - - -   Checking - - ); - } - } - } - ]} - /> - )) || ( - - )} -
- ); - - const steps = [ - { - title: 'Check the Wazuh API service status', - children: ( -
- For Systemd - - $ sudo systemctl status wazuh-manager - - For SysV Init - - $ sudo service wazuh-manager status -
- ) - }, - { - title: 'Check the configuration', - children: ( -
- - Review the settings in the{' '} - - {getPluginDataPath('config/wazuh.yml')} - {' '} - file. - - - {apiExample} -
- ) - }, - { - title: 'Test the configuration', - children: checkConnectionChildren, - status: this.state.status - } - ]; - return ( - - - - - -

Wazuh API seems to be down

-
- - -
-
- -
- ); - } -}); - -ApiIsDown.propTypes = { - apiEntries: PropTypes.array, - checkManager: PropTypes.func, - setDefault: PropTypes.func, - closeApiIsDown: PropTypes.func, - updateClusterInfoInRegistry: PropTypes.func, - getHosts: PropTypes.func, - copyToClipboard: PropTypes.func -}; diff --git a/plugins/main/public/components/settings/api/api-table.js b/plugins/main/public/components/settings/api/api-table.js index 4778cfc62f..66e6d747a3 100644 --- a/plugins/main/public/components/settings/api/api-table.js +++ b/plugins/main/public/components/settings/api/api-table.js @@ -10,7 +10,6 @@ * * Find more information about this on the LICENSE file. */ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { EuiFlexGroup, @@ -26,6 +25,12 @@ import { EuiText, EuiLoadingSpinner, EuiIcon, + EuiCallOut, + EuiSpacer, + EuiSteps, + EuiCopy, + EuiCodeBlock, + EuiButton, } from '@elastic/eui'; import { WzButtonPermissions } from '../../common/permissions/button'; import { AppState } from '../../../react-services/app-state'; @@ -39,6 +44,17 @@ import { getWazuhCorePlugin, } from '../../../kibana-services'; import { AvailableUpdatesFlyout } from './available-updates-flyout'; +import { AddAPIHostForm } from './add-api'; +import { + WzButtonOpenFlyout, + WzButtonPermissionsOpenFlyout, + WzButtonPermissionsModalConfirm, +} from '../../common/buttons'; +import { + ApiCheck, + ErrorHandler, + GenericRequest, +} from '../../../react-services'; export const ApiTable = compose( withErrorBoundary, @@ -48,12 +64,22 @@ export const ApiTable = compose( constructor(props) { super(props); + let selectedAPIConnection = null; + try { + const currentApi = AppState.getCurrentAPI(); + + if (currentApi) { + const { id } = JSON.parse(currentApi); + selectedAPIConnection = id; + } + } catch (error) {} + this.state = { apiEntries: [], + selectedAPIConnection, refreshingEntries: false, availableUpdates: {}, refreshingAvailableUpdates: true, - apiAvailableUpdateDetails: undefined, }; } @@ -88,71 +114,174 @@ export const ApiTable = compose( } componentDidMount() { - this.setState({ - apiEntries: this.props.apiEntries, - }); + this.refresh(); this.getApisAvailableUpdates(); } + copyToClipBoard(msg) { + const el = document.createElement('textarea'); + el.value = msg; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + ErrorHandler.info('Error copied to the clipboard'); + } + + async checkManager(APIConnection, silent = false) { + try { + // Get the Api information + const { username, url, port, id } = APIConnection; + + // Test the connection + const response = await ApiCheck.checkApi( + { + username: username, + url: url, + port: port, + cluster_info: {}, + insecure: 'true', + id: id, + }, + true, + ); + APIConnection.cluster_info = response.data; + // Updates the cluster-information in the registry + await GenericRequest.request('PUT', `/hosts/update-hostname/${id}`, { + cluster_info: APIConnection.cluster_info, + }); + APIConnection.status = 'online'; + APIConnection.allow_run_as = response.data.allow_run_as; + !silent && ErrorHandler.info('Connection success', 'Settings'); + // WORKAROUND: Update the apiEntries with the modifications of the APIConnection object + this.setState({ + apiEntries: this.state.apiEntries, + }); + } catch (error) { + if (!silent) { + const options = { + context: `${ApiTable.name}.checkManager`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + throw error; + } + } + + async setDefault(APIconnection) { + try { + await this.checkManager(APIconnection, true); + const { cluster_info, id } = APIconnection; + const { manager, cluster, status } = cluster_info; + + // Check the connection before set as default + AppState.setClusterInfo(cluster_info); + const clusterEnabled = status === 'disabled'; + AppState.setCurrentAPI( + JSON.stringify({ + name: clusterEnabled ? manager : cluster, + id: id, + }), + ); + + const currentApi = AppState.getCurrentAPI(); + const currentApiJSON = JSON.parse(currentApi); + + ErrorHandler.info(`API with id ${currentApiJSON.id} set as default`); + + this.setState({ selectedAPIConnection: currentApiJSON.id }); + } catch (error) { + const options = { + context: `${ApiTable.name}.setDefault`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + async refreshAPI(APIconnection, options) { + try { + const data = await ApiCheck.checkApi(APIconnection, true); + const clusterInfo = data.data || {}; + APIconnection.status = 'online'; + APIconnection.cluster_info = clusterInfo; + //Updates the cluster info in the registry + await GenericRequest.request( + 'PUT', + `/hosts/update-hostname/${APIconnection.id}`, + { + cluster_info: clusterInfo, + }, + ); + if (options?.selectAPIHostOnAvailable) { + this.setDefault(entry); + } + } catch (error) { + const code = ((error || {}).data || {}).code; + const downReason = + typeof error === 'string' + ? error + : (error || {}).message || + ((error || {}).data || {}).message || + 'Wazuh is not reachable'; + const status = code === 3099 ? 'down' : 'unknown'; + APIconnection.status = { status, downReason }; + if (APIconnection.id === this.state.selectedAPIConnection) { + // if the selected API is down, we remove it so a new one will selected + AppState.removeCurrentAPI(); + } + throw new Error(error); + } + } /** * Refresh the API entries */ - async refresh() { + async refresh(options = { selectAPIHostOnAvailable: false }) { try { let status = 'complete'; - this.setState({ error: false }); - const hosts = await this.props.getHosts(); + this.setState({ error: false, refreshingEntries: true }); + const responseAPIHosts = await GenericRequest.request( + 'GET', + '/hosts/apis', + {}, + ); + const hosts = responseAPIHosts.data || []; this.setState({ - refreshingEntries: true, - apiEntries: hosts, + apiEntries: hosts.map(host => ({ ...host, status: 'checking' })), }); - const entries = this.state.apiEntries; + const entries = [...hosts]; let numErr = 0; for (let idx in entries) { const entry = entries[idx]; try { - const data = await this.props.testApi(entry, true); // token refresh is forced - const clusterInfo = data.data || {}; - const id = entries[idx].id; - entries[idx].status = 'online'; - entries[idx].cluster_info = clusterInfo; - //Updates the cluster info in the registry - await this.props.updateClusterInfoInRegistry(id, clusterInfo); + await this.refreshAPI(entry, options); } catch (error) { numErr = numErr + 1; - const code = ((error || {}).data || {}).code; - const downReason = - typeof error === 'string' - ? error - : (error || {}).message || - ((error || {}).data || {}).message || - 'Wazuh is not reachable'; - const status = code === 3099 ? 'down' : 'unknown'; - entries[idx].status = { status, downReason }; - if (entries[idx].id === this.props.currentDefault) { - // if the selected API is down, we remove it so a new one will selected - AppState.removeCurrentAPI(); - } } } - if (numErr) { - if (numErr >= entries.length) this.props.showApiIsDown(); - } this.setState({ apiEntries: entries, status: status, refreshingEntries: false, + apiIsDown: entries.length > 0 && numErr >= entries.length, }); } catch (error) { - if ( - error && - error.data && - error.data.message && - error.data.code === 2001 - ) { - this.props.showAddApiWithInitialError(error); - } + this.setState({ + refreshingEntries: false, + }); } } @@ -165,7 +294,7 @@ export const ApiTable = compose( const entries = this.state.apiEntries; const idx = entries.map(e => e.id).indexOf(api.id); try { - await this.props.checkManager(api); + await this.checkManager(api); entries[idx].status = 'online'; } catch (error) { const code = ((error || {}).data || {}).code; @@ -202,6 +331,29 @@ export const ApiTable = compose( } } + async deleteAPIHost(id) { + try { + const response = await GenericRequest.request( + 'DELETE', + `/hosts/apis/${id}`, + ); + ErrorHandler.info(response.data.message); + await this.refresh(); + } catch (error) { + const options = { + context: `${ApiTable.name}.deleteAPIHost`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error removing the API host ${id}`, + }, + }; + getErrorOrchestrator().handleError(options); + } + } + render() { const { DismissNotificationCheck } = getWazuhCheckUpdatesPlugin(); @@ -285,6 +437,14 @@ export const ApiTable = compose( align: 'left', sortable: true, render: item => { + if (item === 'checking') { + return ( + + +   Checking + + ); + } if (item) { return item === 'online' ? ( @@ -307,9 +467,7 @@ export const ApiTable = compose( color='primary' iconType='questionInCircle' aria-label='Info about the error' - onClick={() => - this.props.copyToClipBoard(item.downReason) - } + onClick={() => this.copyToClipBoard(item.downReason)} /> @@ -331,21 +489,12 @@ export const ApiTable = compose( color='primary' iconType='questionInCircle' aria-label='Info about the error' - onClick={() => - this.props.copyToClipBoard(item.downReason) - } + onClick={() => this.copyToClipBoard(item.downReason)} /> ); - } else { - return ( - - -   Checking - - ); } }, }, @@ -396,18 +545,17 @@ export const ApiTable = compose( ) : null} {item === 'availableUpdates' ? ( - View available updates

} - > - - this.setState({ apiAvailableUpdateDetails: api }) - } - /> -
+ { + return ; + }} + buttonProps={{ + buttonType: 'icon', + iconType: 'eye', + }} + />
) : null} {item === 'error' && api.error?.detail ? ( @@ -421,9 +569,7 @@ export const ApiTable = compose( color='primary' iconType='questionInCircle' aria-label='Info about the error' - onClick={() => - this.props.copyToClipBoard(api.error.detail) - } + onClick={() => this.copyToClipBoard(api.error.detail)} /> @@ -477,35 +623,73 @@ export const ApiTable = compose( name: 'Actions', render: item => ( - - Set as default

}} - iconType={ - item.id === this.props.currentDefault - ? 'starFilled' - : 'starEmpty' - } - aria-label='Set as default' - onClick={async () => { - const currentDefault = await this.props.setDefault(item); - this.setState({ - currentDefault, - }); - }} + Set as default

}} + iconType={ + item.id === this.state.selectedAPIConnection + ? 'starFilled' + : 'starEmpty' + } + aria-label='Set as default' + onClick={async () => { + const currentDefault = await this.setDefault(item); + this.setState({ + currentDefault, + }); + }} + /> + Check connection

}> + await this.checkApi(item)} + color='success' /> -
- - Check connection

}> - await this.checkApi(item)} - color='success' +
+ ( + { + onClose(); + await this.refresh(); + }} /> - -
+ )} + buttonProps={{ + administrator: true, + buttonType: 'icon', + iconType: 'pencil', + tooltip: { + content: 'Edit', + }, + }} + > + this.deleteAPIHost(item.id)} + modalProps={{ buttonColor: 'danger' }} + iconType='trash' + color='danger' + aria-label='Delete API connection' + />
), }, @@ -518,6 +702,19 @@ export const ApiTable = compose( }, }; + const checkAPIHostsConnectionButton = ( + + await this.refresh({ + selectAPIHostOnAvailable: true, + }) + } + isDisabled={this.state.refreshingEntries} + > + Check connection + + ); + return ( @@ -532,14 +729,26 @@ export const ApiTable = compose( - this.props.showAddApi()} + ( + { + onClose(); + await this.refresh(); + }} + /> + )} + buttonProps={{ + administrator: true, + buttonType: 'empty', + iconType: 'plusInCircle', + }} > - Add new - + Add API connection + + {this.state.apiIsDown && ( + + + + + + { + const steps = [ + { + title: 'Check the API server service status', + children: ( + <> + {[ + { + label: 'For Systemd', + command: + 'sudo systemctl status wazuh-manager', + }, + { + label: 'For SysV Init', + command: + 'sudo service wazuh-manager status', + }, + ].map(({ label, command }) => ( + <> + {label} +
+ + {command} + + + {copy => ( +
+

+ Copy + command +

+
+ )} +
+
+ + + ))} + + ), + }, + { + title: 'Review the API hosts configuration', + }, + { + title: 'Check the API hosts connection', + children: checkAPIHostsConnectionButton, + }, + ]; + + return ( + + ); + }} + buttonProps={{ + buttonType: 'empty', + }} + > + Troubleshooting +
+
+ + {checkAPIHostsConnectionButton} + +
+
+
+
+ )}
- - this.setState({ apiAvailableUpdateDetails: undefined }) - } - />
); } }, ); - -ApiTable.propTypes = { - apiEntries: PropTypes.array, - currentDefault: PropTypes.string, - setDefault: PropTypes.func, - checkManager: PropTypes.func, - updateClusterInfoInRegistry: PropTypes.func, - getHosts: PropTypes.func, - testApi: PropTypes.func, - showAddApiWithInitialError: PropTypes.func, - showApiIsDown: PropTypes.func, - copyToClipBoard: PropTypes.func, -}; diff --git a/plugins/main/public/components/settings/api/available-updates-flyout/__snapshots__/index.test.tsx.snap b/plugins/main/public/components/settings/api/available-updates-flyout/__snapshots__/index.test.tsx.snap index 4f6c6b4d05..223e9a2b75 100644 --- a/plugins/main/public/components/settings/api/available-updates-flyout/__snapshots__/index.test.tsx.snap +++ b/plugins/main/public/components/settings/api/available-updates-flyout/__snapshots__/index.test.tsx.snap @@ -1,8 +1,58 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AvailableUpdatesFlyout component should return the AvailableUpdatesFlyout component 1`] = ` -