diff --git a/plugins/main/public/components/overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern.tsx b/plugins/main/public/components/overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern.tsx index 5e47789b24..c5c819eab2 100644 --- a/plugins/main/public/components/overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern.tsx @@ -28,7 +28,7 @@ async function checkExistenceIndices(indexPatternId: string) { async function createIndexPattern(indexPattern, fields: any) { try { - await SavedObject.createSavedObjectIndexPattern( + await SavedObject.createSavedObject( 'index-pattern', indexPattern, { diff --git a/plugins/main/public/utils/vulnerabibility-states-fields.json b/plugins/main/public/utils/vulnerabibility-states-fields.json new file mode 100644 index 0000000000..216cfdc68f --- /dev/null +++ b/plugins/main/public/utils/vulnerabibility-states-fields.json @@ -0,0 +1,515 @@ +[ + { + "count": 0, + "name": "_index", + "type": "string", + "esTypes": ["_index"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": false + }, + { + "count": 0, + "name": "_source", + "type": "_source", + "esTypes": ["_source"], + "scripted": false, + "searchable": false, + "aggregatable": false, + "readFromDocValues": false + }, + { + "count": 0, + "name": "agent.build.original", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "agent.ephemeral_id", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "agent.id", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "agent.name", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "agent.type", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "agent.version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.family", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.full", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.full.text", + "type": "string", + "esTypes": ["text"], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "subType": { "multi": { "parent": "host.os.full" } } + }, + { + "count": 0, + "name": "host.os.kernel", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.name", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.name.text", + "type": "string", + "esTypes": ["text"], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "subType": { "multi": { "parent": "host.os.name" } } + }, + { + "count": 0, + "name": "host.os.platform", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.type", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "host.os.version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "message", + "type": "string", + "esTypes": ["text"], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false + }, + { + "count": 0, + "name": "package.architecture", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.build_version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.checksum", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.description", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.install_scope", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.installed", + "type": "date", + "esTypes": ["date"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.license", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.name", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.path", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.reference", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.size", + "type": "number", + "esTypes": ["long"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.type", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "package.version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "tags", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.category", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.classification", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.description", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.description.text", + "type": "string", + "esTypes": ["text"], + "scripted": false, + "searchable": true, + "aggregatable": false, + "readFromDocValues": false, + "subType": { "multi": { "parent": "vulnerability.description" } } + }, + { + "count": 0, + "name": "vulnerability.detected_at", + "type": "date", + "esTypes": ["date"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.enumeration", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.id", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.published_at", + "type": "date", + "esTypes": ["date"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.reference", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.report_id", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.scanner.vendor", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.score.base", + "type": "number", + "esTypes": ["float"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.score.environmental", + "type": "number", + "esTypes": ["float"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.score.temporal", + "type": "number", + "esTypes": ["float"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.score.version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "vulnerability.severity", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "wazuh.cluster.name", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "wazuh.cluster.node", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + }, + { + "count": 0, + "name": "wazuh.schema.version", + "type": "string", + "esTypes": ["keyword"], + "scripted": false, + "searchable": true, + "aggregatable": true, + "readFromDocValues": true + } +] diff --git a/plugins/main/server/lib/initialization/index-patterns.ts b/plugins/main/server/lib/initialization/index-patterns.ts new file mode 100644 index 0000000000..230cc71d5a --- /dev/null +++ b/plugins/main/server/lib/initialization/index-patterns.ts @@ -0,0 +1,217 @@ +import { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; + +interface ensureIndexPatternExistenceContextTask { + indexPatternID: string; + options: any; +} + +interface ensureIndexPatternExistenceContextTaskWithCondifurationSetting + extends ensureIndexPatternExistenceContextTask { + configurationSettingKey: string; +} + +const decoratorCheckIsEnabled = fn => { + return async ( + ctx, + { + configurationSettingKey, + ...ctxTask + }: ensureIndexPatternExistenceContextTaskWithCondifurationSetting, + ) => { + if (await ctx.configuration.get(configurationSettingKey)) { + await fn(ctx, ctxTask); + } else { + ctx.logger.info(`Check [${configurationSettingKey}]: disabled. Skipped.`); + } + }; +}; + +export const ensureIndexPatternExistence = async ( + { logger, savedObjectsClient, indexPatternsClient }, + { indexPatternID, options = {} }: ensureIndexPatternExistenceContextTask, +) => { + try { + logger.debug( + `Checking existence of index pattern with ID [${indexPatternID}]`, + ); + const response = await savedObjectsClient.get( + 'index-pattern', + indexPatternID, + ); + logger.debug(`Index pattern with ID [${indexPatternID}] exists`); + return response; + } catch (e) { + // Get not found saved object + if (e?.output?.statusCode === 404) { + // Create index pattern + logger.info(`Index pattern with ID [${indexPatternID}] does not exist`); + return await createIndexPattern( + { logger, savedObjectsClient, indexPatternsClient }, + indexPatternID, + options, + ); + } else { + throw new Error( + `index pattern with ID [${indexPatternID}] existence could not be checked due to: ${e.message}`, + ); + } + } +}; + +async function getFieldMappings( + { logger, indexPatternsClient }, + indexPatternTitle: string, +) { + logger.debug(`Getting index pattern fields for title [${indexPatternTitle}]`); + + // https://github.com/opensearch-project/OpenSearch-Dashboards/blob/2.16.0/src/plugins/data/server/index_patterns/routes.ts#L74 + const fields = await indexPatternsClient.getFieldsForWildcard({ + pattern: indexPatternTitle, + // meta_fields=_source&meta_fields=_id&meta_fields=_type&meta_fields=_index&meta_fields=_score + metaFields: ['_source', '_id', '_type', '_index', '_score'], + }); + logger.debug( + `Fields for index pattern with title [${indexPatternTitle}]: ${JSON.stringify( + fields, + )}`, + ); + return fields; +} + +async function createIndexPattern( + { logger, savedObjectsClient, indexPatternsClient }, + indexPatternID, + options: { + fieldsNoIndices?: any; + savedObjectOverwrite?: { [key: string]: any }; + } = {}, +) { + try { + let fields; + try { + fields = await getFieldMappings( + { logger, indexPatternsClient }, + indexPatternID, + ); + } catch (e) { + if (e?.output?.statusCode === 404 && options.fieldsNoIndices) { + const message = `Fields for index pattern with ID [${indexPatternID}] could not be obtained. This could indicate there are not matching indices because they were not generated or there is some error in the process that generates and indexes that data. The index pattern will be created with a set of pre-defined fields.`; + logger.warn(message); + fields = options.fieldsNoIndices; + } else { + throw e; + } + } + + const savedObjectData = { + title: indexPatternID, + fields: JSON.stringify(fields), + ...(options?.savedObjectOverwrite || {}), + }; + + logger.debug( + `Creating index pattern with ID [${indexPatternID}] title [${savedObjectData.title}]`, + ); + + const response = await savedObjectsClient.create( + 'index-pattern', + savedObjectData, + { + id: indexPatternID, + overwrite: true, + refresh: true, + }, + ); + + const indexPatternCreatedMessage = `Created index pattern with ID [${response.id}] title [${response.attributes.title}]`; + logger.info(indexPatternCreatedMessage); + return response; + } catch (e) { + throw new Error( + `index pattern with ID [${indexPatternID}] could not be created due to: ${e.message}`, + ); + } +} + +function getSavedObjectsClient(ctx: any, scope) { + switch (scope) { + case 'internal': + return ctx.core.savedObjects.createInternalRepository(); + break; + case 'user': + return ctx.core.savedObjects.savedObjectsStart.getScopedClient( + ctx.request, + ); + break; + default: + break; + } +} + +function getIndexPatternsClient(ctx: any, scope) { + switch (scope) { + case 'internal': + return new IndexPatternsFetcher( + ctx.core.opensearch.legacy.client.callAsInternalUser, + ); + break; + case 'user': + return new IndexPatternsFetcher( + ctx.core.opensearch.legacy.client.callAsCurrentUser, + ); + break; + default: + break; + } +} + +function getIndexPatternID(ctx: any, scope: string, rest: any) { + switch (scope) { + case 'internal': + return rest.getIndexPatternID(ctx); + break; + case 'user': + return ctx.getIndexPatternID(ctx); + break; + default: + break; + } +} + +export const initializationTaskCreatorIndexPattern = ({ + taskName, + options = {}, + configurationSettingKey, + ...rest +}: { + getIndexPatternID: (ctx: any) => Promise; + taskName: string; + options: {}; + configurationSettingKey: string; +}) => ({ + name: taskName, + async run(ctx) { + let indexPatternID; + try { + ctx.logger.debug('Starting index pattern saved object'); + indexPatternID = await getIndexPatternID(ctx, ctx.scope, rest); + + // Get clients depending on the scope + const savedObjectsClient = getSavedObjectsClient(ctx, ctx.scope); + const indexPatternsClient = getIndexPatternsClient(ctx, ctx.scope); + + return await ensureIndexPatternExistence( + { ...ctx, indexPatternsClient, savedObjectsClient }, + { + indexPatternID, + options, + configurationSettingKey, + }, + ); + } catch (e) { + const message = `Error initilizating index pattern with ID [${indexPatternID}]: ${e.message}`; + ctx.logger.error(message); + throw new Error(message); + } + }, +}); diff --git a/plugins/main/server/lib/initialization/index.ts b/plugins/main/server/lib/initialization/index.ts new file mode 100644 index 0000000000..e7712b3abc --- /dev/null +++ b/plugins/main/server/lib/initialization/index.ts @@ -0,0 +1,3 @@ +export * from './index-patterns'; +export * from './settings'; +export * from './templates'; diff --git a/plugins/main/server/lib/initialization/settings.ts b/plugins/main/server/lib/initialization/settings.ts new file mode 100644 index 0000000000..84c0b83a61 --- /dev/null +++ b/plugins/main/server/lib/initialization/settings.ts @@ -0,0 +1,182 @@ +/* + * Wazuh app - Check PluginPlatform settings service + * + * 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. + * + */ + +import _ from 'lodash'; + +const decoratorCheckIsEnabled = fn => { + return async ( + ctx, + { + configurationSetting, + ...ctxTask + }: { key: string; value: any; configurationSetting: string }, + ) => { + if (await ctx.configuration.get(configurationSetting)) { + await fn(ctx, ctxTask); + } else { + ctx.logger.info(`Check [${configurationSetting}]: disabled. Skipped.`); + } + }; +}; + +export const checkPluginPlatformSettings = decoratorCheckIsEnabled( + async ( + { logger, uiSettingsClient }, + { + key: pluginPlatformSettingName, + value: defaultAppValue, + }: { key: string; value: any }, + ) => { + logger.debug(`Getting setting [${pluginPlatformSettingName}]...`); + const valuePluginPlatformSetting = await uiSettingsClient.get( + pluginPlatformSettingName, + ); + const settingsAreDifferent = !_.isEqual( + valuePluginPlatformSetting, + defaultAppValue, + ); + logger.debug( + `Check setting [${pluginPlatformSettingName}]: ${stringifySetting( + valuePluginPlatformSetting, + )}`, + ); + logger.debug( + `App setting [${pluginPlatformSettingName}]: ${stringifySetting( + defaultAppValue, + )}`, + ); + logger.debug( + `Setting mismatch [${pluginPlatformSettingName}]: ${ + settingsAreDifferent ? 'yes' : 'no' + }`, + ); + logger.debug( + `Setting is user defined [${pluginPlatformSettingName}]: ${ + valuePluginPlatformSetting ? 'yes' : 'no' + }`, + ); + if (!valuePluginPlatformSetting || settingsAreDifferent) { + logger.debug(`Updating [${pluginPlatformSettingName}] setting...`); + await updateSetting( + uiSettingsClient, + pluginPlatformSettingName, + defaultAppValue, + ); + logger.info( + `Updated [${pluginPlatformSettingName}] setting to: ${stringifySetting( + defaultAppValue, + )}`, + ); + } + }, +); + +async function updateSetting( + uiSettingsClient, + pluginPlatformSettingName, + defaultAppValue, + retries = 3, +) { + return await uiSettingsClient + .set(pluginPlatformSettingName, defaultAppValue) + .catch(async error => { + if (retries > 0) { + return await updateSetting( + uiSettingsClient, + pluginPlatformSettingName, + defaultAppValue, + --retries, + ); + } + throw error; + }); +} + +function stringifySetting(setting: any) { + try { + return JSON.stringify(setting); + } catch (error) { + return setting; + } +} + +function getSavedObjectsClient(ctx: any, scope) { + switch (scope) { + case 'internal': + return ctx.core.savedObjects.createInternalRepository(); + break; + case 'user': + return ctx.core.savedObjects.savedObjectsStart.getScopedClient( + ctx.request, + ); + break; + default: + break; + } +} + +function getUiSettingsClient(ctx, scope, client) { + switch (scope) { + case 'internal': + return ctx.core.uiSettings.asScopedToClient(client); + break; + + case 'user': + return ctx.core.uiSettings.uiSettingsStart.asScopedToClient(client); + break; + + default: + break; + } +} + +export const initializationTaskCreatorSetting = ( + setting: { key: string; value: any; configurationSetting: string }, + taskName: string, +) => ({ + name: taskName, + async run(ctx) { + try { + ctx.logger.debug('Starting setting'); + + // Get clients depending on the scope + const savedObjectsClient = getSavedObjectsClient(ctx, ctx.scope); + const uiSettingsClient = getUiSettingsClient( + ctx, + ctx.scope, + savedObjectsClient, + ); + + const { key, value, configurationSetting } = setting; + + await checkPluginPlatformSettings( + { + logger: ctx.logger, + uiSettingsClient, + configuration: ctx.configuration, + }, + { + key, + value, + configurationSetting, + }, + ); + ctx.logger.info('Start setting finished'); + } catch (e) { + const message = `Error initilizating setting [${setting.key}]: ${e.message}`; + ctx.logger.error(message); + throw new Error(message); + } + }, +}); diff --git a/plugins/main/server/lib/initialization/templates.test.ts b/plugins/main/server/lib/initialization/templates.test.ts new file mode 100644 index 0000000000..e00bba3548 --- /dev/null +++ b/plugins/main/server/lib/initialization/templates.test.ts @@ -0,0 +1,43 @@ +import { getTemplateForIndexPattern } from './templates'; + +const templates = [ + { + name: 'wazuh', + index_patterns: '[wazuh-alerts-4.x-*, wazuh-archives-4.x-*]', + order: '0', + version: '1', + composed_of: '', + }, + { + name: 'wazuh-agent', + index_patterns: '[wazuh-monitoring-*]', + order: '0', + version: null, + composed_of: '', + }, + { + name: 'wazuh-statistics', + index_patterns: '[wazuh-statistics-*]', + order: '0', + version: null, + composed_of: '', + }, +]; + +describe('getTemplateForIndexPattern', () => { + it.each` + indexPatternTitle | templateNameFound + ${'custom-alerts-*'} | ${[]} + ${'wazuh-alerts-*'} | ${['wazuh']} + ${'wazuh-alerts-'} | ${['wazuh']} + `( + `indexPatternTitle: $indexPatternTitle`, + ({ indexPatternTitle, templateNameFound }) => { + expect( + getTemplateForIndexPattern(indexPatternTitle, templates).map( + ({ name }) => name, + ), + ).toEqual(templateNameFound); + }, + ); +}); diff --git a/plugins/main/server/lib/initialization/templates.ts b/plugins/main/server/lib/initialization/templates.ts new file mode 100644 index 0000000000..4315c186d3 --- /dev/null +++ b/plugins/main/server/lib/initialization/templates.ts @@ -0,0 +1,91 @@ +export const checkIndexPatternHasTemplate = async ( + { logger }, + { indexPatternTitle, opensearchClient }, +) => { + logger.debug('Getting templates'); + const data = await opensearchClient.cat.templates({ format: 'json' }); + + logger.debug( + 'Checking the index pattern with title [${indexPatternTitle}] has defined some template', + ); + const templatesFound = getTemplateForIndexPattern( + indexPatternTitle, + data.body, + ); + if (!templatesFound.length) { + throw new Error( + `No template found for index pattern with title [${indexPatternTitle}]`, + ); + } + + logger.info( + `Template [${templatesFound + .map(({ name }) => name) + .join( + ', ', + )}] found for index pattern with title [${indexPatternTitle}]: `, + ); +}; + +export function getTemplateForIndexPattern( + indexPatternTitle: string, + templates: { name: string; index_patterns: string }[], +) { + return templates.filter(({ index_patterns }: { index_patterns: string }) => { + const [, cleanIndexPatterns] = index_patterns.match(/\[(.+)\]/) || [ + null, + null, + ]; + if (!cleanIndexPatterns) { + return false; + } + const indexPatterns = cleanIndexPatterns.match(/([^\s,]+)/g); + + if (!indexPatterns) { + return false; + } + + const lastChar = indexPatternTitle[indexPatternTitle.length - 1]; + const indexPatternTitleCleaned = + lastChar === '*' ? indexPatternTitle.slice(0, -1) : indexPatternTitle; + return indexPatterns.some(indexPattern => { + const lastChar = indexPattern[indexPattern.length - 1]; + const indexPatternCleaned = + lastChar === '*' ? indexPattern.slice(0, -1) : indexPattern; + return ( + indexPatternCleaned.includes(indexPatternTitleCleaned) || + indexPatternTitleCleaned.includes(indexPatternCleaned) + ); + }); + }); +} + +export const initializationTaskCreatorExistTemplate = ({ + getOpenSearchClient, + getIndexPatternTitle, + taskName, +}: { + getOpenSearchClient: (ctx: any) => any; + getIndexPatternTitle: (ctx: any) => Promise; + taskName: string; +}) => ({ + name: taskName, + async run(ctx) { + let indexPatternTitle; + try { + ctx.logger.debug('Starting check of existent template'); + + const opensearchClient = getOpenSearchClient(ctx); + indexPatternTitle = await getIndexPatternTitle(ctx); + await checkIndexPatternHasTemplate(ctx, { + opensearchClient, + indexPatternTitle, + }); + ctx.logger.info('Start check of existent template finished'); + } catch (e) { + const message = `Error checking of existent template for index pattern with title [${indexPatternTitle}]: ${e.message}`; + ctx.logger.error(message); + throw new Error(message); + } + }, +}); diff --git a/plugins/main/server/plugin.ts b/plugins/main/server/plugin.ts index e37ec01792..a9d74d5db6 100644 --- a/plugins/main/server/plugin.ts +++ b/plugins/main/server/plugin.ts @@ -37,6 +37,23 @@ import { jobSanitizeUploadedFilesTasksRun, } from './start'; import { first } from 'rxjs/operators'; +import { + initializationTaskCreatorIndexPattern, + initializationTaskCreatorSetting, + initializationTaskCreatorExistTemplate, +} from './lib/initialization'; +import { + PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS, + PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS, + PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER, + WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS, + WAZUH_PLUGIN_PLATFORM_SETTING_METAFIELDS, + WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER, +} from '../common/constants'; +import { KnownFields } from '../public/utils/known-fields'; +import { FieldsStatistics } from '../public/utils/statistics-fields'; +import { FieldsMonitoring } from '../public/utils/monitoring-fields'; +import VulnerabilitiesStatesFields from '../public/utils/vulnerabibility-states-fields.json'; declare module 'opensearch_dashboards/server' { interface RequestHandlerContext { @@ -109,6 +126,113 @@ export class WazuhPlugin implements Plugin { const router = core.http.createRouter(); setupRoutes(router, plugins.wazuhCore); + // Register initialization + // Index pattern: alerts + // TODO: this task should be registered by the related plugin + plugins.wazuhCore.initialization.register( + initializationTaskCreatorIndexPattern({ + getIndexPatternID: ctx => ctx.configuration.get('pattern'), + taskName: 'index-pattern:alerts', + options: { + savedObjectOverwrite: { + timeFieldName: 'timestamp', + }, + fieldsNoIndices: KnownFields, + }, + configurationSettingKey: 'checks.pattern', + }), + ); + // Index pattern: monitoring + // TODO: this task should be registered by the related plugin + plugins.wazuhCore.initialization.register( + initializationTaskCreatorIndexPattern({ + getIndexPatternID: ctx => + ctx.configuration.get('wazuh.monitoring.pattern'), + taskName: 'index-pattern:monitoring', + options: { + savedObjectOverwrite: { + timeFieldName: 'timestamp', + }, + fieldsNoIndices: FieldsMonitoring, + }, + configurationSettingKey: 'checks.monitoring', + }), + ); + // Index pattern: vulnerabilities + // TODO: this task should be registered by the related plugin + plugins.wazuhCore.initialization.register( + initializationTaskCreatorIndexPattern({ + getIndexPatternID: ctx => + ctx.configuration.get('vulnerabilities.pattern'), + taskName: 'index-pattern:vulnerabilities-states', + options: { + fieldsNoIndices: VulnerabilitiesStatesFields, + }, + configurationSettingKey: 'checks.monitoring', + }), + ); + + // Index pattern: statistics + // TODO: this task should be registered by the related plugin + plugins.wazuhCore.initialization.register( + initializationTaskCreatorIndexPattern({ + getIndexPatternID: async ctx => { + const appConfig = await ctx.configuration.get( + 'cron.prefix', + 'cron.statistics.index.name', + ); + + const prefixTemplateName = appConfig['cron.prefix']; + const statisticsIndicesTemplateName = + appConfig['cron.statistics.index.name']; + return `${prefixTemplateName}-${statisticsIndicesTemplateName}-*`; + }, + taskName: 'index-pattern:statistics', + options: { + savedObjectOverwrite: { + timeFieldName: 'timestamp', + }, + fieldsNoIndices: FieldsStatistics, + }, + configurationSettingKey: 'checks.statistics', + }), + ); + + // Settings + // TODO: this task should be registered by the related plugin + [ + { + key: PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS, + value: WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS, + configurationSetting: 'checks.maxBuckets', + }, + { + key: PLUGIN_PLATFORM_SETTING_NAME_METAFIELDS, + value: WAZUH_PLUGIN_PLATFORM_SETTING_METAFIELDS, + configurationSetting: 'checks.metaFields', + }, + { + key: PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER, + value: JSON.stringify(WAZUH_PLUGIN_PLATFORM_SETTING_TIME_FILTER), + configurationSetting: 'checks.timeFilter', + }, + ].forEach(setting => { + plugins.wazuhCore.initialization.register( + initializationTaskCreatorSetting(setting, `setting:${setting.key}`), + ); + }); + + // Index pattern templates + // Index pattern template: alerts + // TODO: this task should be registered by the related plugin + plugins.wazuhCore.initialization.register( + initializationTaskCreatorExistTemplate({ + getOpenSearchClient: ctx => ctx.core.opensearch.client.asInternalUser, + getIndexPatternTitle: ctx => ctx.configuration.get('pattern'), + taskName: 'index-pattern-template:alerts', + }), + ); + return {}; } diff --git a/plugins/wazuh-core/server/initialization/server-api.test.ts b/plugins/wazuh-core/server/initialization/server-api.test.ts new file mode 100644 index 0000000000..bc9d56667b --- /dev/null +++ b/plugins/wazuh-core/server/initialization/server-api.test.ts @@ -0,0 +1,96 @@ +import { + PLUGIN_APP_NAME, + PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING, +} from '../../common/constants'; +import { webDocumentationLink } from '../../common/services/web_documentation'; +import { version as appVersion } from '../../package.json'; +import { + ServerAPIConnectionCompatibility, + checkAppServerCompatibility, +} from './server-api'; + +describe('checkAppServerCompatibility', () => { + it.each` + appVersion | serverAPIVersion | result + ${'5.0.0'} | ${'5.0.0'} | ${true} + ${'5.0.0'} | ${'5.0.1'} | ${true} + ${'5.0.0'} | ${'5.0.10'} | ${true} + ${'5.0.0'} | ${'5.0.100'} | ${true} + ${'5.0.0'} | ${'4.9.1'} | ${false} + ${'5.0.0'} | ${'4.9.10'} | ${false} + ${'5.0.0'} | ${'4.9.100'} | ${false} + ${'5.0.0'} | ${'4.0.1'} | ${false} + ${'5.0.0'} | ${'4.0.10'} | ${false} + ${'5.0.0'} | ${'4.0.100'} | ${false} + ${'5.0.0'} | ${'4.10.1'} | ${false} + ${'5.0.0'} | ${'4.10.10'} | ${false} + ${'5.0.0'} | ${'4.10.100'} | ${false} + `( + `appVersion: $appVersion, serverAPIVersion: $serverAPIVersion, result: $result`, + ({ appVersion, serverAPIVersion, result }) => { + expect(checkAppServerCompatibility(appVersion, serverAPIVersion)).toBe( + result, + ); + }, + ); +}); + +describe('ServerAPIConnectionCompatibility', () => { + it.each` + apiHostID | apiVersionResponse | isCompatible + ${'server1'} | ${{ api_version: '5.0.0' }} | ${true} + ${'server2'} | ${{ api_version: '0.0.0' }} | ${false} + ${'server3'} | ${{ missing_api_version_field: null }} | ${false} + `( + `Check server API connection and compatibility for the server API hosts`, + async ({ apiHostID, apiVersionResponse, isCompatible }) => { + const loggerMock = jest.fn(); + const result = await ServerAPIConnectionCompatibility( + { + manageHosts: { + get: () => hosts, + }, + logger: { + debug: loggerMock, + info: loggerMock, + warn: loggerMock, + error: loggerMock, + }, + serverAPIClient: { + asInternalUser: { + request: () => ({ + data: { + data: apiVersionResponse, + }, + }), + }, + }, + }, + apiHostID, + appVersion, + ); + expect(loggerMock).toHaveBeenCalledWith( + `Checking the connection and compatibility with server API [${apiHostID}]`, + ); + if (apiVersionResponse.api_version) { + if (isCompatible === true) { + expect(loggerMock).toHaveBeenCalledWith( + `Server API [${apiHostID}] version [${apiVersionResponse.api_version}] is compatible with the ${PLUGIN_APP_NAME} version`, + ); + } else if (isCompatible === false) { + expect(loggerMock).toHaveBeenCalledWith( + `Server API [${apiHostID}] version [${ + apiVersionResponse.api_version + }] is not compatible with the ${PLUGIN_APP_NAME} version [${appVersion}]. Major and minor number must match at least. It is recommended the server API and ${PLUGIN_APP_NAME} version are equals. Read more about this error in our troubleshooting guide: ${webDocumentationLink( + PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING, + )}.`, + ); + } + } else { + expect(loggerMock).toHaveBeenCalledWith( + `Error checking the connection and compatibility with server API [${apiHostID}]: version is not found in the response of server API`, + ); + } + }, + ); +}); diff --git a/plugins/wazuh-core/server/initialization/server-api.ts b/plugins/wazuh-core/server/initialization/server-api.ts new file mode 100644 index 0000000000..7aab23626e --- /dev/null +++ b/plugins/wazuh-core/server/initialization/server-api.ts @@ -0,0 +1,114 @@ +import { + PLUGIN_APP_NAME, + PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING, +} from '../../common/constants'; +import { webDocumentationLink } from '../../common/services/web_documentation'; +import { version as appVersion } from '../../package.json'; + +export const initializationTaskCreatorServerAPIConnectionCompatibility = ({ + taskName, +}: { + taskName: string; +}) => ({ + name: taskName, + async run(ctx) { + try { + ctx.logger.debug( + 'Starting check server API connection and compatibility', + ); + const results = await ServersAPIConnectionCompatibility(ctx); + ctx.logger.info( + 'Start check server API connection and compatibility finished', + ); + return results; + } catch (e) { + const message = `Error checking server API connection and compatibility: ${e.message}`; + ctx.logger.error(message); + throw new Error(message); + } + }, +}); + +async function ServersAPIConnectionCompatibility(ctx) { + if (ctx.scope === 'user' && ctx.request?.query?.apiHostID) { + const host = await ctx.manageHosts.get(ctx.request.query.apiHostID, { + excludePassword: true, + }); + + ctx.logger.debug(`APP version [${appVersion}]`); + + return await ServerAPIConnectionCompatibility(ctx, host.id, appVersion); + } else { + const hosts = await ctx.manageHosts.get(undefined, { + excludePassword: true, + }); + + ctx.logger.debug(`APP version [${appVersion}]`); + + return await Promise.all( + hosts.map(async ({ id: apiHostID }: { id: string }) => + ServerAPIConnectionCompatibility(ctx, apiHostID, appVersion), + ), + ); + } +} + +export async function ServerAPIConnectionCompatibility( + ctx: any, + apiHostID: string, + appVersion: string, +) { + let connection = null, + compatibility = null, + api_version = null; + try { + ctx.logger.debug( + `Checking the connection and compatibility with server API [${apiHostID}]`, + ); + const response = await ctx.serverAPIClient.asInternalUser.request( + 'GET', + '/', + {}, + { apiHostID }, + ); + connection = true; + api_version = response?.data?.data?.api_version; + if (!api_version) { + throw new Error('version is not found in the response of server API'); + } + ctx.logger.debug(`Server API version [${api_version}]`); + if (!checkAppServerCompatibility(appVersion, api_version)) { + compatibility = false; + ctx.logger.warn( + `Server API [${apiHostID}] version [${api_version}] is not compatible with the ${PLUGIN_APP_NAME} version [${appVersion}]. Major and minor number must match at least. It is recommended the server API and ${PLUGIN_APP_NAME} version are equals. Read more about this error in our troubleshooting guide: ${webDocumentationLink( + PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING, + )}.`, + ); + } else { + compatibility = true; + ctx.logger.info( + `Server API [${apiHostID}] version [${api_version}] is compatible with the ${PLUGIN_APP_NAME} version`, + ); + } + } catch (e) { + ctx.logger.warn( + `Error checking the connection and compatibility with server API [${apiHostID}]: ${e.message}`, + ); + } finally { + return { connection, compatibility, api_version, id: apiHostID }; + } +} + +export function checkAppServerCompatibility( + appVersion: string, + serverAPIVersion: string, +) { + const api = /v?(?\d+)\.(?\d+)\.(?\d+)/.exec( + serverAPIVersion, + ); + const [appVersionMajor, appVersionMinor] = appVersion.split('.'); + return ( + api?.groups?.major === appVersionMajor && + api?.groups?.minor === appVersionMinor + ); +} diff --git a/plugins/wazuh-core/server/plugin.ts b/plugins/wazuh-core/server/plugin.ts index 4c1587ce50..3cad414216 100644 --- a/plugins/wazuh-core/server/plugin.ts +++ b/plugins/wazuh-core/server/plugin.ts @@ -16,8 +16,8 @@ import { ManageHosts, createDashboardSecurity, ServerAPIClient, - UpdateRegistry, ConfigurationStore, + InitializationService, } from './services'; import { Configuration } from '../common/services/configuration'; import { @@ -27,6 +27,7 @@ import { WAZUH_DATA_CONFIG_APP_PATH, } from '../common/constants'; import { enhanceConfiguration } from './services/enhance-configuration'; +import { initializationTaskCreatorServerAPIConnectionCompatibility } from './initialization/server-api'; export class WazuhCorePlugin implements Plugin @@ -107,10 +108,27 @@ export class WazuhCorePlugin this.services.manageHosts.setServerAPIClient(this.services.serverAPIClient); + this.services.initialization = new InitializationService( + this.logger.get('initialization'), + this.services, + ); + + this.services.initialization.setup({ core }); + + // Register initialization tasks + this.services.initialization.register( + initializationTaskCreatorServerAPIConnectionCompatibility({ + taskName: 'check-server-api-connection-compatibility', + }), + ); + // Register a property to the context parameter of the endpoint handlers core.http.registerRouteHandlerContext('wazuh_core', (context, request) => { return { ...this.services, + logger: this.logger.get( + `${request.route.method.toUpperCase()} ${request.route.path}`, + ), api: { client: { asInternalUser: this.services.serverAPIClient.asInternalUser, @@ -141,6 +159,7 @@ export class WazuhCorePlugin await this.services.configuration.start(); await this.services.manageHosts.start(); + await this.services.initialization.start({ core }); return { ...this.services, diff --git a/plugins/wazuh-core/server/services/index.ts b/plugins/wazuh-core/server/services/index.ts index 8a794e559f..9b064c7e99 100644 --- a/plugins/wazuh-core/server/services/index.ts +++ b/plugins/wazuh-core/server/services/index.ts @@ -16,3 +16,4 @@ export * from './filesystem'; export * from './manage-hosts'; export * from './security-factory'; export * from './server-api-client'; +export * from './initialization'; diff --git a/plugins/wazuh-core/server/services/initialization/README.md b/plugins/wazuh-core/server/services/initialization/README.md new file mode 100644 index 0000000000..a2f0cee064 --- /dev/null +++ b/plugins/wazuh-core/server/services/initialization/README.md @@ -0,0 +1,109 @@ +# InitializationService + +The `InitializationService` provides a mechanism to register and run tasks when the `wazuhCore` plugin starts (plugin lifecycle). + +Other plugins can register tasks in the plugin `setup` lifecycle that will be run on the `wazuhCore` plugin starts. + +The tasks run on parallel. + +Optionally the registered tasks could be retrieved to run in API endpoints or getting information about its status. + +There are 2 scopes: + +- `internal`: run through the internal user + - on plugin starts + - on demand +- `user`: run through the logged (requester) user + - on demand + +The scopes can be used to get a specific context (clients, parameters) that is set in the `scope` property of the task context. + +The `internal` scoped tasks keep the same execution data (see [Task execution data](#task-execution-data)), and the `user` scoped task are newly created on demand. + +# InitializationService tasks + +A task can be defined with: + +```ts +interface InitializationTaskDefinition { + name: string; + run: (ctx: any) => any; +} +``` + +The `ctx` is the context of the task execution and includes core services and task context services or dependencies. + +The `name` is used to identify the task and this is rendered in the context logger. + +For example, in the server log: + +``` +server log [11:57:39.648] [info][index-pattern-vulnerabilities-states][initialization][plugins][wazuhCore] Index pattern with ID [wazuh-states-vulnerabilities-*] does not exist + +``` + +the task name is `index-pattern-vulnerabilities-states`. + +## Task name convention + +- lowercase +- kebab case (`word1-word2`) +- use colon ( `:` ) for tasks related to some entity that have different subentities. + +``` +entity_identifier:entity_specific +``` + +For example: + +``` +index-pattern:alerts +index-pattern:statistics +index-pattern:vulnerabilities-states +``` + +## Register a task + +```ts +// plugin setup +setup(){ + + // Register a task + plugins.wazuhCore.initialization.register({ + name: 'custom-task', + run: (ctx) => { + console.log('Run from wazuhCore starts' ) + } + }); + +} +``` + +## Task execution data + +The task has the following data related to the execution: + +```ts +interface InitializationTaskRunData { + name: string; + status: 'not_started' | 'running' | 'finished'; + result: 'success' | 'fail'; + createdAt: string | null; + startedAt: string | null; + finishedAt: string | null; + duration: number | null; // seconds + data: any; + error: string | null; +} +``` + +## Create a task instance + +This is used to create the user scoped tasks. + +```ts +const newTask = + context.wazuh_core.initialization.createNewTaskFromRegisteredTask( + 'example-task', + ); +``` diff --git a/plugins/wazuh-core/server/services/initialization/index.ts b/plugins/wazuh-core/server/services/initialization/index.ts new file mode 100644 index 0000000000..c7f504bd7b --- /dev/null +++ b/plugins/wazuh-core/server/services/initialization/index.ts @@ -0,0 +1,2 @@ +export * from './initialization'; +export * from './types'; diff --git a/plugins/wazuh-core/server/services/initialization/initialization.ts b/plugins/wazuh-core/server/services/initialization/initialization.ts new file mode 100644 index 0000000000..d79531b9e3 --- /dev/null +++ b/plugins/wazuh-core/server/services/initialization/initialization.ts @@ -0,0 +1,171 @@ +import { Logger } from 'opensearch-dashboards/server'; +import { + InitializationTaskDefinition, + IInitializationService, + InitializationTaskRunData, + IInitializationTask, + InitializationTaskContext, +} from './types'; +import { addRoutes } from './routes'; + +export class InitializationService implements IInitializationService { + private items: Map; + private _coreStart: any; + constructor(private logger: Logger, private services: any) { + this.items = new Map(); + } + async setup({ core }) { + this.logger.debug('Setup starts'); + this.logger.debug('Adding routes'); + const router = core.http.createRouter(); + addRoutes(router, { initialization: this }); + this.logger.debug('Added routes'); + this.logger.debug('Setup finished'); + } + async start({ core }) { + this.logger.debug('Start starts'); + this._coreStart = core; + await this.runAsInternal(); + this.logger.debug('Start finished'); + } + async stop() { + this.logger.debug('Stop starts'); + this.logger.debug('Stop finished'); + } + register(task: InitializationTaskDefinition) { + this.logger.debug(`Registering ${task.name}`); + if (this.items.has(task.name)) { + throw new Error( + `[${task.name}] was already registered. Ensure the name is unique or remove the duplicated registration of same task.`, + ); + } + this.items.set(task.name, new InitializationTask(task)); + this.logger.debug(`Registered ${task.name}`); + } + get(name?: string) { + this.logger.debug(`Getting tasks: ${name ? `[${name}]` : ''}`); + if (name) { + return this.items.get(name); + } + return Array.from(this.items.values()); + } + createRunContext(scope: InitializationTaskContext, context: any = {}) { + return { ...this.services, ...context, scope }; + } + async runAsInternal(taskNames?: string[]) { + const ctx = this.createRunContext('internal', { core: this._coreStart }); + return await this.run(ctx, taskNames); + } + createNewTaskFromRegisteredTask(name: string) { + const task = this.get(name) as InitializationTask; + if (!task) { + throw new Error(`Task [${name}] is not registered`); + } + return new InitializationTask({ name, run: task._run }); + } + private async run(ctx, taskNames?: string[]) { + try { + if (this.items.size) { + const allTasks = Array.from(this.items.values()); + const tasks = taskNames + ? allTasks.filter(({ name }) => + taskNames.some(taskName => taskName === name), + ) + : allTasks; + const results = await Promise.all( + tasks.map(async item => { + const logger = this.logger.get(item.name); + + try { + return await item.run({ + ...this.services, + ...ctx, + logger, + }); + } catch (e) { + logger.error(`Error running task [${item.name}]: ${e.message}`); + return item.getInfo(); + } + }), + ); + return results; + } else { + this.logger.info('No tasks'); + } + } catch (error) { + this.logger.error(`Error starting: ${error.message}`); + } + } +} + +class InitializationTask implements IInitializationTask { + public name: string; + private _run: any; + public status: InitializationTaskRunData['status'] = 'not_started'; + public result: InitializationTaskRunData['result'] = null; + public data: any = null; + public createdAt: InitializationTaskRunData['createdAt'] = + new Date().toISOString(); + public startedAt: InitializationTaskRunData['startedAt'] = null; + public finishedAt: InitializationTaskRunData['finishedAt'] = null; + public duration: InitializationTaskRunData['duration'] = null; + public error = null; + constructor(task: InitializationTaskDefinition) { + this.name = task.name; + this._run = task.run; + } + private init() { + this.status = 'running'; + this.result = null; + this.data = null; + this.startedAt = new Date().toISOString(); + this.finishedAt = null; + this.duration = null; + this.error = null; + } + async run(...params) { + if (this.status === 'running') { + throw new Error(`Another instance of task ${this.name} is running`); + } + let error; + try { + this.init(); + this.data = await this._run(...params); + this.result = 'success'; + } catch (e) { + error = e; + this.result = 'fail'; + this.error = e.message; + } finally { + this.status = 'finished'; + this.finishedAt = new Date().toISOString(); + const dateStartedAt = new Date(this.startedAt!); + const dateFinishedAt = new Date(this.finishedAt); + this.duration = ((dateFinishedAt - dateStartedAt) as number) / 1000; + } + if (error) { + throw error; + } + return this.getInfo(); + } + + getInfo() { + return [ + 'name', + 'status', + 'result', + 'data', + 'createdAt', + 'startedAt', + 'finishedAt', + 'duration', + 'error', + ].reduce( + (accum, item) => ({ + ...accum, + [item]: this[item], + }), + {}, + ) as IInitializationTask; + } +} diff --git a/plugins/wazuh-core/server/services/initialization/routes.ts b/plugins/wazuh-core/server/services/initialization/routes.ts new file mode 100644 index 0000000000..cf7e523827 --- /dev/null +++ b/plugins/wazuh-core/server/services/initialization/routes.ts @@ -0,0 +1,242 @@ +import { schema } from '@osd/config-schema'; + +export function addRoutes(router, { initialization }) { + const getTaskList = (tasksAsString: string) => tasksAsString.split(','); + + const validateTaskList = schema.maybe( + schema.string({ + validate(value: string) { + const tasks = initialization.get(); + const requestTasks = getTaskList(value); + const invalidTasks = requestTasks.filter(requestTask => + tasks.every(({ name }) => requestTask !== name), + ); + if (invalidTasks.length) { + return `Invalid tasks: ${invalidTasks.join(', ')}`; + } + return undefined; + }, + }), + ); + + const apiEndpointBase = '/api/initialization'; + + // Get the status of internal initialization tasks + router.get( + { + path: `${apiEndpointBase}/internal`, + validate: { + tasks: schema.object({ + tasks: validateTaskList, + }), + }, + }, + async (context, request, response) => { + try { + const tasksNames = request.query.tasks + ? getTaskList(request.query.tasks) + : undefined; + const logger = context.wazuh_core.logger; + logger.debug(`Getting initialization tasks related to internal scope`); + const tasks = tasksNames + ? tasksNames.map(taskName => + context.wazuh_core.initialization.get(taskName), + ) + : context.wazuh_core.initialization.get(); + + const tasksData = tasks.map(task => task.getInfo()); + + logger.debug( + `Initialzation tasks related to internal scope: [${[...tasksData] + .map(({ name }) => name) + .join(', ')}]`, + ); + + return response.ok({ + body: { + message: `All initialization tasks are returned: ${tasks + .map(({ name }) => name) + .join(', ')}`, + tasks: tasksData, + }, + }); + } catch (e) { + return response.internalError({ + body: { + message: `Error getting the internal initialization tasks: ${e.message}`, + }, + }); + } + }, + ); + + // Run the internal initialization tasks + // TODO: protect with administrator privilegies + router.post( + { + path: `${apiEndpointBase}/internal`, + validate: { + query: schema.object({ + tasks: validateTaskList, + }), + }, + }, + async (context, request, response) => { + try { + const tasksNames = request.query.tasks + ? getTaskList(request.query.tasks) + : undefined; + const logger = context.wazuh_core.logger; + + logger.debug(`Running initialization tasks related to internal scope`); + const results = await context.wazuh_core.initialization.runAsInternal( + tasksNames, + ); + logger.info( + `Initialization tasks related to internal scope were executed`, + ); + + return response.ok({ + body: { + message: `All initialization tasks are returned: ${results + .map(({ name }) => name) + .join(', ')}`, + tasks: results, + }, + }); + } catch (e) { + return response.internalError({ + body: { + message: `Error running the internal initialization tasks: ${e.message}`, + }, + }); + } + }, + ); + + router.post( + { + path: `${apiEndpointBase}/user`, + validate: { + // TODO: restrict to user tasks + query: schema.object({ + tasks: validateTaskList, + }), + }, + }, + async (context, request, response) => { + try { + const tasksNames = request.query.tasks + ? getTaskList(request.query.tasks) + : undefined; + const logger = context.wazuh_core.logger; + const username = ''; // TODO: get value + const scope = 'user'; + logger.debug( + `Getting initialization tasks related to user [${username}] scope [${scope}]`, + ); + const initializationTasks = context.wazuh_core.initialization.get(); + + const indexPatternTasks = initializationTasks + .filter(({ name }) => name.startsWith('index-pattern:')) + .map(({ name }) => + context.wazuh_core.initialization.createNewTaskFromRegisteredTask( + name, + ), + ); + const settingsTasks = initializationTasks + .filter(({ name }) => name.startsWith('setting:')) + .map(({ name }) => + context.wazuh_core.initialization.createNewTaskFromRegisteredTask( + name, + ), + ); + + const allUserTasks = [...indexPatternTasks, ...settingsTasks]; + const tasks = tasksNames + ? allUserTasks.filter(({ name }) => + tasksNames.some(taskName => taskName === name), + ) + : allUserTasks; + + logger.debug( + `Initialzation tasks related to user [${username}] scope [${scope}]: [${tasks + .map(({ name }) => name) + .join(', ')}]`, + ); + + const taskContext = context.wazuh_core.initialization.createRunContext( + 'user', + { core: context.core, request }, + ); + + logger.debug(`Running tasks for user [${username}] scope [${scope}]`); + const results = await Promise.all( + tasks.map(async task => { + const taskLogger = enhanceTaskLogger(logger); + let data; + try { + data = await task.run({ + ...taskContext, + // TODO: use user selection index patterns + logger: taskLogger, + ...(task.name.includes('index-pattern:') + ? { + getIndexPatternID: () => + task.name /* TODO: use request parameters/body/cookies */, + } + : {}), + }); + } catch (e) { + } finally { + return { + logs: taskLogger.getLogs(), + ...task.getInfo(), + }; + } + }), + ); + + logger.debug(`All tasks for user [${username}] scope [${scope}] run`); + + const initialMessage = + 'All the initialization tasks related to user scope were executed.'; + + const message = [ + initialMessage, + results.some(({ error }) => error) && 'There was some errors.', + ] + .filter(v => v) + .join(' '); + + return response.ok({ + body: { + message, + tasks: results, + }, + }); + } catch (e) { + return response.internalError({ + body: { + message: `Error initializating the tasks: ${e.message}`, + }, + }); + } + }, + ); +} + +function enhanceTaskLogger(logger) { + const logs = []; + + return ['debug', 'info', 'warn', 'error'].reduce( + (accum, level) => ({ + ...accum, + [level]: message => { + logs.push({ timestamp: new Date().toISOString(), level, message }); + logger[level].message; + }, + }), + { getLogs: () => logs }, + ); +} diff --git a/plugins/wazuh-core/server/services/initialization/types.ts b/plugins/wazuh-core/server/services/initialization/types.ts new file mode 100644 index 0000000000..e08faee1f4 --- /dev/null +++ b/plugins/wazuh-core/server/services/initialization/types.ts @@ -0,0 +1,39 @@ +import { LifecycleService } from '../types'; + +export interface InitializationTaskDefinition { + name: string; + run: (ctx: any) => any; +} + +export interface InitializationTaskRunData { + name: InitializationTaskDefinition['name']; + status: 'not_started' | 'running' | 'finished'; + result: 'success' | 'fail' | null; + createdAt: string | null; + startedAt: string | null; + finishedAt: string | null; + duration: number | null; // seconds + data: any; + error: string | null; +} + +export interface IInitializationTask extends InitializationTaskRunData { + run(ctx: any): Promise; + getInfo(): InitializationTaskRunData; +} + +export type InitializationTaskContext = 'internal' | 'user'; +export interface IInitializationService + extends LifecycleService { + register(task: InitializationTaskDefinition): void; + get( + taskName?: string, + ): InitializationTaskRunData | InitializationTaskRunData[]; + createRunContext( + scope: InitializationTaskContext, + context: any, + ): { + scope: InitializationTaskContext; + }; + runAsInternal(tasks?: string[]): Promise; +} diff --git a/plugins/wazuh-core/server/services/types.ts b/plugins/wazuh-core/server/services/types.ts new file mode 100644 index 0000000000..bb6c43c39f --- /dev/null +++ b/plugins/wazuh-core/server/services/types.ts @@ -0,0 +1,12 @@ +export interface LifecycleService< + SetupDeps, + SetupReturn, + StartDeps, + StartReturn, + StopDeps, + StopReturn, +> { + setup: (deps: SetupDeps) => SetupReturn; + start: (deps: StartDeps) => StartReturn; + stop: (deps: StopDeps) => StopReturn; +} diff --git a/plugins/wazuh-core/server/types.ts b/plugins/wazuh-core/server/types.ts index 509a74600f..f706b3ef28 100644 --- a/plugins/wazuh-core/server/types.ts +++ b/plugins/wazuh-core/server/types.ts @@ -1,4 +1,5 @@ import { + IInitializationService, ISecurityFactory, ManageHosts, ServerAPIClient, @@ -13,6 +14,7 @@ export interface WazuhCorePluginSetup { configuration: IConfigurationEnhanced; manageHosts: ManageHosts; serverAPIClient: ServerAPIClient; + initialization: IInitializationService; api: { client: { asInternalUser: ServerAPIInternalUserClient; @@ -26,6 +28,7 @@ export interface WazuhCorePluginStart { configuration: IConfigurationEnhanced; manageHosts: ManageHosts; serverAPIClient: ServerAPIClient; + initialization: IInitializationService; api: { client: { asInternalUser: ServerAPIInternalUserClient;