diff --git a/docker/osd-dev/config/2.x/osd/opensearch_dashboards.yml b/docker/osd-dev/config/2.x/osd/opensearch_dashboards.yml index d73088264b..eb8d5883dd 100755 --- a/docker/osd-dev/config/2.x/osd/opensearch_dashboards.yml +++ b/docker/osd-dev/config/2.x/osd/opensearch_dashboards.yml @@ -6,17 +6,23 @@ opensearch.ssl.verificationMode: certificate # opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"] # # osd 2.0 -opensearch.requestHeadersAllowlist: ["securitytenant", "Authorization"] +opensearch.requestHeadersAllowlist: ['securitytenant', 'Authorization'] # opensearch_security.multitenancy.enabled: false -opensearch_security.readonly_mode.roles: ["kibana_read_only"] +opensearch_security.readonly_mode.roles: ['kibana_read_only'] server.ssl.enabled: true -server.ssl.key: "/home/node/kbn/certs/osd.key" -server.ssl.certificate: "/home/node/kbn/certs/osd.pem" -opensearch.ssl.certificateAuthorities: ["/home/node/kbn/certs/ca.pem"] +server.ssl.key: '/home/node/kbn/certs/osd.key' +server.ssl.certificate: '/home/node/kbn/certs/osd.pem' +opensearch.ssl.certificateAuthorities: ['/home/node/kbn/certs/ca.pem'] uiSettings.overrides.defaultRoute: /app/wz-home -opensearch.username: "kibanaserver" -opensearch.password: "kibanaserver" +opensearch.username: 'kibanaserver' +opensearch.password: 'kibanaserver' opensearchDashboards.branding: useExpandedHeader: false - +wazuh_core.hosts: + manager: + url: 'https://wazuh.manager' + port: 55000 + username: wazuh-wui + password: MyS3cr37P450r.*- + run_as: false diff --git a/plugins/main/public/plugin.ts b/plugins/main/public/plugin.ts index 87271f0731..3145f2b4e1 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -5,6 +5,9 @@ import { Plugin, PluginInitializerContext, } from 'opensearch_dashboards/public'; +import { Cookies } from 'react-cookie'; +import { euiPaletteColorBlind } from '@elastic/eui'; +import { createHashHistory } from 'history'; import { setDataPlugin, setHttp, @@ -27,7 +30,6 @@ import { setWazuhEnginePlugin, setWazuhFleetPlugin, } from './kibana-services'; -import { validate as validateNodeCronInterval } from 'node-cron'; import { AppPluginStartDependencies, WazuhSetup, @@ -35,53 +37,42 @@ import { WazuhStart, WazuhStartPlugins, } from './types'; -import { Cookies } from 'react-cookie'; import { AppState } from './react-services/app-state'; import { setErrorOrchestrator } from './react-services/common-services'; import { ErrorOrchestratorService } from './react-services/error-orchestrator/error-orchestrator.service'; -import store from './redux/store'; -import { updateAppConfig } from './redux/actions/appConfigActions'; import { initializeInterceptor, unregisterInterceptor, } from './services/request-handler'; import { Applications, Categories } from './utils/applications'; -import { euiPaletteColorBlind } from '@elastic/eui'; import NavigationService from './react-services/navigation-service'; -import { createHashHistory } from 'history'; -import { reportingDefinitions } from './react-services/reporting/reporting-definitions'; export class WazuhPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} + private hideTelemetryBanner?: () => void; + public async setup( core: CoreSetup, plugins: WazuhSetupPlugins, ): Promise { // Get custom logos configuration to start up the app with the correct logos - let logosInitialState = {}; - try { - logosInitialState = await core.http.get(`/api/logos`); - } catch (error) { - console.error('plugin.ts: Error getting logos configuration', error); - } // Redefine the mapKeys method to change the properties sent to euiPaletteColorBlind. // This is a workaround until the issue reported in Opensearch Dashboards is fixed. // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5422 // This should be reomved when the issue is fixed. Probably in OSD 2.12.0 plugins.charts.colors.mappedColors.mapKeys = function ( - keys: Array, + keys: (string | number)[], ) { const configMapping = this.getConfigColorMapping(); const configColors = _.values(configMapping); - const oldColors = _.values(this._oldMap); - let alreadyUsedColors: string[] = []; - const keysToMap: Array = []; + const keysToMap: (string | number)[] = []; + _.each(keys, key => { // If this key is mapped in the config, it's unnecessary to have it mapped here if (configMapping[key as any]) { @@ -90,7 +81,9 @@ export class WazuhPlugin } // If this key is mapped to a color used by the config color mapping, we need to remap it - if (_.includes(configColors, this._mapping[key])) keysToMap.push(key); + if (_.includes(configColors, this._mapping[key])) { + keysToMap.push(key); + } // if key exist in oldMap, move it to mapping if (this._oldMap[key]) { @@ -99,13 +92,16 @@ export class WazuhPlugin } // If this key isn't mapped, we need to map it - if (this.get(key) == null) keysToMap.push(key); + if (this.get(key) === null) { + keysToMap.push(key); + } }); alreadyUsedColors.push(...Object.values(this._mapping)); alreadyUsedColors = alreadyUsedColors.map(color => color.toLocaleLowerCase(), ); + // Choose colors from euiPaletteColorBlind and filter out any already assigned to keys const colorPalette = euiPaletteColorBlind({ rotations: Math.ceil( @@ -120,57 +116,46 @@ export class WazuhPlugin }; // Register the applications - Applications.forEach(app => { + for (const app of Applications) { const { category, id, title, redirectTo, order } = app; + core.application.register({ id, title, order, mount: async (params: AppMountParameters) => { try { - /* Workaround: Redefine the validation functions of cron.statistics.interval setting. - There is an optimization error of the frontend side source code due to some modules can - not be loaded - */ - 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.'; - }); setWzCurrentAppID(id); // Set the dynamic redirection setWzMainParams(redirectTo()); initializeInterceptor(core); - // Update redux app state logos with the custom logos - if (logosInitialState?.logos) { - store.dispatch(updateAppConfig(logosInitialState.logos)); - } // hide the telemetry banner. // Set the flag in the telemetry saved object as the notice was seen and dismissed - this.hideTelemetryBanner && (await this.hideTelemetryBanner()); + if (this.hideTelemetryBanner) { + await this.hideTelemetryBanner(); + } + setScopedHistory(params.history); // This allows you to add the selectors to the navbar setHeaderActionMenuMounter(params.setHeaderActionMenu); NavigationService.getInstance(createHashHistory()); + // Load application bundle const { renderApp } = await import('./application'); + setErrorOrchestrator(ErrorOrchestratorService); setHttp(core.http); setCookies(new Cookies()); + if (!AppState.checkCookies()) { NavigationService.getInstance().reload(); } + params.element.classList.add('dscAppWrapper', 'wz-app'); + const unmount = await renderApp(params); + return () => { unmount(); unregisterInterceptor(); @@ -183,9 +168,11 @@ export class WazuhPlugin ({ id: categoryID }) => categoryID === category, ), }); - }); + } + return {}; } + public start( core: CoreStart, plugins: AppPluginStartDependencies, @@ -194,11 +181,13 @@ export class WazuhPlugin if (plugins.securityOss) { plugins.securityOss.insecureCluster.hideAlert(true); } + if (plugins?.telemetry?.telemetryNotifications?.setOptedInNoticeSeen) { // assign to a method to hide the telemetry banner used when the app is mounted this.hideTelemetryBanner = () => plugins.telemetry.telemetryNotifications.setOptedInNoticeSeen(); } + setCore(core); setPlugins(plugins); setHttp(core.http); @@ -215,6 +204,7 @@ export class WazuhPlugin setWazuhCorePlugin(plugins.wazuhCore); setWazuhEnginePlugin(plugins.wazuhEngine); setWazuhFleetPlugin(plugins.wazuhFleet); + return {}; } } diff --git a/plugins/main/server/plugin.ts b/plugins/main/server/plugin.ts index e37ec01792..6be993f1b7 100644 --- a/plugins/main/server/plugin.ts +++ b/plugins/main/server/plugin.ts @@ -25,18 +25,10 @@ import { PluginInitializerContext, SharedGlobalConfig, } from 'opensearch_dashboards/server'; - +import { first } from 'rxjs/operators'; import { WazuhPluginSetup, WazuhPluginStart, PluginSetup } from './types'; import { setupRoutes } from './routes'; -import { - jobInitializeRun, - jobMonitoringRun, - jobSchedulerRun, - jobQueueRun, - jobMigrationTasksRun, - jobSanitizeUploadedFilesTasksRun, -} from './start'; -import { first } from 'rxjs/operators'; +import { jobInitializeRun, jobQueueRun, jobMigrationTasksRun } from './start'; declare module 'opensearch_dashboards/server' { interface RequestHandlerContext { @@ -82,31 +74,31 @@ export class WazuhPlugin implements Plugin { const serverInfo = core.http.getServerInfo(); - core.http.registerRouteHandlerContext('wazuh', (context, request) => { - return { - // Create a custom logger with a tag composed of HTTP method and path endpoint - logger: this.logger.get( - `${request.route.method.toUpperCase()} ${request.route.path}`, - ), - server: { - info: serverInfo, - }, - plugins, - security: plugins.wazuhCore.dashboardSecurity, - api: context.wazuh_core.api, - }; - }); + core.http.registerRouteHandlerContext('wazuh', (context, request) => ({ + // Create a custom logger with a tag composed of HTTP method and path endpoint + logger: this.logger.get( + `${request.route.method.toUpperCase()} ${request.route.path}`, + ), + server: { + info: serverInfo, + }, + plugins, + security: plugins.wazuhCore.dashboardSecurity, + api: context.wazuh_core.api, + })); // Add custom headers to the responses core.http.registerOnPreResponse((request, response, toolkit) => { const additionalHeaders = { 'x-frame-options': 'sameorigin', }; + return toolkit.next({ headers: additionalHeaders }); }); // Routes const router = core.http.createRouter(); + setupRoutes(router, plugins.wazuhCore); return {}; @@ -117,7 +109,6 @@ export class WazuhPlugin implements Plugin { await this.initializerContext.config.legacy.globalConfig$ .pipe(first()) .toPromise(); - const contextServer = { config: globalConfiguration, }; @@ -134,6 +125,8 @@ export class WazuhPlugin implements Plugin { }); // Sanitize uploaded files tasks + // error: [error][plugins][sanitize-uploaded-files-task][wazuh] sanitize:sanitizeUploadedSVG: Error: Configuration undefined not found + /* jobSanitizeUploadedFilesTasksRun({ core, wazuh: { @@ -143,6 +136,7 @@ export class WazuhPlugin implements Plugin { wazuh_core: plugins.wazuhCore, server: contextServer, }); + */ // Migration tasks jobMigrationTasksRun({ @@ -155,7 +149,7 @@ export class WazuhPlugin implements Plugin { server: contextServer, }); - // Monitoring + /* Monitoring jobMonitoringRun({ core, wazuh: { @@ -165,8 +159,9 @@ export class WazuhPlugin implements Plugin { wazuh_core: plugins.wazuhCore, server: contextServer, }); + */ - // Scheduler + /* Scheduler jobSchedulerRun({ core, wazuh: { @@ -176,6 +171,7 @@ export class WazuhPlugin implements Plugin { wazuh_core: plugins.wazuhCore, server: contextServer, }); + */ // Queue jobQueueRun({ @@ -187,6 +183,7 @@ export class WazuhPlugin implements Plugin { wazuh_core: plugins.wazuhCore, server: contextServer, }); + return {}; } diff --git a/plugins/main/server/routes/wazuh-api.ts b/plugins/main/server/routes/wazuh-api.ts index 4130331b01..0fd86dbb46 100644 --- a/plugins/main/server/routes/wazuh-api.ts +++ b/plugins/main/server/routes/wazuh-api.ts @@ -1,6 +1,6 @@ import { IRouter } from 'opensearch_dashboards/server'; -import { WazuhApiCtrl } from '../controllers'; import { schema } from '@osd/config-schema'; +import { WazuhApiCtrl } from '../controllers'; export function WazuhApiRoutes(router: IRouter) { const ctrl = new WazuhApiCtrl(); @@ -28,11 +28,11 @@ export function WazuhApiRoutes(router: IRouter) { validate: { body: schema.any({ // TODO: not ready - //id: schema.string(), + // id: schema.string(), // url: schema.string(), // port: schema.number(), // username: schema.string(), - //forceRefresh: schema.boolean({defaultValue:false}), + // forceRefresh: schema.boolean({defaultValue:false}), // cluster_info: schema.object({ // status: schema.string(), // manager: schema.string(), @@ -128,7 +128,8 @@ export function WazuhApiRoutes(router: IRouter) { ctrl.getSyscollector(context, request, response), ); - // Return app logos configuration + /* Return app logos configuration + ToDo: Change (maybe) to get the opensearch logo settings router.get( { path: '/api/logos', @@ -138,6 +139,7 @@ export function WazuhApiRoutes(router: IRouter) { async (context, request, response) => ctrl.getAppLogos(context, request, response), ); + */ // Return binary dashboard router.get( diff --git a/plugins/main/server/start/index.ts b/plugins/main/server/start/index.ts index 9c68b84340..435e61ea22 100644 --- a/plugins/main/server/start/index.ts +++ b/plugins/main/server/start/index.ts @@ -1,6 +1,5 @@ export * from './cron-scheduler'; export * from './initialize'; -export * from './monitoring'; export * from './queue'; export * from './tryCatchForIndexPermissionError'; export * from './migration-tasks'; diff --git a/plugins/main/server/start/monitoring/index.ts b/plugins/main/server/start/monitoring/index.ts deleted file mode 100644 index 44d6886469..0000000000 --- a/plugins/main/server/start/monitoring/index.ts +++ /dev/null @@ -1,530 +0,0 @@ -/* - * Wazuh app - Module for agent info fetching functions - * 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 cron from 'node-cron'; -import { monitoringTemplate } from '../../integration-files/monitoring-template'; -import { parseCron } from '../../lib/parse-cron'; -import { indexDate } from '../../lib/index-date'; -import { - WAZUH_MONITORING_DEFAULT_CRON_FREQ, - WAZUH_MONITORING_TEMPLATE_NAME, -} from '../../../common/constants'; -import { tryCatchForIndexPermissionError } from '../tryCatchForIndexPermissionError'; -import { delayAsPromise } from '../../../common/utils'; - -let MONITORING_ENABLED, - MONITORING_FREQUENCY, - MONITORING_CRON_FREQ, - MONITORING_CREATION, - MONITORING_INDEX_PATTERN, - MONITORING_INDEX_PREFIX; - -/** - * Set the monitoring variables - * @param context - */ -async function initMonitoringConfiguration(context) { - try { - context.wazuh.logger.debug('Reading configuration'); - const appConfig = await context.wazuh_core.configuration.get(); - MONITORING_ENABLED = - (appConfig['wazuh.monitoring.enabled'] && - appConfig['wazuh.monitoring.enabled'] !== 'worker') || - appConfig['wazuh.monitoring.enabled']; - MONITORING_FREQUENCY = appConfig['wazuh.monitoring.frequency']; - try { - MONITORING_CRON_FREQ = parseCron(MONITORING_FREQUENCY); - } catch (error) { - context.wazuh.logger.warn( - `Using default value ${WAZUH_MONITORING_DEFAULT_CRON_FREQ} due to: ${ - error.message || error - }`, - ); - MONITORING_CRON_FREQ = WAZUH_MONITORING_DEFAULT_CRON_FREQ; - } - MONITORING_CREATION = appConfig['wazuh.monitoring.creation']; - - MONITORING_INDEX_PATTERN = appConfig['wazuh.monitoring.pattern']; - - const lastCharIndexPattern = - MONITORING_INDEX_PATTERN[MONITORING_INDEX_PATTERN.length - 1]; - if (lastCharIndexPattern !== '*') { - MONITORING_INDEX_PATTERN += '*'; - } - MONITORING_INDEX_PREFIX = MONITORING_INDEX_PATTERN.slice( - 0, - MONITORING_INDEX_PATTERN.length - 1, - ); - - context.wazuh.logger.debug( - `wazuh.monitoring.enabled: ${MONITORING_ENABLED}`, - ); - - context.wazuh.logger.debug( - `wazuh.monitoring.frequency: ${MONITORING_FREQUENCY} (${MONITORING_CRON_FREQ})`, - ); - - context.wazuh.logger.debug( - `wazuh.monitoring.creation: ${MONITORING_CREATION}`, - ); - - context.wazuh.logger.debug( - `wazuh.monitoring.pattern: ${MONITORING_INDEX_PATTERN} (index prefix: ${MONITORING_INDEX_PREFIX})`, - ); - } catch (error) { - context.wazuh.logger.error(error.message); - } -} - -/** - * Main. First execution when installing / loading App. - * @param context - */ -async function init(context) { - try { - if (MONITORING_ENABLED) { - await checkTemplate(context); - } - } catch (error) { - const errorMessage = error.message || error; - context.wazuh.logger.error(errorMessage); - } -} - -/** - * Verify wazuh-agent template - */ -async function checkTemplate(context) { - try { - try { - context.wazuh.logger.debug( - `Getting the ${WAZUH_MONITORING_TEMPLATE_NAME} template`, - ); - // Check if the template already exists - const currentTemplate = - await context.core.opensearch.client.asInternalUser.indices.getTemplate( - { - name: WAZUH_MONITORING_TEMPLATE_NAME, - }, - ); - // Copy already created index patterns - monitoringTemplate.index_patterns = - currentTemplate.body[WAZUH_MONITORING_TEMPLATE_NAME].index_patterns; - } catch (error) { - // Init with the default index pattern - monitoringTemplate.index_patterns = [ - await context.wazuh_core.configuration.get('wazuh.monitoring.pattern'), - ]; - } - - // Check if the user is using a custom pattern and add it to the template if it does - if (!monitoringTemplate.index_patterns.includes(MONITORING_INDEX_PATTERN)) { - monitoringTemplate.index_patterns.push(MONITORING_INDEX_PATTERN); - } - - // Update the monitoring template - context.wazuh.logger.debug( - `Updating the ${WAZUH_MONITORING_TEMPLATE_NAME} template`, - ); - await context.core.opensearch.client.asInternalUser.indices.putTemplate({ - name: WAZUH_MONITORING_TEMPLATE_NAME, - body: monitoringTemplate, - }); - context.wazuh.logger.info( - `Updated the ${WAZUH_MONITORING_TEMPLATE_NAME} template`, - ); - } catch (error) { - const errorMessage = `Something went wrong updating the ${WAZUH_MONITORING_TEMPLATE_NAME} template ${ - error.message || error - }`; - context.wazuh.logger.error(errorMessage); - throw error; - } -} - -/** - * Save agent status into elasticsearch, create index and/or insert document - * @param {*} context - * @param {*} data - */ -async function insertMonitoringDataElasticsearch(context, data) { - const monitoringIndexName = - MONITORING_INDEX_PREFIX + indexDate(MONITORING_CREATION); - if (!MONITORING_ENABLED) { - return; - } - try { - await tryCatchForIndexPermissionError(monitoringIndexName)(async () => { - context.wazuh.logger.debug( - `Checking the existence of ${monitoringIndexName} index`, - ); - const exists = - await context.core.opensearch.client.asInternalUser.indices.exists({ - index: monitoringIndexName, - }); - if (!exists.body) { - context.wazuh.logger.debug( - `The ${monitoringIndexName} index does not exist`, - ); - await createIndex(context, monitoringIndexName); - } else { - context.wazuh.logger.debug(`The ${monitoringIndexName} index exists`); - } - - // Update the index configuration - const appConfig = await context.wazuh_core.configuration.get( - 'wazuh.monitoring.shards', - 'wazuh.monitoring.replicas', - ); - - const indexConfiguration = { - settings: { - index: { - number_of_shards: appConfig['wazuh.monitoring.shards'], - number_of_replicas: appConfig['wazuh.monitoring.replicas'], - }, - }, - }; - - // To update the index settings with this client is required close the index, update the settings and open it - // Number of shards is not dynamic so delete that setting if it's given - delete indexConfiguration.settings.index.number_of_shards; - context.wazuh.logger.debug( - `Adding settings to ${monitoringIndexName} index`, - ); - await context.core.opensearch.client.asInternalUser.indices.putSettings({ - index: monitoringIndexName, - body: indexConfiguration, - }); - - context.wazuh.logger.info( - `Settings added to ${monitoringIndexName} index`, - ); - - // Insert data to the monitoring index - await insertDataToIndex(context, monitoringIndexName, data); - })(); - } catch (error) { - context.wazuh.logger.error(error.message || error); - } -} - -/** - * Inserting one document per agent into Elastic. Bulk. - * @param {*} context Endpoint - * @param {String} indexName The name for the index (e.g. daily: wazuh-monitoring-YYYY.MM.DD) - * @param {*} data - */ -async function insertDataToIndex( - context, - indexName: string, - data: { agents: any[]; apiHost }, -) { - const { agents, apiHost } = data; - try { - if (agents.length > 0) { - context.wazuh.logger.debug( - `Bulk data to index ${indexName} for ${agents.length} agents`, - ); - - const bodyBulk = agents - .map(agent => { - const agentInfo = { ...agent }; - agentInfo['timestamp'] = new Date(Date.now()).toISOString(); - agentInfo.host = agent.manager; - agentInfo.cluster = { - name: apiHost.clusterName ? apiHost.clusterName : 'disabled', - }; - return `{ "index": { "_index": "${indexName}" } }\n${JSON.stringify( - agentInfo, - )}\n`; - }) - .join(''); - - await context.core.opensearch.client.asInternalUser.bulk({ - index: indexName, - body: bodyBulk, - }); - context.wazuh.logger.info( - `Bulk data to index ${indexName} for ${agents.length} agents completed`, - ); - } - } catch (error) { - context.wazuh.logger.error( - `Error inserting agent data into elasticsearch. Bulk request failed due to ${ - error.message || error - }`, - ); - } -} - -/** - * Create the wazuh-monitoring index - * @param {*} context context - * @param {String} indexName The name for the index (e.g. daily: wazuh-monitoring-YYYY.MM.DD) - */ -async function createIndex(context, indexName: string) { - try { - if (!MONITORING_ENABLED) return; - const appConfig = await context.wazuh_core.configuration.get( - 'wazuh.monitoring.shards', - 'wazuh.monitoring.replicas', - ); - - const IndexConfiguration = { - settings: { - index: { - number_of_shards: appConfig['wazuh.monitoring.shards'], - number_of_replicas: appConfig['wazuh.monitoring.replicas'], - }, - }, - }; - - context.wazuh.logger.debug(`Creating ${indexName} index`); - - await context.core.opensearch.client.asInternalUser.indices.create({ - index: indexName, - body: IndexConfiguration, - }); - - context.wazuh.logger.info(`${indexName} index created`); - } catch (error) { - context.wazuh.logger.error( - `Could not create ${indexName} index: ${error.message || error}`, - ); - } -} - -/** - * Wait until Kibana server is ready - */ -async function checkPluginPlatformStatus(context) { - try { - context.wazuh.logger.debug('Waiting for platform servers to be ready...'); - - await checkElasticsearchServer(context); - await init(context); - } catch (error) { - context.wazuh.logger.error(error.message || error); - try { - await delayAsPromise(3000); - await checkPluginPlatformStatus(context); - } catch (error) {} - } -} - -/** - * Check Elasticsearch Server status and Kibana index presence - */ -async function checkElasticsearchServer(context) { - try { - context.wazuh.logger.debug( - `Checking the existence of ${context.server.config.opensearchDashboards.index} index`, - ); - const data = - await context.core.opensearch.client.asInternalUser.indices.exists({ - index: context.server.config.opensearchDashboards.index, - }); - - return data.body; - // TODO: check if Elasticsearch can receive requests - // if (data) { - // const pluginsData = await this.server.plugins.elasticsearch.waitUntilReady(); - // return pluginsData; - // } - return Promise.reject(data); - } catch (error) { - context.wazuh.logger.error(error.message || error); - return Promise.reject(error); - } -} - -/** - * Task used by the cron job. - */ -async function cronTask(context) { - try { - const templateMonitoring = - await context.core.opensearch.client.asInternalUser.indices.getTemplate({ - name: WAZUH_MONITORING_TEMPLATE_NAME, - }); - - const apiHosts = await context.wazuh_core.manageHosts.getEntries({ - excludePassword: true, - }); - - if (!apiHosts.length) { - context.wazuh.logger.warn('There are no API host entries. Skip.'); - return; - } - const apiHostsUnique = (apiHosts || []).filter( - (apiHost, index, self) => - index === - self.findIndex( - t => - t.user === apiHost.user && - t.password === apiHost.password && - t.url === apiHost.url && - t.port === apiHost.port, - ), - ); - for (let apiHost of apiHostsUnique) { - try { - const { agents, apiHost: host } = await getApiInfo(context, apiHost); - await insertMonitoringDataElasticsearch(context, { - agents, - apiHost: host, - }); - } catch (error) {} - } - } catch (error) { - // Retry to call itself again if Kibana index is not ready yet - // try { - // if ( - // this.wzWrapper.buildingKibanaIndex || - // ((error || {}).status === 404 && - // (error || {}).displayName === 'NotFound') - // ) { - // await delayAsPromise(1000); - // return cronTask(context); - // } - // } catch (error) {} //eslint-disable-line - context.wazuh.logger.error(error.message || error); - } -} - -/** - * Get API and agents info - * @param context - * @param apiHost - */ -async function getApiInfo(context, apiHost) { - try { - context.wazuh.logger.debug(`Getting API info for ${apiHost.id}`); - const responseIsCluster = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - '/cluster/status', - {}, - { apiHostID: apiHost.id }, - ); - const isCluster = - (((responseIsCluster || {}).data || {}).data || {}).enabled === 'yes'; - if (isCluster) { - const responseClusterInfo = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - `/cluster/local/info`, - {}, - { apiHostID: apiHost.id }, - ); - apiHost.clusterName = - responseClusterInfo.data.data.affected_items[0].cluster; - } - const agents = await fetchAllAgentsFromApiHost(context, apiHost); - return { agents, apiHost }; - } catch (error) { - context.wazuh.logger.error(error.message || error); - throw error; - } -} - -/** - * Fetch all agents for the API provided - * @param context - * @param apiHost - */ -async function fetchAllAgentsFromApiHost(context, apiHost) { - let agents = []; - try { - context.wazuh.logger.debug(`Getting all agents from ApiID: ${apiHost.id}`); - const responseAgentsCount = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - '/agents', - { - params: { - offset: 0, - limit: 1, - q: 'id!=000', - }, - }, - { apiHostID: apiHost.id }, - ); - - const agentsCount = responseAgentsCount.data.data.total_affected_items; - context.wazuh.logger.debug( - `ApiID: ${apiHost.id}, Agent count: ${agentsCount}`, - ); - - let payload = { - offset: 0, - limit: 500, - q: 'id!=000', - }; - - while (agents.length < agentsCount && payload.offset < agentsCount) { - try { - /* - TODO: Improve the performance of request with: - - Reduce the number of requests to the Wazuh API - - Reduce (if possible) the quantity of data to index by document - - Requirements: - - Research about the neccesary data to index. - - How to do: - - Wazuh API request: - - select the required data to retrieve depending on is required to index (using the `select` query param) - - increase the limit of results to retrieve (currently, the requests use the recommended value: 500). - See the allowed values. This depends on the selected data because the response could fail if contains a lot of data - */ - const responseAgents = - await context.wazuh.api.client.asInternalUser.request( - 'GET', - `/agents`, - { params: payload }, - { apiHostID: apiHost.id }, - ); - agents = [...agents, ...responseAgents.data.data.affected_items]; - payload.offset += payload.limit; - } catch (error) { - context.wazuh.logger.error( - `ApiID: ${apiHost.id}, Error request with offset/limit ${ - payload.offset - }/${payload.limit}: ${error.message || error}`, - ); - } - } - return agents; - } catch (error) { - context.wazuh.logger.error( - `ApiID: ${apiHost.id}. Error: ${error.message || error}`, - ); - throw error; - } -} - -/** - * Start the cron job - */ -export async function jobMonitoringRun(context) { - context.wazuh.logger.debug('Task:Monitoring initializing'); - // Init the monitoring variables - await initMonitoringConfiguration(context); - // Check Kibana index and if it is prepared, start the initialization of Wazuh App. - await checkPluginPlatformStatus(context); - // // Run the cron job only it it's enabled - if (MONITORING_ENABLED) { - cronTask(context); - cron.schedule(MONITORING_CRON_FREQ, () => cronTask(context)); - } -} diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 1950b1ec9c..af0f9feb0b 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -391,6 +391,11 @@ export const NOT_TIME_FIELD_NAME_INDEX_PATTERN = // Customization export const CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES = 1048576; +export enum EConfigurationProviders { + INITIALIZER_CONTEXT = 'initializerContext', + PLUGIN_UI_SETTINGS = 'uiSettings', +} + // Plugin settings export enum SettingCategory { GENERAL, @@ -403,27 +408,23 @@ export enum SettingCategory { API_CONNECTION, } -interface TPluginSettingOptionsArrayOf { - arrayOf: Record; -} - -interface TPluginSettingOptionsTextArea { +export interface TPluginSettingOptionsTextArea { maxRows?: number; minRows?: number; maxLength?: number; } -interface TPluginSettingOptionsSelect { +export interface TPluginSettingOptionsSelect { select: { text: string; value: any }[]; } -interface TPluginSettingOptionsEditor { +export interface TPluginSettingOptionsEditor { editor: { language: string; }; } -interface TPluginSettingOptionsFile { +export interface TPluginSettingOptionsFile { file: { type: 'image'; extensions?: string[]; @@ -446,7 +447,7 @@ interface TPluginSettingOptionsFile { }; } -interface TPluginSettingOptionsNumber { +export interface TPluginSettingOptionsNumber { number: { min?: number; max?: number; @@ -454,7 +455,7 @@ interface TPluginSettingOptionsNumber { }; } -interface TPluginSettingOptionsSwitch { +export interface TPluginSettingOptionsSwitch { switch: { values: { disabled: { label?: string; value: any }; @@ -474,8 +475,27 @@ export enum EpluginSettingType { password = 'password', arrayOf = 'arrayOf', custom = 'custom', + objectOf = 'objectOf', +} + +export interface TPluginSettingOptionsObjectOf { + /* eslint-disable no-use-before-define */ + objectOf: Record; } +interface TPluginSettingOptionsArrayOf { + arrayOf: TPluginSetting; +} + +type TPlugingSettingOptions = + | TPluginSettingOptionsTextArea + | TPluginSettingOptionsSelect + | TPluginSettingOptionsEditor + | TPluginSettingOptionsFile + | TPluginSettingOptionsNumber + | TPluginSettingOptionsSwitch + | TPluginSettingOptionsObjectOf + | TPluginSettingOptionsArrayOf; export interface TPluginSetting { // Define the text displayed in the UI. title: string; @@ -485,68 +505,22 @@ export interface TPluginSetting { category: SettingCategory; // Type. type: EpluginSettingType; - // Store - store: { - file: { - // 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; - }; - }; + source: EConfigurationProviders; + options?: TPlugingSettingOptions; // Default value. defaultValue: any; - /* 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; - // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean; - // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean; - // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean; - // Define options related to the `type`. - options?: - | TPluginSettingOptionsEditor - | TPluginSettingOptionsFile - | TPluginSettingOptionsNumber - | TPluginSettingOptionsSelect - | TPluginSettingOptionsSwitch - | TPluginSettingOptionsTextArea - | TPluginSettingOptionsArrayOf; - // Transform the input value. The result is saved in the form global state of Settings/Configuration - uiFormTransformChangedInputValue?: (value: any) => any; - // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any; - // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any; - // Validate the value in the form of Settings/Configuration. It returns a string if there is some validation error. - validateUIForm?: (value: any) => string | undefined; - // Validate function creator to validate the setting in the backend. - validate?: (value: unknown) => string | undefined; + validate?: (value: any) => string | undefined; } + export const PLUGIN_SETTINGS: Record = { 'alerts.sample.prefix': { title: 'Sample alerts prefix', description: 'Define the index name prefix of sample alerts. It must match the template used by the index pattern to avoid unknown fields in dashboards.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.text, 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, @@ -567,407 +541,23 @@ export const PLUGIN_SETTINGS: Record = { ), ), }, - 'checks.api': { - title: 'API connection', - description: 'Enable or disable the API health check when opening the app.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.fields': { - title: 'Known fields', - description: - 'Enable or disable the known fields health check when opening the app.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.maxBuckets': { - title: 'Set max buckets to 200000', - description: - 'Change the default value of the plugin platform max buckets configuration.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.metaFields': { - title: 'Remove meta fields', - description: - 'Change the default value of the plugin platform metaField configuration.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.pattern': { - title: 'Index pattern', - description: - 'Enable or disable the index pattern health check when opening the app.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.setup': { - title: 'API version', - description: - 'Enable or disable the setup health check when opening the app.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.template': { - title: 'Index template', - description: - 'Enable or disable the template health check when opening the app.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'checks.timeFilter': { - title: 'Set time filter to 24h', - description: - 'Change the default value of the plugin platform timeFilter configuration.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.HEALTH_CHECK, - type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, 'configuration.ui_api_editable': { title: 'Configuration UI editable', description: 'Enable or disable the ability to edit the configuration from UI or API endpoints. When disabled, this can only be edited from the configuration file, the related API endpoints are disabled, and the UI is inaccessible.', - store: { - file: { - configurableManaged: false, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: true, - isConfigurableFromSettings: false, - requiresRestartingPluginPlatform: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, + validate: SettingsValidator.isBoolean, }, 'cron.prefix': { title: 'Cron prefix', description: 'Define the index prefix of predefined jobs.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, 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('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - '*', - ), - ), - }, - 'cron.statistics.apis': { - title: 'Includes APIs', - description: - 'Enter the ID of the hosts you want to save data from, leave this empty to run the task on every host.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.editor, - defaultValue: [], - isConfigurableFromSettings: true, - options: { - editor: { - language: 'json', - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: any): any { - return JSON.stringify(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): any { - try { - return JSON.parse(value); - } catch { - return value; - } - }, - validateUIForm: function (value) { - return SettingsValidator.json( - this.validate as (value: unknown) => string | undefined, - )(value); - }, - validate: SettingsValidator.compose( - SettingsValidator.array( - SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - ), - ), - ), - }, - 'cron.statistics.index.creation': { - title: 'Index creation', - description: 'Define the interval in which a new index will be created.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.select, - options: { - select: [ - { - text: 'Hourly', - value: 'h', - }, - { - text: 'Daily', - value: 'd', - }, - { - text: 'Weekly', - value: 'w', - }, - { - text: 'Monthly', - value: 'm', - }, - ], - }, - defaultValue: WAZUH_STATISTICS_DEFAULT_CREATION, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: function (value) { - return SettingsValidator.literal( - this.options?.select.map(({ value }) => value), - )(value); - }, - }, - 'cron.statistics.index.name': { - title: 'Index name', - description: - 'Define the name of the index in which the documents will be saved.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.text, - 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, @@ -987,358 +577,23 @@ export const PLUGIN_SETTINGS: Record = { ), ), }, - 'cron.statistics.index.replicas': { - title: 'Index replicas', - description: - 'Define the number of replicas to use for the statistics indices.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.number, - defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_REPLICAS, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 0, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, - }, - 'cron.statistics.index.shards': { - title: 'Index shards', - description: - 'Define the number of shards to use for the statistics indices.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.number, - defaultValue: WAZUH_STATISTICS_DEFAULT_INDICES_SHARDS, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 1, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, - }, - 'cron.statistics.interval': { - title: 'Interval', - description: - 'Define the frequency of task execution using cron schedule expressions.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.text, - defaultValue: WAZUH_STATISTICS_DEFAULT_CRON_FREQ, - 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. - // validateUIForm: function (value) { - // }, - // validate: function (value) { - // }, - }, - 'cron.statistics.status': { - title: 'Status', - description: 'Enable or disable the statistics tasks.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.STATISTICS, - type: EpluginSettingType.switch, - defaultValue: WAZUH_STATISTICS_DEFAULT_STATUS, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, 'customization.enabled': { title: 'Status', description: 'Enable or disable the customization.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, + defaultValue: false, category: SettingCategory.CUSTOMIZATION, type: EpluginSettingType.switch, - defaultValue: true, - isConfigurableFromSettings: true, - requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'customization.logo.app': { - title: 'App main logo', - description: `This logo is used as loading indicator while the user is logging into Wazuh API.`, - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - isConfigurableFromSettings: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.app', - resolveStaticURL: (filename: string) => - `custom/images/${filename}?v=${Date.now()}`, - // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - }, - }, - }, - validateUIForm: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...(this.options as TPluginSettingOptionsFile)?.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], - ), - )(value); - }, - }, - 'customization.logo.healthcheck': { - title: 'Healthcheck logo', - description: `This logo is displayed during the Healthcheck routine of the app.`, - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - isConfigurableFromSettings: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png', '.svg'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 300, - height: 70, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.healthcheck', - resolveStaticURL: (filename: string) => - `custom/images/${filename}?v=${Date.now()}`, - // ?v=${Date.now()} is used to force the browser to reload the image when a new file is uploaded - }, - }, - }, - validateUIForm: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...(this.options as TPluginSettingOptionsFile)?.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], - ), - )(value); - }, - }, - 'customization.logo.reports': { - title: 'PDF reports logo', - description: `This logo is used in the PDF reports generated by the app. It's placed at the top left corner of every page of the PDF.`, - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.filepicker, - defaultValue: '', - defaultValueIfNotSet: REPORTS_LOGO_IMAGE_ASSETS_RELATIVE_PATH, - isConfigurableFromSettings: true, - options: { - file: { - type: 'image', - extensions: ['.jpeg', '.jpg', '.png'], - size: { - maxBytes: - CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES, - }, - recommended: { - dimensions: { - width: 190, - height: 40, - unit: 'px', - }, - }, - store: { - relativePathFileSystem: 'public/assets/custom/images', - filename: 'customization.logo.reports', - resolveStaticURL: (filename: string) => `custom/images/${filename}`, - }, - }, - }, - validateUIForm: function (value) { - return SettingsValidator.compose( - SettingsValidator.filePickerFileSize({ - ...(this.options as TPluginSettingOptionsFile)?.file.size, - meaningfulUnit: true, - }), - SettingsValidator.filePickerSupportedExtensions( - (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], - ), - )(value); - }, - }, - 'customization.reports.footer': { - title: 'Reports footer', - description: 'Set the footer of the reports.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: '', - defaultValueIfNotSet: REPORTS_PAGE_FOOTER_TEXT, - isConfigurableFromSettings: true, - options: { maxRows: 2, maxLength: 50 }, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: function (value) { - return SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.multipleLinesString({ - maxRows: (this.options as TPluginSettingOptionsTextArea)?.maxRows, - maxLength: (this.options as TPluginSettingOptionsTextArea)?.maxLength, - }), - )(value); - }, - }, - 'customization.reports.header': { - title: 'Reports header', - description: 'Set the header of the reports.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.CUSTOMIZATION, - type: EpluginSettingType.textarea, - defaultValue: '', - defaultValueIfNotSet: REPORTS_PAGE_HEADER_TEXT, - isConfigurableFromSettings: true, - options: { maxRows: 3, maxLength: 40 }, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: function (value) { - return SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.multipleLinesString({ - maxRows: (this.options as TPluginSettingOptionsTextArea)?.maxRows, - maxLength: (this.options as TPluginSettingOptionsTextArea)?.maxLength, - }), - )(value); - }, + validate: SettingsValidator.isBoolean, }, 'enrollment.dns': { title: 'Enrollment DNS', description: 'Specifies the Wazuh registration server, used for the agent enrollment.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: '', - isConfigurableFromSettings: true, - validateUIForm: function (value) { - return this.validate?.(value); - }, validate: SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.serverAddressHostnameFQDNIPv4IPv6, @@ -1348,62 +603,22 @@ export const PLUGIN_SETTINGS: Record = { title: 'Enrollment password', description: 'Specifies the password used to authenticate during the agent enrollment.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: '', - isConfigurableFromSettings: false, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - ), + validate: SettingsValidator.compose(SettingsValidator.isString), }, hideManagerAlerts: { title: 'Hide manager alerts', description: 'Hide the alerts of the manager in every dashboard.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: false, - isConfigurableFromSettings: true, - requiresReloadingBrowserTab: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, + validate: SettingsValidator.isBoolean, }, - hosts: { - title: 'Server hosts', - 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. + /* `# The following configuration is the default structure to define a host. # # hosts: # # Host ID / name, @@ -1432,39 +647,21 @@ hosts: username: wazuh-wui password: wazuh-wui run_as: false`, - transformFrom: value => - value.map((hostData: Record) => { - const key = Object.keys(hostData)?.[0]; - - return { ...hostData[key], id: key }; - }), - }, - }, + */ + hosts: { + title: 'Server hosts', + description: 'Configure the API connections.', + source: EConfigurationProviders.INITIALIZER_CONTEXT, + category: SettingCategory.API_CONNECTION, + type: EpluginSettingType.objectOf, + defaultValue: [], options: { - arrayOf: { - id: { - title: 'Identifier', - description: 'Identifier of the API connection. This must be unique.', - type: EpluginSettingType.text, - defaultValue: 'default', - isConfigurableFromSettings: true, - validateUIForm: function (value: string) { - return this.validate(value); - }, - validate: SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - ), - }, + objectOf: { url: { title: 'URL', description: 'Server URL address', type: EpluginSettingType.text, defaultValue: 'https://localhost', - isConfigurableFromSettings: true, - validateUIForm: function (value: string) { - return this.validate(value); - }, validate: SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.isNotEmptyString, @@ -1475,7 +672,6 @@ hosts: description: 'Port', type: EpluginSettingType.number, defaultValue: 55000, - isConfigurableFromSettings: true, options: { number: { min: 0, @@ -1483,15 +679,8 @@ hosts: integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value: number) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options.number)(value); + validate: function (value) { + return SettingsValidator.number(this.options?.number)(value); }, }, username: { @@ -1499,10 +688,6 @@ hosts: description: 'Server API username', type: EpluginSettingType.text, defaultValue: 'wazuh-wui', - isConfigurableFromSettings: true, - validateUIForm: function (value: string) { - return this.validate(value); - }, validate: SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.isNotEmptyString, @@ -1513,10 +698,6 @@ hosts: description: "User's Password", type: EpluginSettingType.password, defaultValue: 'wazuh-wui', - isConfigurableFromSettings: true, - validateUIForm: function (value: string) { - return this.validate(value); - }, validate: SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.isNotEmptyString, @@ -1527,7 +708,6 @@ hosts: description: 'Use the authentication context.', type: EpluginSettingType.switch, defaultValue: false, - isConfigurableFromSettings: true, options: { switch: { values: { @@ -1536,16 +716,11 @@ hosts: }, }, }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value: string) { - return this.validate(value); - }, validate: SettingsValidator.isBoolean, }, }, }, isConfigurableFromSettings: false, - uiFormTransformChangedInputValue: Boolean, // TODO: add validation // validate: SettingsValidator.isBoolean, // validate: function (schema) { @@ -1556,38 +731,10 @@ hosts: title: 'Index pattern ignore', description: 'Disable certain index pattern names from being available in index pattern selector.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.editor, defaultValue: [], - isConfigurableFromSettings: true, - options: { - editor: { - language: 'json', - }, - }, - uiFormTransformConfigurationValueToInputValue: function (value: any): any { - return JSON.stringify(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): any { - try { - return JSON.parse(value); - } catch { - return value; - } - }, - // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc - validateUIForm: function (value) { - return SettingsValidator.json( - this.validate as (value: unknown) => string | undefined, - )(value); - }, validate: SettingsValidator.compose( SettingsValidator.array( SettingsValidator.compose( @@ -1615,74 +762,29 @@ hosts: title: 'IP selector', description: 'Define if the user is allowed to change the selected index pattern directly from the top menu bar.', - store: { - file: { - configurableManaged: true, - }, - }, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: true, - isConfigurableFromSettings: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, + validate: SettingsValidator.isBoolean, }, 'wazuh.updates.disabled': { title: 'Check updates', description: 'Define if the check updates service is active.', + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.switch, defaultValue: false, - store: { - file: { - configurableManaged: false, - }, - }, - isConfigurableFromSettings: false, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, + validate: SettingsValidator.isBoolean, }, pattern: { title: 'Index pattern', - store: { - file: { - configurableManaged: true, - }, - }, description: "Default index pattern to use on the app. If there's no valid index pattern, the app will automatically create one with the name indicated in this option.", + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: WAZUH_ALERTS_PATTERN, - 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, @@ -1704,251 +806,29 @@ hosts: }, timeout: { title: 'Request timeout', - store: { - file: { - configurableManaged: true, - }, - }, description: 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, category: SettingCategory.GENERAL, type: EpluginSettingType.number, defaultValue: 20000, - isConfigurableFromSettings: true, options: { number: { min: 1500, integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, - }, - 'wazuh.monitoring.creation': { - title: 'Index creation', - description: - 'Define the interval in which a new wazuh-monitoring index will be created.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.select, - options: { - select: [ - { - text: 'Hourly', - value: 'h', - }, - { - text: 'Daily', - value: 'd', - }, - { - text: 'Weekly', - value: 'w', - }, - { - text: 'Monthly', - value: 'm', - }, - ], - }, - defaultValue: WAZUH_MONITORING_DEFAULT_CREATION, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - validateUIForm: function (value) { - return this.validate?.(value); - }, validate: function (value) { - return SettingsValidator.literal( - this.options?.select.map(({ value }) => value), - )(value); - }, - }, - 'wazuh.monitoring.enabled': { - title: 'Status', - description: - 'Enable or disable the wazuh-monitoring index creation and/or visualization.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.switch, - defaultValue: WAZUH_MONITORING_DEFAULT_ENABLED, - isConfigurableFromSettings: true, - requiresRestartingPluginPlatform: true, - options: { - switch: { - values: { - disabled: { label: 'false', value: false }, - enabled: { label: 'true', value: true }, - }, - }, - }, - uiFormTransformChangedInputValue: Boolean, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.isBoolean as ( - value: unknown, - ) => string | undefined, - }, - 'wazuh.monitoring.frequency': { - title: 'Frequency', - description: - 'Frequency, in seconds, of API requests to get the state of the agents and create a new document in the wazuh-monitoring index with this data.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_FREQUENCY, - isConfigurableFromSettings: true, - requiresRestartingPluginPlatform: true, - options: { - number: { - min: 60, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, - }, - 'wazuh.monitoring.pattern': { - title: 'Index pattern', - description: 'Default index pattern to use for Wazuh monitoring.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.text, - defaultValue: WAZUH_MONITORING_PATTERN, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - validateUIForm: function (value) { - return this.validate?.(value); - }, - validate: SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.isNotEmptyString, - SettingsValidator.hasNoSpaces, - SettingsValidator.noLiteralString('.', '..'), - SettingsValidator.noStartsWithString('-', '_', '+', '.'), - SettingsValidator.hasNotInvalidCharacters( - '\\', - '/', - '?', - '"', - '<', - '>', - '|', - ',', - '#', - ), - ), - }, - 'wazuh.monitoring.replicas': { - title: 'Index replicas', - description: - 'Define the number of replicas to use for the wazuh-monitoring-* indices.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_REPLICAS, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 0, - integer: true, - }, - }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, - }, - 'wazuh.monitoring.shards': { - title: 'Index shards', - description: - 'Define the number of shards to use for the wazuh-monitoring-* indices.', - store: { - file: { - configurableManaged: true, - }, - }, - category: SettingCategory.MONITORING, - type: EpluginSettingType.number, - defaultValue: WAZUH_MONITORING_DEFAULT_INDICES_SHARDS, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: true, - options: { - number: { - min: 1, - integer: true, - }, }, - uiFormTransformConfigurationValueToInputValue: String, - uiFormTransformInputValueToConfigurationValue: Number, - validateUIForm: function (value) { - return this.validate?.( - this.uiFormTransformInputValueToConfigurationValue?.(value), - ); - }, - validate: function (value: number) { - return SettingsValidator.number(this.options?.number)(value); - } as (value: unknown) => string | undefined, }, 'vulnerabilities.pattern': { title: 'Index pattern', description: 'Default index pattern to use for vulnerabilities.', - store: { - file: { - configurableManaged: true, - }, - }, category: SettingCategory.VULNERABILITIES, + source: EConfigurationProviders.PLUGIN_UI_SETTINGS, type: EpluginSettingType.text, defaultValue: WAZUH_VULNERABILITIES_PATTERN, - isConfigurableFromSettings: true, - requiresRunningHealthCheck: false, - validateUIForm: function (value) { - return this.validate(value); - }, validate: SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.isNotEmptyString, @@ -2119,4 +999,34 @@ export const WAZUH_ROLE_ADMINISTRATOR_ID = 1; // ID used to refer the createOsdUrlStateStorage state export const OSD_URL_STATE_STORAGE_ID = 'state:storeInSessionStorage'; -export { version as PLUGIN_VERSION } from '../package.json'; +// uiSettings + +export const HIDE_MANAGER_ALERTS_SETTING = 'hideManagerAlerts'; +export const ALERTS_SAMPLE_PREFIX = 'alerts.sample.prefix'; +// Checks +export const CHECKS_API = 'checks.api'; +export const CHECKS_FIELDS = 'checks.fields'; +export const CHECKS_MAX_BUCKETS = 'checks.max_buckets'; +export const CHECKS_META_FIELDS = 'checks.meta_fields'; +export const CHECKS_PATTERN = 'checks.pattern'; +export const CHECKS_SETUP = 'checks.setup'; +export const CHECKS_TEMPLATE = 'checks.template'; +export const CHECKS_TIMEFILTER = 'checks.timefilter'; + +export const CONFIG_UI_API_EDITABLE = 'configuration.ui_api_editable'; + +export const CRON_PREFIX = 'cron.prefix'; + +export const CUSTOMIZATION_ENABLED = 'customization.enabled'; + +export const ENROLLMENT_DNS = 'enrollment.dns'; +export const ENROLLMENT_PASSWORD = 'enrollment.password'; + +export const IP_IGNORE = 'ip.ignore'; +export const IP_SELECTOR = 'ip.selector'; + +export const WAZUH_UPDATES_DISABLED = 'wazuh.updates.disabled'; + +export const REQUEST_TIMEOUT = 'timeout'; + +export const DEFAULT_COLUMNS_SETTING = 'defaultColumns2'; diff --git a/plugins/wazuh-core/common/plugin-settings.test.ts b/plugins/wazuh-core/common/plugin-settings.test.ts index 9444fdd31d..a611fab1e9 100644 --- a/plugins/wazuh-core/common/plugin-settings.test.ts +++ b/plugins/wazuh-core/common/plugin-settings.test.ts @@ -1,249 +1,112 @@ -import { PLUGIN_SETTINGS } from './constants'; import { validate as validateNodeCronInterval } from 'node-cron'; +import { PLUGIN_SETTINGS } from './constants'; function validateCronStatisticsInterval(value) { return validateNodeCronInterval(value) ? undefined : 'Interval is not valid.'; } -describe('[settings] Input validation', () => { +describe.skip('[settings] Input validation', () => { it.each` - setting | value | expectedValidation - ${'alerts.sample.prefix'} | ${'test'} | ${undefined} - ${'alerts.sample.prefix'} | ${''} | ${'Value can not be empty.'} - ${'alerts.sample.prefix'} | ${'test space'} | ${'No whitespaces allowed.'} - ${'alerts.sample.prefix'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'alerts.sample.prefix'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'alerts.sample.prefix'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'alerts.sample.prefix'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'alerts.sample.prefix'} | ${'test\\'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test/'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test?'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test"'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test<'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test>'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test|'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'alerts.sample.prefix'} | ${'test*'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'checks.api'} | ${true} | ${undefined} - ${'checks.api'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.fields'} | ${true} | ${undefined} - ${'checks.fields'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.maxBuckets'} | ${true} | ${undefined} - ${'checks.maxBuckets'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.pattern'} | ${true} | ${undefined} - ${'checks.pattern'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.setup'} | ${true} | ${undefined} - ${'checks.setup'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.template'} | ${true} | ${undefined} - ${'checks.template'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'configuration.ui_api_editable'} | ${true} | ${undefined} - ${'configuration.ui_api_editable'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'checks.timeFilter'} | ${true} | ${undefined} - ${'checks.timeFilter'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'cron.prefix'} | ${'test'} | ${undefined} - ${'cron.prefix'} | ${'test space'} | ${'No whitespaces allowed.'} - ${'cron.prefix'} | ${''} | ${'Value can not be empty.'} - ${'cron.prefix'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'cron.prefix'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'cron.prefix'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'cron.prefix'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'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.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.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.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 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} - ${'cron.statistics.index.creation'} | ${'w'} | ${undefined} - ${'cron.statistics.index.creation'} | ${'m'} | ${undefined} - ${'cron.statistics.index.creation'} | ${'test'} | ${'Invalid value. Allowed values: h, d, w, m.'} - ${'cron.statistics.index.name'} | ${'test'} | ${undefined} - ${'cron.statistics.index.name'} | ${''} | ${'Value can not be empty.'} - ${'cron.statistics.index.name'} | ${'test space'} | ${'No whitespaces allowed.'} - ${'cron.statistics.index.name'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'cron.statistics.index.name'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'cron.statistics.index.name'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'cron.statistics.index.name'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'cron.statistics.index.name'} | ${'test\\'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test/'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test?'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test"'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test<'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test>'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test|'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.name'} | ${'test*'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #, *.'} - ${'cron.statistics.index.replicas'} | ${0} | ${undefined} - ${'cron.statistics.index.replicas'} | ${-1} | ${'Value should be greater or equal than 0.'} - ${'cron.statistics.index.replicas'} | ${'1.2'} | ${'Number should be an integer.'} - ${'cron.statistics.index.replicas'} | ${1.2} | ${'Number should be an integer.'} - ${'cron.statistics.index.shards'} | ${1} | ${undefined} - ${'cron.statistics.index.shards'} | ${-1} | ${'Value should be greater or equal than 1.'} - ${'cron.statistics.index.shards'} | ${'1.2'} | ${'Number should be an integer.'} - ${'cron.statistics.index.shards'} | ${1.2} | ${'Number should be an integer.'} - ${'cron.statistics.interval'} | ${'0 */5 * * * *'} | ${undefined} - ${'cron.statistics.interval'} | ${'0 */5 * * *'} | ${undefined} - ${'cron.statistics.interval'} | ${'custom'} | ${'Interval is not valid.'} - ${'cron.statistics.interval'} | ${true} | ${'Interval is not valid.'} - ${'cron.statistics.status'} | ${true} | ${undefined} - ${'cron.statistics.status'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} - ${'customization.enabled'} | ${true} | ${undefined} - ${'customization.logo.app'} | ${{ size: 124000, name: 'image.jpg' }} | ${undefined} - ${'customization.logo.app'} | ${{ size: 124000, name: 'image.jpeg' }} | ${undefined} - ${'customization.logo.app'} | ${{ size: 124000, name: 'image.png' }} | ${undefined} - ${'customization.logo.app'} | ${{ size: 124000, name: 'image.svg' }} | ${undefined} - ${'customization.logo.app'} | ${{ size: 124000, name: 'image.txt' }} | ${'File extension is invalid. Allowed file extensions: .jpeg, .jpg, .png, .svg.'} - ${'customization.logo.app'} | ${{ size: 1240000, name: 'image.txt' }} | ${'File size should be lower or equal than 1 MB.'} - ${'customization.logo.healthcheck'} | ${{ size: 124000, name: 'image.jpg' }} | ${undefined} - ${'customization.logo.healthcheck'} | ${{ size: 124000, name: 'image.jpeg' }} | ${undefined} - ${'customization.logo.healthcheck'} | ${{ size: 124000, name: 'image.png' }} | ${undefined} - ${'customization.logo.healthcheck'} | ${{ size: 124000, name: 'image.svg' }} | ${undefined} - ${'customization.logo.healthcheck'} | ${{ size: 124000, name: 'image.txt' }} | ${'File extension is invalid. Allowed file extensions: .jpeg, .jpg, .png, .svg.'} - ${'customization.logo.healthcheck'} | ${{ size: 1240000, name: 'image.txt' }} | ${'File size should be lower or equal than 1 MB.'} - ${'customization.logo.reports'} | ${{ size: 124000, name: 'image.jpg' }} | ${undefined} - ${'customization.logo.reports'} | ${{ size: 124000, name: 'image.jpeg' }} | ${undefined} - ${'customization.logo.reports'} | ${{ size: 124000, name: 'image.png' }} | ${undefined} - ${'customization.logo.reports'} | ${{ size: 124000, name: 'image.svg' }} | ${'File extension is invalid. Allowed file extensions: .jpeg, .jpg, .png.'} - ${'customization.logo.reports'} | ${{ size: 124000, name: 'image.txt' }} | ${'File extension is invalid. Allowed file extensions: .jpeg, .jpg, .png.'} - ${'customization.logo.reports'} | ${{ size: 1240000, name: 'image.txt' }} | ${'File size should be lower or equal than 1 MB.'} - ${'customization.reports.footer'} | ${'Test'} | ${undefined} - ${'customization.reports.footer'} | ${'Test\nTest'} | ${undefined} - ${'customization.reports.footer'} | ${'Test\nTest\nTest\nTest\nTest'} | ${'The string should have less or equal to 2 line/s.'} - ${'customization.reports.footer'} | ${'Line with 30 characters \nTest'} | ${undefined} - ${'customization.reports.footer'} | ${'Testing maximum length of a line of more than 50 characters\nTest'} | ${'The maximum length of a line is 50 characters.'} - ${'customization.reports.header'} | ${'Test'} | ${undefined} - ${'customization.reports.header'} | ${'Test\nTest'} | ${undefined} - ${'customization.reports.header'} | ${'Test\nTest\nTest\nTest\nTest'} | ${'The string should have less or equal to 3 line/s.'} - ${'customization.reports.header'} | ${'Line with 20 charact\nTest'} | ${undefined} - ${'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'} | ${'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'} | ${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"\']'} | ${"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} - ${'pattern'} | ${'test*'} | ${undefined} - ${'pattern'} | ${''} | ${'Value can not be empty.'} - ${'pattern'} | ${'test space'} | ${'No whitespaces allowed.'} - ${'pattern'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'pattern'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'pattern'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'pattern'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'pattern'} | ${'test\\'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test/'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test?'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test"'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test<'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test>'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test|'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'pattern'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'timeout'} | ${15000} | ${undefined} - ${'timeout'} | ${1000} | ${'Value should be greater or equal than 1500.'} - ${'timeout'} | ${''} | ${'Value should be greater or equal than 1500.'} - ${'timeout'} | ${'1.2'} | ${'Number should be an integer.'} - ${'timeout'} | ${1.2} | ${'Number should be an integer.'} - ${'wazuh.monitoring.creation'} | ${'h'} | ${undefined} - ${'wazuh.monitoring.creation'} | ${'d'} | ${undefined} - ${'wazuh.monitoring.creation'} | ${'w'} | ${undefined} - ${'wazuh.monitoring.creation'} | ${'m'} | ${undefined} - ${'wazuh.monitoring.creation'} | ${'test'} | ${'Invalid value. Allowed values: h, d, w, m.'} - ${'wazuh.monitoring.enabled'} | ${true} | ${undefined} - ${'wazuh.monitoring.frequency'} | ${100} | ${undefined} - ${'wazuh.monitoring.frequency'} | ${40} | ${'Value should be greater or equal than 60.'} - ${'wazuh.monitoring.frequency'} | ${'1.2'} | ${'Number should be an integer.'} - ${'wazuh.monitoring.frequency'} | ${1.2} | ${'Number should be an integer.'} - ${'wazuh.monitoring.pattern'} | ${'test'} | ${undefined} - ${'wazuh.monitoring.pattern'} | ${'test*'} | ${undefined} - ${'wazuh.monitoring.pattern'} | ${''} | ${'Value can not be empty.'} - ${'wazuh.monitoring.pattern'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'wazuh.monitoring.pattern'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'wazuh.monitoring.pattern'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'wazuh.monitoring.pattern'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'wazuh.monitoring.pattern'} | ${'test\\'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test/'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test?'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test"'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test<'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test>'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test|'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.pattern'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'wazuh.monitoring.replicas'} | ${0} | ${undefined} - ${'wazuh.monitoring.replicas'} | ${-1} | ${'Value should be greater or equal than 0.'} - ${'wazuh.monitoring.replicas'} | ${'1.2'} | ${'Number should be an integer.'} - ${'wazuh.monitoring.replicas'} | ${1.2} | ${'Number should be an integer.'} - ${'wazuh.monitoring.shards'} | ${1} | ${undefined} - ${'wazuh.monitoring.shards'} | ${-1} | ${'Value should be greater or equal than 1.'} - ${'wazuh.monitoring.shards'} | ${'1.2'} | ${'Number should be an integer.'} - ${'wazuh.monitoring.shards'} | ${1.2} | ${'Number should be an integer.'} - ${'vulnerabilities.pattern'} | ${'test'} | ${undefined} - ${'vulnerabilities.pattern'} | ${'test*'} | ${undefined} - ${'vulnerabilities.pattern'} | ${''} | ${'Value can not be empty.'} - ${'vulnerabilities.pattern'} | ${'-test'} | ${"It can't start with: -, _, +, .."} - ${'vulnerabilities.pattern'} | ${'_test'} | ${"It can't start with: -, _, +, .."} - ${'vulnerabilities.pattern'} | ${'+test'} | ${"It can't start with: -, _, +, .."} - ${'vulnerabilities.pattern'} | ${'.test'} | ${"It can't start with: -, _, +, .."} - ${'vulnerabilities.pattern'} | ${'test\\'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test/'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test?'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test"'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test<'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test>'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test|'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test,'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} - ${'vulnerabilities.pattern'} | ${'test#'} | ${'It can\'t contain invalid characters: \\, /, ?, ", <, >, |, ,, #.'} + setting | value | expectedValidation + ${'alerts.sample.prefix'} | ${'test'} | ${undefined} + ${'alerts.sample.prefix'} | ${''} | ${'Value can not be empty.'} + ${'alerts.sample.prefix'} | ${'test space'} | ${'No whitespaces allowed.'} + ${'alerts.sample.prefix'} | ${'-test'} | ${"It can't start with: -, _, +, .."} + ${'alerts.sample.prefix'} | ${'_test'} | ${"It can't start with: -, _, +, .."} + ${'alerts.sample.prefix'} | ${'+test'} | ${"It can't start with: -, _, +, .."} + ${'alerts.sample.prefix'} | ${'.test'} | ${"It can't start with: -, _, +, .."} + ${'alerts.sample.prefix'} | ${'test\\'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test/'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test?'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test"'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test<'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test>'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test|'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test,'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test#'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'alerts.sample.prefix'} | ${'test*'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #, *.`} + ${'configuration.ui_api_editable'} | ${true} | ${undefined} + ${'configuration.ui_api_editable'} | ${0} | ${'It should be a boolean. Allowed values: true or false.'} + ${'enrollment.dns'} | ${'test'} | ${undefined} + ${'enrollment.dns'} | ${''} | ${undefined} + ${'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'} | ${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'} | ${String.raw`["test\""]`} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test/"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test?"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test"\']'} | ${"Value can't be parsed. There is some error."} + ${'ip.ignore'} | ${'["test<"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test>"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test|"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test,"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test#"]'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'ip.ignore'} | ${'["test", "test#"]'} | ${String.raw`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} + ${'pattern'} | ${'test*'} | ${undefined} + ${'pattern'} | ${''} | ${'Value can not be empty.'} + ${'pattern'} | ${'test space'} | ${'No whitespaces allowed.'} + ${'pattern'} | ${'-test'} | ${"It can't start with: -, _, +, .."} + ${'pattern'} | ${'_test'} | ${"It can't start with: -, _, +, .."} + ${'pattern'} | ${'+test'} | ${"It can't start with: -, _, +, .."} + ${'pattern'} | ${'.test'} | ${"It can't start with: -, _, +, .."} + ${'pattern'} | ${'test\\'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test/'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test?'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test"'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test<'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test>'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test|'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test,'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'pattern'} | ${'test#'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'timeout'} | ${15000} | ${undefined} + ${'timeout'} | ${1000} | ${'Value should be greater or equal than 1500.'} + ${'timeout'} | ${''} | ${'Value should be greater or equal than 1500.'} + ${'timeout'} | ${'1.2'} | ${'Number should be an integer.'} + ${'timeout'} | ${1.2} | ${'Number should be an integer.'} + ${'vulnerabilities.pattern'} | ${'test'} | ${undefined} + ${'vulnerabilities.pattern'} | ${'test*'} | ${undefined} + ${'vulnerabilities.pattern'} | ${''} | ${'Value can not be empty.'} + ${'vulnerabilities.pattern'} | ${'-test'} | ${"It can't start with: -, _, +, .."} + ${'vulnerabilities.pattern'} | ${'_test'} | ${"It can't start with: -, _, +, .."} + ${'vulnerabilities.pattern'} | ${'+test'} | ${"It can't start with: -, _, +, .."} + ${'vulnerabilities.pattern'} | ${'.test'} | ${"It can't start with: -, _, +, .."} + ${'vulnerabilities.pattern'} | ${'test\\'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test/'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test?'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test"'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test<'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test>'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test|'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test,'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} + ${'vulnerabilities.pattern'} | ${'test#'} | ${String.raw`It can't contain invalid characters: \, /, ?, ", <, >, |, ,, #.`} `( '$setting | $value | $expectedValidation', ({ setting, value, expectedValidation }) => { // FIXME: use the plugins definition - if (setting === 'cron.statistics.interval') { - expect(validateCronStatisticsInterval(value)).toBe(expectedValidation); + + if (expectedValidation === undefined) { + expect(PLUGIN_SETTINGS[setting].validate(value)).toBeUndefined(); } else { - expect(PLUGIN_SETTINGS[setting].validateUIForm(value)).toBe( - expectedValidation, - ); + expect(PLUGIN_SETTINGS[setting].validate(value)).not.toBeUndefined(); } }, ); diff --git a/plugins/wazuh-core/common/services/configuration.test.ts b/plugins/wazuh-core/common/services/configuration.test.ts deleted file mode 100644 index e307c3c748..0000000000 --- a/plugins/wazuh-core/common/services/configuration.test.ts +++ /dev/null @@ -1,431 +0,0 @@ -import { Configuration } from './configuration'; - -// No operation -const noop = () => {}; - -const mockLogger = { - debug: noop, - info: noop, - warn: noop, - error: noop, -}; - -function createMockConfigurationStore() { - return { - setConfiguration(configuration) { - this.configuration = configuration; - }, - _config: {}, - set(settings) { - this._config = { - ...this._config, - ...settings, - }; - return settings || {}; - }, - get(...settings: string[]) { - return Object.fromEntries( - Object.entries(this._config) - .filter(([key]) => (settings.length ? settings.includes(key) : true)) - .map(([key, value]) => [key, value]), - ); - }, - clear(...settings: string[]) { - (settings.length ? settings : Object.keys(this._config)).forEach( - key => - typeof this._config[key] !== 'undefined' && delete this._config[key], - ); - return settings; - }, - }; -} - -const settingsSuite = { - 0: [ - [ - 'text', - { - type: 'text', - defaultValue: 'test', - }, - ], - [ - 'number', - { - type: 'number', - defaultValue: 1, - }, - ], - ], - 1: [ - [ - 'text', - { - type: 'text', - defaultValue: 'test', - }, - ], - [ - 'text', - { - type: 'text', - defaultValue: 'test2', - _test_meta: { - failOnRegister: true, - }, - }, - ], - ], - 1: [ - [ - 'text', - { - type: 'text', - defaultValue: 'test', - }, - ], - [ - 'text', - { - type: 'text', - defaultValue: 'test2', - _test_meta: { - failOnRegister: true, - }, - }, - ], - [ - 'text', - { - type: 'text', - defaultValue: 'test3', - _test_meta: { - failOnRegister: true, - }, - }, - ], - ], - 2: [ - [ - 'text1', - { - type: 'text', - defaultValue: 'defaultValue1', - }, - ], - [ - 'text2', - { - type: 'text', - defaultValue: 'defaultValue2', - }, - ], - ], -}; - -describe('Configuration service', () => { - it.each` - settings - ${settingsSuite[0]} - ${settingsSuite[1]} - `( - `settings are registered or throwing errors if they are registered previously`, - ({ settings }) => { - const configurationStore = createMockConfigurationStore(); - const configuration = new Configuration(mockLogger, configurationStore); - - settings.forEach(([key, value]) => { - if (value?._test_meta?.failOnRegister) { - expect(() => configuration.register(key, value)).toThrow( - `Setting ${key} exists`, - ); - } else { - configuration.register(key, value); - expect(configuration._settings.get(key) === value).toBeTruthy(); - } - }); - }, - ); - - it.each` - 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); - - settingsSuite[2].forEach(([key, value]) => - configuration.register(key, value), - ); - - // Redefine the stored value - configurationStore._config = settings.reduce( - (accum, { key, store }) => ({ - ...accum, - ...(store ? { [key]: store } : {}), - }), - {}, - ); - - if (settings.length === 1) { - // Get a setting - const { key, value } = settings[0]; - expect(await configuration.get(key)).toBe(value); - } else if (settings.length > 1) { - // Get more than one setting - expect( - await configuration.get(...settings.map(({ key }) => key)), - ).toEqual( - settings.reduce( - (accum, { key, value }) => ({ ...accum, [key]: value }), - {}, - ), - ); - } - }); - - it.each` - title | settings - ${'set setting storedValue1'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - finalValue: 'storedValue1', - store: 'storedValue1', - }, { - key: 'text2', - initialValue: 'defaultValue2', - finalValue: 'defaultValue2', - store: undefined, - }]} - `( - 'register setting, set the stored values and check each modified setting has the expected value', - async ({ settings }) => { - const configurationStore = createMockConfigurationStore(); - const configuration = new Configuration(mockLogger, configurationStore); - - settingsSuite[2].forEach(([key, value]) => { - configuration.register(key, value); - }); - - settings.forEach(async ({ key, initialValue }) => { - expect(await configuration.get(key)).toBe(initialValue); - }); - - const storeNewConfiguration = Object.fromEntries( - settings - .filter(({ store }) => store) - .map(({ key, store }) => [key, store]), - ); - - await configuration.set(storeNewConfiguration); - - settings.forEach(async ({ key, finalValue }) => { - expect(await configuration.get(key)).toBe(finalValue); - }); - }, - ); - - it.each` - title | settings - ${'clean all settings'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - afterSetValue: 'storedValue1', - afterCleanValue: 'defaultValue1', - store: 'storedValue1', - clear: true, - }, { - key: 'text2', - initialValue: 'defaultValue2', - afterSetValue: 'defaultValue2', - afterCleanValue: 'defaultValue1', - store: undefined, - clear: false, - }]} - `( - 'register setting, set the stored values, check each modified setting has the expected value and clear someone values and check again the setting value', - async ({ settings }) => { - const configurationStore = createMockConfigurationStore(); - const configuration = new Configuration(mockLogger, configurationStore); - - settingsSuite[2].forEach(([key, value]) => { - configuration.register(key, value); - }); - - settings.forEach(async ({ key, initialValue }) => { - expect(await configuration.get(key)).toBe(initialValue); - }); - - const storeNewConfiguration = Object.fromEntries( - settings - .filter(({ store }) => store) - .map(({ key, store }) => [key, store]), - ); - - await configuration.set(storeNewConfiguration); - - settings.forEach(async ({ key, afterSetValue }) => { - expect(await configuration.get(key)).toBe(afterSetValue); - }); - - const cleanSettings = settings - .filter(({ clear }) => clear) - .map(({ key }) => key); - - await configuration.clear(cleanSettings); - }, - ); - - it.each` - title | settings | clearSpecificSettings - ${'clean all settings'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - afterSetValue: 'storedValue1', - afterCleanValue: 'defaultValue1', - store: 'storedValue1', - clear: true, - }, { - key: 'text2', - initialValue: 'defaultValue2', - afterSetValue: 'defaultValue2', - afterCleanValue: 'defaultValue2', - store: undefined, - clear: false, - }]} | ${true} - ${'clean all settings'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - afterSetValue: 'storedValue1', - afterCleanValue: 'defaultValue1', - store: 'storedValue1', - clear: true, - }, { - key: 'text2', - initialValue: 'defaultValue2', - afterSetValue: 'defaultValue2', - afterCleanValue: 'defaultValue2', - store: undefined, - clear: false, - }]} | ${false} - `( - 'register setting, set the stored values, check each modified setting has the expected value and clear someone values and check again the setting value', - async ({ settings, clearSpecificSettings }) => { - const configurationStore = createMockConfigurationStore(); - const configuration = new Configuration(mockLogger, configurationStore); - - settingsSuite[2].forEach(([key, value]) => { - configuration.register(key, value); - }); - - settings.forEach(async ({ key, initialValue }) => { - expect(await configuration.get(key)).toBe(initialValue); - }); - - const storeNewConfiguration = Object.fromEntries( - settings - .filter(({ store }) => store) - .map(({ key, store }) => [key, store]), - ); - - await configuration.set(storeNewConfiguration); - - settings.forEach(async ({ key, afterSetValue }) => { - expect(await configuration.get(key)).toBe(afterSetValue); - }); - - if (!clearSpecificSettings) { - await configuration.clear(); - } else { - const cleanSettings = settings - .filter(({ clear }) => clear) - .map(({ key }) => key); - - await configuration.clear(cleanSettings); - } - - settings.forEach(async ({ key, afterCleanValue }) => { - expect(await configuration.get(key)).toBe(afterCleanValue); - }); - }, - ); - - it.each` - title | settings | clearSpecificSettings - ${'clean all settings'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - afterSetValue: 'storedValue1', - afterCleanValue: 'defaultValue1', - store: 'storedValue1', - clear: true, - }, { - key: 'text2', - initialValue: 'defaultValue2', - afterSetValue: 'defaultValue2', - afterCleanValue: 'defaultValue2', - store: undefined, - clear: false, - }]} | ${true} - ${'clean all settings'} | ${[{ - key: 'text1', - initialValue: 'defaultValue1', - afterSetValue: 'storedValue1', - afterCleanValue: 'defaultValue1', - store: 'storedValue1', - clear: true, - }, { - key: 'text2', - initialValue: 'defaultValue2', - afterSetValue: 'defaultValue2', - afterCleanValue: 'defaultValue2', - store: undefined, - clear: false, - }]} | ${false} - `( - 'register setting, set the stored values, check each modified setting has the expected value and clear someone values and check again the setting value', - async ({ settings, clearSpecificSettings }) => { - const configurationStore = createMockConfigurationStore(); - const configuration = new Configuration(mockLogger, configurationStore); - - settingsSuite[2].forEach(([key, value]) => { - configuration.register(key, value); - }); - - settings.forEach(async ({ key, initialValue }) => { - expect(await configuration.get(key)).toBe(initialValue); - }); - - const storeNewConfiguration = Object.fromEntries( - settings - .filter(({ store }) => store) - .map(({ key, store }) => [key, store]), - ); - - await configuration.set(storeNewConfiguration); - - settings.forEach(async ({ key, afterSetValue }) => { - expect(await configuration.get(key)).toBe(afterSetValue); - }); - - if (!clearSpecificSettings) { - await configuration.clear(); - } else { - const cleanSettings = settings - .filter(({ clear }) => clear) - .map(({ key }) => key); - - await configuration.clear(cleanSettings); - } - - settings.forEach(async ({ key, afterCleanValue }) => { - expect(await configuration.get(key)).toBe(afterCleanValue); - }); - }, - ); - - // TODO: add test for reset -}); diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts deleted file mode 100644 index f1d305918c..0000000000 --- a/plugins/wazuh-core/common/services/configuration.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { cloneDeep } from 'lodash'; -import { EpluginSettingType } from '../constants'; -import { formatLabelValuePair } from './settings'; -import { formatBytes } from './file-size'; - -export interface Logger { - debug: (message: string) => void; - info: (message: string) => void; - warn: (message: string) => void; - error: (message: string) => void; -} - -interface TConfigurationSettingOptionsPassword { - password: { - dual?: 'text' | 'password' | 'dual'; - }; -} - -interface TConfigurationSettingOptionsTextArea { - maxRows?: number; - minRows?: number; - maxLength?: number; -} - -interface TConfigurationSettingOptionsSelect { - select: { text: string; value: any }[]; -} - -interface TConfigurationSettingOptionsEditor { - editor: { - language: string; - }; -} - -interface TConfigurationSettingOptionsFile { - file: { - type: 'image'; - extensions?: string[]; - size?: { - maxBytes?: number; - minBytes?: number; - }; - recommended?: { - dimensions?: { - width: number; - height: number; - unit: string; - }; - }; - store?: { - relativePathFileSystem: string; - filename: string; - resolveStaticURL: (filename: string) => string; - }; - }; -} - -interface TConfigurationSettingOptionsNumber { - number: { - min?: number; - max?: number; - integer?: boolean; - }; -} - -interface TConfigurationSettingOptionsSwitch { - switch: { - values: { - disabled: { label?: string; value: any }; - enabled: { label?: string; value: any }; - }; - }; -} - -export interface TConfigurationSetting { - // Define the text displayed in the UI. - title: string; - // Description. - description: string; - // Category. - category: number; - // Type. - type: EpluginSettingType; - // Default value. - defaultValue: any; - /* 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; - // Modify the setting requires running the plugin health check (frontend). - requiresRunningHealthCheck?: boolean; - // Modify the setting requires reloading the browser tab (frontend). - requiresReloadingBrowserTab?: boolean; - // Modify the setting requires restarting the plugin platform to take effect. - requiresRestartingPluginPlatform?: boolean; - // Define options related to the `type`. - options?: - | TConfigurationSettingOptionsEditor - | TConfigurationSettingOptionsFile - | TConfigurationSettingOptionsNumber - | TConfigurationSettingOptionsPassword - | TConfigurationSettingOptionsSelect - | TConfigurationSettingOptionsSwitch - | TConfigurationSettingOptionsTextArea; - store?: { - file: { - // 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 - uiFormTransformChangedInputValue?: (value: any) => any; - // Transform the configuration value or default as initial value for the input in Settings/Configuration - uiFormTransformConfigurationValueToInputValue?: (value: any) => any; - // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm - uiFormTransformInputValueToConfigurationValue?: (value: any) => any; - // Validate the value in the form of 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 & { - key: string; -}; -export interface TConfigurationSettingCategory { - title: string; - description?: string; - documentationLink?: string; - renderOrder?: number; -} -type TConfigurationSettings = Record; - -export interface IConfigurationStore { - setup: () => Promise; - start: () => Promise; - stop: () => Promise; - get: (...settings: string[]) => Promise; - set: (settings: TConfigurationSettings) => Promise; - clear: (...settings: string[]) => Promise; - // eslint-disable-next-line no-use-before-define - setConfiguration: (configuration: IConfiguration) => void; -} - -export interface IConfiguration { - setStore: (store: IConfigurationStore) => void; - setup: () => Promise; - start: () => Promise; - stop: () => Promise; - register: (id: string, value: any) => void; - get: (...settings: string[]) => Promise; - set: (settings: TConfigurationSettings) => Promise; - clear: (...settings: string[]) => Promise; - reset: (...settings: string[]) => Promise; - _settings: Map>; - getSettingValue: (settingKey: string, value?: any) => any; - getSettingValueIfNotSet: (settingKey: string, value?: any) => any; -} - -export class Configuration implements IConfiguration { - store: IConfigurationStore | null = null; - _settings: Map>; - _categories: Map>; - - constructor( - private readonly logger: Logger, - store: IConfigurationStore, - ) { - this._settings = new Map(); - this._categories = new Map(); - this.setStore(store); - } - - setStore(store: IConfigurationStore) { - this.store = store; - this.store.setConfiguration(this); - } - - async setup(dependencies: any = {}) { - return this.store.setup(dependencies); - } - - async start(dependencies: any = {}) { - return this.store.start(dependencies); - } - - async stop(dependencies: any = {}) { - return this.store.stop(dependencies); - } - - /** - * Register a setting - * @param id - * @param value - */ - register(id: string, value: any) { - if (this._settings.has(id)) { - const message = `Setting ${id} exists`; - - this.logger.error(message); - throw new Error(message); - } else { - // 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}`); - } - } - - private checkRequirementsOnUpdatedSettings(settings: string[]) { - return { - requiresRunningHealthCheck: settings.some( - key => this._settings.get(key)?.requiresRunningHealthCheck, - ), - requiresReloadingBrowserTab: settings.some( - key => this._settings.get(key)?.requiresReloadingBrowserTab, - ), - requiresRestartingPluginPlatform: settings.some( - key => this._settings.get(key)?.requiresRestartingPluginPlatform, - ), - }; - } - - /** - * 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 - */ - getSettingValueIfNotSet(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 (value !== undefined) { - return value; - } - - const setting = this._settings.get(settingKey); - const finalValue = - setting.defaultValueIfNotSet === undefined - ? setting.defaultValue - : setting.defaultValueIfNotSet; - - this.logger.debug( - `Value for [${settingKey}]: [${JSON.stringify(finalValue)}]`, - ); - - 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 (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 - * @returns - */ - async get(...settings: string[]) { - this.logger.debug( - settings.length > 0 - ? `Getting settings [${settings.join(',')}]` - : 'Getting settings', - ); - - const stored = await this.store.get(...settings); - - this.logger.debug(`configuration stored: ${JSON.stringify({ stored })}`); - - const result = - settings && settings.length === 1 - ? this.getSettingValue(settings[0], stored[settings[0]]) - : Object.fromEntries( - (settings.length > 1 ? settings : [...this._settings.keys()]).map( - key => [key, this.getSettingValue(key, stored[key])], - ), - ); - - // Clone the result. This avoids the object reference can be changed when managing the result. - return cloneDeep(result); - } - - /** - * Set a the value for a subset of settings - * @param settings - * @returns - */ - async set(settings: Record) { - const settingsAreRegistered = Object.entries(settings) - .map(([key]) => - this._settings.has(key) ? null : `${key} is not registered`, - ) - .filter(Boolean); - - if (settingsAreRegistered.length > 0) { - throw new Error(`${settingsAreRegistered.join(', ')} are not registered`); - } - - const validationErrors = Object.entries(settings) - .map(([key, value]) => { - const validationError = this._settings.get(key)?.validate?.(value); - - return validationError - ? `setting [${key}]: ${validationError}` - : undefined; - }) - .filter(Boolean); - - if (validationErrors.length > 0) { - throw new Error(`Validation errors: ${validationErrors.join('\n')}`); - } - - const responseStore = await this.store.set(settings); - - return { - requirements: this.checkRequirementsOnUpdatedSettings( - Object.keys(responseStore), - ), - update: responseStore, - }; - } - - /** - * Clean the values for all settings or a subset of them - * @param rest - * @returns - */ - async clear(...settings: string[]) { - if (settings.length > 0) { - this.logger.debug(`Clean settings: ${settings.join(', ')}`); - - const responseStore = await this.store.clear(...settings); - - this.logger.info('Settings were cleared'); - - return { - requirements: this.checkRequirementsOnUpdatedSettings( - Object.keys(responseStore), - ), - update: responseStore, - }; - } else { - return await this.clear(...this._settings.keys()); - } - } - - /** - * Reset the values for all settings or a subset of them - * @param settings - * @returns - */ - async reset(...settings: string[]) { - if (settings.length > 0) { - this.logger.debug(`Reset settings: ${settings.join(', ')}`); - - const updatedSettings = Object.fromEntries( - settings.map((settingKey: string) => [ - settingKey, - this.getSettingValue(settingKey), - ]), - ); - const responseStore = await this.store.set(updatedSettings); - - this.logger.info('Settings were reset'); - - return { - requirements: this.checkRequirementsOnUpdatedSettings( - Object.keys(responseStore), - ), - update: responseStore, - }; - } else { - 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( - [...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 && options.file.size.minBytes !== undefined - ? [`Minimum file size: ${formatBytes(options.file.size.minBytes)}.`] - : []), - ...(options?.file?.size && options.file.size.maxBytes !== undefined - ? [`Maximum file size: ${formatBytes(options.file.size.maxBytes)}.`] - : []), - // Multi line text - ...(options?.maxRows && options.maxRows !== undefined - ? [`Maximum amount of lines: ${options.maxRows}.`] - : []), - ...(options?.minRows && options.minRows !== undefined - ? [`Minimum amount of lines: ${options.minRows}.`] - : []), - ...(options?.maxLength && options.maxLength !== undefined - ? [`Maximum lines length is ${options.maxLength} characters.`] - : []), - ].join(' '); - } - - groupSettingsByCategory( - settings: string[] | null = null, - filterFunction: - | ((setting: TConfigurationSettingWithKey) => boolean) - | null = null, - ) { - const settingsMapped = ( - settings && Array.isArray(settings) - ? [...this._settings.entries()].filter(([key]) => - settings.includes(key), - ) - : [...this._settings.entries()] - ).map(([key, value]) => ({ - ...value, - key, - })); - const settingsSortedByCategories = ( - filterFunction - ? settingsMapped.filter(element => filterFunction(element)) - : settingsMapped - ).sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)); - const result: any = {}; - - for (const pluginSettingConfiguration of settingsSortedByCategories) { - const category = pluginSettingConfiguration.category; - - if (!result[category]) { - result[category] = []; - } - - result[category].push({ ...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/common/services/configuration/configuration-provider.ts b/plugins/wazuh-core/common/services/configuration/configuration-provider.ts new file mode 100644 index 0000000000..23684a35e8 --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/configuration-provider.ts @@ -0,0 +1,7 @@ +export interface IConfigurationProvider { + get: (key: string) => Promise; + set?: (key: string, value: any) => Promise; + getAll: () => Promise; + setName: (name: string) => void; + getName: () => string; +} diff --git a/plugins/wazuh-core/server/services/configuration-store.md b/plugins/wazuh-core/common/services/configuration/configuration-store.md similarity index 100% rename from plugins/wazuh-core/server/services/configuration-store.md rename to plugins/wazuh-core/common/services/configuration/configuration-store.md diff --git a/plugins/wazuh-core/common/services/configuration/configuration-store.test.ts b/plugins/wazuh-core/common/services/configuration/configuration-store.test.ts new file mode 100644 index 0000000000..897722acfb --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/configuration-store.test.ts @@ -0,0 +1,118 @@ +import { createMockLogger } from '../../../test/mocks/logger-mocked'; +import { IConfigurationProvider } from './configuration-provider'; +import { ConfigurationStore } from './configuration-store'; + +const mockedProvider: IConfigurationProvider = { + getAll: jest.fn(), + get: jest.fn(), + set: jest.fn(), + setName: jest.fn(), + getName: jest.fn(), +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe(`[service] ConfigurationStore`, () => { + // Create instance + it('should create an instance of ConfigurationStore', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + expect(configurationStore).toBeInstanceOf(ConfigurationStore); + }); + + it('should register a provider', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + configurationStore.registerProvider('test', mockedProvider); + expect(mockedProvider.setName).toBeCalledWith('test'); + }); + + it('should return error if provider is not defined when registering', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + expect(() => + // @ts-expect-error Testing error case + configurationStore.registerProvider('test', null), + ).toThrowError('Provider is required'); + }); + + it('should return a configuration from a provider', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + configurationStore.registerProvider('test', mockedProvider); + mockedProvider.getAll.mockResolvedValue({ test: 'test' }); + expect( + configurationStore.getProviderConfiguration('test'), + ).resolves.toEqual({ test: 'test' }); + }); + + it('should return error if provider is not defined when getting configuration', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + expect( + configurationStore.getProviderConfiguration('test'), + ).rejects.toThrowError('Provider test not found'); + }); + + it('should return the provider', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + configurationStore.registerProvider('test', mockedProvider); + expect(configurationStore.getProvider('test')).toBe(mockedProvider); + }); + + it('should return error if provider is not defined when getting provider', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + expect(() => configurationStore.getProvider('test')).toThrowError( + 'Provider test not found', + ); + }); + + it('should return a configuration value by key', async () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + mockedProvider.getAll.mockResolvedValue({ test: 'test' }); + mockedProvider.get.mockResolvedValue('test'); + configurationStore.registerProvider('test', mockedProvider); + expect(await configurationStore.get('test')).toEqual('test'); + }); + + it('should return error if the configuration key is not found', async () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + mockedProvider.getAll.mockResolvedValue({ test: 'test' }); + configurationStore.registerProvider('test', mockedProvider); + mockedProvider.get.mockRejectedValue(new Error('test error')); + + try { + await configurationStore.get('test'); + } catch (error) { + expect(error).toEqual(new Error('test error')); + } + }); + + it('should return all the configuration from the registered providers', () => { + const logger = createMockLogger(); + const configurationStore = new ConfigurationStore(logger); + + configurationStore.registerProvider('test', mockedProvider); + mockedProvider.getAll.mockResolvedValue({ test: 'test' }); + expect(configurationStore.getAll()).resolves.toEqual({ test: 'test' }); + }); +}); diff --git a/plugins/wazuh-core/common/services/configuration/configuration-store.ts b/plugins/wazuh-core/common/services/configuration/configuration-store.ts new file mode 100644 index 0000000000..87c3da894f --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/configuration-store.ts @@ -0,0 +1,117 @@ +import { Logger } from 'opensearch-dashboards/server'; +import { IConfigurationStore } from './types'; +import { IConfigurationProvider } from './configuration-provider'; + +export class ConfigurationStore implements IConfigurationStore { + private readonly providers: Map; + + constructor(private readonly logger: Logger) { + this.providers = new Map(); + } + + registerProvider(name: string, provider: IConfigurationProvider) { + if (!provider) { + // ToDo: Create custom error and implement error handling + throw new Error('Provider is required'); + } + + provider.setName(name); + this.providers.set(name, provider); + } + + getProvider(name: string): IConfigurationProvider { + const provider = this.providers.get(name); + + if (!provider) { + // ToDo: Create custom error and implement error handling + throw new Error(`Provider ${name} not found`); + } + + return provider; + } + + async getProviderConfiguration(key: string): Promise> { + try { + const provider = this.providers.get(key); + + if (!provider) { + // ToDo: Create custom error and implement error handling + throw new Error(`Provider ${key} not found`); + } + + const configuration = await provider.getAll(); + + return configuration; + } catch (error) { + const errorCasted = error as Error; + // ToDo: Create custom error and implement error handling + const enhancedError = new Error( + `Error getting configuration: ${errorCasted?.message}`, + ); + + this.logger.error(enhancedError.message); + throw enhancedError; + } + } + + async setup(_dependencies: any = {}) { + this.logger.debug('Setup'); + } + + async start() { + try { + this.logger.debug('Start'); + } catch (error) { + const errorCasted = error as Error; + + this.logger.error(`Error starting: ${errorCasted?.message}`); + } + } + + async stop() { + this.logger.debug('Stop'); + } + + async get(configName: string): Promise> { + // get all the providers and check if the setting is in any of them + const configuration = await this.getAll(); + + // check if the configName exist in the object + if (Object.keys(configuration).includes(configName)) { + return configuration[configName]; + } + + // ToDo: Create custom error and implement error handling + throw new Error(`Configuration ${configName} not found`); + } + + async getAll(): Promise> { + const providers = [...this.providers.values()]; + const configurations = await Promise.all( + providers.map(async provider => { + try { + return await provider.getAll(); + } catch (error) { + const errorCasted = error as Error; + + this.logger.error( + `Error getting configuration from ${provider.constructor.name}: ${errorCasted?.message}`, + ); + + return {}; + } + }), + ); + const result: Record = {}; + + for (const config of configurations) { + for (const key of Object.keys(config)) { + if (result[key] === undefined) { + result[key] = config[key]; + } + } + } + + return result; + } +} diff --git a/plugins/wazuh-core/common/services/configuration.md b/plugins/wazuh-core/common/services/configuration/configuration.md similarity index 100% rename from plugins/wazuh-core/common/services/configuration.md rename to plugins/wazuh-core/common/services/configuration/configuration.md diff --git a/plugins/wazuh-core/common/services/configuration/configuration.test.ts b/plugins/wazuh-core/common/services/configuration/configuration.test.ts new file mode 100644 index 0000000000..716b0b0c1f --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/configuration.test.ts @@ -0,0 +1,96 @@ +import { createMockLogger } from '../../../test/mocks/logger-mocked'; +import { Configuration } from './configuration'; +import { IConfigurationStore } from './types'; + +const mockConfigurationStore: IConfigurationStore = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + set: jest.fn(), + getProviderConfiguration: jest.fn(), + registerProvider: jest.fn(), + getProvider: jest.fn(), +}; + +describe('Configuration service', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an instance of Configuration', () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + expect(configuration).toBeInstanceOf(Configuration); + }); + + it('should set store', () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + configuration.setStore(mockConfigurationStore); + expect(configuration.store).toBe(mockConfigurationStore); + }); + + it('should return error if store is not provided', () => { + const logger = createMockLogger(); + + try { + // @ts-expect-error Testing error case + new Configuration(logger, null); + } catch (error) { + expect(error).toEqual(new Error('Configuration store is required')); + } + }); + + it('should return a configuration setting value', () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + configuration.get('key'); + expect(mockConfigurationStore.get).toBeCalledWith('key'); + expect(mockConfigurationStore.get).toBeCalledTimes(1); + }); + + it('should return error if the configuration setting key not exists', async () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + mockConfigurationStore.get.mockRejectedValue( + new Error('Configuration setting not found'), + ); + + try { + await configuration.get('key'); + } catch (error) { + expect(mockConfigurationStore.get).toBeCalledWith('key'); + expect(error).toEqual(new Error('Configuration setting not found')); + } + }); + + it('should return all configuration settings', () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + configuration.getAll(); + expect(mockConfigurationStore.getAll).toBeCalledTimes(1); + }); + + it('should return error if the configuration settings not exists', async () => { + const logger = createMockLogger(); + const configuration = new Configuration(logger, mockConfigurationStore); + + mockConfigurationStore.getAll.mockRejectedValue( + new Error('Configuration settings not found'), + ); + + try { + await configuration.getAll(); + } catch (error) { + expect(mockConfigurationStore.getAll).toBeCalledTimes(1); + expect(error).toEqual(new Error('Configuration settings not found')); + } + }); +}); diff --git a/plugins/wazuh-core/common/services/configuration/configuration.ts b/plugins/wazuh-core/common/services/configuration/configuration.ts new file mode 100644 index 0000000000..a2ec6fd7a5 --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/configuration.ts @@ -0,0 +1,43 @@ +import { IConfiguration, IConfigurationStore, ILogger } from './types'; + +export class Configuration implements IConfiguration { + store: IConfigurationStore | null = null; + + constructor( + private readonly logger: ILogger, + store: IConfigurationStore, + ) { + this.setStore(store); + } + + setStore(store: IConfigurationStore) { + this.logger.debug('Setting store'); + this.store = store; + } + + async setup(_dependencies: any = {}) { + this.logger.debug('Setup configuration service'); + + return this.store?.setup(); + } + + async start(_dependencies: any = {}) { + this.logger.debug('Start configuration service'); + + return this.store?.start(); + } + + async stop(_dependencies: any = {}) { + this.logger.debug('Stop configuration service'); + + return this.store?.stop(); + } + + async get(settingKey: string) { + return this.store?.get(settingKey); + } + + async getAll() { + return (await this.store?.getAll()) || {}; + } +} diff --git a/plugins/wazuh-core/common/services/configuration/index.ts b/plugins/wazuh-core/common/services/configuration/index.ts new file mode 100644 index 0000000000..fcf8ce820b --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/index.ts @@ -0,0 +1,3 @@ +export * from './configuration'; +export * from './configuration-provider'; +export * from './configuration-store'; diff --git a/plugins/wazuh-core/common/services/configuration/types.ts b/plugins/wazuh-core/common/services/configuration/types.ts new file mode 100644 index 0000000000..b4e04c253a --- /dev/null +++ b/plugins/wazuh-core/common/services/configuration/types.ts @@ -0,0 +1,160 @@ +import { EpluginSettingType } from '../../constants'; +import { IConfigurationProvider } from './configuration-provider'; + +export interface ILogger { + debug: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; +} + +export interface TConfigurationSettingOptionsPassword { + password: { + dual?: 'text' | 'password' | 'dual'; + }; +} + +export interface TConfigurationSettingOptionsTextArea { + maxRows?: number; + minRows?: number; + maxLength?: number; +} + +export interface TConfigurationSettingOptionsSelect { + select: { text: string; value: any }[]; +} + +export interface TConfigurationSettingOptionsEditor { + editor: { + language: string; + }; +} + +export interface TConfigurationSettingOptionsFile { + file: { + type: 'image'; + extensions?: string[]; + size?: { + maxBytes?: number; + minBytes?: number; + }; + recommended?: { + dimensions?: { + width: number; + height: number; + unit: string; + }; + }; + store?: { + relativePathFileSystem: string; + filename: string; + resolveStaticURL: (filename: string) => string; + }; + }; +} + +export interface TConfigurationSettingOptionsNumber { + number: { + min?: number; + max?: number; + integer?: boolean; + }; +} + +export interface TConfigurationSettingOptionsSwitch { + switch: { + values: { + disabled: { label?: string; value: any }; + enabled: { label?: string; value: any }; + }; + }; +} + +export enum E_PLUGIN_SETTING_TYPE { + TEXT = 'text', + PASSWORD = 'password', + TEXTAREA = 'textarea', + SWITCH = 'switch', + NUMBER = 'number', + EDITOR = 'editor', + SELECT = 'select', + FILEPICKER = 'filepicker', +} + +export interface TConfigurationSetting { + // Define the text displayed in the UI. + title: string; + // Description. + description: string; + // Category. + category: number; + // Type. + type: EpluginSettingType; + // Default value. + defaultValue: any; + /* 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; + // Modify the setting requires running the plugin health check (frontend). + requiresRunningHealthCheck?: boolean; + // Modify the setting requires reloading the browser tab (frontend). + requiresReloadingBrowserTab?: boolean; + // Modify the setting requires restarting the plugin platform to take effect. + requiresRestartingPluginPlatform?: boolean; + // Define options related to the `type`. + options?: + | TConfigurationSettingOptionsEditor + | TConfigurationSettingOptionsFile + | TConfigurationSettingOptionsNumber + | TConfigurationSettingOptionsPassword + | TConfigurationSettingOptionsSelect + | TConfigurationSettingOptionsSwitch + | TConfigurationSettingOptionsTextArea; + store?: { + file: { + // 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 + uiFormTransformChangedInputValue?: (value: any) => any; + // Transform the configuration value or default as initial value for the input in Settings/Configuration + uiFormTransformConfigurationValueToInputValue?: (value: any) => any; + // Transform the input value changed in the form of Settings/Configuration and returned in the `changed` property of the hook useForm + uiFormTransformInputValueToConfigurationValue?: (value: any) => any; + // Validate the value in the form of 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 & { + key: string; +}; +export interface TConfigurationSettingCategory { + title: string; + description?: string; + documentationLink?: string; + renderOrder?: number; +} + +export interface IConfiguration { + setup: () => Promise; + start: () => Promise; + stop: () => Promise; + get: (settingsKey: string) => Promise; + getAll: () => Promise>; +} + +export type TConfigurationSettings = Record; +export type IConfigurationStore = { + getProviderConfiguration: (key: string) => Promise>; + registerProvider: (name: string, provider: IConfigurationProvider) => void; + getProvider: (name: string) => IConfigurationProvider; +} & IConfiguration; diff --git a/plugins/wazuh-core/common/settings-adapter.ts b/plugins/wazuh-core/common/settings-adapter.ts new file mode 100644 index 0000000000..3e1019ba7e --- /dev/null +++ b/plugins/wazuh-core/common/settings-adapter.ts @@ -0,0 +1,155 @@ +import { UiSettingsParams } from 'opensearch-dashboards/public'; +import { schema, Schema } from '@osd/config-schema'; +import { TypeOptions } from 'packages/osd-config-schema/target/types/types'; +import { + EConfigurationProviders, + EpluginSettingType, + SettingCategory, + TPlugginSettingOptionsObjectOf, + TPluginSetting, +} from './constants'; + +/** + * Transform the plugin setting type to the schema type + * @param type + * @param validate + * @returns + */ +const schemaMapper = (setting: TPluginSetting) => { + if (!setting) { + // ToDo: Create custom error and implement error handling + throw new Error('Invalid setting'); + } + + const type = setting.type; + const validate = setting.validate; + let schemaConfig; + const schemaDef = { + validate: validate, + } as TypeOptions; + + switch (type) { + case EpluginSettingType.objectOf: { + if (!setting?.options?.objectOf) { + // ToDo: Create custom error and implement error handling + throw new Error('Invalid objectOf setting'); + } + + const options = setting?.options + ?.objectOf as TPlugginSettingOptionsObjectOf; + const mappedSchema = {}; + + for (const key of Object.keys(options)) { + mappedSchema[key] = schemaMapper(options[key] as TPluginSetting); + } + + const innerSchema = schema.object(mappedSchema); + + schemaConfig = schema.recordOf(schema.string(), innerSchema); + break; + } + + case EpluginSettingType.text: { + schemaConfig = schema.string(schemaDef); + break; + } + + case EpluginSettingType.number: { + schemaConfig = schema.number(schemaDef); + break; + } + + case EpluginSettingType.editor: { + schemaConfig = schema.arrayOf(schema.string(schemaDef)); + break; + } + + case EpluginSettingType.switch: { + schemaConfig = schema.boolean(schemaDef); + break; + } + + default: { + schemaConfig = schema.string(schemaDef); + } + } + + return schemaConfig; +}; + +export const uiSettingsAdapter = ( + settings: Record, +): Record => { + const result: Record = {}; + + for (const [key, setting] of Object.entries(settings)) { + result[key] = { + name: setting.title, + value: setting.defaultValue, + description: setting.description, + category: [ + SettingCategory[setting.category] + .split(' ') + .map((word, index) => (index === 0 ? word.toLowerCase() : word)) + .join(''), + ], + schema: schemaMapper(setting), + }; + } + + return result; +}; + +export const configSettingsAdapter = ( + settings: Record, +) => { + const config = {}; + + for (const [key, setting] of Object.entries(settings)) { + config[key] = schemaMapper(setting); + } + + return config as Schema; +}; + +export const getSettingsByType = ( + settings: Record, + type: EConfigurationProviders, +): Record => { + if (!settings || !type) { + // ToDo: Create custom error and implement error handling + throw new Error('Invalid settings or type'); + } + + const filteredSettings: Record = {}; + + for (const [key, setting] of Object.entries(settings)) { + if (setting.source === type) { + filteredSettings[key] = setting; + } + } + + return filteredSettings; +}; + +export const getUiSettingsDefinitions = ( + settings: Record, +): Record => { + const onlyUiSettings = getSettingsByType( + settings, + EConfigurationProviders.PLUGIN_UI_SETTINGS, + ); + + return uiSettingsAdapter(onlyUiSettings); +}; + +export const getConfigSettingsDefinitions = ( + settings: Record, +) => { + const onlyConfigSettings = getSettingsByType( + settings, + EConfigurationProviders.INITIALIZER_CONTEXT, + ); + + return configSettingsAdapter(onlyConfigSettings); +}; diff --git a/plugins/wazuh-core/public/index.ts b/plugins/wazuh-core/public/index.ts index 6e0b1699b1..4251bf3d46 100644 --- a/plugins/wazuh-core/public/index.ts +++ b/plugins/wazuh-core/public/index.ts @@ -1,8 +1,10 @@ +import { PluginInitializerContext } from 'opensearch-dashboards/public'; import { WazuhCorePlugin } from './plugin'; // This exports static code and TypeScript types, // as well as, OpenSearch Dashboards Platform `plugin()` initializer. -export function plugin() { - return new WazuhCorePlugin(); +export function plugin(initializerContext: PluginInitializerContext) { + return new WazuhCorePlugin(initializerContext); } + export { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index df8e36184b..ea44edb8c1 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -1,15 +1,19 @@ -import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; +import { + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'opensearch-dashboards/public'; +import { ConfigurationStore } from '../common/services/configuration/configuration-store'; +import { EConfigurationProviders } from '../common/constants'; import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; -import { - PLUGIN_SETTINGS, - PLUGIN_SETTINGS_CATEGORIES, -} from '../common/constants'; import { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; import { setChrome, setCore, setUiSettings } from './plugin-services'; +import { UISettingsConfigProvider } from './services/configuration/ui-settings-provider'; +import { InitializerConfigProvider } from './services/configuration/initializer-context-provider'; import * as utils from './utils'; import * as uiComponents from './components'; -import { ConfigurationStore } from './utils/configuration-store'; import { DashboardSecurity } from './services/dashboard-security'; import * as hooks from './hooks'; import { CoreServerSecurity } from './services'; @@ -24,35 +28,43 @@ export class WazuhCorePlugin internal: Record = {}; services: Record = {}; + constructor(private readonly initializerContext: PluginInitializerContext) { + this.services = {}; + this.internal = {}; + } + public async setup(core: CoreSetup): Promise { // No operation logger - const noopLogger = { + + const logger = { info: noop, error: noop, debug: noop, warn: noop, + trace: noop, + fatal: noop, + log: noop, + get: () => logger, }; - const logger = noopLogger; - this.internal.configurationStore = new ConfigurationStore( - logger, - core.http, + this.internal.configurationStore = new ConfigurationStore(logger); + + this.internal.configurationStore.registerProvider( + EConfigurationProviders.INITIALIZER_CONTEXT, + new InitializerConfigProvider(this.initializerContext), + ); + + // register the uiSettins on the configuration store to avoid the use inside of configuration service + this.internal.configurationStore.registerProvider( + EConfigurationProviders.PLUGIN_UI_SETTINGS, + new UISettingsConfigProvider(core.uiSettings), ); + this.services.configuration = new Configuration( logger, this.internal.configurationStore, ); - // Register the plugin settings - for (const [key, value] of Object.entries(PLUGIN_SETTINGS)) { - this.services.configuration.register(key, value); - } - - // Add categories to the configuration - for (const [key, value] of Object.entries(PLUGIN_SETTINGS_CATEGORIES)) { - this.services.configuration.registerCategory({ ...value, id: key }); - } - // Create dashboardSecurity this.services.dashboardSecurity = new DashboardSecurity(logger, core.http); diff --git a/plugins/wazuh-core/public/services/configuration/initializer-context-provider.ts b/plugins/wazuh-core/public/services/configuration/initializer-context-provider.ts new file mode 100644 index 0000000000..14aa858974 --- /dev/null +++ b/plugins/wazuh-core/public/services/configuration/initializer-context-provider.ts @@ -0,0 +1,49 @@ +import { PluginInitializerContext } from 'opensearch-dashboards/public'; +import { CorePluginConfigType } from '../../../server/index'; +import { IConfigurationProvider } from '../../../common/services/configuration/configuration-provider'; +import { EConfigurationProviders } from '../../../common/constants'; + +export class InitializerConfigProvider implements IConfigurationProvider { + private config: CorePluginConfigType = {} as CorePluginConfigType; + private name: string = EConfigurationProviders.INITIALIZER_CONTEXT; + + constructor( + private readonly initializerContext: PluginInitializerContext, + ) { + this.initializeConfig(); + } + + setName(name: string): void { + this.name = name; + } + + getName(): string { + return this.name; + } + + private async initializeConfig(): Promise { + this.config = this.initializerContext.config.get(); + } + + async get( + key: keyof CorePluginConfigType, + ): Promise { + if (!this.config) { + await this.initializeConfig(); + } + + if (!this.config[key]) { + throw new Error(`Key ${key} not found`); + } + + return this.config[key]; + } + + async getAll(): Promise { + if (!this.config) { + await this.initializeConfig(); + } + + return this.config; + } +} diff --git a/plugins/wazuh-core/public/services/configuration/ui-settings-provider.ts b/plugins/wazuh-core/public/services/configuration/ui-settings-provider.ts new file mode 100644 index 0000000000..e86ec1a158 --- /dev/null +++ b/plugins/wazuh-core/public/services/configuration/ui-settings-provider.ts @@ -0,0 +1,48 @@ +import { + IUiSettingsClient, + PublicUiSettingsParams, + UserProvidedValues, +} from 'opensearch-dashboards/public'; +import { IConfigurationProvider } from '../../../common/services/configuration/configuration-provider'; +import { EConfigurationProviders } from '../../../common/constants'; + +export class UISettingsConfigProvider implements IConfigurationProvider { + private name: string = EConfigurationProviders.PLUGIN_UI_SETTINGS; + + constructor(private readonly uiSettings: IUiSettingsClient) {} + + setName(name: string): void { + this.name = name; + } + + getName(): string { + return this.name; + } + + async get(key: string): Promise { + return this.uiSettings.get(key); + } + + async set(key: string, value: any): Promise { + await this.uiSettings.set(key, value); + } + + async getAll() { + const settings = this.uiSettings.getAll(); + const wazuhCoreSettings: Record< + string, + PublicUiSettingsParams & UserProvidedValues + > = {}; + + for (const key in settings) { + if ( + settings[key].category && + settings[key].category.includes('wazuhCore') + ) { + wazuhCoreSettings[key] = settings[key]; + } + } + + return wazuhCoreSettings; + } +} diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index 53509fea1c..8a4cab144a 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -13,8 +13,9 @@ import { } from './services/dashboard-security'; export interface WazuhCorePluginSetup { + _internal: any; utils: { formatUIDate: (date: Date) => string }; - API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; + API_USER_STATUS_RUN_AS: typeof API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurityService; http: HTTPClient; @@ -38,7 +39,7 @@ export interface WazuhCorePluginSetup { export interface WazuhCorePluginStart { utils: { formatUIDate: (date: Date) => string }; - API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; + API_USER_STATUS_RUN_AS: typeof API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurityService; http: HTTPClient; diff --git a/plugins/wazuh-core/public/utils/configuration-store.md b/plugins/wazuh-core/public/utils/configuration-store.md deleted file mode 100644 index 4d714b1ca1..0000000000 --- a/plugins/wazuh-core/public/utils/configuration-store.md +++ /dev/null @@ -1,4 +0,0 @@ -# Description - -The ConfigurationStore implementation for the frontend side stores the configuration in memory of -the browser. diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts deleted file mode 100644 index 993ed745d1..0000000000 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - IConfigurationStore, - Logger, - IConfiguration, -} from '../../common/services/configuration'; - -export class ConfigurationStore implements IConfigurationStore { - private stored: any; - file = ''; - configuration: IConfiguration | null = null; - - constructor( - private readonly logger: Logger, - private readonly http: any, - ) { - this.stored = {}; - } - - setConfiguration(configuration: IConfiguration) { - this.configuration = configuration; - } - - async setup() { - this.logger.debug('Setup'); - } - - async start() { - try { - this.logger.debug('Start'); - - const response = await this.http.get('/api/setup'); - - this.file = response.data.configuration_file; - } catch (error) { - this.logger.error(`Error on start: ${error.message}`); - } - } - - async stop() { - this.logger.debug('Stop'); - } - - private storeGet() { - return this.stored; - } - - private storeSet(value: any) { - this.stored = value; - } - - async get(...settings: string[]): Promise> { - const stored = this.storeGet(); - - return settings.length > 0 - ? Object.fromEntries( - settings.map((settingKey: string) => [ - settingKey, - stored[settingKey], - ]), - ) - : stored; - } - - async set(settings: Record): Promise { - try { - const attributes = this.storeGet(); - const newSettings = { - ...attributes, - ...settings, - }; - - this.logger.debug(`Updating store with ${JSON.stringify(newSettings)}`); - - const response = this.storeSet(newSettings); - - this.logger.debug('Store was updated'); - - return response; - } catch (error) { - this.logger.error(error.message); - throw error; - } - } - - async clear(...settings: string[]): Promise { - try { - const attributes = await this.get(); - const updatedSettings = { - ...attributes, - }; - - for (const setting of settings) { - delete updatedSettings[setting]; - } - - const response = this.storeSet(updatedSettings); - - return response; - } catch (error) { - this.logger.error(error.message); - throw error; - } - } -} diff --git a/plugins/wazuh-core/server/index.ts b/plugins/wazuh-core/server/index.ts index 26e39fdf47..cb6811cb12 100644 --- a/plugins/wazuh-core/server/index.ts +++ b/plugins/wazuh-core/server/index.ts @@ -1,4 +1,10 @@ -import { PluginInitializerContext } from '../../../src/core/server'; +import { schema, TypeOf } from '@osd/config-schema'; +import { + PluginConfigDescriptor, + PluginInitializerContext, +} from '../../../src/core/server'; +import { PLUGIN_SETTINGS } from '../common/constants'; +import { getConfigSettingsDefinitions } from '../common/settings-adapter'; import { WazuhCorePlugin } from './plugin'; // This exports static code and TypeScript types, @@ -8,5 +14,17 @@ export function plugin(initializerContext: PluginInitializerContext) { return new WazuhCorePlugin(initializerContext); } +const initiliazerConfig = getConfigSettingsDefinitions(PLUGIN_SETTINGS); + +export const configSchema = schema.object(initiliazerConfig); +export type CorePluginConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + hosts: true, + }, + schema: configSchema, +}; + +export type { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; export * from './types'; -export type { IConfigurationEnhanced } from './services/enhance-configuration'; diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index 35bd419cab..0fd2b918bc 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -5,20 +5,21 @@ import { Plugin, Logger, } from 'opensearch-dashboards/server'; -import { validate as validateNodeCronInterval } from 'node-cron'; -import { Configuration } from '../common/services/configuration'; import { PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS, PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS, PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER, - PLUGIN_SETTINGS, - PLUGIN_SETTINGS_CATEGORIES, - WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, - WAZUH_DATA_CONFIG_APP_PATH, WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS, WAZUH_PLUGIN_PLATFORM_SETTING_METAFIELDS, WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER, + EConfigurationProviders, + PLUGIN_SETTINGS, } from '../common/constants'; +import { + Configuration, + ConfigurationStore, +} from '../common/services/configuration'; +import { getUiSettingsDefinitions } from '../common/settings-adapter'; import { PluginSetup, WazuhCorePluginSetup, @@ -29,10 +30,11 @@ import { ManageHosts, createDashboardSecurity, ServerAPIClient, - ConfigurationStore, InitializationService, } from './services'; -import { enhanceConfiguration } from './services/enhance-configuration'; +// configuration common +// configuration server +import { InitializerConfigProvider } from './services/configuration'; import { initializationTaskCreatorServerAPIConnectionCompatibility } from './initialization/server-api'; import { initializationTaskCreatorExistTemplate, @@ -63,55 +65,28 @@ export class WazuhCorePlugin ): Promise { this.logger.debug('wazuh_core: Setup'); - this.services.dashboardSecurity = createDashboardSecurity(plugins); + // register the uiSetting to use in the public context (advanced settings) + const uiSettingsDefs = getUiSettingsDefinitions(PLUGIN_SETTINGS); + core.uiSettings.register(uiSettingsDefs); + + this.services.dashboardSecurity = createDashboardSecurity(plugins); this.internal.configurationStore = new ConfigurationStore( this.logger.get('configuration-store'), - { - cache_seconds: WAZUH_CORE_CONFIGURATION_CACHE_SECONDS, - file: WAZUH_DATA_CONFIG_APP_PATH, - }, ); + + // add the initializer context config to the configuration store + this.internal.configurationStore.registerProvider( + EConfigurationProviders.PLUGIN_UI_SETTINGS, + new InitializerConfigProvider(this.initializerContext), + ); + + // create the configuration service to use like a facede pattern this.services.configuration = new Configuration( this.logger.get('configuration'), this.internal.configurationStore, ); - // Enhance configuration service - enhanceConfiguration(this.services.configuration); - - // Register the plugin settings - for (const [key, value] of Object.entries(PLUGIN_SETTINGS)) { - this.services.configuration.register(key, value); - } - - // Add categories to the configuration - for (const [key, value] of Object.entries(PLUGIN_SETTINGS_CATEGORIES)) { - 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 - */ - const setting = this.services.configuration._settings.get( - 'cron.statistics.interval', - ); - - if (!setting.validateUIForm) { - setting.validateUIForm = function (value) { - return this.validate(value); - }; - } - - if (!setting.validate) { - setting.validate = function (value: string) { - return validateNodeCronInterval(value) - ? undefined - : 'Interval is not valid.'; - }; - } - this.services.configuration.setup(); this.services.manageHosts = new ManageHosts( @@ -272,7 +247,7 @@ export class WazuhCorePlugin asScoped: this.services.serverAPIClient.asScoped, }, }, - }; + } as WazuhCorePluginSetup; } public async start(core: CoreStart): Promise { @@ -292,7 +267,7 @@ export class WazuhCorePlugin asScoped: this.services.serverAPIClient.asScoped, }, }, - }; + } as WazuhCorePluginSetup; } public stop() {} diff --git a/plugins/wazuh-core/server/services/configuration-store.test.ts b/plugins/wazuh-core/server/services/configuration-store.test.ts deleted file mode 100644 index 4b8ea8b123..0000000000 --- a/plugins/wazuh-core/server/services/configuration-store.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -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 | createFile - ${'Ensure the file is created'} | ${'config.yml'} | ${false} - ${'Ensure the file is created. Already exist'} | ${'config.yml'} | ${true} - `('$title', ({ file, createFile }) => { - const configurationStore = new ConfigurationStore(createMockLogger(), { - file, - cache_seconds: 10, - }); - - // Mock configuration - configurationStore.setConfiguration({ - groupSettingsByCategory: () => [], - }); - - if (createFile) { - fs.writeFileSync(configurationStore.file, '', { encoding: 'utf8' }); - } - expect(fs.existsSync(configurationStore.file)).toBe(createFile); - 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: { configurableManaged: true } } }], - ['key2', { store: { file: { configurableManaged: 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 deleted file mode 100644 index 73511a515b..0000000000 --- a/plugins/wazuh-core/server/services/configuration-store.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { Logger } from 'opensearch-dashboards/server'; -import { - IConfigurationStore, - IConfiguration, -} from '../../common/services/configuration'; -import { CacheTTL } from '../../common/services/cache'; -import fs from 'fs'; -import yml from 'js-yaml'; -import { createDirectoryIfNotExists } from './filesystem'; -import { webDocumentationLink } from '../../common/services/web_documentation'; -import { TPluginSettingWithKey } from '../../common/constants'; -import path from 'path'; - -interface IConfigurationStoreOptions { - cache_seconds: number; - file: string; -} - -interface IStoreGetOptions { - ignoreCache: boolean; -} - -export class ConfigurationStore implements IConfigurationStore { - private configuration: IConfiguration; - private _cache: CacheTTL; - private _cacheKey: string; - private _fileEncoding: string = 'utf-8'; - file: string = ''; - constructor(private logger: Logger, options: IConfigurationStoreOptions) { - this.file = options.file; - - if (!this.file) { - const error = new Error('File is not defined'); - this.logger.error(error.message); - throw error; - } - - /* The in-memory ttl cache is used to reduce the access to the persistence */ - this._cache = new CacheTTL(this.logger.get('cache'), { - ttlSeconds: options.cache_seconds, - }); - this._cacheKey = 'configuration'; - } - private readContentConfigurationFile() { - 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.file}]`); - fs.writeFileSync(this.file, content, { - encoding: this._fileEncoding, - ...options, - }); - this.logger.debug(`Wrote file [${this.file}]`); - } - private readConfigurationFile() { - const content = this.readContentConfigurationFile(); - const contentAsObject = yml.load(content) || {}; // Ensure the contentAsObject is an object - this.logger.debug( - `Content file [${this.file}]: ${JSON.stringify(contentAsObject)}`, - ); - // Transform value for key in the configuration file - return Object.fromEntries( - Object.entries(contentAsObject).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]) => { - const setting = this.configuration._settings.get(key); - return setting?.store?.file?.configurableManaged; - }) - .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 match = accum.match(re); - - // Remove the setting if value is null - if (value === null) { - return accum.replace(re, ''); - } - - const formattedValue = formatSettingValueToFile(value); - const updateSettingEntry = `${key}: ${formattedValue}`; - 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); - return pluginSettingsConfigurableFile; - } - setConfiguration(configuration: IConfiguration) { - this.configuration = configuration; - } - 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; - } - private async storeSet(attributes: any) { - const configuration = await this.updateConfigurationFile(attributes); - this._cache.set(attributes, this._cacheKey); - return configuration; - } - 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('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 pluginSettingsConfigurationFileGroupByCategory = - this.configuration.groupSettingsByCategory( - null, - setting => setting?.store?.file, - ); - - const pluginSettingsConfiguration = - pluginSettingsConfigurationFileGroupByCategory - .map(({ category: categorySetting, settings }) => { - const category = printSettingCategory(categorySetting); - - 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].join('\n#\n')}\n\n`; - } - ensureConfigurationFileIsCreated() { - try { - this.logger.debug( - `Ensuring the configuration file is created [${this.file}]`, - ); - const dirname = path.resolve(path.dirname(this.file)); - createDirectoryIfNotExists(dirname); - if (!fs.existsSync(this.file)) { - this.writeContentConfigurationFile( - this.getDefaultConfigurationFileContent(), - { - mode: 0o600, - }, - ); - this.logger.info(`Configuration file was created [${this.file}]`); - } else { - this.logger.debug(`Configuration file exists [${this.file}]`); - } - } catch (error) { - const enhancedError = new Error( - `Error ensuring the configuration file is created: ${error.message}`, - ); - this.logger.error(enhancedError.message); - throw enhancedError; - } - } - async setup() { - this.logger.debug('Setup'); - } - async start() { - try { - this.logger.debug('Start'); - this.ensureConfigurationFileIsCreated(); - } catch (error) { - this.logger.error(`Error starting: ${error.message}`); - } - } - async stop() { - this.logger.debug('Stop'); - } - async get(...settings: string[]): Promise { - try { - const storeGetOptions = - settings.length && typeof settings[settings.length - 1] !== 'string' - ? settings.pop() - : {}; - this.logger.debug( - `Getting settings: [${JSON.stringify( - settings, - )}] with store get options [${JSON.stringify(storeGetOptions)}]`, - ); - const stored = await this.storeGet(storeGetOptions); - return settings.length - ? settings.reduce( - (accum, settingKey: string) => ({ - ...accum, - [settingKey]: stored?.[settingKey], - }), - {}, - ) - : stored; - } catch (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'); - const stored = await this.storeGet({ ignoreCache: true }); - - const newSettings = { - ...stored, - ...settings, - }; - this.logger.debug(`Updating with ${JSON.stringify(newSettings)}`); - await this.storeSet(newSettings); - this.logger.debug('Configuration was updated'); - return settings; - } catch (error) { - const enhancedError = new Error( - `Error setting configuration: ${error.message}`, - ); - this.logger.error(enhancedError.message); - throw enhancedError; - } - } - async clear(...settings: string[]): Promise { - 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] = newSettings[setting] = null; - }); - await this.storeSet(newSettings); - return removedSettings; - } catch (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} - # ?{{settingDefaultBlock} || {{settingKey}: {settingDefaultValue}}} - */ - return [ - splitDescription(setting.description), - setting?.store?.file?.defaultBlock || - `# ${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'); -} diff --git a/plugins/wazuh-core/server/services/configuration/index.ts b/plugins/wazuh-core/server/services/configuration/index.ts new file mode 100644 index 0000000000..5afc1997b5 --- /dev/null +++ b/plugins/wazuh-core/server/services/configuration/index.ts @@ -0,0 +1 @@ +export * from './initializer-context-provider'; diff --git a/plugins/wazuh-core/server/services/configuration/initializer-context-provider.ts b/plugins/wazuh-core/server/services/configuration/initializer-context-provider.ts new file mode 100644 index 0000000000..abff315026 --- /dev/null +++ b/plugins/wazuh-core/server/services/configuration/initializer-context-provider.ts @@ -0,0 +1,53 @@ +import { PluginInitializerContext } from 'opensearch-dashboards/server'; +import { first } from 'rxjs/operators'; +import { CorePluginConfigType } from '../../index'; +import { IConfigurationProvider } from '../../../common/services/configuration/configuration-provider'; +import { EConfigurationProviders } from '../../../common/constants'; + +export class InitializerConfigProvider implements IConfigurationProvider { + private config: CorePluginConfigType = {} as CorePluginConfigType; + private name: string = EConfigurationProviders.INITIALIZER_CONTEXT; + + constructor( + private readonly initializerContext: PluginInitializerContext, + ) { + this.initializeConfig(); + } + + private async initializeConfig(): Promise { + const config$ = + this.initializerContext.config.create(); + + this.config = await config$.pipe(first()).toPromise(); + } + + setName(name: string): void { + this.name = name; + } + + getName(): string { + return this.name; + } + + async get( + key: keyof CorePluginConfigType, + ): Promise { + if (!this.config) { + await this.initializeConfig(); + } + + if (!this.config[key]) { + throw new Error(`Key ${key} not found`); + } + + return this.config[key]; + } + + async getAll(): Promise { + if (!this.config) { + await this.initializeConfig(); + } + + return this.config; + } +} diff --git a/plugins/wazuh-core/server/services/enhance-configuration.test.ts b/plugins/wazuh-core/server/services/enhance-configuration.test.ts deleted file mode 100644 index b5c3f60106..0000000000 --- a/plugins/wazuh-core/server/services/enhance-configuration.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { enhanceConfiguration } from './enhance-configuration'; -import { Configuration } from '../../common/services/configuration'; - -const noop = () => undefined; -const mockLogger = { - debug: noop, - info: noop, - warn: noop, - error: noop, -}; - -const mockConfigurationStore = { - get: jest.fn(), - set: jest.fn(), - setConfiguration(configuration) { - this.configuration = configuration; - }, -}; - -const configuration = new Configuration(mockLogger, mockConfigurationStore); -enhanceConfiguration(configuration); - -[ - { key: 'customization.enabled', type: 'switch', defaultValue: true }, - { - key: 'customization.test', - type: 'text', - defaultValueIfNotSet: 'Default customization value', - }, -].forEach(({ key, ...rest }) => configuration.register(key, rest)); - -describe('enhanceConfiguration', () => { - it('ensure the .getCustomizationSetting is defined and is a function', () => { - expect(configuration.getCustomizationSetting).toBeDefined(); - expect(typeof configuration.getCustomizationSetting).toBe('function'); - }); -}); - -describe('enhanceConfiguration', () => { - it.each` - enabledCustomization | customize | expectedSettingValue - ${true} | ${'Customized'} | ${'Customized'} - ${true} | ${''} | ${'Default customization value'} - ${false} | ${'Customized'} | ${'Default customization value'} - `( - 'call to .getCustomizationSetting returns the expected value', - async ({ enabledCustomization, customize, expectedSettingValue }) => { - mockConfigurationStore.get.mockImplementation((...settings) => { - return Object.fromEntries( - settings.map(key => { - if (key === 'customization.enabled') { - return [key, enabledCustomization]; - } - return [key, customize]; - }), - ); - }); - expect( - await configuration.getCustomizationSetting('customization.test'), - ).toEqual({ 'customization.test': expectedSettingValue }); - }, - ); -}); diff --git a/plugins/wazuh-core/server/services/enhance-configuration.ts b/plugins/wazuh-core/server/services/enhance-configuration.ts deleted file mode 100644 index 329ff8fe47..0000000000 --- a/plugins/wazuh-core/server/services/enhance-configuration.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { IConfiguration } from '../../common/services/configuration'; -/** - * Returns the default value if not set when the setting is an empty string - * @param settingKey plugin setting - * @param value value of the plugin setting - * @returns - */ -function resolveEmptySetting( - configurationService: IConfiguration, - settingKey: string, - value: unknown, -) { - return typeof value === 'string' && - value.length === 0 && - configurationService._settings.get(settingKey).defaultValueIfNotSet - ? configurationService.getSettingValueIfNotSet(settingKey) - : value; -} - -export interface IConfigurationEnhanced extends IConfiguration { - getCustomizationSetting(...settingKeys: string[]): { [key: string]: any }; -} - -function getCustomizationSetting( - configuration: IConfigurationEnhanced, - currentConfiguration: { [key: string]: any }, - settingKey: string, -) { - const isCustomizationEnabled = currentConfiguration['customization.enabled']; - const defaultValue = configuration.getSettingValueIfNotSet(settingKey); - - if ( - isCustomizationEnabled && - settingKey.startsWith('customization') && - settingKey !== 'customization.enabled' - ) { - return typeof currentConfiguration[settingKey] !== 'undefined' - ? resolveEmptySetting( - configuration, - settingKey, - currentConfiguration[settingKey], - ) - : defaultValue; - } else { - return defaultValue; - } -} - -export function enhanceConfiguration(configuration: IConfiguration) { - /** - * 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', - ...settingKeys, - ); - - return Object.fromEntries( - settingKeys.map(settingKey => [ - settingKey, - getCustomizationSetting(this, currentConfiguration, settingKey), - ]), - ); - }; -} diff --git a/plugins/wazuh-core/server/services/index.ts b/plugins/wazuh-core/server/services/index.ts index 1cf09c22bf..d5270c9879 100644 --- a/plugins/wazuh-core/server/services/index.ts +++ b/plugins/wazuh-core/server/services/index.ts @@ -10,7 +10,7 @@ * Find more information about this on the LICENSE file. */ -export * from './configuration-store'; +export * from './configuration'; export * from './cookie'; export * from './filesystem'; export * from './manage-hosts'; diff --git a/plugins/wazuh-core/server/services/manage-hosts.ts b/plugins/wazuh-core/server/services/manage-hosts.ts index a1db94593c..78cb82c1fb 100644 --- a/plugins/wazuh-core/server/services/manage-hosts.ts +++ b/plugins/wazuh-core/server/services/manage-hosts.ts @@ -11,9 +11,9 @@ */ import { Logger } from 'opensearch-dashboards/server'; import { IConfiguration } from '../../common/services/configuration'; -import { ServerAPIClient } from './server-api-client'; import { API_USER_STATUS_RUN_AS } from '../../common/api-user-status-run-as'; import { HTTP_STATUS_CODES } from '../../common/constants'; +import { ServerAPIClient } from './server-api-client'; interface IAPIHost { id: string; @@ -28,7 +28,7 @@ interface IAPIHostRegistry { node: string | null; status: string; cluster: string; - allow_run_as: API_USER_STATUS_RUN_AS; + allowRunAs: API_USER_STATUS_RUN_AS; } /** @@ -43,12 +43,17 @@ interface IAPIHostRegistry { */ export class ManageHosts { public serverAPIClient: ServerAPIClient | null = null; - private cacheRegistry: Map = new Map(); - constructor(private logger: Logger, private configuration: IConfiguration) {} + private readonly cacheRegistry = new Map(); + + constructor( + private readonly logger: Logger, + private readonly configuration: IConfiguration, + ) {} setServerAPIClient(client: ServerAPIClient) { this.serverAPIClient = client; } + /** * Exclude fields from an API host data * @param host @@ -56,15 +61,19 @@ export class ManageHosts { * @returns */ private filterAPIHostData(host: IAPIHost, exclude: string[]) { - return exclude?.length - ? Object.entries(host).reduce( - (accum, [key, value]) => ({ - ...accum, - ...(!exclude.includes(key) ? { [key]: value } : {}), - }), - {}, - ) - : host; + if (!exclude?.length) { + return host; + } + + const filteredHost: Partial = {}; + + for (const key in host) { + if (!exclude.includes(key)) { + filteredHost[key] = host[key]; + } + } + + return filteredHost; } /** @@ -72,28 +81,40 @@ export class ManageHosts { */ async get( hostID?: string, - options: { excludePassword: boolean } = { excludePassword: false }, + options?: { excludePassword: boolean }, ): Promise { try { - hostID - ? this.logger.debug(`Getting API connection with ID [${hostID}]`) - : this.logger.debug('Getting API connections'); + const { excludePassword = false } = options || {}; + + if (hostID) { + this.logger.debug(`Getting API connection with ID [${hostID}]`); + } else { + 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); + const host = hosts[hostID]; + if (host) { this.logger.debug(`API connection with ID [${hostID}] found`); + return this.filterAPIHostData( host, - options.excludePassword ? ['password'] : undefined, + excludePassword ? ['password'] : undefined, ); } + const APIConnectionNotFound = `API connection with ID [${hostID}] not found`; + this.logger.debug(APIConnectionNotFound); throw new Error(APIConnectionNotFound); } - return hosts.map(host => + + return Object.values(hosts).map(host => this.filterAPIHostData( host, options.excludePassword ? ['password'] : undefined, @@ -112,16 +133,19 @@ export class ManageHosts { * @param {Object} response * API entries */ - async getEntries( - options: { excludePassword: boolean } = { excludePassword: false }, - ) { + async getEntries(options?: { excludePassword: boolean }) { try { this.logger.debug('Getting the API connections'); + const hosts = (await this.get(undefined, options)) as IAPIHost[]; + this.logger.debug('Getting registry'); - const registry = Object.fromEntries([...this.cacheRegistry.entries()]); + + const registry = Object.fromEntries(this.cacheRegistry.entries()); + return hosts.map(host => { const { id } = host; + return { ...host, cluster_info: registry[id] }; }); } catch (error) { @@ -135,7 +159,7 @@ export class ManageHosts { } /** - * Get the cluster info and allow_run_as values for the API host and store into the registry cache + * Get the cluster info and allowRunAs values for the API host and store into the registry cache * @param host * @returns */ @@ -143,6 +167,7 @@ export class ManageHosts { host: IAPIHost, ): Promise { const apiHostID = host.id; + this.logger.debug(`Getting registry data from host [${apiHostID}]`); // Get cluster info data @@ -150,7 +175,7 @@ export class ManageHosts { node = null, status = 'disabled', cluster = 'Disabled', - allow_run_as = API_USER_STATUS_RUN_AS.ALL_DISABLED; + allowRunAs = API_USER_STATUS_RUN_AS.ALL_DISABLED; try { const responseAgents = await this.serverAPIClient.asInternalUser.request( @@ -164,10 +189,8 @@ export class ManageHosts { manager = responseAgents.data.data.affected_items[0].manager; } - // Get allow_run_as - if (!host.run_as) { - allow_run_as = API_USER_STATUS_RUN_AS.HOST_DISABLED; - } else { + // Get allowRunAs + if (host.run_as) { const responseAllowRunAs = await this.serverAPIClient.asInternalUser.request( 'GET', @@ -175,12 +198,15 @@ export class ManageHosts { {}, { apiHostID }, ); + if (this.isServerAPIClientResponseOk(responseAllowRunAs)) { - allow_run_as = responseAllowRunAs.data.data.affected_items[0] + allowRunAs = responseAllowRunAs.data.data.affected_items[0] .allow_run_as ? API_USER_STATUS_RUN_AS.ENABLED : API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED; } + } else { + allowRunAs = API_USER_STATUS_RUN_AS.HOST_DISABLED; } const responseClusterStatus = @@ -191,7 +217,10 @@ export class ManageHosts { { apiHostID }, ); - if (this.isServerAPIClientResponseOk(responseClusterStatus) && responseClusterStatus.data?.data?.enabled === 'yes') { + if ( + this.isServerAPIClientResponseOk(responseClusterStatus) && + responseClusterStatus.data?.data?.enabled === 'yes' + ) { status = 'enabled'; const responseClusterLocal = @@ -207,16 +236,22 @@ export class ManageHosts { cluster = responseClusterLocal.data.data.affected_items[0].cluster; } } - } catch (error) {} + } catch { + this.logger.debug( + `Error getting the cluster info and allowRunAs values for the API host [${apiHostID}]`, + ); + } const data = { manager, node, status, cluster, - allow_run_as, + allowRunAs, }; + this.updateRegistryByHost(apiHostID, data); + return data; } @@ -228,11 +263,14 @@ export class ManageHosts { async start() { try { this.logger.debug('Start'); + const hosts = (await this.get(undefined, { excludePassword: true, })) as IAPIHost[]; - if (!hosts.length) { + + if (hosts.length === 0) { this.logger.debug('No hosts found. Skip.'); + return; } @@ -250,22 +288,31 @@ export class ManageHosts { private getRegistryByHost(hostID: string) { this.logger.debug(`Getting cache for API host [${hostID}]`); + const result = this.cacheRegistry.get(hostID); + this.logger.debug(`Get cache for APIhost [${hostID}]`); + return result; } private updateRegistryByHost(hostID: string, data: any) { this.logger.debug(`Updating cache for APIhost [${hostID}]`); + const result = this.cacheRegistry.set(hostID, data); + this.logger.debug(`Updated cache for APIhost [${hostID}]`); + return result; } private deleteRegistryByHost(hostID: string) { this.logger.debug(`Deleting cache for API host [${hostID}]`); + const result = this.cacheRegistry.delete(hostID); + this.logger.debug(`Deleted cache for API host [${hostID}]`); + return result; } @@ -278,16 +325,19 @@ export class ManageHosts { this.logger.debug(`Checking if the API host [${apiId}] can use the run_as`); const registryHost = this.getRegistryByHost(apiId); + if (!registryHost) { throw new Error( `API host with ID [${apiId}] was not found in the registry. This could be caused by a problem getting and storing the registry data or the API host was removed.`, ); } - if (registryHost.allow_run_as === API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED) { + + if (registryHost.allowRunAs === API_USER_STATUS_RUN_AS.USER_NOT_ALLOWED) { throw new Error( `API host with host ID [${apiId}] misconfigured. The configurated API user is not allowed to use [run_as]. Allow it in the API user configuration or set [run_as] host setting with [false] value.`, ); } - return registryHost.allow_run_as === API_USER_STATUS_RUN_AS.ENABLED; + + return registryHost.allowRunAs === API_USER_STATUS_RUN_AS.ENABLED; } } diff --git a/plugins/wazuh-core/test/mocks/logger-mocked.ts b/plugins/wazuh-core/test/mocks/logger-mocked.ts new file mode 100644 index 0000000000..848cf55b53 --- /dev/null +++ b/plugins/wazuh-core/test/mocks/logger-mocked.ts @@ -0,0 +1,17 @@ +const createMockLogger = () => { + const noop = () => {}; + const logger = { + info: noop, + error: noop, + debug: noop, + warn: noop, + trace: noop, + fatal: noop, + log: noop, + get: () => logger, + }; + + return logger; +}; + +export { createMockLogger };