From 0c71645cf1cf222c29295bf4ea5d959756df5573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 11:13:23 +0200 Subject: [PATCH 01/34] rollback(configuration-store): rollback the store system to configuration file - Rollback the store system to configuration file - Change WAZUH_CORE_CONFIGURATION_CACHE_SECONDS from 60 to 10 - Create logic to ensure the configuration file is created else create it with default configuration on start - Remove task to migrate the configuration file to saved object - Enhance the Configuration service to include references to categories and some utility functions - Remove plugin settings: - wazuh_core.instance - wazuh_core.encryption_key --- plugins/wazuh-core/common/constants.ts | 246 ++++----- .../common/services/configuration.ts | 153 +++++- plugins/wazuh-core/server/index.ts | 21 +- plugins/wazuh-core/server/plugin.ts | 21 +- .../server/services/configuration-store.ts | 468 ++++++++++++------ 5 files changed, 580 insertions(+), 329 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 10e3e128ab..5c7dd66a09 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -129,6 +129,11 @@ export const WAZUH_DATA_CONFIG_REGISTRY_PATH = path.join( 'wazuh-registry.json', ); +export const WAZUH_DATA_CONFIG_APP_PATH = path.join( + WAZUH_DATA_CONFIG_DIRECTORY_PATH, + 'wazuh.yml', +); + // Wazuh data path - downloads export const WAZUH_DATA_DOWNLOADS_DIRECTORY_PATH = path.join( WAZUH_DATA_ABSOLUTE_PATH, @@ -465,6 +470,13 @@ export type TPluginSetting = { category: SettingCategory; // Type. type: EpluginSettingType; + // Store + store: { + file: { + configurable?: boolean; + transformFrom?: (value: any) => any; + }; + }; // Default value. defaultValue: any; // Default value if it is not set. It has preference over `default`. @@ -557,10 +569,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 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.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -594,10 +604,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'API connection', description: 'Enable or disable the API health check when opening the app.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -627,10 +635,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the known fields health check when opening the app.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -660,10 +666,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Change the default value of the plugin platform max buckets configuration.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -693,10 +697,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Change the default value of the plugin platform metaField configuration.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -726,10 +728,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the index pattern health check when opening the app.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -759,10 +759,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the setup health check when opening the app.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -792,10 +790,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the template health check when opening the app.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -825,10 +821,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Change the default value of the plugin platform timeFilter configuration.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -857,10 +851,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Cron prefix', description: 'Define the index prefix of predefined jobs.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -894,10 +886,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -947,10 +937,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Index creation', description: 'Define the interval in which a new index will be created.', store: { - savedObject: { - mapping: { - type: 'keyword', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -994,10 +982,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the name of the index in which the documents will be saved.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -1032,10 +1018,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the number of replicas to use for the statistics indices.', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -1071,10 +1055,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the number of shards to use for the statistics indices.', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -1108,10 +1090,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the frequency of task execution using cron schedule expressions.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -1133,10 +1113,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Status', description: 'Enable or disable the statistics tasks.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.STATISTICS, @@ -1165,10 +1143,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Status', description: 'Enable or disable the customization.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1198,10 +1174,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'App main logo', description: `This logo is used as loading indicator while the user is logging into Wazuh API.`, store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1248,10 +1222,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Healthcheck logo', description: `This logo is displayed during the Healthcheck routine of the app.`, store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1298,10 +1270,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 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.`, store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1347,10 +1317,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Reports footer', description: 'Set the footer of the reports.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1373,10 +1341,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Reports header', description: 'Set the header of the reports.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1400,10 +1366,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Specifies the Wazuh registration server, used for the agent enrollment.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -1420,10 +1384,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Specifies the password used to authenticate during the agent enrollment.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -1439,10 +1401,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Hide manager alerts', description: 'Hide the alerts of the manager in every dashboard.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -1475,11 +1435,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.arrayOf, defaultValue: [], store: { - savedObject: { - mapping: { - type: 'text', + file: { + transformFrom: value => { + return value.map(hostData => { + const key = Object.keys(hostData)?.[0]; + return { ...hostData[key], id: key }; + }); }, - encrypted: true, }, }, options: { @@ -1601,10 +1563,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Disable certain index pattern names from being available in index pattern selector.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -1682,10 +1642,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.GENERAL, @@ -1713,10 +1671,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { pattern: { title: 'Index pattern', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, description: @@ -1751,10 +1707,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { timeout: { title: 'Request timeout', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, description: @@ -1789,10 +1743,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the interval in which a new wazuh-monitoring index will be created.', store: { - savedObject: { - mapping: { - type: 'keyword', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -1836,10 +1788,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the wazuh-monitoring index creation and/or visualization.', store: { - savedObject: { - mapping: { - type: 'boolean', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -1870,10 +1820,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 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.', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -1906,10 +1854,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Index pattern', description: 'Default index pattern to use for Wazuh monitoring.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -1943,10 +1889,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the number of replicas to use for the wazuh-monitoring-* indices.', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -1980,10 +1924,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the number of shards to use for the wazuh-monitoring-* indices.', store: { - savedObject: { - mapping: { - type: 'integer', - }, + file: { + configurable: true, }, }, category: SettingCategory.MONITORING, @@ -2016,10 +1958,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Index pattern', description: 'Default index pattern to use for vulnerabilities.', store: { - savedObject: { - mapping: { - type: 'text', - }, + file: { + configurable: true, }, }, category: SettingCategory.VULNERABILITIES, @@ -2133,4 +2073,4 @@ export const WAZUH_CORE_ENCRYPTION_PASSWORD = 'secretencryptionkey!'; // Configuration backend service export const WAZUH_CORE_CONFIGURATION_INSTANCE = 'wazuh-dashboard'; -export const WAZUH_CORE_CONFIGURATION_CACHE_SECONDS = 60; +export const WAZUH_CORE_CONFIGURATION_CACHE_SECONDS = 10; diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index e58d72c391..b7aca57104 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -1,4 +1,6 @@ import { cloneDeep } from 'lodash'; +import { formatLabelValuePair } from './settings'; +import { formatBytes } from './file-size'; export interface ILogger { debug(message: string): void; @@ -111,10 +113,9 @@ export type TConfigurationSetting = { | TConfigurationSettingOptionsSwitch | TConfigurationSettingOptionsTextArea; store?: { - savedObject?: { - mapping: any; - get?: (value: any, configuration: any) => any; - set?: (value: any, configuration: any) => any; + file: { + configurable?: boolean; + transfromFrom?: (value: any) => any; }; }; // Transform the input value. The result is saved in the form global state of Settings/Configuration @@ -171,8 +172,11 @@ export interface IConfiguration { export class Configuration implements IConfiguration { private store: IConfigurationStore; + _settings: Map; + _categories: Map; constructor(private logger: ILogger, store: IConfigurationStore) { this._settings = new Map(); + this._categories = new Map(); this.setStore(store); } setStore(store: IConfigurationStore) { @@ -195,7 +199,13 @@ export class Configuration implements IConfiguration { */ register(id: string, value: any) { if (!this._settings.has(id)) { - this._settings.set(id, value); + // Enhance the setting + const enhancedValue = value; + // Enhance the description + enhancedValue._description = value.description; + enhancedValue.description = this.enhanceSettingDescription(value); + // Register the setting + this._settings.set(id, enhancedValue); this.logger.debug(`Registered ${id}`); } else { const message = `Setting ${id} exists`; @@ -353,4 +363,137 @@ export class Configuration implements IConfiguration { return await this.reset(...this._settings.keys()); } } + + registerCategory({ id, ...rest }) { + if (this._categories.has(id)) { + this.logger.error(`Registered category [${id}]`); + throw new Error(`Category exists [${id}]`); + } + this._categories.set(id, rest); + this.logger.debug(`Registered category [${id}]`); + } + + getUniqueCategories() { + return [ + ...new Set( + Array.from(this._settings.entries()) + .filter( + ([, { isConfigurableFromSettings }]) => isConfigurableFromSettings, + ) + .map(([, { category }]) => category), + ), + ] + .map(categoryID => this._categories.get(String(categoryID))) + .sort((categoryA, categoryB) => { + if (categoryA.title > categoryB.title) { + return 1; + } else if (categoryA.title < categoryB.title) { + return -1; + } + return 0; + }); + } + private enhanceSettingDescription(setting: TConfigurationSetting) { + const { description, options } = setting; + 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(' '); + } + groupSettingsByCategory( + _settings: string[] | null = null, + filterFunction: + | ((setting: TConfigurationSettingWithKey) => boolean) + | null = null, + ) { + const settings = ( + _settings && Array.isArray(_settings) + ? Array.from(this._settings.entries()).filter(([key]) => + _settings.includes(key), + ) + : Array.from(this._settings.entries()) + ).map(([key, value]) => ({ + ...value, + key, + })); + + const settingsSortedByCategories = ( + filterFunction ? settings.filter(filterFunction) : 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: this._categories.get(String(category)), + settings, + })) + .filter(categoryEntry => categoryEntry.settings.length); + } } diff --git a/plugins/wazuh-core/server/index.ts b/plugins/wazuh-core/server/index.ts index 48cd10ef7f..b96ef93f9f 100644 --- a/plugins/wazuh-core/server/index.ts +++ b/plugins/wazuh-core/server/index.ts @@ -1,10 +1,6 @@ import { PluginInitializerContext } from '../../../src/core/server'; -import { - WAZUH_CORE_CONFIGURATION_INSTANCE, - WAZUH_CORE_ENCRYPTION_PASSWORD, -} from '../common/constants'; import { WazuhCorePlugin } from './plugin'; -import { schema, TypeOf } from '@osd/config-schema'; +import { TypeOf } from '@osd/config-schema'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. @@ -13,21 +9,6 @@ export function plugin(initializerContext: PluginInitializerContext) { return new WazuhCorePlugin(initializerContext); } -const configSchema = schema.object({ - instance: schema.string({ - defaultValue: WAZUH_CORE_CONFIGURATION_INSTANCE, - }), - configuration: schema.object({ - encryption_key: schema.string({ - defaultValue: WAZUH_CORE_ENCRYPTION_PASSWORD, - }), - }), -}); - -export const config = { - schema: configSchema, -}; - export type WazuhCorePluginConfigType = TypeOf; export { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index 19709814d0..ae86842980 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -22,12 +22,13 @@ import { import { Configuration } from '../common/services/configuration'; import { PLUGIN_SETTINGS, + PLUGIN_SETTINGS_CATEGORIES, WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, + WAZUH_DATA_CONFIG_APP_PATH, } from '../common/constants'; import { enhanceConfiguration } from './services/enhance-configuration'; import { first } from 'rxjs/operators'; import { WazuhCorePluginConfigType } from '.'; -import MigrationConfigFile from './start/tasks/config-file'; export class WazuhCorePlugin implements Plugin @@ -59,11 +60,11 @@ export class WazuhCorePlugin this._internal.configurationStore = new ConfigurationStore( this.logger.get('configuration-saved-object'), - core.savedObjects, { ...config.configuration, instance: config.instance, cache_seconds: WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, + file: WAZUH_DATA_CONFIG_APP_PATH, }, ); this.services.configuration = new Configuration( @@ -79,6 +80,11 @@ export class WazuhCorePlugin this.services.configuration.register(key, value), ); + // Add categories to the configuration + Object.entries(PLUGIN_SETTINGS_CATEGORIES).forEach(([key, value]) => { + this.services.configuration.registerCategory({ ...value, id: key }); + }); + /* Workaround: Redefine the validation functions of cron.statistics.interval setting. Because the settings are defined in the backend and frontend side using the same definitions, the validation funtions are not defined there and has to be defined in the frontend side and backend side @@ -149,19 +155,8 @@ export class WazuhCorePlugin setCore(core); - this._internal.configurationStore.setSavedObjectRepository( - core.savedObjects.createInternalRepository(), - ); - await this.services.configuration.start(); - // Migrate the configuration file - await MigrationConfigFile.start({ - ...this.services, - configurationStore: this._internal.configurationStore, - logger: this.logger.get(MigrationConfigFile.name), - }); - return { ...this.services, api: { diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index 70a2ed7fa9..d4a9b52734 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -4,13 +4,17 @@ import { TConfigurationSetting, IConfiguration, } from '../../common/services/configuration'; -import { Encryption } from './encryption'; import { CacheTTL } from '../../common/services/cache'; +import fs from 'fs'; +import yml from 'js-yaml'; +import { createDataDirectoryIfNotExists } from './filesystem'; +import { webDocumentationLink } from '../../common/services/web_documentation'; interface IConfigurationStoreOptions { instance: string; encryption_key: string; cache_seconds: number; + file: string; } interface IStoreGetOptions { @@ -18,21 +22,12 @@ interface IStoreGetOptions { } export class ConfigurationStore implements IConfigurationStore { - private type = 'wazuh-dashboard-plugins-config'; - private savedObjectRepository: any; private configuration: IConfiguration; - private encryption: any; private _config: IConfigurationStoreOptions; private _cache: CacheTTL; private _cacheKey: string; - constructor( - private logger: Logger, - private savedObjects: any, - options: IConfigurationStoreOptions, - ) { - this.encryption = new Encryption(this.logger.get('encryption'), { - key: options.encryption_key, - }); + private _fileEncoding: string = 'utf-8'; + constructor(private logger: Logger, options: IConfigurationStoreOptions) { this._config = options; /* The ttl cache is used to support sharing configuration through different instances of the @@ -42,140 +37,214 @@ export class ConfigurationStore implements IConfigurationStore { }); this._cacheKey = 'configuration'; } - private getSavedObjectDefinition(settings: { - [key: string]: TConfigurationSetting; - }) { - return { - name: this.type, - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: Object.entries(settings).reduce( - (accum, [key, value]) => ({ - ...accum, - ...(value?.store?.savedObject?.mapping - ? { [key]: value.store.savedObject.mapping } - : {}), - }), - {}, - ), - }, - }; - } - setSavedObjectRepository(savedObjectRepository) { - this.savedObjectRepository = savedObjectRepository; + private readContentConfigurationFile() { + this.logger.debug(`Reading file [${this._config.file}]`); + const content = fs.readFileSync(this._config.file, { + encoding: this._fileEncoding, + }); + return content; + } + private writeContentConfigurationFile(content: string, options = {}) { + this.logger.debug(`Writing file [${this._config.file}]`); + fs.writeFileSync(this._config.file, content, { + encoding: this._fileEncoding, + ...options, + }); + this.logger.debug(`Wrote file [${this._config.file}]`); + } + private readConfigurationFile() { + const content = this.readContentConfigurationFile(); + const contentAsJSON = yml.load(content); + this.logger.debug( + `Content file [${this._config.file}]: ${JSON.stringify(contentAsJSON)}`, + ); + // Transform value for key in the configuration file + return Object.fromEntries( + Object.entries(contentAsJSON).map(([key, value]) => { + const setting = this.configuration._settings.get(key); + return [key, setting?.store?.file?.transformFrom?.(value) ?? value]; + }), + ); + } + private updateConfigurationFile(attributes: any) { + // Plugin settings configurables in the configuration file. + const pluginSettingsConfigurableFile = Object.fromEntries( + Object.entries(attributes) + .filter( + ([key]) => + this.configuration._settings.get(key)?.store?.file?.configurable, + ) + .map(([key, value]) => [key, value]), + ); + + const content = this.readContentConfigurationFile(); + + const contentUpdated = Object.entries( + pluginSettingsConfigurableFile, + ).reduce((accum, [key, value]) => { + const re = new RegExp(`^${key}\\s{0,}:\\s{1,}.*`, 'gm'); + const formatedValue = formatSettingValueToFile(value); + const match = accum.match(re); + return match /*|| exists*/ + ? accum.replace(re, `${key}: ${formatedValue}`) + : `${accum}\n${key}: ${formatedValue}`; + }, content); + + this.writeContentConfigurationFile(contentUpdated); + return pluginSettingsConfigurableFile; } setConfiguration(configuration: IConfiguration) { this.configuration = configuration; } - getSettingValue(key: string, value: any) { - const setting = this.configuration._settings.get(key); - return setting?.store?.savedObject?.encrypted - ? JSON.parse(this.encryption.decrypt(value)) - : value; + private async storeGet(params?: IStoreGetOptions) { + if (!params?.ignoreCache && this._cache.has(null, this._cacheKey)) { + return this._cache.get(null, this._cacheKey); + } + const configuration = await this.readConfigurationFile(); + + // Cache the values + this._cache.set(configuration, this._cacheKey); + return configuration; } - setSettingValue(key: string, value: any) { - const setting = this.configuration._settings.get(key); - return setting?.store?.savedObject?.encrypted - ? this.encryption.encrypt(JSON.stringify(value)) - : value; + private async storeSet(attributes: any) { + const configuration = await this.updateConfigurationFile(attributes); + this._cache.set(attributes, this._cacheKey); + return configuration; } - private async storeGet(params?: IStoreGetOptions) { - try { - if (!params?.ignoreCache && this._cache.has(null, this._cacheKey)) { - return this._cache.get(null, this._cacheKey); - } - this.logger.debug( - `Fetching saved object [${this.type}:${this._config.instance}]`, - ); - const response = await this.savedObjectRepository.get( - this.type, - this._config.instance, - ); - this.logger.debug( - `Fetched saved object response [${JSON.stringify(response)}]`, - ); + private getDefaultConfigurationFileContent() { + const header: string = `--- +# +# App configuration file +# Copyright (C) 2015-2024 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. +# +${printSection('Wazuh app configuration file', { prefix: '# ', fill: '=' })} +# +# Please check the documentation for more information about configuration options: +# ${webDocumentationLink('user-manual/wazuh-dashboard/config-file.html')} +# +# Also, you can check our repository: +# https://github.com/wazuh/wazuh-dashboard-plugins`; + + const hostsConfiguration = `${printSection('Wazuh hosts', { + prefix: '# ', + fill: '-', + })} +# +# The following configuration is the default structure to define a host. +# +# hosts: +# # Host ID / name, +# - env-1: +# # Host URL +# url: https://env-1.example +# # Host / API port +# port: 55000 +# # Host / API username +# username: wazuh-wui +# # Host / API password +# password: wazuh-wui +# # Use RBAC or not. If set to true, the username must be "wazuh-wui". +# run_as: true +# - env-2: +# url: https://env-2.example +# port: 55000 +# username: wazuh-wui +# password: wazuh-wui +# run_as: true + +hosts: + - default: + url: https://localhost + port: 55000 + username: wazuh-wui + password: wazuh-wui + run_as: false +`; - // Transform the stored values to raw - const attributes = Object.fromEntries( - Object.entries(response.attributes).map(([key, value]) => [ - key, - this.getSettingValue(key, value), - ]), + // const pluginSettingsConfigurationFile = Array.from( + // this.configuration._settings, + // ).filter(setting => setting?.store?.file?.configurable); + + const pluginSettingsConfigurationFileGroupByCategory = + this.configuration.groupSettingsByCategory( + null, + setting => setting?.store?.file?.configurable, ); - // Cache the values - this._cache.set(attributes, this._cacheKey); - return attributes; - } catch (error) { - // Saved object not found - if (error?.output?.payload?.statusCode === 404) { - return {}; - } - throw error; - } - } - private async storeSet(attributes: any) { - this.logger.debug( - `Setting saved object [${this.type}:${this._config.instance}]`, - ); - const enhancedAttributes = Object.fromEntries( - Object.entries(attributes).map(([key, value]) => [ - key, - this.setSettingValue(key, value), - ]), - ); - const response = await this.savedObjectRepository.create( - this.type, - enhancedAttributes, - { - id: this._config.instance, - overwrite: true, - refresh: true, - }, - ); - this.logger.debug( - `Set saved object [${this.type}:${this._config.instance}]`, + const pluginSettingsConfiguration = + pluginSettingsConfigurationFileGroupByCategory + .map(({ category: categorySetting, settings }) => { + const category = printSettingCategory( + categorySetting, + // PLUGIN_SETTINGS_CATEGORIES[categoryID], + ); + + const pluginSettingsOfCategory = settings + .map(setting => printSetting(setting)) + .join('\n#\n'); + /* + #------------------- {category name} -------------- + # + # {category description} + # + # {setting description} + # settingKey: settingDefaultValue + # + # {setting description} + # settingKey: settingDefaultValue + # ... + */ + return [category, pluginSettingsOfCategory].join('\n#\n'); + }) + .join('\n#\n'); + + return [header, pluginSettingsConfiguration, hostsConfiguration].join( + '\n#\n', ); - this._cache.set(attributes, this._cacheKey); - return response.attributes; } - async savedObjectIsCreated() { + ensureConfigurationFileIsCreated() { try { - this.logger.debug( - `Fetching saved object is created [${this.type}:${this._config.instance}]`, - ); - const response = await this.savedObjectRepository.get( - this.type, - this._config.instance, - ); - this.logger.debug( - `Fetched saved object is created response [${JSON.stringify( - response, - )}]`, - ); - return true; - } catch (error) { - // Saved object not found - if (error?.output?.payload?.statusCode === 404) { - return false; + this.logger.debug('Ensuring the configuration file is created'); + createDataDirectoryIfNotExists(); + createDataDirectoryIfNotExists('config'); + if (!fs.existsSync(this._config.file)) { + this.writeContentConfigurationFile( + this.getDefaultConfigurationFileContent(), + { + mode: 0o600, + }, + ); + this.logger.info( + `Configuration file was created [${this._config.file}]`, + ); + } else { + this.logger.debug(`Configuration file exists [${this._config.file}]`); } - throw error; + } catch (error) { + const enhancedError = new Error( + `Error ensuring the configuration file is created: ${error.message}`, + ); + this.logger.error(enhancedError.message); + throw enhancedError; } } - async setup(settings: { [key: string]: TConfigurationSetting }) { - // Register the saved object + async setup(settings: { [key: string]: TConfigurationSetting }) {} + async start() { + // TODO: create the file in start try { - this.logger.debug('Setup'); - const savedObjectSchema = this.getSavedObjectDefinition(settings); - this.logger.info(`Schema: ${JSON.stringify(savedObjectSchema)}`); - await this.savedObjects.registerType(savedObjectSchema); - this.logger.info('Schema registered'); + this.logger.debug('Start'); + this.ensureConfigurationFileIsCreated(); } catch (error) { - this.logger.error(error.message); + this.logger.error(`Error starting: ${error.message}`); } } - async start() {} async stop() {} async get(...settings: string[]): Promise { try { @@ -199,28 +268,32 @@ export class ConfigurationStore implements IConfigurationStore { ) : stored; } catch (error) { - this.logger.error(error.message); - throw error; + const enhancedError = new Error( + `Error getting configuration: ${error.message}`, + ); + this.logger.error(enhancedError.message); + throw enhancedError; } } async set(settings: { [key: string]: any }): Promise { try { - this.logger.debug('Updating saved object'); + this.logger.debug('Updating'); const stored = await this.storeGet({ ignoreCache: true }); const newSettings = { ...stored, ...settings, }; - this.logger.debug( - `Updating saved object with ${JSON.stringify(newSettings)}`, - ); + this.logger.debug(`Updating with ${JSON.stringify(newSettings)}`); await this.storeSet(newSettings); - this.logger.debug('Saved object was updated'); + this.logger.debug('Configuration was updated'); return settings; } catch (error) { - this.logger.error(error.message); - throw error; + const enhancedError = new Error( + `Error setting configuration: ${error.message}`, + ); + this.logger.error(enhancedError.message); + throw enhancedError; } } async clear(...settings: string[]): Promise { @@ -237,8 +310,127 @@ export class ConfigurationStore implements IConfigurationStore { await this.storeSet(updatedSettings); return removedSettings; } catch (error) { - this.logger.error(error.message); - throw error; + const enhancedError = new Error( + `Error clearing configuration: ${error.message}`, + ); + this.logger.error(enhancedError.message); + throw enhancedError; } } } + +// Utils + +// Formatters in configuration file +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, +}; + +/** + * 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 + */ +function formatSettingValueToFile(value: any) { + const formatter = + formatSettingValueToFileType[typeof value] || + formatSettingValueToFileType.default; + return formatter(value); +} + +/** + * Print the setting value + * @param value + * @returns + */ +export function printSettingValue(value: unknown): any { + if (typeof value === 'object') { + return JSON.stringify(value); + } + + if (typeof value === 'string' && value.length === 0) { + return `''`; + } + + return value; +} + +/** + * Print setting on the default configuration file + * @param setting + * @returns + */ +export function printSetting(setting: TPluginSettingWithKey): string { + /* + # {setting description} + # {settingKey}: {settingDefaultValue} + */ + return [ + splitDescription(setting.description), + `# ${setting.key}: ${printSettingValue(setting.defaultValue)}`, + ].join('\n'); +} + +/** + * Print category header on the default configuration file + * @param param0 + * @returns + */ +export function printSettingCategory({ title, description }) { + /* + #------------------------------- {category title} ------------------------------- + # {category description} + # + */ + return [ + printSection(title, { prefix: '# ', fill: '-' }), + ...(description ? [splitDescription(description)] : ['']), + ].join('\n#\n'); +} + +export function printSection( + text: string, + options?: { + maxLength?: number; + prefix?: string; + suffix?: string; + spaceAround?: number; + fill?: string; + }, +) { + const maxLength = options?.maxLength ?? 80; + const prefix = options?.prefix ?? ''; + const sufix = options?.suffix ?? ''; + const spaceAround = options?.spaceAround ?? 1; + const fill = options?.fill ?? ' '; + const fillLength = + maxLength - prefix.length - sufix.length - 2 * spaceAround - text.length; + + return [ + prefix, + fill.repeat(Math.floor(fillLength / 2)), + ` ${text} `, + fill.repeat(Math.ceil(fillLength / 2)), + sufix, + ].join(''); +} + +/** + * Given a string, this function builds a multine string, each line about 70 + * characters long, splitted at the closest whitespace character to that lentgh. + * + * This function is used to transform the settings description + * into a multiline string to be used as the setting documentation. + * + * The # character is also appended to the beginning of each line. + * + * @param text + * @returns multine string + */ +export function splitDescription(text: string = ''): string { + const lines = text.match(/.{1,80}(?=\s|$)/g) || []; + return lines.map(z => '# ' + z.trim()).join('\n'); +} From 00154856c223b5d348f3905ee4edf70948118f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 11:19:06 +0200 Subject: [PATCH 02/34] remove(configuration): remove the migration task of configuration file to saved object --- .../server/start/tasks/config-file.ts | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 plugins/wazuh-core/server/start/tasks/config-file.ts diff --git a/plugins/wazuh-core/server/start/tasks/config-file.ts b/plugins/wazuh-core/server/start/tasks/config-file.ts deleted file mode 100644 index 06bf5ac51a..0000000000 --- a/plugins/wazuh-core/server/start/tasks/config-file.ts +++ /dev/null @@ -1,60 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -export default { - name: 'migration-config-file', - start: async ({ logger, configuration, configurationStore }) => { - try { - logger.debug('Migrate configuration file'); - - logger.debug( - 'Checking if there are previous configuration stored in the saved object', - ); - const savedObjectIsCreated = - await configurationStore.savedObjectIsCreated(); - - logger.debug( - `${ - savedObjectIsCreated ? 'There is' : 'There is not' - } previous configuration stored in the saved object`, - ); - - const configurationFileLocation = path.join( - __dirname, - '../../../../../data/wazuh/config/wazuh.yml', - ); - - logger.debug( - `Check if the configuration file exists at [${configurationFileLocation}]`, - ); - if (!fs.existsSync(configurationFileLocation)) { - logger.debug('Configuration file not found. Skip.'); - return; - } - logger.debug( - `Configuration file found at [${configurationFileLocation}]`, - ); - - if (savedObjectIsCreated) { - logger.info( - `The saved object exists so the configuration defined at the file [${configurationFileLocation}] will not be migrated. Skip.`, - ); - return; - } - - logger.debug(`Reading file [${configurationFileLocation}]`); - const content = fs.readFileSync(configurationFileLocation, 'utf8'); - logger.debug(`Read file [${configurationFileLocation}]`); - - logger.debug(`Importing file [${configurationFileLocation}]`); - const responseImportFile = await configuration.importFile(content); - logger.info(`Imported file [${configurationFileLocation}]`); - - responseImportFile?.warnings?.forEach?.((warning: string) => - logger.warn(warning), - ); - } catch (error) { - logger.error(`Error migrating the configuration file: ${error.message}`); - } - }, -}; From 8fbbfce93707729d4cc3291d8a0e8e432818eecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 12:19:33 +0200 Subject: [PATCH 03/34] rollback(configuration): remove references to plugin settings defition --- plugins/wazuh-core/server/index.ts | 2 -- plugins/wazuh-core/server/plugin.ts | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/plugins/wazuh-core/server/index.ts b/plugins/wazuh-core/server/index.ts index b96ef93f9f..f573e68f89 100644 --- a/plugins/wazuh-core/server/index.ts +++ b/plugins/wazuh-core/server/index.ts @@ -9,6 +9,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new WazuhCorePlugin(initializerContext); } -export type WazuhCorePluginConfigType = TypeOf; - export { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index ae86842980..eb27e1bff0 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -27,8 +27,6 @@ import { WAZUH_DATA_CONFIG_APP_PATH, } from '../common/constants'; import { enhanceConfiguration } from './services/enhance-configuration'; -import { first } from 'rxjs/operators'; -import { WazuhCorePluginConfigType } from '.'; export class WazuhCorePlugin implements Plugin @@ -49,20 +47,11 @@ export class WazuhCorePlugin ): Promise { this.logger.debug('wazuh_core: Setup'); - // Get the plugin configuration - const config$ = - this.initializerContext.config.create(); - const config: WazuhCorePluginConfigType = await config$ - .pipe(first()) - .toPromise(); - this.services.dashboardSecurity = createDashboardSecurity(plugins); this._internal.configurationStore = new ConfigurationStore( this.logger.get('configuration-saved-object'), { - ...config.configuration, - instance: config.instance, cache_seconds: WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, file: WAZUH_DATA_CONFIG_APP_PATH, }, From eebfb94c34ee48d287fd73d95db290ebe8302469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 12:22:58 +0200 Subject: [PATCH 04/34] rollback(configuration): remove enhanceConfiguration from frontend side --- .../settings/configuration/configuration.tsx | 3 +- plugins/wazuh-core/public/plugin.ts | 3 - .../public/utils/enhance-configuration.ts | 136 ------------------ 3 files changed, 1 insertion(+), 141 deletions(-) delete mode 100644 plugins/wazuh-core/public/utils/enhance-configuration.ts diff --git a/plugins/main/public/components/settings/configuration/configuration.tsx b/plugins/main/public/components/settings/configuration/configuration.tsx index c0fdd58e7d..74e0430229 100644 --- a/plugins/main/public/components/settings/configuration/configuration.tsx +++ b/plugins/main/public/components/settings/configuration/configuration.tsx @@ -190,8 +190,7 @@ const WzConfigurationSettingsProvider = props => { type: pluginSetting.type, options: pluginSetting?.options, title: pluginSetting?.title, - description: - getWazuhCorePlugin().configuration.getSettingDescription(fieldKey), + description: pluginSetting.description, }; }, ); diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index 166d3020b2..679be424d7 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -10,7 +10,6 @@ import { PLUGIN_SETTINGS_CATEGORIES, } from '../common/constants'; import { DashboardSecurity } from './utils/dashboard-security'; -import { enhanceConfiguration } from './utils/enhance-configuration'; import * as hooks from './hooks'; export class WazuhCorePlugin @@ -31,8 +30,6 @@ export class WazuhCorePlugin logger, this._internal.configurationStore, ); - // Extend the configuration instance to define the categories - enhanceConfiguration(this.services.configuration); // Register the plugin settings Object.entries(PLUGIN_SETTINGS).forEach(([key, value]) => diff --git a/plugins/wazuh-core/public/utils/enhance-configuration.ts b/plugins/wazuh-core/public/utils/enhance-configuration.ts deleted file mode 100644 index 2ab38cc4f1..0000000000 --- a/plugins/wazuh-core/public/utils/enhance-configuration.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { formatBytes } from '../../common/services/file-size'; -import { formatLabelValuePair } from '../../common/services/settings'; - -export function enhanceConfiguration(configuration) { - configuration.registerCategory = function ({ id, ...rest }) { - if (!this._categories) { - this._categories = new Map(); - } - if (this._categories.has(id)) { - this.logger.error(`Registered category ${id}`); - throw new Error(`Category ${id} exists`); - } - this._categories.set(id, rest); - this.logger.debug(`Registered category ${id}`); - }; - - configuration.getUniqueCategories = function () { - return [ - ...new Set( - Array.from(this._settings.entries()) - .filter( - ([, { isConfigurableFromSettings }]) => isConfigurableFromSettings, - ) - .map(([, { category }]) => category), - ), - ] - .map(categoryID => this._categories.get(String(categoryID))) - .sort((categoryA, categoryB) => { - if (categoryA.title > categoryB.title) { - return 1; - } else if (categoryA.title < categoryB.title) { - return -1; - } - return 0; - }); - }; - - configuration.getSettingDescription = function (key: string) { - const { description, options } = this._settings.get(key); - 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(' '); - }; - - // Group the settings by category - configuration.groupSettingsByCategory = function (_settings) { - const settings = ( - _settings && Array.isArray(_settings) - ? Array.from(this._settings.entries()).filter(([key]) => - _settings.includes(key), - ) - : Array.from(this._settings.entries()) - ).map(([key, value]) => ({ - ...value, - key, - })); - - 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: this._categories.get(String(category)), - settings, - })) - .filter(categoryEntry => categoryEntry.settings.length); - }; -} From 0352d9dc97cec18dcf6ebbb00faef092512565df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 12:24:09 +0200 Subject: [PATCH 05/34] fix(configuration): replace logger methods for noop --- plugins/wazuh-core/public/plugin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index 679be424d7..001375766c 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -18,12 +18,12 @@ export class WazuhCorePlugin _internal: { [key: string]: any } = {}; services: { [key: string]: any } = {}; public setup(core: CoreSetup): WazuhCorePluginSetup { - // TODO: change to noop + const noop = () => {}; const logger = { - info: console.log, - error: console.error, - debug: console.debug, - warn: console.warn, + info: noop, + error: noop, + debug: noop, + warn: noop, }; this._internal.configurationStore = new ConfigurationStore(logger); this.services.configuration = new Configuration( From 7fe34c96a47c0dc5cb9534e489c5225ce7425f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 2 Apr 2024 14:33:42 +0200 Subject: [PATCH 06/34] rollback(configuration): display the path to the configuration file on App Settings application - Display the real path of the configuration file instead of the fixed value on App Settings - Add configuration_file property to the response of GET /api/setup - Refactor the ConfigurationStore services - Minor enhancements typed code --- .../configuration/components/header.tsx | 14 +++++ plugins/main/server/controllers/wazuh-api.ts | 7 ++- .../common/services/configuration.ts | 14 ++--- plugins/wazuh-core/public/plugin.ts | 13 +++-- .../public/utils/configuration-store.ts | 25 +++++---- .../server/services/configuration-store.ts | 51 +++++++++---------- 6 files changed, 76 insertions(+), 48 deletions(-) diff --git a/plugins/main/public/components/settings/configuration/components/header.tsx b/plugins/main/public/components/settings/configuration/components/header.tsx index c7618776ff..9436f88f7c 100644 --- a/plugins/main/public/components/settings/configuration/components/header.tsx +++ b/plugins/main/public/components/settings/configuration/components/header.tsx @@ -20,10 +20,12 @@ import { EuiToolTip, EuiButtonIcon, EuiSearchBar, + EuiText, } from '@elastic/eui'; import { EuiFormErrorText } from '@elastic/eui'; import { PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION } from '../../../../../common/constants'; import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { getWazuhCorePlugin } from '../../../../kibana-services'; export const Header = ({ query, setQuery, searchBarFilters }) => { return ( @@ -31,6 +33,7 @@ export const Header = ({ query, setQuery, searchBarFilters }) => { + <SubTitle /> </EuiFlexGroup> </EuiFlexItem> <EuiFlexItem> @@ -70,6 +73,17 @@ const Title = () => { ); }; +const SubTitle = () => { + return ( + <EuiFlexItem grow={false}> + <EuiText color='subdued' style={{ paddingBottom: '15px' }}> + Configuration file located at{' '} + {getWazuhCorePlugin().configuration.store.file} + </EuiText> + </EuiFlexItem> + ); +}; + const SearchBar = ({ query, setQuery, searchBarFilters }) => { const [error, setError] = useState(); diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 115bc5850a..d2f3df87e5 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -1161,10 +1161,15 @@ export class WazuhApiCtrl { const source = JSON.parse( fs.readFileSync(context.wazuh_core.updateRegistry.file, 'utf8'), ); + const data = !Object.values(source).length ? {} : source; + return response.ok({ body: { statusCode: HTTP_STATUS_CODES.OK, - data: !Object.values(source).length ? '' : source, + data: { + ...data, + configuration_file: context.wazuh_core.configuration.store.file, + }, }, }); } catch (error) { diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index b7aca57104..1e2136aed2 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -171,7 +171,7 @@ export interface IConfiguration { } export class Configuration implements IConfiguration { - private store: IConfigurationStore; + store: IConfigurationStore | null = null; _settings: Map<string, { [key: string]: TConfigurationSetting }>; _categories: Map<string, { [key: string]: any }>; constructor(private logger: ILogger, store: IConfigurationStore) { @@ -183,14 +183,14 @@ export class Configuration implements IConfiguration { this.store = store; this.store.setConfiguration(this); } - async setup() { - return this.store.setup(Object.fromEntries(this._settings.entries())); + async setup(dependencies: any = {}) { + return this.store.setup(dependencies); } - async start() { - return this.store.start(Object.fromEntries(this._settings.entries())); + async start(dependencies: any = {}) { + return this.store.start(dependencies); } - async stop() { - return this.store.stop(Object.fromEntries(this._settings.entries())); + async stop(dependencies: any = {}) { + return this.store.stop(dependencies); } /** * Register a setting diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index 001375766c..ef08e41595 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -17,7 +17,7 @@ export class WazuhCorePlugin { _internal: { [key: string]: any } = {}; services: { [key: string]: any } = {}; - public setup(core: CoreSetup): WazuhCorePluginSetup { + public async setup(core: CoreSetup): Promise<WazuhCorePluginSetup> { const noop = () => {}; const logger = { info: noop, @@ -25,7 +25,10 @@ export class WazuhCorePlugin debug: noop, warn: noop, }; - this._internal.configurationStore = new ConfigurationStore(logger); + this._internal.configurationStore = new ConfigurationStore( + logger, + core.http, + ); this.services.configuration = new Configuration( logger, this._internal.configurationStore, @@ -43,7 +46,7 @@ export class WazuhCorePlugin this.services.dashboardSecurity = new DashboardSecurity(logger, core.http); - this.services.dashboardSecurity.setup(); + await this.services.dashboardSecurity.setup(); return { ...this.services, @@ -52,11 +55,13 @@ export class WazuhCorePlugin }; } - public start(core: CoreStart): WazuhCorePluginStart { + public async start(core: CoreStart): Promise<WazuhCorePluginStart> { setChrome(core.chrome); setCore(core); setUiSettings(core.uiSettings); + await this.services.configuration.start({ http: core.http }); + return { ...this.services, utils, diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts index d114229cb6..305291c21d 100644 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ b/plugins/wazuh-core/public/utils/configuration-store.ts @@ -1,5 +1,4 @@ import { - TConfigurationSetting, IConfigurationStore, ILogger, IConfiguration, @@ -7,21 +6,29 @@ import { export class ConfigurationStore implements IConfigurationStore { private _stored: any; - constructor(private logger: ILogger) { + file: string = ''; + configuration: IConfiguration | null = null; + constructor(private logger: ILogger, private http: any) { this._stored = {}; } - async setup(settings: { [key: string]: TConfigurationSetting }) { + setConfiguration(configuration: IConfiguration) { + this.configuration = configuration; + } + async setup() { + this.logger.debug('Setup'); + } + async start() { try { - this.logger.debug('Setup'); + this.logger.debug('Start'); + const response = await this.http.get('/api/setup'); + this.file = response.data.configuration_file; } catch (error) { - this.logger.error(error.message); + this.logger.error(`Error on start: ${error.message}`); } } - setConfiguration(configuration: IConfiguration) { - this.configuration = configuration; + async stop() { + this.logger.debug('Stop'); } - async start() {} - async stop() {} private storeGet() { return this._stored; } diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index d4a9b52734..b52e0773fc 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -1,7 +1,6 @@ import { Logger } from 'opensearch-dashboards/server'; import { IConfigurationStore, - TConfigurationSetting, IConfiguration, } from '../../common/services/configuration'; import { CacheTTL } from '../../common/services/cache'; @@ -11,8 +10,6 @@ import { createDataDirectoryIfNotExists } from './filesystem'; import { webDocumentationLink } from '../../common/services/web_documentation'; interface IConfigurationStoreOptions { - instance: string; - encryption_key: string; cache_seconds: number; file: string; } @@ -23,12 +20,18 @@ interface IStoreGetOptions { export class ConfigurationStore implements IConfigurationStore { private configuration: IConfiguration; - private _config: IConfigurationStoreOptions; private _cache: CacheTTL<any>; private _cacheKey: string; private _fileEncoding: string = 'utf-8'; + file: string = ''; constructor(private logger: Logger, options: IConfigurationStoreOptions) { - this._config = options; + this.file = options.file; + + if (!this.file) { + const error = new Error('File is not defined'); + this.logger.error(error.message); + throw error; + } /* The ttl cache is used to support sharing configuration through different instances of the platfrom */ @@ -38,25 +41,25 @@ export class ConfigurationStore implements IConfigurationStore { this._cacheKey = 'configuration'; } private readContentConfigurationFile() { - this.logger.debug(`Reading file [${this._config.file}]`); - const content = fs.readFileSync(this._config.file, { + this.logger.debug(`Reading file [${this.file}]`); + const content = fs.readFileSync(this.file, { encoding: this._fileEncoding, }); return content; } private writeContentConfigurationFile(content: string, options = {}) { - this.logger.debug(`Writing file [${this._config.file}]`); - fs.writeFileSync(this._config.file, content, { + this.logger.debug(`Writing file [${this.file}]`); + fs.writeFileSync(this.file, content, { encoding: this._fileEncoding, ...options, }); - this.logger.debug(`Wrote file [${this._config.file}]`); + this.logger.debug(`Wrote file [${this.file}]`); } private readConfigurationFile() { const content = this.readContentConfigurationFile(); const contentAsJSON = yml.load(content); this.logger.debug( - `Content file [${this._config.file}]: ${JSON.stringify(contentAsJSON)}`, + `Content file [${this.file}]: ${JSON.stringify(contentAsJSON)}`, ); // Transform value for key in the configuration file return Object.fromEntries( @@ -168,10 +171,6 @@ hosts: run_as: false `; - // const pluginSettingsConfigurationFile = Array.from( - // this.configuration._settings, - // ).filter(setting => setting?.store?.file?.configurable); - const pluginSettingsConfigurationFileGroupByCategory = this.configuration.groupSettingsByCategory( null, @@ -181,10 +180,7 @@ hosts: const pluginSettingsConfiguration = pluginSettingsConfigurationFileGroupByCategory .map(({ category: categorySetting, settings }) => { - const category = printSettingCategory( - categorySetting, - // PLUGIN_SETTINGS_CATEGORIES[categoryID], - ); + const category = printSettingCategory(categorySetting); const pluginSettingsOfCategory = settings .map(setting => printSetting(setting)) @@ -214,18 +210,16 @@ hosts: this.logger.debug('Ensuring the configuration file is created'); createDataDirectoryIfNotExists(); createDataDirectoryIfNotExists('config'); - if (!fs.existsSync(this._config.file)) { + if (!fs.existsSync(this.file)) { this.writeContentConfigurationFile( this.getDefaultConfigurationFileContent(), { mode: 0o600, }, ); - this.logger.info( - `Configuration file was created [${this._config.file}]`, - ); + this.logger.info(`Configuration file was created [${this.file}]`); } else { - this.logger.debug(`Configuration file exists [${this._config.file}]`); + this.logger.debug(`Configuration file exists [${this.file}]`); } } catch (error) { const enhancedError = new Error( @@ -235,9 +229,10 @@ hosts: throw enhancedError; } } - async setup(settings: { [key: string]: TConfigurationSetting }) {} + async setup() { + this.logger.debug('Setup'); + } async start() { - // TODO: create the file in start try { this.logger.debug('Start'); this.ensureConfigurationFileIsCreated(); @@ -245,7 +240,9 @@ hosts: this.logger.error(`Error starting: ${error.message}`); } } - async stop() {} + async stop() { + this.logger.debug('Stop'); + } async get(...settings: string[]): Promise<any | { [key: string]: any }> { try { const storeGetOptions = From ea180bc2aec2bbb81e2b76ebdff220bd29019dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Apr 2024 14:42:05 +0200 Subject: [PATCH 07/34] remove: unused constant PLUGIN_PLATFORM_BASE_INSTALLATION_PATH --- plugins/main/common/constants.ts | 2 -- plugins/wazuh-core/common/constants.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 726785b5d4..4eb4dd6565 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -280,8 +280,6 @@ export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform export const PLUGIN_PLATFORM_NAME = 'Wazuh dashboard'; -export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = - '/usr/share/wazuh-dashboard/data/wazuh/'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 5c7dd66a09..4350604ff4 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -279,8 +279,6 @@ export const REPORTS_PAGE_HEADER_TEXT = 'info@wazuh.com\nhttps://wazuh.com'; // Plugin platform export const PLUGIN_PLATFORM_NAME = 'Wazuh dashboard'; -export const PLUGIN_PLATFORM_BASE_INSTALLATION_PATH = - '/usr/share/wazuh-dashboard/data/wazuh/'; export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = From f183e98776954800135b4580143ac1bf9d1ba870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Apr 2024 16:14:54 +0200 Subject: [PATCH 08/34] rollback(configuration): remove management of API connections on Server APIs - Remove buttons to edit and remove from the table - Change the form to add an API connection by information about editing the configuration file --- .../components/settings/api/add-api.tsx | 260 +++++------------- .../components/settings/api/api-table.js | 80 +----- plugins/wazuh-core/common/constants.ts | 8 +- 3 files changed, 69 insertions(+), 279 deletions(-) diff --git a/plugins/main/public/components/settings/api/add-api.tsx b/plugins/main/public/components/settings/api/add-api.tsx index 3344d168f8..c13c66b249 100644 --- a/plugins/main/public/components/settings/api/add-api.tsx +++ b/plugins/main/public/components/settings/api/add-api.tsx @@ -1,211 +1,77 @@ -/* - * 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 React from 'react'; import { EuiFlexGroup, EuiFlexItem, + EuiCodeBlock, EuiText, - EuiSpacer, - EuiButton, + EuiCode, + EuiCallOut, } 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 { withErrorBoundary } from '../../common/hocs'; 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; +export const AddApi = withErrorBoundary(() => { + const data = React.useMemo(() => { + const settings = Object.entries( + getWazuhCorePlugin().configuration._settings.get('hosts').options.arrayOf, + ).map(([key, { description }]) => [ + key, + description.charAt(0).toLowerCase() + description.slice(1), // lower case the first letter + ]); - 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 { + settings, + example: `hosts: + - <id>: + ${settings + .filter(([key]) => key !== 'id') + .map(([key]) => `${key}: <${key}>`) + .join('\n ')}`, + }; + }, []); return ( - <div> - {[ - 'id', - 'url', - 'port', - 'username', - 'password', - 'password_confirm', - 'run_as', - ].map(key => { - const { _meta, ...field } = fields[key]; - return ( - <InputForm - label={_meta.title} - description={_meta.description} - {...field} - // Ignore errors due to unchanged password due to empty value - error={ - mode === 'EDIT' && ['password', 'password_confirm'].includes(key) - ? field.changed - ? field.error - : undefined - : field.error - } - /> - ); - })} - <EuiSpacer /> - {passwordNotMatch && ( - <EuiText color='danger' size='s'> - Password must match. - </EuiText> - )} + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiText> + Modify{' '} + <EuiCode>{getWazuhCorePlugin().configuration.store.file}</EuiCode>{' '} + to set the connection information. + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiCodeBlock language='yaml'>{data.example}</EuiCodeBlock> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <EuiText>Where:</EuiText> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem> + <ul style={{ paddingLeft: '4px' }}> + {data.settings.map(([key, description]) => ( + <li key={`add-api-connection-field-${key}`}> + - <EuiCode>{`<${key}>`}</EuiCode>: {description} + </li> + ))} + </ul> + </EuiFlexItem> + </EuiFlexGroup> <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton disabled={disableApplyButton} fill onClick={onSave}> - Apply - </EuiButton> + <EuiFlexItem> + <EuiCallOut title='Warning' color='warning' iconType='alert'> + <p> + The changes of the API connections in the configuration file could + need some time to take effect due to the cache of configuration. + </p> + </EuiCallOut> </EuiFlexItem> </EuiFlexGroup> - </div> + </> ); -}; +}); diff --git a/plugins/main/public/components/settings/api/api-table.js b/plugins/main/public/components/settings/api/api-table.js index c3b6de9671..e5c5bbdb47 100644 --- a/plugins/main/public/components/settings/api/api-table.js +++ b/plugins/main/public/components/settings/api/api-table.js @@ -44,7 +44,7 @@ import { getWazuhCorePlugin, } from '../../../kibana-services'; import { AvailableUpdatesFlyout } from './available-updates-flyout'; -import { AddAPIHostForm } from './add-api'; +import { AddApi } from './add-api'; import { WzButtonOpenFlyout, WzButtonPermissionsOpenFlyout, @@ -331,29 +331,6 @@ 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(); @@ -647,50 +624,6 @@ export const ApiTable = compose( color='success' /> </EuiToolTip> - <WzButtonPermissionsOpenFlyout - flyoutTitle={`Edit API connection: ${item.id} `} - flyoutBody={({ onClose, onUpdateCanClose }) => ( - <AddAPIHostForm - mode='EDIT' - onUpdateCanClose={onUpdateCanClose} - initialValue={{ - id: item.id, - url: item.url, - port: item.port, - run_as: item.run_as, - username: item.username, - password: '', - password_confirm: '', - }} - apiId={item.id} - onSave={async () => { - onClose(); - await this.refresh(); - }} - /> - )} - buttonProps={{ - administrator: true, - buttonType: 'icon', - iconType: 'pencil', - tooltip: { - content: 'Edit', - }, - }} - ></WzButtonPermissionsOpenFlyout> - <WzButtonPermissionsModalConfirm - administrator={true} - buttonType='icon' - tooltip={{ - content: 'Delete', - }} - modalTitle={`Do you want to delete the ${item.id} API connection?`} - onConfirm={() => this.deleteAPIHost(item.id)} - modalProps={{ buttonColor: 'danger' }} - iconType='trash' - color='danger' - aria-label='Delete API connection' - /> </EuiFlexGroup> ), }, @@ -732,16 +665,7 @@ export const ApiTable = compose( <EuiFlexItem grow={false}> <WzButtonPermissionsOpenFlyout flyoutTitle='Add API connection' - flyoutBody={({ onClose, onUpdateCanClose }) => ( - <AddAPIHostForm - mode='CREATE' - onUpdateCanClose={onUpdateCanClose} - onSave={async () => { - onClose(); - await this.refresh(); - }} - /> - )} + flyoutBody={() => <AddApi />} buttonProps={{ administrator: true, buttonType: 'empty', diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 4350604ff4..23a8d4cf8e 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -1446,7 +1446,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { arrayOf: { id: { title: 'Identifier', - description: 'API host identifier', + description: 'Identifier of the API connection. This must be unique.', type: EpluginSettingType.text, defaultValue: 'default', isConfigurableFromSettings: true, @@ -1457,7 +1457,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, url: { title: 'URL', - description: 'URL address', + description: 'Server URL address', type: EpluginSettingType.text, defaultValue: 'https://localhost', isConfigurableFromSettings: true, @@ -1498,7 +1498,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, username: { title: 'Username', - description: 'Username', + description: 'Server API username', type: EpluginSettingType.text, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, @@ -1509,7 +1509,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, password: { title: 'Password', - description: 'Password', + description: "User's Password", type: EpluginSettingType.password, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, From 82327ffd19c0645e0562b05c3e4ee6e24d683c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Apr 2024 16:24:06 +0200 Subject: [PATCH 09/34] rollback(configuration): remove management of API connection from the backend side - Remove API endpoints and related controllers - POST /hosts/apis/{id} - PUT /hosts/apis/{id} - DELETE /hosts/apis/{id} - Remove methods to manage the API connections in the ManageHosts service --- .../main/server/controllers/wazuh-hosts.ts | 126 ------------------ plugins/main/server/routes/wazuh-hosts.ts | 102 -------------- .../server/services/manage-hosts.ts | 97 -------------- 3 files changed, 325 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-hosts.ts b/plugins/main/server/controllers/wazuh-hosts.ts index 48e531270e..67fec4bd91 100644 --- a/plugins/main/server/controllers/wazuh-hosts.ts +++ b/plugins/main/server/controllers/wazuh-hosts.ts @@ -112,130 +112,4 @@ export class WazuhHostsCtrl { ); } } - - /** - * Create or update the API host data stored in the configuration. - * Allow partial updates. - * @param context - * @param request - * @param response - * @returns - */ - createAPIHost = routeDecoratorProtectedAdministrator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - const { id } = request.params; - context.wazuh.logger.debug(`Creating API host with ID [${id}]`); - - const responseSetHost = await context.wazuh_core.manageHosts.create( - id, - request.body, - ); - - context.wazuh.logger.info(`Created API host with ID [${id}]`); - - return response.ok({ - body: { - message: `API host with ID [${id}] was created`, - data: responseSetHost, - }, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse( - `Could not create the API host: ${error.message || error}`, - 2014, - 500, - response, - ); - } - }, - 2014, - ); - - /** - * Create or update the API host data stored in the configuration. - * Allow partial updates. - * @param context - * @param request - * @param response - * @returns - */ - updateAPIHost = routeDecoratorProtectedAdministrator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - const { id: originalID } = request.params; - context.wazuh.logger.debug(`Updating API host with ID [${originalID}]`); - - const responseSetHost = await context.wazuh_core.manageHosts.update( - originalID, - request.body, - ); - - context.wazuh.logger.info(`Updated API host with ID [${originalID}]`); - - return response.ok({ - body: { - message: `API host with ID [${originalID}] was updated`, - data: responseSetHost, - }, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse( - `Could not update the API host: ${error.message || error}`, - 2015, - 500, - response, - ); - } - }, - 2015, - ); - - /** - * Delete an API host from the configuration - * @param context - * @param request - * @param response - * @returns - */ - deleteAPIHost = routeDecoratorProtectedAdministrator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - try { - const { id: originalID } = request.params; - context.wazuh.logger.debug(`Removing API host with ID [${originalID}]`); - - await context.wazuh_core.manageHosts.delete(originalID); - - context.wazuh.logger.info(`Removed API host with ID [${originalID}]`); - return response.ok({ - body: { - message: `API host with ID [${originalID}] was removed`, - }, - }); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return ErrorResponse( - `Could not remove the API host: ${error.message || error}`, - 2015, - 500, - response, - ); - } - }, - 2016, - ); } diff --git a/plugins/main/server/routes/wazuh-hosts.ts b/plugins/main/server/routes/wazuh-hosts.ts index a6205e8de4..34293eae4d 100644 --- a/plugins/main/server/routes/wazuh-hosts.ts +++ b/plugins/main/server/routes/wazuh-hosts.ts @@ -43,108 +43,6 @@ export function WazuhHostsRoutes(router: IRouter, services) { ctrl.updateClusterInfo(context, request, response), ); - // Create the API host entry - router.post( - { - path: '/hosts/apis/{id}', - validate: { - params: schema.object({ - id: - services.configuration._settings - .get('hosts') - ?.options?.arrayOf?.id?.validateBackend?.(schema) ?? - schema.string(), - }), - body: (value, response) => { - const settingHosts = services.configuration._settings.get('hosts'); - - try { - const validation = schema - .object( - Object.fromEntries( - Object.entries(settingHosts.options.arrayOf).map( - ([key, value]) => [ - key, - value.validateBackend - ? value.validateBackend(schema) - : schema.any(), - ], - ), - ), - ) - .validate(value); - return response.ok(validation); - } catch (error) { - return response.badRequest(error.message); - } - }, - }, - }, - async (context, request, response) => - ctrl.createAPIHost(context, request, response), - ); - - // Update the API host entry - router.put( - { - path: '/hosts/apis/{id}', - validate: { - params: schema.object({ - id: - services.configuration._settings - .get('hosts') - ?.options?.arrayOf?.id?.validateBackend?.(schema) ?? - schema.string(), - }), - body: (value, response) => { - const settingHosts = services.configuration._settings.get('hosts'); - - try { - const validation = schema - .object( - Object.fromEntries( - Object.entries(settingHosts.options.arrayOf).map( - ([key, value]) => [ - key, - schema.maybe( - value.validateBackend - ? value.validateBackend(schema) - : schema.any(), - ), - ], - ), - ), - ) - .validate(value); - return response.ok(validation); - } catch (error) { - return response.badRequest(error.message); - } - }, - }, - }, - async (context, request, response) => - ctrl.updateAPIHost(context, request, response), - ); - - // Delete the API host entry - router.delete( - { - path: '/hosts/apis/{id}', - validate: { - params: schema.object({ - id: - services.configuration._settings - .get('hosts') - ?.options?.arrayOf?.id?.validateBackend?.(schema) ?? - schema.string(), - }), - }, - }, - async (context, request, response) => - ctrl.deleteAPIHost(context, request, response), - ); - // Checks the orphan hosts in the registry in order to delete them router.post( { diff --git a/plugins/wazuh-core/server/services/manage-hosts.ts b/plugins/wazuh-core/server/services/manage-hosts.ts index 33fb757f6f..c472d9f92b 100644 --- a/plugins/wazuh-core/server/services/manage-hosts.ts +++ b/plugins/wazuh-core/server/services/manage-hosts.ts @@ -115,103 +115,6 @@ export class ManageHosts { return hostExistIndex; } - async create(hostID: string, data: IAPIHost) { - try { - const hosts = (await this.get()) as IAPIHost[]; - - let updatedHosts = [...hosts]; - - // Check if the API connection does not exist - this.checkHostExistence(updatedHosts, hostID, { shouldExist: false }); - - this.logger.debug(`Adding new API connection with ID [${data.id}]`); - updatedHosts.push(data); - this.logger.debug('Updating API connections'); - await this.configuration.set({ - hosts: updatedHosts, - }); - this.logger.info(`API connection with ID [${hostID}] was created`); - return data; - } catch (error) { - this.logger.error(error.message); - throw error; - } - } - - async update(hostID: string, data: IAPIHost) { - try { - const hosts = (await this.get()) as IAPIHost[]; - - let updatedHosts = [...hosts]; - - // Check if the API connection exists - const updatedHostID = data?.id || hostID; - let hostExistIndex = this.checkHostExistence( - updatedHosts, - updatedHostID, - { - /* when the updatedHostID is the same than the original one, then should exist, - otherwise not */ - shouldExist: updatedHostID === hostID, - }, - ); - - this.logger.debug(`Replacing API connection ID [${hostID}]`); - // Update the API connection info - hostExistIndex = - hostExistIndex === -1 - ? /* Get the index of the API connection with the original ID if does not find the updated - one */ - hosts.findIndex(({ id }) => id === hostID) - : hostExistIndex; - updatedHosts = updatedHosts.map((item, index) => - index === hostExistIndex ? { ...item, ...data } : item, - ); - - this.logger.debug('Updating API connections'); - await this.configuration.set({ - hosts: updatedHosts, - }); - this.logger.info(`API connection with ID [${updatedHostID}] was updated`); - return data; - } catch (error) { - this.logger.error(error.message); - throw error; - } - } - - /** - * Delete an API connection entry by ID from configuration - * @param hostID - */ - async delete(hostID: string) { - try { - const hosts = (await this.get()) as IAPIHost[]; - - const updatedHosts = [...hosts]; - - // Check if the API connection exists - const hostExistIndex = this.checkHostExistence(updatedHosts, hostID, { - shouldExist: true, - }); - - this.logger.debug(`API connection with ID [${hostID}] found`); - // Exist - // Remove host - this.logger.debug(`Removing API connection with ID [${hostID}]`); - updatedHosts.splice(hostExistIndex, 1); - - this.logger.debug('Updating API connections'); - await this.configuration.set({ - hosts: updatedHosts, - }); - this.logger.info(`API connection with ID [${hostID}] was removed`); - } catch (error) { - this.logger.error(error.message); - throw error; - } - } - /** * This get all hosts entries in the plugins configuration and the related info in the wazuh-registry.json * @param {Object} context From 83087569a91ee185ec542ab55b446037dec39e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 2 Apr 2024 16:45:56 +0200 Subject: [PATCH 10/34] fix: fix innaccesible view of Miscellaneous tab on App Settings --- plugins/main/public/controllers/settings/settings.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/main/public/controllers/settings/settings.js b/plugins/main/public/controllers/settings/settings.js index a0c633b5e5..83e884dd3f 100644 --- a/plugins/main/public/controllers/settings/settings.js +++ b/plugins/main/public/controllers/settings/settings.js @@ -133,6 +133,17 @@ export class SettingsController { this.addApiProps = { closeAddApi: () => this.closeAddApi(), }; + + this.settingsTabsProps = { + clickAction: tab => { + this.switchTab(tab, true); + }, + selectedTab: this.tab || 'api', + // Define tabs for Wazuh plugin settings application + tabs: + getWzCurrentAppID() === appSettings.id ? this.tabsConfiguration : null, + wazuhConfig: this.wazuhConfig, + }; } /** From 2339a00473b94b621091145c1699f014355e89e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 3 Apr 2024 17:22:27 +0200 Subject: [PATCH 11/34] fix(configuration): fix error when editing some setting from the UI due to an error in the backend side - Renamed setting properties: - validate -> validateUIForm - validateBackend -> validate - Adapted the validation on the affected API endpoints - Removed transformation of changed values of useForm hook. This transformation is now done externally. --- .../public/components/common/form/hooks.tsx | 4 +- .../public/components/common/form/types.ts | 1 - .../settings/configuration/configuration.tsx | 19 +- plugins/main/public/plugin.ts | 8 +- .../server/routes/wazuh-utils/wazuh-utils.ts | 40 ++- plugins/wazuh-core/common/constants.ts | 316 +++++++++--------- .../common/services/configuration.ts | 8 +- plugins/wazuh-core/server/plugin.ts | 8 +- 8 files changed, 203 insertions(+), 201 deletions(-) diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index a4f61cd573..e270978295 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -239,9 +239,7 @@ export const useForm = (fields: FormConfiguration): UseFormReturn => { pathFormState: [], }, ({ changed, error, value }, _, { pathFormState, fieldDefinition }) => { - changed && - (result.changed[pathFormState] = - fieldDefinition?.transformChangedOutputValue?.(value) ?? value); + changed && (result.changed[pathFormState] = value); error && (result.errors[pathFormState] = error); }, ); diff --git a/plugins/main/public/components/common/form/types.ts b/plugins/main/public/components/common/form/types.ts index 09846abd86..dfe5f60d29 100644 --- a/plugins/main/public/components/common/form/types.ts +++ b/plugins/main/public/components/common/form/types.ts @@ -33,7 +33,6 @@ interface FieldConfiguration { initialValue: any; validate?: (value: any) => string | undefined; transformChangedInputValue?: (value: any) => any; - transformChangedOutputValue?: (value: any) => any; } export interface DefaultFieldConfiguration extends FieldConfiguration { diff --git a/plugins/main/public/components/settings/configuration/configuration.tsx b/plugins/main/public/components/settings/configuration/configuration.tsx index 74e0430229..60b72ff6fd 100644 --- a/plugins/main/public/components/settings/configuration/configuration.tsx +++ b/plugins/main/public/components/settings/configuration/configuration.tsx @@ -73,11 +73,11 @@ const transformPluginSettingsToFormFields = (configuration, pluginSettings) => { key, { type, - validate, + validateUIForm, defaultValue: initialValue, uiFormTransformChangedInputValue, uiFormTransformConfigurationValueToInputValue, - uiFormTransformInputValueToConfigurationValue, + ...rest }, ], @@ -95,11 +95,9 @@ const transformPluginSettingsToFormFields = (configuration, pluginSettings) => { } : { type, - validate: validate?.bind?.(rest), + validate: validateUIForm?.bind?.(rest), transformChangedInputValue: uiFormTransformChangedInputValue?.bind?.(rest), - transformChangedOutputValue: - uiFormTransformInputValueToConfigurationValue?.bind?.(rest), initialValue: uiFormTransformConfigurationValueToInputValue ? uiFormTransformConfigurationValueToInputValue.bind(rest)( configuration?.[key] ?? initialValue, @@ -214,6 +212,11 @@ const WzConfigurationSettingsProvider = props => { (accum, [pluginSettingKey, currentValue]) => { const pluginSetting = getWazuhCorePlugin().configuration._settings.get(pluginSettingKey); + + const transformedValue = + pluginSetting?.uiFormTransformInputValueToConfigurationValue?.( + currentValue, + ) ?? currentValue; if ( pluginSetting.isConfigurableFromSettings && pluginSetting.type === EpluginSettingType.filepicker @@ -221,14 +224,14 @@ const WzConfigurationSettingsProvider = props => { accum.fileUpload = { ...accum.fileUpload, [pluginSettingKey]: { - file: currentValue, - extension: path.extname(currentValue.name), + file: transformedValue, + extension: path.extname(transformedValue.name), }, }; } else if (pluginSetting.isConfigurableFromSettings) { accum.saveOnConfigurationFile = { ...accum.saveOnConfigurationFile, - [pluginSettingKey]: currentValue, + [pluginSettingKey]: transformedValue, }; } return accum; diff --git a/plugins/main/public/plugin.ts b/plugins/main/public/plugin.ts index ee762f1abf..5f4f8b1844 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -140,16 +140,16 @@ export class WazuhPlugin const setting = plugins.wazuhCore.configuration._settings.get( 'cron.statistics.interval', ); + !setting.validateUIForm && + (setting.validateUIForm = function (value) { + return this.validate(value); + }); !setting.validate && (setting.validate = function (value: string) { return validateNodeCronInterval(value) ? undefined : 'Interval is not valid.'; }); - !setting.validateBackend && - (setting.validateBackend = function (schema) { - return schema.string({ validate: this.validate }); - }); // Set the dynamic redirection setWzMainParams(redirectTo()); setWzCurrentAppID(id); diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts index f404d770ff..de626e06f6 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts @@ -37,25 +37,29 @@ export function WazuhUtilsRoutes(router: IRouter, services) { validate: { // body: schema.any(), body: (value, response) => { - const validationSchema = Array.from( - services.configuration._settings.entries(), - ) - .filter( - ([, { isConfigurableFromSettings }]) => - isConfigurableFromSettings, - ) - .reduce( - (accum, [pluginSettingKey, pluginSettingConfiguration]) => ({ - ...accum, - [pluginSettingKey]: schema.maybe( - pluginSettingConfiguration.validateBackend - ? pluginSettingConfiguration.validateBackend(schema) - : schema.any(), - ), - }), - {}, - ); try { + const validationSchema = Array.from( + services.configuration._settings.entries(), + ) + .filter( + ([, { isConfigurableFromSettings }]) => + isConfigurableFromSettings, + ) + .reduce( + (accum, [pluginSettingKey, pluginSettingConfiguration]) => ({ + ...accum, + [pluginSettingKey]: schema.maybe( + schema.any({ + validate: pluginSettingConfiguration.validate + ? pluginSettingConfiguration.validate.bind( + pluginSettingConfiguration, + ) + : () => {}, + }), + ), + }), + {}, + ); const validation = schema.object(validationSchema).validate(value); return response.ok(validation); } catch (error) { diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 23a8d4cf8e..ae920dc00e 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -502,9 +502,9 @@ export type TPluginSetting = { // 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; + validateUIForm?: (value: any) => string | undefined; + // Validate function creator to validate the setting in the backend. + validate?: (value: unknown) => string | undefined; }; export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; @@ -576,8 +576,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_SAMPLE_ALERT_PREFIX, isConfigurableFromSettings: true, requiresRunningHealthCheck: true, + validateUIForm: function (value) { + return this.validate(value); + }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), @@ -594,9 +598,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '*', ), ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, }, 'checks.api': { title: 'API connection', @@ -623,10 +624,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.fields': { title: 'Known fields', @@ -654,10 +655,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.maxBuckets': { title: 'Set max buckets to 200000', @@ -685,10 +686,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.metaFields': { title: 'Remove meta fields', @@ -716,10 +717,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.pattern': { title: 'Index pattern', @@ -747,10 +748,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.setup': { title: 'API version', @@ -778,10 +779,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.template': { title: 'Index template', @@ -809,10 +810,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'checks.timeFilter': { title: 'Set time filter to 24h', @@ -840,10 +841,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'cron.prefix': { title: 'Cron prefix', @@ -857,8 +858,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, isConfigurableFromSettings: true, + validateUIForm: function (value) { + return this.validate(value); + }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), @@ -875,9 +880,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '*', ), ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, }, 'cron.statistics.apis': { title: 'Includes APIs', @@ -909,7 +911,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return value; } }, - validate: SettingsValidator.json( + validateUIForm: SettingsValidator.json( SettingsValidator.compose( SettingsValidator.array( SettingsValidator.compose( @@ -920,16 +922,15 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ), ), ), - validateBackend: function (schema) { - return schema.arrayOf( - schema.string({ - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - ), - }), - ); - }, + validate: SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + ), + ), + ), }, 'cron.statistics.index.creation': { title: 'Index creation', @@ -964,16 +965,14 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, isConfigurableFromSettings: true, requiresRunningHealthCheck: true, + validateUIForm: function (value) { + return this.validate(value); + }, 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', @@ -989,8 +988,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_STATISTICS_DEFAULT_NAME, isConfigurableFromSettings: true, requiresRunningHealthCheck: true, + validateUIForm: function (value) { + return this.validate(value); + }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noStartsWithString('-', '_', '+', '.'), @@ -1007,9 +1010,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '*', ), ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, }, 'cron.statistics.index.replicas': { title: 'Index replicas', @@ -1041,12 +1041,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1076,12 +1076,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1103,8 +1103,8 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { // ? undefined // : 'Interval is not valid.'; // }, - // validateBackend: function (schema) { - // return schema.string({ validate: this.validate }); + // validate: function (schema) { + // return schema.string({ validate: this.validateUIForm }); // }, }, 'cron.statistics.status': { @@ -1132,10 +1132,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'customization.enabled': { title: 'Status', @@ -1163,10 +1163,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'customization.logo.app': { title: 'App main logo', @@ -1204,7 +1204,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - validate: function (value) { + validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ ...this.options.file.size, @@ -1252,7 +1252,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - validate: function (value) { + validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ ...this.options.file.size, @@ -1299,7 +1299,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - validate: function (value) { + validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ ...this.options.file.size, @@ -1325,15 +1325,15 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, isConfigurableFromSettings: true, options: { maxRows: 2, maxLength: 50 }, + validateUIForm: function (value) { + return this.validate(value); + }, 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', @@ -1349,15 +1349,15 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, isConfigurableFromSettings: true, options: { maxRows: 3, maxLength: 40 }, + validateUIForm: function (value) { + return this.validate(value); + }, 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', @@ -1372,10 +1372,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: '', isConfigurableFromSettings: true, - validate: SettingsValidator.hasNoSpaces, // TODO: replace by the validator of Deploy new agent - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, + validateUIForm: function (value) { + return this.validate(value); + }, // TODO: replace by the validator of Deploy new agent + validate: SettingsValidator.hasNoSpaces, }, 'enrollment.password': { title: 'Enrollment password', @@ -1390,10 +1390,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: '', isConfigurableFromSettings: false, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isNotEmptyString, }, hideManagerAlerts: { title: 'Hide manager alerts', @@ -1421,10 +1421,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, hosts: { title: 'Server hosts', @@ -1450,10 +1450,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: 'default', isConfigurableFromSettings: true, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isNotEmptyString, }, url: { title: 'URL', @@ -1461,10 +1461,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: 'https://localhost', isConfigurableFromSettings: true, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isNotEmptyString, }, port: { title: 'Port', @@ -1489,12 +1489,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(value); + }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); }, - validateBackend: function (schema) { - return schema.number({ validate: this.validate.bind(this) }); - }, }, username: { title: 'Username', @@ -1502,10 +1502,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.text, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isNotEmptyString, }, password: { title: 'Password', @@ -1513,10 +1513,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { type: EpluginSettingType.password, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, - validate: SettingsValidator.isNotEmptyString, - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isNotEmptyString, }, run_as: { title: 'Run as', @@ -1537,10 +1537,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, }, }, @@ -1552,7 +1552,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, // TODO: add validation // validate: SettingsValidator.isBoolean, - // validateBackend: function (schema) { + // validate: function (schema) { // return schema.boolean(); // }, }, @@ -1587,7 +1587,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validate: SettingsValidator.json( + validateUIForm: SettingsValidator.json( SettingsValidator.compose( SettingsValidator.array( SettingsValidator.compose( @@ -1611,29 +1611,28 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ), ), ), - validateBackend: function (schema) { - return schema.arrayOf( - schema.string({ - validate: SettingsValidator.compose( - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), + validate: SettingsValidator.compose( + SettingsValidator.array( + SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + SettingsValidator.hasNoSpaces, + SettingsValidator.noLiteralString('.', '..'), + SettingsValidator.noStartsWithString('-', '_', '+', '.'), + SettingsValidator.hasNotInvalidCharacters( + '\\', + '/', + '?', + '"', + '<', + '>', + '|', + ',', + '#', ), - }), - ); - }, + ), + ), + ), }, 'ip.selector': { title: 'IP selector', @@ -1661,10 +1660,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, pattern: { title: 'Index pattern', @@ -1681,7 +1680,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, requiresRunningHealthCheck: true, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc + validateUIForm: function (value) { + return this.validate(value); + }, validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), @@ -1698,9 +1701,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '#', ), ), - validateBackend: function (schema) { - return schema.string({ validate: this.validate }); - }, }, timeout: { title: 'Request timeout', @@ -1729,12 +1729,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1770,16 +1770,14 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, isConfigurableFromSettings: true, requiresRunningHealthCheck: true, + validateUIForm: function (value) { + return this.validate(value); + }, 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', @@ -1808,10 +1806,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): boolean { return Boolean(value); }, - validate: SettingsValidator.isBoolean, - validateBackend: function (schema) { - return schema.boolean(); + validateUIForm: function (value) { + return this.validate(value); }, + validate: SettingsValidator.isBoolean, }, 'wazuh.monitoring.frequency': { title: 'Frequency', @@ -1841,12 +1839,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1861,6 +1859,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_MONITORING_PATTERN, isConfigurableFromSettings: true, requiresRunningHealthCheck: true, + validateUIForm: function (value) { + return this.validate(value); + }, validate: SettingsValidator.compose( SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, @@ -1878,9 +1879,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '#', ), ), - validateBackend: function (schema) { - return schema.string({ minLength: 1, validate: this.validate }); - }, }, 'wazuh.monitoring.replicas': { title: 'Index replicas', @@ -1910,12 +1908,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1945,12 +1943,12 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): number { return Number(value); }, + validateUIForm: function (value) { + return this.validate(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', @@ -1965,6 +1963,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_VULNERABILITIES_PATTERN, isConfigurableFromSettings: true, requiresRunningHealthCheck: false, + validateUIForm: function (value) { + return this.validate(value); + }, validate: SettingsValidator.compose( SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, @@ -1982,9 +1983,6 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '#', ), ), - validateBackend: function (schema) { - return schema.string({ minLength: 1, validate: this.validate }); - }, }, }; diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 1e2136aed2..7fbb47d836 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -124,10 +124,10 @@ export type TConfigurationSetting = { 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; + // Validate the value in the form of App Settings. It returns a string if there is some validation error. + validateUIForm?: (value: any) => string | undefined; + // Validate function creator to validate the setting in the backend. + validate?: (schema: any) => (value: unknown) => string | undefined; }; export type TConfigurationSettingWithKey = TConfigurationSetting & { diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index eb27e1bff0..45bcd4cffb 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -81,16 +81,16 @@ export class WazuhCorePlugin const setting = this.services.configuration._settings.get( 'cron.statistics.interval', ); + !setting.validateUIForm && + (setting.validateUIForm = function (value) { + return this.validate(value); + }); !setting.validate && (setting.validate = function (value: string) { return validateNodeCronInterval(value) ? undefined : 'Interval is not valid.'; }); - !setting.validateBackend && - (setting.validateBackend = function (schema) { - return schema.string({ validate: this.validate }); - }); this.services.configuration.setup(); From c36335412a03631f043a38cf7ac9b015302310ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 3 Apr 2024 17:56:45 +0200 Subject: [PATCH 12/34] rollback(configuration): clear entry on configuration file --- .../server/services/configuration-store.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index b52e0773fc..e5104a1b6d 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -86,11 +86,21 @@ export class ConfigurationStore implements IConfigurationStore { pluginSettingsConfigurableFile, ).reduce((accum, [key, value]) => { const re = new RegExp(`^${key}\\s{0,}:\\s{1,}.*`, 'gm'); - const formatedValue = formatSettingValueToFile(value); const match = accum.match(re); - return match /*|| exists*/ - ? accum.replace(re, `${key}: ${formatedValue}`) - : `${accum}\n${key}: ${formatedValue}`; + + // Remove the setting if value is null + if (value === null) { + return accum.replace(re, ''); + } + + const formatedValue = formatSettingValueToFile(value); + const updateSettingEntry = `${key}: ${formatedValue}`; + return match + ? /* Replace the setting if it is defined */ + accum.replace(re, `${updateSettingEntry}`) + : /* Append the new setting entry to the end of file */ `${accum}${ + accum.endsWith('\n') ? '' : '\n' + }${updateSettingEntry}` /*exists*/; }, content); this.writeContentConfigurationFile(contentUpdated); @@ -295,16 +305,11 @@ hosts: } async clear(...settings: string[]): Promise<any> { try { - const stored = await this.storeGet({ ignoreCache: true }); - const updatedSettings = { - ...stored, - }; const removedSettings = {}; settings.forEach(setting => { - delete updatedSettings[setting]; removedSettings[setting] = null; }); - await this.storeSet(updatedSettings); + await this.storeSet(removedSettings); return removedSettings; } catch (error) { const enhancedError = new Error( From d0947e915fc32c2706634347e41caffb6b25e75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Wed, 3 Apr 2024 17:58:22 +0200 Subject: [PATCH 13/34] rollback(configuration): remove API endpoint POST /utils/configuration/import --- .../controllers/wazuh-utils/wazuh-utils.ts | 27 ------------------- .../server/routes/wazuh-utils/wazuh-utils.ts | 21 --------------- 2 files changed, 48 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts index 6fe7894b55..faa0bced60 100644 --- a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts @@ -249,33 +249,6 @@ export class WazuhUtilsCtrl { 3023, ); - /** - * Import the configuration from a configuration file - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Object} Configuration File or ErrorResponse - */ - importConfiguration = routeDecoratorProtectedAdministrator( - async ( - context: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory, - ) => { - const { file: fileBuffer } = request.body; - const responseImportFile = - await context.wazuh_core.configuration.importFile(fileBuffer); - - return response.ok({ - body: { - message: 'Configuration was imported', - ...responseImportFile, - }, - }); - }, - 3024, - ); - /** * Get the plugin scoped account * @param {Object} context diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts index de626e06f6..e2bc0cce0c 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts @@ -154,27 +154,6 @@ export function WazuhUtilsRoutes(router: IRouter, services) { ctrl.clearConfiguration(context, request, response), ); - // Import the configuration file - router.post( - { - path: '/utils/configuration/import', - validate: { - body: schema.object({ - // file: buffer - file: schema.buffer(), - }), - }, - options: { - body: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - }, - }, - async (context, request, response) => - ctrl.importConfiguration(context, request, response), - ); - // Get if the current user is an administrator router.get( { From 38d05c83b706aab82bb931552b51ed54c710bd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 11:09:43 +0200 Subject: [PATCH 14/34] feat(configuration): enhance validation of plugin settings --- plugins/wazuh-core/common/constants.ts | 126 +++++------ .../common/services/settings-validator.ts | 195 +++++++++++------- 2 files changed, 186 insertions(+), 135 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index ae920dc00e..0a9be8c1c3 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -911,17 +911,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return value; } }, - validateUIForm: SettingsValidator.json( - SettingsValidator.compose( - SettingsValidator.array( - SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - ), - ), - ), - ), + validateUIForm: function (value) { + return SettingsValidator.json(this.validate)(value); + }, validate: SettingsValidator.compose( SettingsValidator.array( SettingsValidator.compose( @@ -1042,7 +1034,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1077,7 +1071,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1098,13 +1094,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, requiresRestartingPluginPlatform: true, // Workaround: this need to be defined in the frontend side and backend side because an optimization error in the frontend side related to some module can not be loaded. - // validate: function (value: string) { - // return validateNodeCronInterval(value) - // ? undefined - // : 'Interval is not valid.'; + // validateUIForm: function (value) { // }, - // validate: function (schema) { - // return schema.string({ validate: this.validateUIForm }); + // validate: function (value) { // }, }, 'cron.statistics.status': { @@ -1329,10 +1321,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return this.validate(value); }, validate: function (value) { - return SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, - })(value); + return SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.multipleLinesString({ + maxRows: this.options?.maxRows, + maxLength: this.options?.maxLength, + }), + )(value); }, }, 'customization.reports.header': { @@ -1353,10 +1348,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return this.validate(value); }, validate: function (value) { - return SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, - })(value); + return SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.multipleLinesString({ + maxRows: this.options?.maxRows, + maxLength: this.options?.maxLength, + }), + )(value); }, }, 'enrollment.dns': { @@ -1393,7 +1391,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return this.validate(value); }, - validate: SettingsValidator.isNotEmptyString, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + ), }, hideManagerAlerts: { title: 'Hide manager alerts', @@ -1453,7 +1454,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return this.validate(value); }, - validate: SettingsValidator.isNotEmptyString, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + ), }, url: { title: 'URL', @@ -1464,7 +1468,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return this.validate(value); }, - validate: SettingsValidator.isNotEmptyString, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + ), }, port: { title: 'Port', @@ -1490,7 +1497,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1505,7 +1514,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return this.validate(value); }, - validate: SettingsValidator.isNotEmptyString, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + ), }, password: { title: 'Password', @@ -1516,7 +1528,10 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return this.validate(value); }, - validate: SettingsValidator.isNotEmptyString, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.isNotEmptyString, + ), }, run_as: { title: 'Run as', @@ -1587,30 +1602,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validateUIForm: SettingsValidator.json( - SettingsValidator.compose( - SettingsValidator.array( - SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - ), - ), - ), + validateUIForm: function (value) { + return SettingsValidator.json(this.validate)(value); + }, validate: SettingsValidator.compose( SettingsValidator.array( SettingsValidator.compose( @@ -1730,7 +1724,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1840,7 +1836,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1863,6 +1861,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return this.validate(value); }, validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), @@ -1909,7 +1908,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1944,7 +1945,9 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return Number(value); }, validateUIForm: function (value) { - return this.validate(value); + return this.validate( + this.uiFormTransformInputValueToConfigurationValue(value), + ); }, validate: function (value) { return SettingsValidator.number(this.options.number)(value); @@ -1967,6 +1970,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { return this.validate(value); }, validate: SettingsValidator.compose( + SettingsValidator.isString, SettingsValidator.isNotEmptyString, SettingsValidator.hasNoSpaces, SettingsValidator.noLiteralString('.', '..'), diff --git a/plugins/wazuh-core/common/services/settings-validator.ts b/plugins/wazuh-core/common/services/settings-validator.ts index b62675f0f9..64790b7305 100644 --- a/plugins/wazuh-core/common/services/settings-validator.ts +++ b/plugins/wazuh-core/common/services/settings-validator.ts @@ -13,10 +13,10 @@ export class SettingsValidator { const result = fn(value); if (typeof result === 'string' && result.length > 0) { return result; - }; - }; + } + } }; - }; + } /** * Check the value is a string @@ -24,8 +24,8 @@ export class SettingsValidator { * @returns */ static isString(value: unknown): string | undefined { - return typeof value === 'string' ? undefined : "Value is not a string."; - }; + return typeof value === 'string' ? undefined : 'Value is not a string.'; + } /** * Check the string has no spaces @@ -33,8 +33,8 @@ export class SettingsValidator { * @returns */ static hasNoSpaces(value: string): string | undefined { - return /^\S*$/.test(value) ? undefined : "No whitespaces allowed."; - }; + return /^\S*$/.test(value) ? undefined : 'No whitespaces allowed.'; + } /** * Check the string has no empty @@ -44,32 +44,37 @@ export class SettingsValidator { static isNotEmptyString(value: string): string | undefined { if (typeof value === 'string') { if (value.length === 0) { - return "Value can not be empty." + 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) { + static multipleLinesString( + options: { minRows?: number; maxRows?: number; maxLength?: number } = {}, + ) { + return function (value: string) { const lines = value.split(/\r\n|\r|\n/).length; - if (typeof options.maxLength !== 'undefined' && value.split('\n').some(line => line.length > options.maxLength)) { + 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 @@ -78,11 +83,15 @@ export class SettingsValidator { */ 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(', ')}.` + 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 @@ -91,11 +100,13 @@ export class SettingsValidator { */ static noStartsWithString(...invalidStartingCharacters: string[]) { return function (value: string): string | undefined { - return invalidStartingCharacters.some(invalidStartingCharacter => value.startsWith(invalidStartingCharacter)) + 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 @@ -108,7 +119,7 @@ export class SettingsValidator { ? `It can't be: ${invalidLiterals.join(', ')}.` : undefined; }; - }; + } /** * Check the value is a boolean @@ -118,35 +129,43 @@ export class SettingsValidator { static isBoolean(value: string): string | undefined { return typeof value === 'boolean' ? undefined - : "It should be a boolean. Allowed values: true or false."; - }; + : 'It should be a boolean. Allowed values: true or false.'; + } + + /** + * Check the value is a number + * @param value + * @returns + */ + static isNumber(value: string): string | undefined { + return typeof value === 'number' ? undefined : 'Value is not a number.'; + } /** * Check the value is a number between some optional limits * @param options * @returns */ - static number(options: { min?: number, max?: number, integer?: boolean } = {}) { + 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.' - }; + if (typeof value !== 'number') { + return 'Value is not a number.'; + } - const valueNumber = typeof value === 'string' ? Number(value) : value; + if (options.integer && !Number.isInteger(Number(value))) { + return 'Number should be an integer.'; + } - if (typeof options.min !== 'undefined' && valueNumber < options.min) { + if (typeof options.min !== 'undefined' && value < options.min) { return `Value should be greater or equal than ${options.min}.`; - }; - if (typeof options.max !== 'undefined' && valueNumber > options.max) { + } + if (typeof options.max !== 'undefined' && value > options.max) { return `Value should be lower or equal than ${options.max}.`; - }; + } }; - }; + } /** * Creates a function that checks if the value is a json @@ -161,11 +180,11 @@ export class SettingsValidator { 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 @@ -177,24 +196,24 @@ export class SettingsValidator { // 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; - }; + if (accum) { + return accum; + } - const resultValidationElement = validationElement(elementValue); - if (resultValidationElement) { - return resultValidationElement; - }; + const resultValidationElement = validationElement(elementValue); + if (resultValidationElement) { + return resultValidationElement; + } - return accum; - }, undefined) + return accum; + }, undefined) : undefined; }; - }; + } /** * Creates a function that checks if the value is equal to list of values @@ -203,33 +222,61 @@ export class SettingsValidator { */ static literal(literals: unknown[]) { return function (value: any): string | undefined { - return literals.includes(value) ? undefined : `Invalid value. Allowed values: ${literals.map(String).join(', ')}.`; + 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(', ')}.`; + 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`}.`; + 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` + }.`; + } }; - }; -}; +} From 1d2905d175bc60db5848b5deb13027fac02bfd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 11:29:12 +0200 Subject: [PATCH 15/34] feat(configuration): enhance validation of enrollment.dns plugin setting - Enhance the validation of enrollment.dns setting - Replace the validation of the server address input on Deploy new agent guide by the validation of enrollment.dns plugin setting - Remove validateServerAddress method and related tests. Move tests to apply to SettingsValidator --- .../register-agent/register-agent.tsx | 11 ++-- .../register-agent/utils/validations.test.tsx | 49 +---------------- .../register-agent/utils/validations.tsx | 26 --------- plugins/wazuh-core/common/constants.ts | 7 ++- .../services/settings-validator.test.ts | 54 +++++++++++++++++++ .../common/services/settings-validator.ts | 26 +++++++++ 6 files changed, 91 insertions(+), 82 deletions(-) create mode 100644 plugins/wazuh-core/common/services/settings-validator.test.ts diff --git a/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx b/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx index 99a4f86f22..43570a4cf5 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/containers/register-agent/register-agent.tsx @@ -33,13 +33,10 @@ import { } from '../../../../common/hocs'; import GroupInput from '../../components/group-input/group-input'; import { OsCard } from '../../components/os-selector/os-card/os-card'; -import { - validateServerAddress, - validateAgentName, -} from '../../utils/validations'; +import { validateAgentName } from '../../utils/validations'; import { compose } from 'redux'; import { endpointSummary } from '../../../../../utils/applications'; -import { getCore } from '../../../../../kibana-services'; +import { getCore, getWazuhCorePlugin } from '../../../../../kibana-services'; import { getErrorOrchestrator } from '../../../../../react-services/common-services'; export const RegisterAgent = compose( @@ -80,7 +77,9 @@ export const RegisterAgent = compose( serverAddress: { type: 'text', initialValue: configuration['enrollment.dns'] || '', - validate: validateServerAddress, + validate: + getWazuhCorePlugin().configuration._settings.get('enrollment.dns') + .validate, }, agentName: { type: 'text', diff --git a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx index beca15c672..1d457ec2b4 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.test.tsx @@ -1,53 +1,6 @@ -import { validateServerAddress, validateAgentName } from './validations'; +import { validateAgentName } from './validations'; describe('Validations', () => { - it('should return undefined for an empty value', () => { - const result = validateServerAddress(''); - expect(result).toBeUndefined(); - }); - - it('should return undefined for a valid FQDN', () => { - const validFQDN = 'example.fqdn.valid'; - const result = validateServerAddress(validFQDN); - expect(result).toBeUndefined(); - }); - - it('should return undefined for a valid IPv4', () => { - const validIP = '192.168.1.1'; - const result = validateServerAddress(validIP); - expect(result).toBeUndefined(); - }); - - it('should return undefined for a valid IPv6', () => { - const validIP = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; - const result = validateServerAddress(validIP); - expect(result).toBeUndefined(); - }); - - it('should return an error message for an invalid IPv6', () => { - const invalidIPV6 = '2001:db8:85a3::8a2e:370:7334'; - const result = validateServerAddress(invalidIPV6); - expect(result).toBe( - 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', - ); - }); - - it('should return an error message for a compressed IPv6', () => { - const compressedIPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12'; - const result = validateServerAddress(compressedIPV6); - expect(result).toBe( - 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', - ); - }); - - it('should return an error message for an invalid FQDN', () => { - const invalidFQDN = 'example.'; - const result = validateServerAddress(invalidFQDN); - expect(result).toBe( - 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', - ); - }); - test('should return undefined for an empty value', () => { const emptyValue = ''; const result = validateAgentName(emptyValue); diff --git a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx index 6d5c402739..06d9aaf940 100644 --- a/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx +++ b/plugins/main/public/components/endpoints-summary/register-agent/utils/validations.tsx @@ -1,29 +1,3 @@ -//IPv4: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 -//IPv6: This is a set or eight hexadecimal expressions, each from 0000 to FFFF. 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - -// FQDN: Maximum of 63 characters per label. -// Can only contain numbers, letters and hyphens (-) -// Labels cannot begin or end with a hyphen -// Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. -// Minimum 3 labels -// A label can contain only numbers - -// Hostname: Maximum of 63 characters per label. Same rules as FQDN apply. - -export const validateServerAddress = (value: string) => { - const isFQDNOrHostname = - /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ](?:\.[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ]){0,}$/; - const isIPv6 = /^(?:[0-9a-fA-F]{4}:){7}[0-9a-fA-F]{4}$/; - - if ( - value.length > 255 || - (value.length > 0 && !isFQDNOrHostname.test(value) && !isIPv6.test(value)) - ) { - return 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'; - } - return undefined; -}; - export const validateAgentName = (value: any) => { if (value.length === 0) { return undefined; diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 0a9be8c1c3..241e436d74 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -1372,8 +1372,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, validateUIForm: function (value) { return this.validate(value); - }, // TODO: replace by the validator of Deploy new agent - validate: SettingsValidator.hasNoSpaces, + }, + validate: SettingsValidator.compose( + SettingsValidator.isString, + SettingsValidator.serverAddressHostnameFQDNIPv4IPv6, + ), }, 'enrollment.password': { title: 'Enrollment password', diff --git a/plugins/wazuh-core/common/services/settings-validator.test.ts b/plugins/wazuh-core/common/services/settings-validator.test.ts new file mode 100644 index 0000000000..4dee8a63f7 --- /dev/null +++ b/plugins/wazuh-core/common/services/settings-validator.test.ts @@ -0,0 +1,54 @@ +import { SettingsValidator } from './settings-validator'; + +describe('Validations', () => { + it('should return undefined for an empty value', () => { + const result = SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(''); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid FQDN', () => { + const validFQDN = 'example.fqdn.valid'; + const result = + SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(validFQDN); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid IPv4', () => { + const validIP = '192.168.1.1'; + const result = SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(validIP); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid IPv6', () => { + const validIP = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; + const result = SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(validIP); + expect(result).toBeUndefined(); + }); + + it('should return an error message for an invalid IPv6', () => { + const invalidIPV6 = '2001:db8:85a3::8a2e:370:7334'; + const result = + SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(invalidIPV6); + expect(result).toBe( + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', + ); + }); + + it('should return an error message for a compressed IPv6', () => { + const compressedIPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12'; + const result = + SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(compressedIPV6); + expect(result).toBe( + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', + ); + }); + + it('should return an error message for an invalid FQDN', () => { + const invalidFQDN = 'example.'; + const result = + SettingsValidator.serverAddressHostnameFQDNIPv4IPv6(invalidFQDN); + expect(result).toBe( + 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6', + ); + }); +}); diff --git a/plugins/wazuh-core/common/services/settings-validator.ts b/plugins/wazuh-core/common/services/settings-validator.ts index 64790b7305..f0392e314a 100644 --- a/plugins/wazuh-core/common/services/settings-validator.ts +++ b/plugins/wazuh-core/common/services/settings-validator.ts @@ -279,4 +279,30 @@ export class SettingsValidator { }.`; } }; + + //IPv4: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 + //IPv6: This is a set or eight hexadecimal expressions, each from 0000 to FFFF. 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + + // FQDN: Maximum of 63 characters per label. + // Can only contain numbers, letters and hyphens (-) + // Labels cannot begin or end with a hyphen + // Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. + // Minimum 3 labels + // A label can contain only numbers + + // Hostname: Maximum of 63 characters per label. Same rules as FQDN apply. + + static serverAddressHostnameFQDNIPv4IPv6(value: string) { + const isFQDNOrHostname = + /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ](?:\.[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ]){0,}$/; + const isIPv6 = /^(?:[0-9a-fA-F]{4}:){7}[0-9a-fA-F]{4}$/; + + if ( + value.length > 255 || + (value.length > 0 && !isFQDNOrHostname.test(value) && !isIPv6.test(value)) + ) { + return 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'; + } + return undefined; + } } From 30b8ae0f0330208aad905289937c4899198f478a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 11:33:05 +0200 Subject: [PATCH 16/34] clean: import of non-existent method --- plugins/wazuh-core/common/services/settings.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/common/services/settings.test.ts b/plugins/wazuh-core/common/services/settings.test.ts index c828b99fb2..4c2c435bd4 100644 --- a/plugins/wazuh-core/common/services/settings.test.ts +++ b/plugins/wazuh-core/common/services/settings.test.ts @@ -1,4 +1,4 @@ -import { formatLabelValuePair, getCustomizationSetting } from './settings'; +import { formatLabelValuePair } from './settings'; describe('[settings] Methods', () => { describe('formatLabelValuePair: Format the label-value pairs used to display the allowed values', () => { From 09e40e41bf928512667af31b3f2c2128b4d6c59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 12:12:49 +0200 Subject: [PATCH 17/34] rollback(configuration): remove import configuration from file --- .../server/services/enhance-configuration.ts | 82 ------------------- 1 file changed, 82 deletions(-) diff --git a/plugins/wazuh-core/server/services/enhance-configuration.ts b/plugins/wazuh-core/server/services/enhance-configuration.ts index 34b8b09b44..32bdf9a858 100644 --- a/plugins/wazuh-core/server/services/enhance-configuration.ts +++ b/plugins/wazuh-core/server/services/enhance-configuration.ts @@ -65,86 +65,4 @@ export function enhanceConfiguration(configuration: IConfiguration) { return getCustomizationSetting(this, currentConfiguration, settingKey); }; - - configuration.importFile = async function (file: string | Buffer) { - const fileContent = typeof file === 'string' ? file : file.toString(); - this.logger.debug('Loading imported file content as JSON'); - const configAsJSON = yml.load(fileContent); - this.logger.debug('Loaded imported file content as JSON'); - - const { hosts: configFileHosts, ...otherSettings } = configAsJSON; - - // Transform hosts - const hosts = configFileHosts - ? Object.values(configFileHosts).map(item => { - const id = Object.keys(item)[0]; - return { - ...item[id], - id: id, - }; - }) - : []; - - const settingsFromFile = { - ...otherSettings, - ...(hosts.length ? { hosts } : {}), - }; - - const warnings: string[] = []; - // Filter the settings by the supported - const validSettings = Object.fromEntries( - Object.entries(settingsFromFile).filter(([key]) => { - if (this._settings.has(key)) { - return true; - } else { - warnings.push(`[${key}] is not supported. This is ignored.`); - return false; - } - }), - ); - - const thereIsOtherSettings = Object.keys(validSettings).length; - - if (!thereIsOtherSettings) { - const message = - 'There are no valid settings defined in the configuration file'; - this.logger.debug(message); - return response.badRequest({ - body: { - message, - ...(warnings.length ? { warnings } : {}), - }, - }); - } - - this.logger.debug('Clearing configuration before importing the file'); - await this.clear(); - this.logger.info('Cleared configuration before importing the file'); - - this.logger.debug('Storing configuration from imported file'); - const responseSetConfig = await this.set(validSettings); - this.logger.info('Stored configuration from imported file'); - - Object.entries(responseSetConfig?.requirements ?? {}) - .filter(([_, value]) => value) - .forEach(([key]) => { - messagesRequirements?.[key] && - warnings.push( - `The imported configuration requires: ${messagesRequirements[key]}`, - ); - }); - - return { - message: 'Configuration file was imported', - data: responseSetConfig, - ...(warnings.length ? { warnings } : {}), - }; - }; } - -// TODO: try to move to common because these messages are displayed on the UI too -const messagesRequirements = { - requiresReloadingBrowserTab: 'Reload the page to apply the changes', - requiresRunningHealthCheck: 'Run a health check to apply the changes.', - requiresRestartingPluginPlatform: `Restart ${PLUGIN_PLATFORM_NAME} to apply the changes`, -}; From 096c7e49c9a18669f197bca05a2a415d50da64aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 14:11:48 +0200 Subject: [PATCH 18/34] fix(configuration): remove the visibility of default values of customization settings on App Settings - Create getSettingValueIfNotSet method for Configuration services to manage the customization settings - Adapt the getCustomizationSetting method of Configuration service to use getSettingValueIfNotSet - Change the parameters of getCustomizationSetting of Configuration service to support multiple settings - Adapt printer to the usage of getCustomizationSettings redefinition - Enhance code typings --- plugins/main/server/lib/reporting/printer.ts | 50 +++++++++++-------- plugins/wazuh-core/common/constants.ts | 2 +- .../common/services/configuration.ts | 34 +++++++++++-- plugins/wazuh-core/server/index.ts | 4 +- .../server/services/enhance-configuration.ts | 39 ++++++++------- plugins/wazuh-core/server/types.ts | 6 +-- 6 files changed, 87 insertions(+), 48 deletions(-) diff --git a/plugins/main/server/lib/reporting/printer.ts b/plugins/main/server/lib/reporting/printer.ts index baa35f94a3..0c21e518b6 100644 --- a/plugins/main/server/lib/reporting/printer.ts +++ b/plugins/main/server/lib/reporting/printer.ts @@ -10,7 +10,7 @@ import { import * as TimSort from 'timsort'; import { REPORTS_PRIMARY_COLOR } from '../../../common/constants'; import { Logger } from 'opensearch-dashboards/server'; -import { IConfiguration } from '../../../../wazuh-core/common/services/configuration'; +import { IConfigurationEnhanced } from '../../../../wazuh-core/server'; const COLORS = { PRIMARY: REPORTS_PRIMARY_COLOR, @@ -129,7 +129,10 @@ const fonts = { export class ReportPrinter { private _content: any[]; private _printer: PdfPrinter; - constructor(public logger: Logger, private configuration: IConfiguration) { + constructor( + public logger: Logger, + private configuration: IConfigurationEnhanced, + ) { this._printer = new PdfPrinter(fonts); this._content = []; } @@ -604,28 +607,33 @@ export class ReportPrinter { async print(reportPath: string) { return new Promise((resolve, reject) => { // Get configuration settings - Promise.all( - [ + this.configuration + .getCustomizationSetting( 'customization.logo.reports', 'customization.reports.header', 'customization.reports.footer', - ].map(key => this.configuration.getCustomizationSetting(key)), - ).then(([pathToLogo, pageHeader, pageFooter]) => { - try { - const document = this._printer.createPdfKitDocument({ - ...pageConfiguration({ pathToLogo, pageHeader, pageFooter }), - content: this._content, - }); - - document.on('error', reject); - document.on('end', resolve); - - document.pipe(fs.createWriteStream(reportPath)); - document.end(); - } catch (error) { - reject(error); - } - }); + ) + .then(configuration => { + try { + const { + 'customization.logo.reports': pathToLogo, + 'customization.reports.header': pageHeader, + 'customization.reports.footer': pageFooter, + } = configuration; + const document = this._printer.createPdfKitDocument({ + ...pageConfiguration({ pathToLogo, pageHeader, pageFooter }), + content: this._content, + }); + + document.on('error', reject); + document.on('end', resolve); + + document.pipe(fs.createWriteStream(reportPath)); + document.end(); + } catch (error) { + reject(error); + } + }); }); } diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 241e436d74..8f68a35cfd 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -477,7 +477,7 @@ export type TPluginSetting = { }; // Default value. defaultValue: any; - // Default value if it is not set. It has preference over `default`. + /* Special: This is used for the settings of customization to get the hidden default value, because the default value is empty to not to be displayed on the App Settings. */ defaultValueIfNotSet?: any; // Configurable from the App Settings app. isConfigurableFromSettings: boolean; diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 7fbb47d836..868503a9a2 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -93,7 +93,7 @@ export type TConfigurationSetting = { type: EpluginSettingType; // Default value. defaultValue: any; - // Default value if it is not set. It has preference over `default`. + /* Special: This is used for the settings of customization to get the hidden default value, because the default value is empty to not to be displayed on the App Settings. */ defaultValueIfNotSet?: any; // Configurable from the configuration file. isConfigurableFromSettings: boolean; @@ -168,6 +168,7 @@ export interface IConfiguration { } >; getSettingValue(settingKey: string, value?: any): any; + getSettingValueIfNotSet(settingKey: string, value?: any): any; } export class Configuration implements IConfiguration { @@ -229,13 +230,13 @@ export class Configuration implements IConfiguration { } /** - * Get the value for a setting from a value or someone of the default values: - * defaultValueIfNotSet or defaultValue + * Special: Get the value for a setting from a value or someone of the default values. This is used for the settings of customization to get the hidden default value, because the default value is empty to not to be displayed on the App Settings + * It retunts defaultValueIfNotSet or defaultValue * @param settingKey * @param value * @returns */ - getSettingValue(settingKey: string, value?: any) { + getSettingValueIfNotSet(settingKey: string, value?: any) { this.logger.debug( `Getting value for [${settingKey}]: stored [${JSON.stringify(value)}]`, ); @@ -255,6 +256,31 @@ export class Configuration implements IConfiguration { ); return finalValue; } + + /** + * Get the value for a setting from a value or someone of the default values: + * It returns defaultValue + * @param settingKey + * @param value + * @returns + */ + getSettingValue(settingKey: string, value?: any) { + this.logger.debug( + `Getting value for [${settingKey}]: stored [${JSON.stringify(value)}]`, + ); + if (!this._settings.has(settingKey)) { + throw new Error(`${settingKey} is not registered`); + } + if (typeof value !== 'undefined') { + return value; + } + const setting = this._settings.get(settingKey); + const finalValue = setting.defaultValue; + this.logger.debug( + `Value for [${settingKey}]: [${JSON.stringify(finalValue)}]`, + ); + return finalValue; + } /** * Get the value for all settings or a subset of them * @param rest diff --git a/plugins/wazuh-core/server/index.ts b/plugins/wazuh-core/server/index.ts index f573e68f89..adf9ef623d 100644 --- a/plugins/wazuh-core/server/index.ts +++ b/plugins/wazuh-core/server/index.ts @@ -1,6 +1,5 @@ import { PluginInitializerContext } from '../../../src/core/server'; import { WazuhCorePlugin } from './plugin'; -import { TypeOf } from '@osd/config-schema'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. @@ -9,4 +8,5 @@ export function plugin(initializerContext: PluginInitializerContext) { return new WazuhCorePlugin(initializerContext); } -export { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; +export type { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; +export type { IConfigurationEnhanced } from './services/enhance-configuration'; diff --git a/plugins/wazuh-core/server/services/enhance-configuration.ts b/plugins/wazuh-core/server/services/enhance-configuration.ts index 32bdf9a858..329ff8fe47 100644 --- a/plugins/wazuh-core/server/services/enhance-configuration.ts +++ b/plugins/wazuh-core/server/services/enhance-configuration.ts @@ -1,7 +1,4 @@ -import { PLUGIN_PLATFORM_NAME } from '../../common/constants'; import { IConfiguration } from '../../common/services/configuration'; -import yml from 'js-yaml'; - /** * Returns the default value if not set when the setting is an empty string * @param settingKey plugin setting @@ -16,16 +13,12 @@ function resolveEmptySetting( return typeof value === 'string' && value.length === 0 && configurationService._settings.get(settingKey).defaultValueIfNotSet - ? configurationService.getSettingValue(settingKey) + ? configurationService.getSettingValueIfNotSet(settingKey) : value; } export interface IConfigurationEnhanced extends IConfiguration { - getCustomizationSetting( - currentConfiguration: { [key: string]: any }, - settingKey: string, - ): any; - importFile(fileContent: string | Buffer): any; + getCustomizationSetting(...settingKeys: string[]): { [key: string]: any }; } function getCustomizationSetting( @@ -33,11 +26,8 @@ function getCustomizationSetting( currentConfiguration: { [key: string]: any }, settingKey: string, ) { - const isCustomizationEnabled = - typeof currentConfiguration['customization.enabled'] === 'undefined' - ? configuration.getSettingValue('customization.enabled') - : currentConfiguration['customization.enabled']; - const defaultValue = configuration.getSettingValue(settingKey); + const isCustomizationEnabled = currentConfiguration['customization.enabled']; + const defaultValue = configuration.getSettingValueIfNotSet(settingKey); if ( isCustomizationEnabled && @@ -57,12 +47,27 @@ function getCustomizationSetting( } export function enhanceConfiguration(configuration: IConfiguration) { - configuration.getCustomizationSetting = async function (settingKey: string) { + /** + * Get the customiztion settings taking into account if this is enabled + * @param settingKeys + * @returns + */ + configuration.getCustomizationSetting = async function ( + ...settingKeys: string[] + ) { + if (!settingKeys.length) { + throw new Error('No settings defined'); + } const currentConfiguration = await this.get( 'customization.enabled', - settingKey, + ...settingKeys, ); - return getCustomizationSetting(this, currentConfiguration, settingKey); + return Object.fromEntries( + settingKeys.map(settingKey => [ + settingKey, + getCustomizationSetting(this, currentConfiguration, settingKey), + ]), + ); }; } diff --git a/plugins/wazuh-core/server/types.ts b/plugins/wazuh-core/server/types.ts index 364bceacc9..1208f548bb 100644 --- a/plugins/wazuh-core/server/types.ts +++ b/plugins/wazuh-core/server/types.ts @@ -6,12 +6,12 @@ import { ServerAPIScopedUserClient, UpdateRegistry, } from './services'; -import { Configuration } from '../common/services/configuration'; +import { IConfigurationEnhanced } from './services/enhance-configuration'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginSetup { dashboardSecurity: ISecurityFactory; - configuration: Configuration; + configuration: IConfigurationEnhanced; manageHosts: ManageHosts; serverAPIClient: ServerAPIClient; updateRegistry: UpdateRegistry; @@ -25,7 +25,7 @@ export interface WazuhCorePluginSetup { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginStart { dashboardSecurity: ISecurityFactory; - configuration: Configuration; + configuration: IConfigurationEnhanced; manageHosts: ManageHosts; serverAPIClient: ServerAPIClient; updateRegistry: UpdateRegistry; From 4fd06a29f2fee02a0964bf634d7bfee67b2ca048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Thu, 4 Apr 2024 14:37:49 +0200 Subject: [PATCH 19/34] fix(configuration): minimize the references to branding on the configuration file --- plugins/wazuh-core/server/services/configuration-store.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index e5104a1b6d..53be5d5d6d 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -8,6 +8,7 @@ import fs from 'fs'; import yml from 'js-yaml'; import { createDataDirectoryIfNotExists } from './filesystem'; import { webDocumentationLink } from '../../common/services/web_documentation'; +import { TPluginSettingWithKey } from '../../common/constants'; interface IConfigurationStoreOptions { cache_seconds: number; @@ -137,7 +138,7 @@ export class ConfigurationStore implements IConfigurationStore { # # Find more information about this on the LICENSE file. # -${printSection('Wazuh app configuration file', { prefix: '# ', fill: '=' })} +${printSection('App configuration file', { prefix: '# ', fill: '=' })} # # Please check the documentation for more information about configuration options: # ${webDocumentationLink('user-manual/wazuh-dashboard/config-file.html')} @@ -145,7 +146,7 @@ ${printSection('Wazuh app configuration file', { prefix: '# ', fill: '=' })} # Also, you can check our repository: # https://github.com/wazuh/wazuh-dashboard-plugins`; - const hostsConfiguration = `${printSection('Wazuh hosts', { + const hostsConfiguration = `${printSection('API connections', { prefix: '# ', fill: '-', })} From 3863e56173458a09c36617ce0e832f8d079b3755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 11:03:16 +0200 Subject: [PATCH 20/34] rollback(administrator): consideration of administrator user - Rollback to the consideration of administrator user based on the administrator API connection role - Create logic to check this consideration to the DashboardSecurity services in frontend and backend side - Add getAccountFromJWTAPIDecodedToken method to frontend side - Adapt the WzAuthentication service to use the getAccountFromJWTAPIDecodedToken - Add a constant that defines the ID for the administrator role of the API connection - Remove previous consideration based on the access to the security REST API of the platform - Remove API endpoint `GET /utils/account` and getPluginScopedAccount controller - Remove fetch user account data from the frontend side - Remove fetchAccount method of DashboardSecurity service on the frontend side - Adapt routeDecoratorProtectedAdministrator route decorator - Enhance the message about the administrator missing requirements - Rename administrator_error_message to administrator_requirements related to the consideration of administrator user --- plugins/main/public/app.js | 8 --- .../common/hooks/use-user-is-admin.ts | 2 +- .../react-services/wz-authentication.ts | 20 +++++- .../public/redux/reducers/appStateReducers.js | 2 +- plugins/main/server/controllers/decorators.ts | 6 +- .../controllers/wazuh-utils/wazuh-utils.ts | 32 --------- .../server/routes/wazuh-utils/wazuh-utils.ts | 10 --- plugins/wazuh-core/common/constants.ts | 3 + .../public/utils/dashboard-security.ts | 27 +++---- .../factories/default-factory.ts | 4 +- .../opensearch-dashboards-security-factory.ts | 8 +-- .../security-factory/security-factory.ts | 71 ++++++++++++++++++- 12 files changed, 110 insertions(+), 83 deletions(-) diff --git a/plugins/main/public/app.js b/plugins/main/public/app.js index bca52fa9ec..35de083e45 100644 --- a/plugins/main/public/app.js +++ b/plugins/main/public/app.js @@ -82,14 +82,6 @@ app.run([ }) .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); 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 index 65c8e98102..525e3a53f1 100644 --- a/plugins/main/public/components/common/hooks/use-user-is-admin.ts +++ b/plugins/main/public/components/common/hooks/use-user-is-admin.ts @@ -3,5 +3,5 @@ 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]; + return [account.administrator_requirements, account]; }; diff --git a/plugins/main/public/react-services/wz-authentication.ts b/plugins/main/public/react-services/wz-authentication.ts index acff170c52..83138bdf7b 100644 --- a/plugins/main/public/react-services/wz-authentication.ts +++ b/plugins/main/public/react-services/wz-authentication.ts @@ -18,9 +18,10 @@ import { updateUserPermissions, updateWithUserLogged, updateAllowedAgents, + updateUserAccount, } from '../redux/actions/appStateActions'; import { UI_LOGGER_LEVELS } from '../../common/constants'; -import { getToasts } from '../kibana-services'; +import { getWazuhCorePlugin } from '../kibana-services'; import { getAuthorizedAgents } from '../react-services/wz-agents'; import { UI_ERROR_SEVERITIES, @@ -91,8 +92,16 @@ export class WzAuthentication { } store.dispatch(updateAllowedAgents(allowedAgents)); - // Dispatch actions to set permissions and roles + // Dispatch actions to set permissions and administrator consideration store.dispatch(updateUserPermissions(userPolicies)); + + store.dispatch( + updateUserAccount( + getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + jwtPayload, + ), + ), + ); store.dispatch(updateWithUserLogged(true)); } catch (error) { const options: UIErrorLog = { @@ -106,6 +115,13 @@ export class WzAuthentication { }, }; getErrorOrchestrator().handleError(options); + store.dispatch( + updateUserAccount( + getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + {}, // This value should cause the user is not considered as an administrator + ), + ), + ); store.dispatch(updateWithUserLogged(true)); return Promise.reject(error); } diff --git a/plugins/main/public/redux/reducers/appStateReducers.js b/plugins/main/public/redux/reducers/appStateReducers.js index cc206672c2..fe583948ff 100644 --- a/plugins/main/public/redux/reducers/appStateReducers.js +++ b/plugins/main/public/redux/reducers/appStateReducers.js @@ -27,7 +27,7 @@ const initialState = { logtestToken: '', userAccount: { administrator: false, - administrator_error_message: '', + administrator_requirements: '', }, }; diff --git a/plugins/main/server/controllers/decorators.ts b/plugins/main/server/controllers/decorators.ts index 8f6aeacada..d96765f8bd 100644 --- a/plugins/main/server/controllers/decorators.ts +++ b/plugins/main/server/controllers/decorators.ts @@ -6,13 +6,13 @@ export function routeDecoratorProtectedAdministrator( ) { return async (context, request, response) => { try { - try { + const { administrator, administrator_requirements } = await context.wazuh_core.dashboardSecurity.isAdministratorUser( context, request, ); - } catch (error) { - return ErrorResponse(error.message, 403, 403, response); + if (!administrator) { + return ErrorResponse(administrator_requirements, 403, 403, response); } return await routeHandler(context, request, response); } catch (error) { diff --git a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts index faa0bced60..671dbc77cf 100644 --- a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts @@ -248,36 +248,4 @@ export class WazuhUtilsCtrl { }, 3023, ); - - /** - * Get the plugin scoped account - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Object} Scoped user account or ErrorResponse - */ - async getPluginScopedAccount( - context: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory, - ) { - try { - await context.wazuh_core.dashboardSecurity.isAdministratorUser( - context, - request, - ); - return response.ok({ - body: { - administrator: true, - }, - }); - } catch (error) { - return response.ok({ - body: { - administrator: false, - administrator_error_message: error.message, - }, - }); - } - } } diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts index e2bc0cce0c..da148433cc 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts @@ -153,14 +153,4 @@ export function WazuhUtilsRoutes(router: IRouter, services) { async (context, request, response) => ctrl.clearConfiguration(context, request, response), ); - - // Get if the current user is an administrator - router.get( - { - path: '/utils/account', - validate: false, - }, - async (context, request, response) => - ctrl.getPluginScopedAccount(context, request, response), - ); } diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 8f68a35cfd..05086b2a20 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -2077,3 +2077,6 @@ export const WAZUH_CORE_ENCRYPTION_PASSWORD = 'secretencryptionkey!'; // Configuration backend service export const WAZUH_CORE_CONFIGURATION_INSTANCE = 'wazuh-dashboard'; export const WAZUH_CORE_CONFIGURATION_CACHE_SECONDS = 10; + +// API connection permissions +export const WAZUH_ROLE_ADMINISTRATOR_ID = 1; diff --git a/plugins/wazuh-core/public/utils/dashboard-security.ts b/plugins/wazuh-core/public/utils/dashboard-security.ts index 3094e1d57f..669ef6fbae 100644 --- a/plugins/wazuh-core/public/utils/dashboard-security.ts +++ b/plugins/wazuh-core/public/utils/dashboard-security.ts @@ -1,4 +1,4 @@ -import { WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY } from '../../common/constants'; +import { WAZUH_ROLE_ADMINISTRATOR_ID } from '../../common/constants'; import { ILogger } from '../../common/services/configuration'; export class DashboardSecurity { @@ -28,20 +28,15 @@ export class DashboardSecurity { } async start() {} async stop() {} - async fetchAccount() { - if ( - this.securityPlatform === - WAZUH_SECURITY_PLUGIN_OPENSEARCH_DASHBOARDS_SECURITY - ) { - try { - this.logger.debug('Fetching the account'); - const response = await this.http.get('/utils/account'); - this.logger.debug(`Fetched account: ${JSON.stringify(response)}`); - return response; - } catch (error) { - this.logger.error(error.message); - throw error; - } - } + getAccountFromJWTAPIDecodedToken(decodedToken: number[]) { + const isAdministrator = decodedToken?.rbac_roles?.some?.( + (role: number) => role === WAZUH_ROLE_ADMINISTRATOR_ID, + ); + return { + administrator: isAdministrator, + administrator_requirements: !isAdministrator + ? 'User has no administrator role in the selected API connection.' + : null, + }; } } diff --git a/plugins/wazuh-core/server/services/security-factory/factories/default-factory.ts b/plugins/wazuh-core/server/services/security-factory/factories/default-factory.ts index 6ac2039984..ee64a80920 100644 --- a/plugins/wazuh-core/server/services/security-factory/factories/default-factory.ts +++ b/plugins/wazuh-core/server/services/security-factory/factories/default-factory.ts @@ -21,5 +21,7 @@ export class DefaultFactory implements ISecurityFactory { async isAdministratorUser( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, - ) {} + ) { + // This is replaced after creating the instance + } } diff --git a/plugins/wazuh-core/server/services/security-factory/factories/opensearch-dashboards-security-factory.ts b/plugins/wazuh-core/server/services/security-factory/factories/opensearch-dashboards-security-factory.ts index 23b8aa0de3..c547e48b70 100644 --- a/plugins/wazuh-core/server/services/security-factory/factories/opensearch-dashboards-security-factory.ts +++ b/plugins/wazuh-core/server/services/security-factory/factories/opensearch-dashboards-security-factory.ts @@ -39,12 +39,6 @@ export class OpenSearchDashboardsSecurityFactory implements ISecurityFactory { context: RequestHandlerContext, request: OpenSearchDashboardsRequest, ) { - const response = await context.security_plugin.esClient - .asScoped(request) - .callAsCurrentUser('opensearch_security.restapiinfo'); - - if (!response.has_api_access) { - throw new Error(`User has no permission for rest API access.`); - } + // This is replaced after creating the instance } } diff --git a/plugins/wazuh-core/server/services/security-factory/security-factory.ts b/plugins/wazuh-core/server/services/security-factory/security-factory.ts index 400de44f12..b2b1c1c695 100644 --- a/plugins/wazuh-core/server/services/security-factory/security-factory.ts +++ b/plugins/wazuh-core/server/services/security-factory/security-factory.ts @@ -7,6 +7,9 @@ import { RequestHandlerContext, } from 'src/core/server'; import { PluginSetup } from '../../types'; +import { getCookieValueByName } from '../cookie'; +import jwtDecode from 'jwt-decode'; +import { WAZUH_ROLE_ADMINISTRATOR_ID } from '../../../common/constants'; type CurrentUser = { username?: string; @@ -27,8 +30,72 @@ export interface ISecurityFactory { export function createDashboardSecurity({ securityDashboards, -}: PluginSetup): Promise<ISecurityFactory> { - return !!securityDashboards +}: PluginSetup): ISecurityFactory { + const dashboardSecurity = !!securityDashboards ? new OpenSearchDashboardsSecurityFactory() : new DefaultFactory(); + + enhanceDashboardSecurity(dashboardSecurity); + return dashboardSecurity; +} + +function enhanceDashboardSecurity(dashboardSecurity) { + dashboardSecurity.isAdministratorUser = async function (context, request) { + try { + // Check if user has administrator role in token + const token = getCookieValueByName(request.headers.cookie, 'wz-token'); + if (!token) { + return { + administrator: false, + administrator_requirements: 'No token provider', + }; + } + const decodedToken = jwtDecode(token); + if (!decodedToken) { + return { + administrator: false, + administrator_requirements: 'No permissions in token', + }; + } + if ( + !decodedToken.rbac_roles || + !decodedToken.rbac_roles.includes(WAZUH_ROLE_ADMINISTRATOR_ID) + ) { + return { + administrator: false, + administrator_requirements: 'No administrator role', + }; + } + // Check the provided token is valid + const apiHostID = getCookieValueByName(request.headers.cookie, 'wz-api'); + if (!apiHostID) { + return { + administrator: false, + administrator_requirements: 'No API id provided', + }; + } + const responseTokenIsWorking = + await context.wazuh_core.api.client.asCurrentUser.request( + 'GET', + '/', + {}, + { apiHostID }, + ); + if (responseTokenIsWorking.status !== 200) { + return { + administrator: false, + administrator_requirements: 'Token is not valid', + }; + } + return { + administrator: true, + administrator_requirements: null, + }; + } catch (error) { + return { + administror: false, + administrator_requirements: `It could not check if the current user is administrator due to: ${error.message}`, + }; + } + }; } From 87ce49cf35cd69dd283f0db82e8c4c59330aae18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 11:12:58 +0200 Subject: [PATCH 21/34] changelog: adapt some entries related to rollback the configuartion store in the backend side --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc03b76dfc..020be1fbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ 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) [#6519](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6519) - Improve fleet management by adding 'Edit Agent Groups' and 'Upgrade Agents' actions, as well as a filter to show only outdated agents [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250) [#6476](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6476) [#6274](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6274) [#6501](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6501) [#6529](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6529) - Added propagation of updates from the table to dashboard visualizations in Endpoints summary [#6460](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6460) - Handle index pattern selector on new discover [#6499](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6499) @@ -20,7 +19,7 @@ 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) [#6434](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6434) - 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 how the configuration is managed in the backend side [#6337](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6337) [#6519](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6519) [#6570](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6570) - 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) @@ -29,6 +28,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Fixed - Fixed the scripted fields disappear when the fields of the events index pattern was refreshed [#6237](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6237) +- Fixed the validation for `enrollment.dns` on App Settings application [#6570](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6570) ## Wazuh v4.8.2 - OpenSearch Dashboards 2.10.0 - Revision 00 From 21ad74fe6ef0522c25835c556dd2b5c02362fa92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 11:44:45 +0200 Subject: [PATCH 22/34] test: fix reporting tests --- .../server/routes/wazuh-reporting.test.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/main/server/routes/wazuh-reporting.test.ts b/plugins/main/server/routes/wazuh-reporting.test.ts index c4dc4f4765..00e5c8c523 100644 --- a/plugins/main/server/routes/wazuh-reporting.test.ts +++ b/plugins/main/server/routes/wazuh-reporting.test.ts @@ -291,12 +291,18 @@ describe('[endpoint] PUT /utils/configuration', () => { context.wazuh_core.configuration.get.mockReturnValueOnce(initialConfig); context.wazuh_core.configuration.getCustomizationSetting.mockImplementation( - setting => { - return ( - afterUpdateConfiguration?.[setting] ?? - SettingsDefinitions?.[setting]?.defaultValueIfNotSet - ); - }, + (...settings) => ({ + then: fn => + fn( + Object.fromEntries( + settings.map(setting => [ + setting, + afterUpdateConfiguration?.[setting] ?? + SettingsDefinitions?.[setting]?.defaultValueIfNotSet, + ]), + ), + ), + }), ); context.wazuh_core.dashboardSecurity.isAdministratorUser.mockImplementation( @@ -325,6 +331,8 @@ describe('[endpoint] PUT /utils/configuration', () => { .send(reportBody); // .expect(200); + console.log({ responseReport }); + const fileName = responseReport.body?.message.match(/([A-Z-0-9]*\.pdf)/gi)[0]; const userPath = md5(USER_NAME); From c812fbae6578059ff16d9bb63fded8d5e9c8500f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 14:42:21 +0200 Subject: [PATCH 23/34] tests: fix of core plugin --- .../wazuh-core/common/plugin-settings.test.ts | 72 +++++++++---------- .../services/enhance-configuration.test.ts | 2 +- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/plugins/wazuh-core/common/plugin-settings.test.ts b/plugins/wazuh-core/common/plugin-settings.test.ts index 96a0994504..9345d688ce 100644 --- a/plugins/wazuh-core/common/plugin-settings.test.ts +++ b/plugins/wazuh-core/common/plugin-settings.test.ts @@ -56,11 +56,11 @@ describe('[settings] Input validation', () => { ${'cron.prefix'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} ${'cron.prefix'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} ${'cron.prefix'} | ${'test*'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.apis'} | ${['test']} | ${undefined} - ${'cron.statistics.apis'} | ${['test ']} | ${'No whitespaces allowed.'} - ${'cron.statistics.apis'} | ${['']} | ${'Value can not be empty.'} - ${'cron.statistics.apis'} | ${['test', 4]} | ${'Value is not a string.'} - ${'cron.statistics.apis'} | ${'test space'} | ${'Value is not a valid list.'} + ${'cron.statistics.apis'} | ${'["test"]'} | ${undefined} + ${'cron.statistics.apis'} | ${'["test "]'} | ${'No whitespaces allowed.'} + ${'cron.statistics.apis'} | ${'[""]'} | ${'Value can not be empty.'} + ${'cron.statistics.apis'} | ${'["test", 4]'} | ${'Value is not a string.'} + ${'cron.statistics.apis'} | ${'test space'} | ${"Value can't be parsed. There is some error."} ${'cron.statistics.apis'} | ${true} | ${'Value is not a valid list.'} ${'cron.statistics.index.creation'} | ${'h'} | ${undefined} ${'cron.statistics.index.creation'} | ${'d'} | ${undefined} @@ -129,29 +129,35 @@ describe('[settings] Input validation', () => { ${'customization.reports.header'} | ${'Testing maximum length of a line of 40 characters\nTest'} | ${'The maximum length of a line is 40 characters.'} ${'enrollment.dns'} | ${'test'} | ${undefined} ${'enrollment.dns'} | ${''} | ${undefined} - ${'enrollment.dns'} | ${'test space'} | ${'No whitespaces allowed.'} + ${'enrollment.dns'} | ${'example.fqdn.valid'} | ${undefined} + ${'enrollment.dns'} | ${'127.0.0.1'} | ${undefined} + ${'enrollment.dns'} | ${'2001:0db8:85a3:0000:0000:8a2e:0370:7334'} | ${undefined} + ${'enrollment.dns'} | ${'2001:db8:85a3::8a2e:370:7334'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'} + ${'enrollment.dns'} | ${'2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'} + ${'enrollment.dns'} | ${'example.'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'} + ${'enrollment.dns'} | ${'127.0.0.1'} | ${undefined} ${'enrollment.password'} | ${'test'} | ${undefined} ${'enrollment.password'} | ${''} | ${'Value can not be empty.'} ${'enrollment.password'} | ${'test space'} | ${undefined} - ${'ip.ignore'} | ${['test']} | ${undefined} - ${'ip.ignore'} | ${['test*']} | ${undefined} - ${'ip.ignore'} | ${['']} | ${'Value can not be empty.'} - ${'ip.ignore'} | ${['test space']} | ${'No whitespaces allowed.'} + ${'ip.ignore'} | ${'["test"]'} | ${undefined} + ${'ip.ignore'} | ${'["test*"]'} | ${undefined} + ${'ip.ignore'} | ${'[""]'} | ${'Value can not be empty.'} + ${'ip.ignore'} | ${'["test space"]'} | ${'No whitespaces allowed.'} ${'ip.ignore'} | ${true} | ${'Value is not a valid list.'} - ${'ip.ignore'} | ${['-test']} | ${"It can't start with: -, _, +, .."} - ${'ip.ignore'} | ${['_test']} | ${"It can't start with: -, _, +, .."} - ${'ip.ignore'} | ${['+test']} | ${"It can't start with: -, _, +, .."} - ${'ip.ignore'} | ${['.test']} | ${"It can't start with: -, _, +, .."} - ${'ip.ignore'} | ${['test\\']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test/']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test?']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test"']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test<']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test>']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test|']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test,']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test#']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'ip.ignore'} | ${['test', 'test#']} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["-test"]'} | ${"It can't start with: -, _, +, .."} + ${'ip.ignore'} | ${'["_test"]'} | ${"It can't start with: -, _, +, .."} + ${'ip.ignore'} | ${'["+test"]'} | ${"It can't start with: -, _, +, .."} + ${'ip.ignore'} | ${'[".test"]'} | ${"It can't start with: -, _, +, .."} + ${'ip.ignore'} | ${'["test\\""]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test/"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test?"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test"\']'} | ${"Value can't be parsed. There is some error."} + ${'ip.ignore'} | ${'["test<"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test>"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test|"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test,"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test#"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + ${'ip.ignore'} | ${'["test", "test#"]'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} ${'ip.selector'} | ${true} | ${undefined} ${'ip.selector'} | ${''} | ${'It should be a boolean. Allowed values: true or false.'} ${'pattern'} | ${'test'} | ${undefined} @@ -231,21 +237,11 @@ describe('[settings] Input validation', () => { ({ setting, value, expectedValidation }) => { // FIXME: use the plugins definition if (setting === 'cron.statistics.interval') { - expect( - validateCronStatisticsInterval( - PLUGIN_SETTINGS[ - setting - ]?.uiFormTransformConfigurationValueToInputValue?.(value) ?? value, - ), - ).toBe(expectedValidation); + expect(validateCronStatisticsInterval(value)).toBe(expectedValidation); } else { - expect( - PLUGIN_SETTINGS[setting].validate( - PLUGIN_SETTINGS[ - setting - ]?.uiFormTransformConfigurationValueToInputValue?.(value) ?? value, - ), - ).toBe(expectedValidation); + expect(PLUGIN_SETTINGS[setting].validateUIForm(value)).toBe( + expectedValidation, + ); } }, ); diff --git a/plugins/wazuh-core/server/services/enhance-configuration.test.ts b/plugins/wazuh-core/server/services/enhance-configuration.test.ts index 44013fd47c..b5c3f60106 100644 --- a/plugins/wazuh-core/server/services/enhance-configuration.test.ts +++ b/plugins/wazuh-core/server/services/enhance-configuration.test.ts @@ -57,7 +57,7 @@ describe('enhanceConfiguration', () => { }); expect( await configuration.getCustomizationSetting('customization.test'), - ).toEqual(expectedSettingValue); + ).toEqual({ 'customization.test': expectedSettingValue }); }, ); }); From 32852375995ee31d4f351cd7644809b8a4eee84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 14:43:59 +0200 Subject: [PATCH 24/34] rollback: remove encryption service --- .../server/services/encryption.test.ts | 38 -------- .../wazuh-core/server/services/encryption.ts | 87 ------------------- 2 files changed, 125 deletions(-) delete mode 100644 plugins/wazuh-core/server/services/encryption.test.ts delete mode 100644 plugins/wazuh-core/server/services/encryption.ts diff --git a/plugins/wazuh-core/server/services/encryption.test.ts b/plugins/wazuh-core/server/services/encryption.test.ts deleted file mode 100644 index 9960224946..0000000000 --- a/plugins/wazuh-core/server/services/encryption.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Encryption } from './encryption'; - -const noop = () => undefined; -const mockLogger = { - debug: noop, - info: noop, - warn: noop, - error: noop, -}; - -describe('Encryption service', () => { - it('ensure the Encryption throws an error when the key is not defined', () => { - expect(() => new Encryption(mockLogger, {})).toThrow('key must be defined'); - }); - - it('ensure the Encryption is created', () => { - expect( - () => new Encryption(mockLogger, { key: 'customPassword' }), - ).not.toThrow(''); - }); -}); - -describe('Encryption service usage', () => { - it.each` - encryptionKey | text | encryptedTextAsHex - ${'key123'} | ${'custom text'} | ${'6b657931323300000000000069fb9170a1c96951031e53b321126b7bde3718119ac3eebcc8a1f2'} - ${'custom key'} | ${'custom text'} | ${'637573746f6d206b65790000c89f1347f06f5b321bbd75450d6148d45492f0298f99232adbc22c'} - ${'custom key'} | ${"[{id: 'default',username:'wazuh-wui',password:'wazuh-wui'}]"} | ${'637573746f6d206b65790000f0910957a5225c221ba3603bf588054a4b535e33cd9d7bb2cd3da87b2a070d9569b929c40998a5e967da1d7138cbff28272c22c95592ce4bf7303fd5d839aa13da076994ae4facab185b4c'} - `('encrypt and decrypt', ({ text, encryptionKey, encryptedTextAsHex }) => { - const encryption = new Encryption(mockLogger, { - key: encryptionKey, - }); - const cypherText = encryption.encrypt(text); - expect(cypherText).toBe(encryptedTextAsHex); - const decypherText = encryption.decrypt(cypherText); - expect(decypherText).toBe(text); - }); -}); diff --git a/plugins/wazuh-core/server/services/encryption.ts b/plugins/wazuh-core/server/services/encryption.ts deleted file mode 100644 index 05c73b00c3..0000000000 --- a/plugins/wazuh-core/server/services/encryption.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ILogger } from '../../common/services/configuration'; -import { Buffer } from 'buffer'; -import crypto from 'crypto'; - -// Encrypt service based on https://stackoverflow.com/questions/6953286/how-to-encrypt-data-that-needs-to-be-decrypted-in-node-js -// algorithm: aes-256-gcm -export class Encryption { - private algorithm: string; - private iv: Uint8Array; - private key: string; - private salt: Uint8Array; - private AUTH_TAG_BYTE_LEN: number = 16; - private SALT_BYTE_LEN: number = 12; - constructor(logger: ILogger, options: { key: string }) { - if (!options.key) { - throw new Error('key must be defined'); - } - this.algorithm = 'aes-256-gcm'; - // This value is generated from the key - this.iv = this.getIV(options.key); - // This value is generated from the key - this.salt = this.getSalt(options.key); - this.key = this.getKeyFromKey(options.key); - } - - /** - * Encrypt a plain text and returns a string encoded as HEX - * @param plainText - * @returns - */ - encrypt(plainText: string): string { - const cipher = crypto.createCipheriv(this.algorithm, this.key, this.iv, { - authTagLength: this.AUTH_TAG_BYTE_LEN, - }); - const buffer = Buffer.concat([ - this.iv, - Buffer.concat([cipher.update(plainText), cipher.final()]), // encrypted message - cipher.getAuthTag(), - ]); - return buffer.toString('hex'); - } - - /** - * Decrypt a HEX string and returns the plain text - * @param ciphertextAsHex - * @returns - */ - decrypt(ciphertextAsHex: string): string { - const ciphertext = Buffer.from(ciphertextAsHex, 'hex'); - const authTag = ciphertext.slice(-this.AUTH_TAG_BYTE_LEN); - const iv = ciphertext.slice(0, this.SALT_BYTE_LEN); - const encryptedMessage = ciphertext.slice( - this.SALT_BYTE_LEN, - -this.AUTH_TAG_BYTE_LEN, - ); - const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv, { - authTagLength: this.AUTH_TAG_BYTE_LEN, - }); - decipher.setAuthTag(authTag); - const buffer = Buffer.concat([ - decipher.update(encryptedMessage), - decipher.final(), - ]); - return buffer.toString('utf8'); - } - - private getKeyFromKey(key: string) { - return crypto.scryptSync(key, this.salt, 32); - } - - private getSalt(key: string): Uint8Array { - return this.str2ArrayBuffer(key).slice(0, this.AUTH_TAG_BYTE_LEN); - } - - private getIV(key: string): Uint8Array { - return this.str2ArrayBuffer(key).slice(0, this.SALT_BYTE_LEN); - } - - private str2ArrayBuffer(str) { - var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char - var bufView = new Uint8Array(buf); - for (var i = 0, strLen = str.length; i < strLen; i++) { - bufView[i] = str.charCodeAt(i); - } - return bufView; - } -} From 6763d121390528bb1456d1a0879d999daf713ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 15:16:12 +0200 Subject: [PATCH 25/34] tests: fix tests --- .../common/services/configuration.test.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/plugins/wazuh-core/common/services/configuration.test.ts b/plugins/wazuh-core/common/services/configuration.test.ts index 727fcfbbb3..e307c3c748 100644 --- a/plugins/wazuh-core/common/services/configuration.test.ts +++ b/plugins/wazuh-core/common/services/configuration.test.ts @@ -117,7 +117,6 @@ const settingsSuite = { 'text2', { type: 'text', - defaultValueIfNotSet: 'defaultValueIfNotSet2', defaultValue: 'defaultValue2', }, ], @@ -149,13 +148,11 @@ describe('Configuration service', () => { ); it.each` - title | settings - ${'get setting defaultValue'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }]} - ${'get setting defaultValueIfNotSet'} | ${[{ key: 'text2', value: 'defaultValueIfNotSet2', store: undefined }]} - ${'get setting stored value from a setting with defaultValueIfNotSet'} | ${[{ key: 'text2', value: 'storedValue2', store: 'storedValue2' }]} - ${'get setting stored value from a setting without defaultValueIfNotSet'} | ${[{ key: 'text1', value: 'storedValue1', store: 'storedValue1' }]} - ${'get multiple settings combining without stored values'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }, { key: 'text2', value: 'defaultValueIfNotSet2', store: undefined }]} - ${'get multiple settings combining stored values and defaults'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }, { key: 'text2', value: 'storedValue', store: 'storedValue' }]} + title | settings + ${'get setting defaultValue1'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }]} + ${'get setting defaultValue2'} | ${[{ key: 'text2', value: 'defaultValue2', store: undefined }]} + ${'get multiple settings combining without stored values'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }, { key: 'text2', value: 'defaultValue2', store: undefined }]} + ${'get multiple settings combining stored values and defaults'} | ${[{ key: 'text1', value: 'defaultValue1', store: undefined }, { key: 'text2', value: 'storedValue', store: 'storedValue' }]} `('$title ', async ({ settings }) => { const configurationStore = createMockConfigurationStore(); const configuration = new Configuration(mockLogger, configurationStore); @@ -199,8 +196,8 @@ describe('Configuration service', () => { store: 'storedValue1', }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - finalValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + finalValue: 'defaultValue2', store: undefined, }]} `( @@ -242,8 +239,8 @@ describe('Configuration service', () => { clear: true, }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - afterSetValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + afterSetValue: 'defaultValue2', afterCleanValue: 'defaultValue1', store: undefined, clear: false, @@ -293,9 +290,9 @@ describe('Configuration service', () => { clear: true, }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - afterSetValue: 'defaultValueIfNotSet2', - afterCleanValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + afterSetValue: 'defaultValue2', + afterCleanValue: 'defaultValue2', store: undefined, clear: false, }]} | ${true} @@ -308,9 +305,9 @@ describe('Configuration service', () => { clear: true, }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - afterSetValue: 'defaultValueIfNotSet2', - afterCleanValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + afterSetValue: 'defaultValue2', + afterCleanValue: 'defaultValue2', store: undefined, clear: false, }]} | ${false} @@ -367,9 +364,9 @@ describe('Configuration service', () => { clear: true, }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - afterSetValue: 'defaultValueIfNotSet2', - afterCleanValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + afterSetValue: 'defaultValue2', + afterCleanValue: 'defaultValue2', store: undefined, clear: false, }]} | ${true} @@ -382,9 +379,9 @@ describe('Configuration service', () => { clear: true, }, { key: 'text2', - initialValue: 'defaultValueIfNotSet2', - afterSetValue: 'defaultValueIfNotSet2', - afterCleanValue: 'defaultValueIfNotSet2', + initialValue: 'defaultValue2', + afterSetValue: 'defaultValue2', + afterCleanValue: 'defaultValue2', store: undefined, clear: false, }]} | ${false} From 7edf984695b731b989d95cbbdd02251e042434ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Fri, 5 Apr 2024 16:52:49 +0200 Subject: [PATCH 26/34] tests: remove references to deprecated validateBackend method of plugin settings --- plugins/main/server/routes/wazuh-reporting.test.ts | 4 ---- plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts | 3 --- 2 files changed, 7 deletions(-) diff --git a/plugins/main/server/routes/wazuh-reporting.test.ts b/plugins/main/server/routes/wazuh-reporting.test.ts index 00e5c8c523..c5ef7494d0 100644 --- a/plugins/main/server/routes/wazuh-reporting.test.ts +++ b/plugins/main/server/routes/wazuh-reporting.test.ts @@ -199,22 +199,18 @@ describe('[endpoint] PUT /utils/configuration', () => { 'customization.enabled': { defaultValueIfNotSet: true, isConfigurableFromSettings: true, - validateBackend: schema => schema.boolean(), }, 'customization.logo.reports': { defaultValueIfNotSet: 'images/logo_reports.png', isConfigurableFromSettings: true, - validateBackend: schema => schema.boolean(), }, 'customization.reports.header': { defaultValueIfNotSet: 'Original header', isConfigurableFromSettings: true, - validateBackend: schema => schema.string(), }, 'customization.reports.footer': { defaultValueIfNotSet: 'Original footer', isConfigurableFromSettings: true, - validateBackend: schema => schema.string(), }, }; beforeAll(() => { diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts index 8f8f7db851..608c59abdb 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.test.ts @@ -121,18 +121,15 @@ describe.skip('[endpoint] PUT /utils/configuration', () => { context.wazuh_core.configuration._settings = new Map(); context.wazuh_core.configuration._settings.set('pattern', { isConfigurableFromSettings: true, - validateBackend: schema => schema.string(), }); context.wazuh_core.configuration._settings.set('hosts', { isConfigurableFromSettings: true, }); context.wazuh_core.configuration._settings.set('timeout', { isConfigurableFromSettings: true, - validateBackend: schema => schema.number(), }); context.wazuh_core.configuration._settings.set('cron.statistics.apis', { isConfigurableFromSettings: true, - validateBackend: schema => schema.arrayOf(schema.string()), }); }); From ccde82efc776d3b0089da62608b214bcbf53a4af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 8 Apr 2024 11:05:28 +0200 Subject: [PATCH 27/34] tests(configuration-store): add tests --- .../server/services/configuration-store.md | 6 +- .../services/configuration-store.test.ts | 127 ++++++++++++++++++ .../server/services/configuration-store.ts | 22 +-- 3 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 plugins/wazuh-core/server/services/configuration-store.test.ts diff --git a/plugins/wazuh-core/server/services/configuration-store.md b/plugins/wazuh-core/server/services/configuration-store.md index 4c30b7de42..891f516b5b 100644 --- a/plugins/wazuh-core/server/services/configuration-store.md +++ b/plugins/wazuh-core/server/services/configuration-store.md @@ -1,7 +1,6 @@ # Description -The ConfigurationStore implementation for the backend side stores the configuration in a saved -object of the platform. +The ConfigurationStore implementation for the backend side stores the configuration in a configuration file. Tasks: @@ -9,4 +8,5 @@ Tasks: - Get and set the value for the saved object: - Get - Set - - Clean + - Delete + - Clear diff --git a/plugins/wazuh-core/server/services/configuration-store.test.ts b/plugins/wazuh-core/server/services/configuration-store.test.ts new file mode 100644 index 0000000000..75f58c5bad --- /dev/null +++ b/plugins/wazuh-core/server/services/configuration-store.test.ts @@ -0,0 +1,127 @@ +import { ConfigurationStore } from './configuration-store'; +import fs from 'fs'; + +function createMockLogger() { + return { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + get: () => createMockLogger(), + }; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe(`[service] ConfigurationStore`, () => { + // Create instance + it.each` + title | file | shouldThrowError + ${'Give an error due to missing parameter of file'} | ${undefined} | ${true} + ${'Give an error due to missing parameter of file'} | ${''} | ${true} + ${'Create instance successful'} | ${'config.yml'} | ${false} + `('$title', ({ file, shouldThrowError }) => { + if (shouldThrowError) { + expect( + () => + new ConfigurationStore(createMockLogger(), { + file, + cache_seconds: 10, + }), + ).toThrow('File is not defined'); + } else { + expect( + () => + new ConfigurationStore(createMockLogger(), { + file, + cache_seconds: 10, + }), + ).not.toThrow(); + } + }); + + // Ensure the configuration file is created + describe('Ensure the file is created', () => { + it.each` + title | file | exist + ${'Ensure the file is created'} | ${'config.yml'} | ${false} + ${'Ensure the file is created. Already exist'} | ${'config.yml'} | ${true} + `('$title', ({ file, exist }) => { + const configurationStore = new ConfigurationStore(createMockLogger(), { + file, + cache_seconds: 10, + }); + + // Mock configuration + configurationStore.setConfiguration({ + groupSettingsByCategory: () => [], + }); + + if (exist) { + fs.writeFileSync(configurationStore.file, '', { encoding: 'utf8' }); + } + expect(fs.existsSync(configurationStore.file)).toBe(exist); + configurationStore.ensureConfigurationFileIsCreated(); + expect(fs.existsSync(configurationStore.file)).toBe(true); + + // Cleaning + if (fs.existsSync(configurationStore.file)) { + fs.unlinkSync(configurationStore.file); + } + }); + }); + + // Update the configuration + describe('Update the configuration', () => { + it.each` + title | updateSettings | prevContent | postContent + ${'Update the configuration'} | ${{ key1: 'value' }} | ${''} | ${'\nkey1: "value"'} + ${'Update the configuration'} | ${{ key1: 'value' }} | ${'key1: default'} | ${'key1: "value"'} + ${'Update the configuration'} | ${{ key1: 1 }} | ${'key1: 0'} | ${'key1: 1'} + ${'Update the configuration'} | ${{ key1: true }} | ${'key1: false'} | ${'key1: true'} + ${'Update the configuration'} | ${{ key1: ['value'] }} | ${'key1: ["default"]'} | ${'key1: ["value"]'} + ${'Update the configuration'} | ${{ key1: 'value' }} | ${'key1: "default"\nkey2: 1'} | ${'key1: "value"\nkey2: 1'} + ${'Update the configuration'} | ${{ key2: 2 }} | ${'key1: "default"\nkey2: 1'} | ${'key1: "default"\nkey2: 2'} + ${'Update the configuration'} | ${{ key1: 'value', key2: 2 }} | ${'key1: "default"\nkey2: 1'} | ${'key1: "value"\nkey2: 2'} + ${'Update the configuration'} | ${{ key1: ['value'] }} | ${'key1: ["default"]\nkey2: 1'} | ${'key1: ["value"]\nkey2: 1'} + ${'Update the configuration'} | ${{ key1: ['value'], key2: false }} | ${'key1: ["default"]\nkey2: true'} | ${'key1: ["value"]\nkey2: false'} + `('$title', async ({ prevContent, postContent, updateSettings }) => { + const configurationStore = new ConfigurationStore(createMockLogger(), { + file: 'config.yml', + cache_seconds: 10, + }); + + // Mock configuration + configurationStore.setConfiguration({ + groupSettingsByCategory: () => [], + _settings: new Map([ + ['key1', { store: { file: { configurable: true } } }], + ['key2', { store: { file: { configurable: true } } }], + ]), + }); + + fs.writeFileSync(configurationStore.file, prevContent, { + encoding: 'utf8', + }); + + await configurationStore.set(updateSettings); + + const content = fs.readFileSync(configurationStore.file, { + encoding: 'utf8', + }); + + expect(content).toBe(postContent); + + // Cleaning + if (fs.existsSync(configurationStore.file)) { + fs.unlinkSync(configurationStore.file); + } + }); + }); +}); diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index 53be5d5d6d..0b5e29763c 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -9,6 +9,7 @@ import yml from 'js-yaml'; import { createDataDirectoryIfNotExists } from './filesystem'; import { webDocumentationLink } from '../../common/services/web_documentation'; import { TPluginSettingWithKey } from '../../common/constants'; +import path from 'path'; interface IConfigurationStoreOptions { cache_seconds: number; @@ -34,8 +35,7 @@ export class ConfigurationStore implements IConfigurationStore { throw error; } - /* The ttl cache is used to support sharing configuration through different instances of the - platfrom */ + /* The in-memory ttl cache is used to reduce the access to the persistence */ this._cache = new CacheTTL<any>(this.logger.get('cache'), { ttlSeconds: options.cache_seconds, }); @@ -58,13 +58,13 @@ export class ConfigurationStore implements IConfigurationStore { } private readConfigurationFile() { const content = this.readContentConfigurationFile(); - const contentAsJSON = yml.load(content); + const contentAsObject = yml.load(content) || {}; // Ensure the contentAsObject is an object this.logger.debug( - `Content file [${this.file}]: ${JSON.stringify(contentAsJSON)}`, + `Content file [${this.file}]: ${JSON.stringify(contentAsObject)}`, ); // Transform value for key in the configuration file return Object.fromEntries( - Object.entries(contentAsJSON).map(([key, value]) => { + Object.entries(contentAsObject).map(([key, value]) => { const setting = this.configuration._settings.get(key); return [key, setting?.store?.file?.transformFrom?.(value) ?? value]; }), @@ -94,8 +94,8 @@ export class ConfigurationStore implements IConfigurationStore { return accum.replace(re, ''); } - const formatedValue = formatSettingValueToFile(value); - const updateSettingEntry = `${key}: ${formatedValue}`; + const formattedValue = formatSettingValueToFile(value); + const updateSettingEntry = `${key}: ${formattedValue}`; return match ? /* Replace the setting if it is defined */ accum.replace(re, `${updateSettingEntry}`) @@ -218,9 +218,11 @@ hosts: } ensureConfigurationFileIsCreated() { try { - this.logger.debug('Ensuring the configuration file is created'); - createDataDirectoryIfNotExists(); - createDataDirectoryIfNotExists('config'); + this.logger.debug( + `Ensuring the configuration file is created [${this.file}]`, + ); + const dirname = path.resolve(path.dirname(this.file)); + createDataDirectoryIfNotExists(dirname); if (!fs.existsSync(this.file)) { this.writeContentConfigurationFile( this.getDefaultConfigurationFileContent(), From e181033731f0fe13f2e3566bf025cf0a49e663d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 8 Apr 2024 11:06:32 +0200 Subject: [PATCH 28/34] remove: deprecated createLogFileIfNotExists method --- plugins/main/server/lib/filesystem.ts | 6 ------ plugins/wazuh-core/server/services/filesystem.ts | 6 ------ 2 files changed, 12 deletions(-) diff --git a/plugins/main/server/lib/filesystem.ts b/plugins/main/server/lib/filesystem.ts index 68fe41c878..23c6720981 100644 --- a/plugins/main/server/lib/filesystem.ts +++ b/plugins/main/server/lib/filesystem.ts @@ -8,12 +8,6 @@ export const createDirectoryIfNotExists = (directory: string): void => { } }; -export const createLogFileIfNotExists = (filePath: string): void => { - if (!fs.existsSync(filePath)) { - fs.closeSync(fs.openSync(filePath, 'w')); - } -}; - export const createDataDirectoryIfNotExists = (directory?: string) => { const absoluteRoute = directory ? path.join(WAZUH_DATA_ABSOLUTE_PATH, directory) diff --git a/plugins/wazuh-core/server/services/filesystem.ts b/plugins/wazuh-core/server/services/filesystem.ts index ab102da9a7..5a885ecc32 100644 --- a/plugins/wazuh-core/server/services/filesystem.ts +++ b/plugins/wazuh-core/server/services/filesystem.ts @@ -8,12 +8,6 @@ export const createDirectoryIfNotExists = (directory: string): void => { } }; -export const createLogFileIfNotExists = (filePath: string): void => { - if (!fs.existsSync(filePath)) { - fs.closeSync(fs.openSync(filePath, 'w')); - } -}; - export const createDataDirectoryIfNotExists = (directory?: string) => { const absoluteRoute = directory ? path.join(WAZUH_DATA_ABSOLUTE_PATH, directory) From a94811a6ac2ba63c8cf42b8b50d6a906a9918c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 8 Apr 2024 11:28:32 +0200 Subject: [PATCH 29/34] fix: comment --- plugins/wazuh-core/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index ba8156442e..f848ee8012 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -61,7 +61,7 @@ export class WazuhCorePlugin this._internal.configurationStore, ); - // Enhance configurationService + // Enhance configuration service enhanceConfiguration(this.services.configuration); // Register the plugin settings From 6965f897705b3a6f00c7a1f65e9e934e7d2657ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 8 Apr 2024 12:43:33 +0200 Subject: [PATCH 30/34] fix(configuration-store): dirname of configuration file --- plugins/wazuh-core/server/services/configuration-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index 0b5e29763c..5c62559794 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -6,7 +6,7 @@ import { import { CacheTTL } from '../../common/services/cache'; import fs from 'fs'; import yml from 'js-yaml'; -import { createDataDirectoryIfNotExists } from './filesystem'; +import { createDirectoryIfNotExists } from './filesystem'; import { webDocumentationLink } from '../../common/services/web_documentation'; import { TPluginSettingWithKey } from '../../common/constants'; import path from 'path'; @@ -222,7 +222,7 @@ hosts: `Ensuring the configuration file is created [${this.file}]`, ); const dirname = path.resolve(path.dirname(this.file)); - createDataDirectoryIfNotExists(dirname); + createDirectoryIfNotExists(dirname); if (!fs.existsSync(this.file)) { this.writeContentConfigurationFile( this.getDefaultConfigurationFileContent(), From 7df5a55c8aeb606079dda154021a595a9024efb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Mon, 8 Apr 2024 12:44:34 +0200 Subject: [PATCH 31/34] rollback(configuration-store): rename logger of ConfigurationStore service --- plugins/wazuh-core/server/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index f848ee8012..4c1587ce50 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -50,7 +50,7 @@ export class WazuhCorePlugin this.services.dashboardSecurity = createDashboardSecurity(plugins); this._internal.configurationStore = new ConfigurationStore( - this.logger.get('configuration-saved-object'), + this.logger.get('configuration-store'), { cache_seconds: WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, file: WAZUH_DATA_CONFIG_APP_PATH, From 4c1645915daf85d112a3d63fcac3a8111884dc19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 9 Apr 2024 13:45:42 +0200 Subject: [PATCH 32/34] rollback(configuration): remove API endpoint POST /utils/configuration/clear - Remove API endpoint POST /utils/configuration/clear - Remove route controller --- .../controllers/wazuh-utils/wazuh-utils.ts | 24 ------------------- .../server/routes/wazuh-utils/wazuh-utils.ts | 10 -------- 2 files changed, 34 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts index 671dbc77cf..7bffe972e3 100644 --- a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts @@ -64,30 +64,6 @@ export class WazuhUtilsCtrl { } } - /** - * Clear the configuration - * @param {Object} context - * @param {Object} request - * @param {Object} response - * @returns {Object} - */ - clearConfiguration = routeDecoratorProtectedAdministrator( - async ( - context: RequestHandlerContext, - request: OpenSearchDashboardsRequest, - response: OpenSearchDashboardsResponseFactory, - ) => { - context.wazuh.logger.debug('Clearing configuration'); - await context.wazuh_core.configuration.clear(); - return response.ok({ - body: { - message: 'Configuration was cleared', - }, - }); - }, - 3020, - ); - /** * Update the configuration * @param {Object} context diff --git a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts index da148433cc..63d8960930 100644 --- a/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/routes/wazuh-utils/wazuh-utils.ts @@ -143,14 +143,4 @@ export function WazuhUtilsRoutes(router: IRouter, services) { async (context, request, response) => ctrl.deleteFile(context, request, response), ); - - // Clear the configuration - router.post( - { - path: '/utils/configuration/clear', - validate: false, - }, - async (context, request, response) => - ctrl.clearConfiguration(context, request, response), - ); } From e85b63b107935fa08b90c7997cb6df3a7d5612e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 9 Apr 2024 14:02:42 +0200 Subject: [PATCH 33/34] fix(configuration): fix error removing customization logos in a short period of time - Add plugin setting category: API_CONNECTION (related to hosts setting) - Replace the category of hosts plugin setting - Refactor the setting.store.file interface: - Remove configurable - Add configurableManaged and defaultBlock - Adapt the plugin settings defitions with these changes - Remove the configuration block related to the API connections from the ConfigurationStore service to the plugin setting definition through the defaultBlock - Enhance logging on ManageHosts --- plugins/wazuh-core/common/constants.ts | 121 ++++++++++++------ .../common/services/configuration.ts | 14 +- .../server/services/configuration-store.ts | 66 +++------- .../server/services/manage-hosts.ts | 6 +- 4 files changed, 115 insertions(+), 92 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index cd48cf2d2c..20a981bbeb 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -388,6 +388,7 @@ export enum SettingCategory { VULNERABILITIES, SECURITY, CUSTOMIZATION, + API_CONNECTION, } type TPluginSettingOptionsTextArea = { @@ -471,7 +472,12 @@ export type TPluginSetting = { // Store store: { file: { - configurable?: boolean; + // Define if the setting is managed by the ConfigurationStore service + configurableManaged?: boolean; + // Define a text to print as the default in the configuration block + defaultBlock?: string; + /* Transform the value defined in the configuration file to be consumed by the Configuration + service */ transformFrom?: (value: any) => any; }; }; @@ -559,6 +565,11 @@ export const PLUGIN_SETTINGS_CATEGORIES: { documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', renderOrder: SettingCategory.CUSTOMIZATION, }, + [SettingCategory.API_CONNECTION]: { + title: 'API connections', + description: 'Options related to the API connections.', + renderOrder: SettingCategory.API_CONNECTION, + }, }; export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { @@ -568,7 +579,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -604,7 +615,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the API health check when opening the app.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -635,7 +646,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the known fields health check when opening the app.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -666,7 +677,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Change the default value of the plugin platform max buckets configuration.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -697,7 +708,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Change the default value of the plugin platform metaField configuration.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -728,7 +739,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the index pattern health check when opening the app.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -759,7 +770,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the setup health check when opening the app.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -790,7 +801,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the template health check when opening the app.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -821,7 +832,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Change the default value of the plugin platform timeFilter configuration.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.HEALTH_CHECK, @@ -851,7 +862,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the index prefix of predefined jobs.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -887,7 +898,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -929,7 +940,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Define the interval in which a new index will be created.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -972,7 +983,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the name of the index in which the documents will be saved.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -1009,7 +1020,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the number of replicas to use for the statistics indices.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -1048,7 +1059,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the number of shards to use for the statistics indices.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -1085,7 +1096,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the frequency of task execution using cron schedule expressions.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -1104,7 +1115,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the statistics tasks.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.STATISTICS, @@ -1134,7 +1145,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Enable or disable the customization.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1165,7 +1176,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: `This logo is used as loading indicator while the user is logging into Wazuh API.`, store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1213,7 +1224,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: `This logo is displayed during the Healthcheck routine of the app.`, store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1261,7 +1272,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 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.`, store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1308,7 +1319,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Set the footer of the reports.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1335,7 +1346,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Set the header of the reports.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.CUSTOMIZATION, @@ -1363,7 +1374,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Specifies the Wazuh registration server, used for the agent enrollment.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -1384,7 +1395,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Specifies the password used to authenticate during the agent enrollment.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -1404,7 +1415,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Hide the alerts of the manager in every dashboard.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -1432,12 +1443,42 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, hosts: { title: 'Server hosts', - description: 'Configure the server hosts.', - category: SettingCategory.GENERAL, + description: 'Configure the API connections.', + category: SettingCategory.API_CONNECTION, type: EpluginSettingType.arrayOf, defaultValue: [], store: { file: { + configurableManaged: false, + defaultBlock: `# The following configuration is the default structure to define a host. +# +# hosts: +# # Host ID / name, +# - env-1: +# # Host URL +# url: https://env-1.example +# # Host / API port +# port: 55000 +# # Host / API username +# username: wazuh-wui +# # Host / API password +# password: wazuh-wui +# # Use RBAC or not. If set to true, the username must be "wazuh-wui". +# run_as: true +# - env-2: +# url: https://env-2.example +# port: 55000 +# username: wazuh-wui +# password: wazuh-wui +# run_as: true + +hosts: + - default: + url: https://localhost + port: 55000 + username: wazuh-wui + password: wazuh-wui + run_as: false`, transformFrom: value => { return value.map(hostData => { const key = Object.keys(hostData)?.[0]; @@ -1580,7 +1621,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Disable certain index pattern names from being available in index pattern selector.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -1637,7 +1678,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.GENERAL, @@ -1666,7 +1707,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Index pattern', store: { file: { - configurable: true, + configurableManaged: true, }, }, description: @@ -1703,7 +1744,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { title: 'Request timeout', store: { file: { - configurable: true, + configurableManaged: true, }, }, description: @@ -1741,7 +1782,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the interval in which a new wazuh-monitoring index will be created.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1784,7 +1825,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Enable or disable the wazuh-monitoring index creation and/or visualization.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1816,7 +1857,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { '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.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1852,7 +1893,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Default index pattern to use for Wazuh monitoring.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1888,7 +1929,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the number of replicas to use for the wazuh-monitoring-* indices.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1925,7 +1966,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { 'Define the number of shards to use for the wazuh-monitoring-* indices.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.MONITORING, @@ -1961,7 +2002,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { description: 'Default index pattern to use for vulnerabilities.', store: { file: { - configurable: true, + configurableManaged: true, }, }, category: SettingCategory.VULNERABILITIES, diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 868503a9a2..fcb642e2bb 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -114,8 +114,13 @@ export type TConfigurationSetting = { | TConfigurationSettingOptionsTextArea; store?: { file: { - configurable?: boolean; - transfromFrom?: (value: any) => any; + // Define if the setting is managed by the ConfigurationStore service + configurableManaged?: boolean; + // Define a text to print as the default in the configuration block + defaultBlock?: string; + /* Transform the value defined in the configuration file to be consumed by the Configuration + service */ + transformFrom?: (value: any) => any; }; }; // Transform the input value. The result is saved in the form global state of Settings/Configuration @@ -287,6 +292,11 @@ export class Configuration implements IConfiguration { * @returns */ async get(...settings: string[]) { + this.logger.debug( + settings.length + ? `Getting settings [${settings.join(',')}]` + : 'Getting settings', + ); const stored = await this.store.get(...settings); this.logger.debug(`configuration stored: ${JSON.stringify({ stored })}`); diff --git a/plugins/wazuh-core/server/services/configuration-store.ts b/plugins/wazuh-core/server/services/configuration-store.ts index 5c62559794..73511a515b 100644 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ b/plugins/wazuh-core/server/services/configuration-store.ts @@ -74,10 +74,10 @@ export class ConfigurationStore implements IConfigurationStore { // Plugin settings configurables in the configuration file. const pluginSettingsConfigurableFile = Object.fromEntries( Object.entries(attributes) - .filter( - ([key]) => - this.configuration._settings.get(key)?.store?.file?.configurable, - ) + .filter(([key]) => { + const setting = this.configuration._settings.get(key); + return setting?.store?.file?.configurableManaged; + }) .map(([key, value]) => [key, value]), ); @@ -146,46 +146,10 @@ ${printSection('App configuration file', { prefix: '# ', fill: '=' })} # Also, you can check our repository: # https://github.com/wazuh/wazuh-dashboard-plugins`; - const hostsConfiguration = `${printSection('API connections', { - prefix: '# ', - fill: '-', - })} -# -# The following configuration is the default structure to define a host. -# -# hosts: -# # Host ID / name, -# - env-1: -# # Host URL -# url: https://env-1.example -# # Host / API port -# port: 55000 -# # Host / API username -# username: wazuh-wui -# # Host / API password -# password: wazuh-wui -# # Use RBAC or not. If set to true, the username must be "wazuh-wui". -# run_as: true -# - env-2: -# url: https://env-2.example -# port: 55000 -# username: wazuh-wui -# password: wazuh-wui -# run_as: true - -hosts: - - default: - url: https://localhost - port: 55000 - username: wazuh-wui - password: wazuh-wui - run_as: false -`; - const pluginSettingsConfigurationFileGroupByCategory = this.configuration.groupSettingsByCategory( null, - setting => setting?.store?.file?.configurable, + setting => setting?.store?.file, ); const pluginSettingsConfiguration = @@ -212,9 +176,7 @@ hosts: }) .join('\n#\n'); - return [header, pluginSettingsConfiguration, hostsConfiguration].join( - '\n#\n', - ); + return `${[header, pluginSettingsConfiguration].join('\n#\n')}\n\n`; } ensureConfigurationFileIsCreated() { try { @@ -265,7 +227,7 @@ hosts: this.logger.debug( `Getting settings: [${JSON.stringify( settings, - )}] with store get options [${storeGetOptions}]`, + )}] with store get options [${JSON.stringify(storeGetOptions)}]`, ); const stored = await this.storeGet(storeGetOptions); return settings.length @@ -308,11 +270,16 @@ hosts: } async clear(...settings: string[]): Promise<any> { try { + this.logger.debug(`Clearing settings: [${settings.join(',')}]`); + const stored = await this.storeGet({ ignoreCache: true }); + const newSettings = { + ...stored, + }; const removedSettings = {}; settings.forEach(setting => { - removedSettings[setting] = null; + removedSettings[setting] = newSettings[setting] = null; }); - await this.storeSet(removedSettings); + await this.storeSet(newSettings); return removedSettings; } catch (error) { const enhancedError = new Error( @@ -371,11 +338,12 @@ export function printSettingValue(value: unknown): any { export function printSetting(setting: TPluginSettingWithKey): string { /* # {setting description} - # {settingKey}: {settingDefaultValue} + # ?{{settingDefaultBlock} || {{settingKey}: {settingDefaultValue}}} */ return [ splitDescription(setting.description), - `# ${setting.key}: ${printSettingValue(setting.defaultValue)}`, + setting?.store?.file?.defaultBlock || + `# ${setting.key}: ${printSettingValue(setting.defaultValue)}`, ].join('\n'); } diff --git a/plugins/wazuh-core/server/services/manage-hosts.ts b/plugins/wazuh-core/server/services/manage-hosts.ts index ceec4acd39..d8c25d43c4 100644 --- a/plugins/wazuh-core/server/services/manage-hosts.ts +++ b/plugins/wazuh-core/server/services/manage-hosts.ts @@ -79,15 +79,19 @@ export class ManageHosts { ? this.logger.debug(`Getting API connection with ID [${hostID}]`) : this.logger.debug('Getting API connections'); const hosts = await this.configuration.get('hosts'); + this.logger.debug(`API connections: [${JSON.stringify(hosts)}]`); if (hostID) { const host = hosts.find(({ id }: { id: string }) => id === hostID); if (host) { + this.logger.debug(`API connection with ID [${hostID}] found`); return this.filterAPIHostData( host, options.excludePassword ? ['password'] : undefined, ); } - throw new Error(`API connection with ID [${hostID}] not found`); + const APIConnectionNotFound = `API connection with ID [${hostID}] not found`; + this.logger.debug(APIConnectionNotFound); + throw new Error(APIConnectionNotFound); } return hosts.map(host => this.filterAPIHostData( From b6f7bfb26e59b1e89f0de9e6cedd9277be836bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= <antonio.gutierrez@wazuh.com> Date: Tue, 9 Apr 2024 14:20:14 +0200 Subject: [PATCH 34/34] tests(configuration-store): adapt to latest changes --- .../server/services/configuration-store.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/wazuh-core/server/services/configuration-store.test.ts b/plugins/wazuh-core/server/services/configuration-store.test.ts index 75f58c5bad..4b8ea8b123 100644 --- a/plugins/wazuh-core/server/services/configuration-store.test.ts +++ b/plugins/wazuh-core/server/services/configuration-store.test.ts @@ -49,10 +49,10 @@ describe(`[service] ConfigurationStore`, () => { // Ensure the configuration file is created describe('Ensure the file is created', () => { it.each` - title | file | exist + title | file | createFile ${'Ensure the file is created'} | ${'config.yml'} | ${false} ${'Ensure the file is created. Already exist'} | ${'config.yml'} | ${true} - `('$title', ({ file, exist }) => { + `('$title', ({ file, createFile }) => { const configurationStore = new ConfigurationStore(createMockLogger(), { file, cache_seconds: 10, @@ -63,10 +63,10 @@ describe(`[service] ConfigurationStore`, () => { groupSettingsByCategory: () => [], }); - if (exist) { + if (createFile) { fs.writeFileSync(configurationStore.file, '', { encoding: 'utf8' }); } - expect(fs.existsSync(configurationStore.file)).toBe(exist); + expect(fs.existsSync(configurationStore.file)).toBe(createFile); configurationStore.ensureConfigurationFileIsCreated(); expect(fs.existsSync(configurationStore.file)).toBe(true); @@ -101,8 +101,8 @@ describe(`[service] ConfigurationStore`, () => { configurationStore.setConfiguration({ groupSettingsByCategory: () => [], _settings: new Map([ - ['key1', { store: { file: { configurable: true } } }], - ['key2', { store: { file: { configurable: true } } }], + ['key1', { store: { file: { configurableManaged: true } } }], + ['key2', { store: { file: { configurableManaged: true } } }], ]), });