From 3e056c59a177bc332437d1503e4b26a3a65ae79e Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Fri, 5 Aug 2022 09:12:46 +0200 Subject: [PATCH] Fix modules settings persistence between updates (#4359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(plugin-initialize): migrate the host configuration in the registry file when changing the plugin version or revision - Migrated the existent host configuration in the registry file when changing the plugin version or revision instead of remove it when it was rebuilt. - Removed not necessary return statements - Removed not necessary try/catch block - Added some logs - Enhanced some log messages - Created tests for the migration registry file * changelog: add PR entry * changelog: removed new line Co-authored-by: Álex --- CHANGELOG.md | 1 + server/start/initialize/index.test.ts | 350 ++++++++++++++++++++++++++ server/start/initialize/index.ts | 119 +++++---- 3 files changed, 426 insertions(+), 44 deletions(-) create mode 100644 server/start/initialize/index.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b34f3d21..fb57ceaea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Fixed a problem with the group's report, when the group has no agents [#4350](https://github.com/wazuh/wazuh-kibana-app/pull/4350) - Fixed path in logo customization section [#4352](https://github.com/wazuh/wazuh-kibana-app/pull/4352) - Fixed an error of an undefined username hash related to reporting when using Kibana with X-Pack and security was disabled [#4358](https://github.com/wazuh/wazuh-kibana-app/pull/4358) +- Fixed persistence of the plugin registry file between updates [#4359](https://github.com/wazuh/wazuh-kibana-app/pull/4359) # Removed diff --git a/server/start/initialize/index.test.ts b/server/start/initialize/index.test.ts new file mode 100644 index 0000000000..dc62b29002 --- /dev/null +++ b/server/start/initialize/index.test.ts @@ -0,0 +1,350 @@ +import fs from 'fs'; +import md5 from 'md5'; +import { execSync } from 'child_process'; +import path from 'path'; +import { jobInitializeRun } from './index'; +import { createDataDirectoryIfNotExists, createDirectoryIfNotExists } from '../../lib/filesystem'; +import { WAZUH_DATA_ABSOLUTE_PATH, WAZUH_DATA_CONFIG_DIRECTORY_PATH, WAZUH_DATA_CONFIG_REGISTRY_PATH } from '../../../common/constants'; +import packageInfo from '../../../package.json'; + +function mockContextCreator(loggerLevel: string) { + const logs = []; + const levels = ['debug', 'info', 'warn', 'error']; + + function createLogger(level: string) { + return jest.fn(function (message: string) { + const levelLogIncluded: number = levels.findIndex((level) => level === loggerLevel); + levelLogIncluded > -1 + && levels.slice(levelLogIncluded).includes(level) + && logs.push({ level, message }); + }); + }; + + const ctx = { + wazuh: { + logger: { + info: createLogger('info'), + warn: createLogger('warn'), + error: createLogger('error'), + debug: createLogger('debug') + }, + }, + server: { + config: { + kibana: { + index: '.kibana' + } + } + }, + core: { + elasticsearch: { + client: { + asInternalUser: { + indices: { + exists: jest.fn(() => ({body: true})) + } + } + } + } + }, + /* Mocked logs getter. It is only for testing purpose.*/ + _getLogs(logLevel: string) { + return logLevel ? logs.filter(({ level }) => level === logLevel) : logs; + } + } + return ctx; +}; + +jest.mock('../../lib/logger', () => ({ + log: jest.fn() +})); + +jest.mock('../../lib/get-configuration', () => ({ + getConfiguration: () => ({pattern: 'wazuh-alerts-*'}) +})); + +beforeAll(() => { + // Create /data/wazuh directory. + createDataDirectoryIfNotExists(); + // Create /data/wazuh/config directory. + createDirectoryIfNotExists(WAZUH_DATA_CONFIG_DIRECTORY_PATH); +}); + +afterAll(() => { + // Remove /data/wazuh directory. + execSync(`rm -rf ${WAZUH_DATA_ABSOLUTE_PATH}`); +}); + +describe("[initialize] `wazuh-registry.json` not created", () => { + let mockContext = mockContextCreator('debug'); + afterEach(() => { + // Remove /data/wazuh/config/wazuh-registry file. + execSync(`rm ${WAZUH_DATA_ABSOLUTE_PATH}/config/wazuh-registry.json || echo ""`); + }); + + it("Create registry file with plugin data and empty hosts", async () => { + // Migrate the directories + await jobInitializeRun(mockContext); + const contentRegistry = JSON.parse(fs.readFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, 'utf8')); + + expect(contentRegistry.name).toMatch('Wazuh App'); + expect(contentRegistry['app-version']).toMatch(packageInfo.version); + expect(contentRegistry['revision']).toMatch(packageInfo.revision); + expect(typeof contentRegistry.installationDate).toBe('string'); + expect(typeof contentRegistry.lastRestart).toBe('string'); + expect(Object.keys(contentRegistry.hosts)).toHaveLength(0); + }); +}); + +describe("[initialize] `wazuh-registry.json` created", () => { + let testID = 0; + const contentRegistryFile = [ + { + before: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: {} + }, + after: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: {} + } + }, + { + before: { + name: 'Wazuh App', + 'app-version': '0.0.0', + revision: '0', + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: {} + }, + after: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: {} + } + }, + { + before: { + name: 'Wazuh App', + 'app-version': '0.0.0', + revision: '0', + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: false, + office: false, + github: false, + gcp: false, + virustotal: false, + osquery: false, + docker: false + } + } + } + }, + after: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: false, + office: false, + github: false, + gcp: false, + virustotal: false, + osquery: false, + docker: false + } + } + } + } + }, + { + before: { + name: 'Wazuh App', + 'app-version': '0.0.0', + revision: '0', + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: true, + office: true, + github: true, + gcp: true, + virustotal: false, + osquery: false + } + } + } + }, + after: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: true, + office: true, + github: true, + gcp: true, + virustotal: false, + osquery: false, + docker: false + } + } + } + } + }, + { + before: { + name: 'Wazuh App', + 'app-version': '0.0.0', + revision: '0', + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: true, + gcp: true, + virustotal: false, + osquery: false + } + } + } + }, + after: { + name: 'Wazuh App', + 'app-version': packageInfo.version, + revision: packageInfo.revision, + installationDate: '2022-07-25T13:55:04.363Z', + lastRestart: '2022-07-25T13:55:04.363Z', + hosts: { + default: { + extensions: { + pci: true, + gdpr: true, + hipaa: true, + nist: true, + tsc: true, + audit: true, + oscap: false, + ciscat: false, + aws: true, + office: false, + github: false, + gcp: true, + virustotal: false, + osquery: false, + docker: false + } + } + } + } + }, + ]; + + beforeEach(() => { + // Remove /data/wazuh/config/wazuh-registry.json. + execSync(`rm ${WAZUH_DATA_ABSOLUTE_PATH}/config/wazuh-registry.json || echo ""`); + // Create the wazuh-registry.json file. + fs.writeFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, JSON.stringify(contentRegistryFile[testID].before), 'utf8'); + testID++; + }); + + it.each` + titleTest | contentRegistryFile + ${'Registry file is not rebuilt due version and revision match'} | ${JSON.stringify(contentRegistryFile[0].after)} + ${'Registry file is rebuilt due to version/revision changed'} | ${JSON.stringify(contentRegistryFile[1].after)} + ${'Registry file is rebuilt due to version/revision changed and keeps the extensions (no modified)'} | ${JSON.stringify(contentRegistryFile[2].after)} + ${'Registry file is rebuilt due to version/revision changed and keeps the extensions (modified)'} | ${JSON.stringify(contentRegistryFile[3].after)} + ${'Registry file is rebuilt due to version/revision changed and adds missing extensions with default values'} | ${JSON.stringify(contentRegistryFile[4].after)} + `(`$titleTest: + content: $contentRegistryFile`, async ({ contentRegistryFile: content }) => { + const mockContext = mockContextCreator('debug'); + + const contentRegistryExpected = JSON.parse(content); + await jobInitializeRun(mockContext); + const contentRegistryFile = JSON.parse(fs.readFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, 'utf8')); + + expect(contentRegistryFile.name).toMatch('Wazuh App'); + expect(contentRegistryFile['app-version']).toMatch(contentRegistryExpected['app-version']); + expect(contentRegistryFile['revision']).toMatch(contentRegistryExpected.revision); + expect(typeof contentRegistryFile.installationDate).toBe('string'); + expect(typeof contentRegistryFile.lastRestart).toBe('string'); + expect(Object.keys(contentRegistryFile.hosts)).toHaveLength(Object.keys(contentRegistryExpected.hosts).length); + + if ( Object.keys(contentRegistryFile.hosts).length ){ + Object.entries(contentRegistryFile.hosts).forEach(([hostID, hostData]) => { + if(hostData.extensions){ + Object.entries(hostData.extensions).forEach(([extensionID, extensionEnabled]) => { + expect(extensionEnabled).toBe(contentRegistryExpected.hosts[hostID].extensions[extensionID]) + }); + }; + }); + }; + }); + }); diff --git a/server/start/initialize/index.ts b/server/start/initialize/index.ts index 9b6fc36669..51281f5112 100644 --- a/server/start/initialize/index.ts +++ b/server/start/initialize/index.ts @@ -15,11 +15,10 @@ import { pluginPlatformTemplate } from '../../integration-files/kibana-template' import { getConfiguration } from '../../lib/get-configuration'; import { totalmem } from 'os'; import fs from 'fs'; -import { ManageHosts } from '../../lib/manage-hosts'; -import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER } from '../../../common/constants'; +import { WAZUH_ALERTS_PATTERN, WAZUH_DATA_CONFIG_REGISTRY_PATH, WAZUH_PLUGIN_PLATFORM_TEMPLATE_NAME, WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH, PLUGIN_PLATFORM_NAME, PLUGIN_PLATFORM_INSTALLATION_USER_GROUP, PLUGIN_PLATFORM_INSTALLATION_USER, WAZUH_DEFAULT_APP_CONFIG } from '../../../common/constants'; import { createDataDirectoryIfNotExists } from '../../lib/filesystem'; +import _ from 'lodash'; -const manageHosts = new ManageHosts(); export function jobInitializeRun(context) { const PLUGIN_PLATFORM_INDEX = context.server.config.kibana.index; @@ -36,11 +35,6 @@ export function jobInitializeRun(context) { configurationFile && typeof configurationFile.pattern !== 'undefined' ? configurationFile.pattern : WAZUH_ALERTS_PATTERN; - // global.XPACK_RBAC_ENABLED = - // configurationFile && - // typeof configurationFile['xpack.rbac.enabled'] !== 'undefined' - // ? configurationFile['xpack.rbac.enabled'] - // : true; } catch (error) { log('initialize', error.message || error); context.wazuh.logger.error( @@ -60,7 +54,7 @@ export function jobInitializeRun(context) { } // Save Wazuh App setup - const saveConfiguration = async () => { + const saveConfiguration = async (hosts = {}) => { try { const commonDate = new Date().toISOString(); @@ -70,15 +64,20 @@ export function jobInitializeRun(context) { revision: packageJSON.revision, installationDate: commonDate, lastRestart: commonDate, - hosts: {} + hosts }; try { createDataDirectoryIfNotExists(); createDataDirectoryIfNotExists('config'); + log( + 'initialize:saveConfiguration', + `Saving configuration in registry file: ${JSON.stringify(configuration)}`, + 'debug' + ); await fs.writeFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, JSON.stringify(configuration), 'utf8'); log( 'initialize:saveConfiguration', - 'Wazuh configuration registry inserted', + 'Wazuh configuration registry saved.', 'debug' ); } catch (error) { @@ -90,59 +89,94 @@ export function jobInitializeRun(context) { } catch (error) { log('initialize:saveConfiguration', error.message || error); context.wazuh.logger.error( - 'Error creating wazuh-version registry' + 'Error creating wazuh-registry.json file.' ); } }; /** - * Checks if the .wazuh-version exists, in this case it will be deleted and the wazuh-registry.json will be created + * Checks if the .wazuh-registry.json file exists: + * - yes: check the plugin version and revision match the values stored in the registry file. + * If not, then it migrates the data rebuilding the registry file. + * - no: create the file with empty hosts */ const checkWazuhRegistry = async () => { - try { + log( + 'initialize:checkwazuhRegistry', + 'Checking wazuh-registry.json file.', + 'debug' + ); + + if(!fs.existsSync(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH)){ + throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); + }; + + if (!fs.existsSync(WAZUH_DATA_CONFIG_REGISTRY_PATH)) { log( 'initialize:checkwazuhRegistry', - 'Checking wazuh-version registry.', + 'wazuh-registry.json file does not exist. Initializing configuration.', 'debug' ); - if(!fs.existsSync(WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH)){ - throw new Error(`The data directory is missing in the ${PLUGIN_PLATFORM_NAME} root instalation. Create the directory in ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH} and give it the required permissions (sudo mkdir ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH};sudo chown -R ${PLUGIN_PLATFORM_INSTALLATION_USER}:${PLUGIN_PLATFORM_INSTALLATION_USER_GROUP} ${WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH}). After restart the ${PLUGIN_PLATFORM_NAME} service.`); - }; + // Create the app registry file for the very first time + await saveConfiguration(); + } else { + // If this function fails, it throws an exception + const source = JSON.parse(fs.readFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, 'utf8')); + + // Check if the stored revision differs from the package.json revision + const isUpgradedApp = packageJSON.revision !== source.revision || packageJSON.version !== source['app-version']; - if (!fs.existsSync(WAZUH_DATA_CONFIG_REGISTRY_PATH)) { + // Rebuild the registry file if revision or version fields are differents + if (isUpgradedApp) { log( 'initialize:checkwazuhRegistry', - 'wazuh-version registry does not exist. Initializing configuration.', - 'debug' + 'Wazuh app revision or version changed, regenerating wazuh-registry.json.', + 'info' ); - // Create the app registry file for the very first time - await saveConfiguration(); - } else { - // If this function fails, it throws an exception - const source = JSON.parse(fs.readFileSync(WAZUH_DATA_CONFIG_REGISTRY_PATH, 'utf8')); + // Rebuild the registry file `wazuh-registry.json` + + // Get the supported extensions for the installed plugin + const supportedDefaultExtensionsConfiguration = Object.entries(WAZUH_DEFAULT_APP_CONFIG) + .filter(([setting]) => setting.startsWith('extensions.')) + .map(([setting, settingValue]) => { + return [setting.split('.')[1], settingValue]; + }); + + // Get the supported extensions by ID + const supportedDefaultExtensionsNames = supportedDefaultExtensionsConfiguration.map(([setting]) => setting); + + // Generate the hosts data, migrating the extensions. + // Keep the supported and existent extensions for the installed plugin with the configurated value + // Add the extensions with default values that didn't exist in the previous configuration + // Remove the unsupported extensions for the installed plugin + const registryHostsData = Object.entries(source.hosts).reduce((accum, [hostID, hostData]) => { + accum[hostID] = hostData; + if(accum[hostID].extensions){ + // Migrate extensions to those supported by the installed plugin + const defaultHostExtentionsConfiguration = Object.fromEntries(supportedDefaultExtensionsConfiguration); + // Select of current configuration the extension IDs that are supported in the installed plugin + const currentHostConfiguration = _.pick(accum[hostID].extensions, supportedDefaultExtensionsNames); + // Merge the default extensions configuration with the configuration stored in the registry file + accum[hostID].extensions = _.merge(defaultHostExtentionsConfiguration, currentHostConfiguration); + } + return accum; + }, {}); - // Check if the stored revision differs from the package.json revision - const isUpgradedApp = packageJSON.revision !== source.revision || packageJSON.version !== source['app-version']; + // Rebuild the registry file with the migrated host data (extensions are migrated to these supported by the installed plugin). + await saveConfiguration(registryHostsData); - // Rebuild the registry file if revision or version fields are differents - if (isUpgradedApp) { - log( - 'initialize:checkwazuhRegistry', - 'Wazuh app revision or version changed, regenerating wazuh-version registry', - 'info' - ); - // Rebuild registry file in blank - await saveConfiguration(); - } + log( + 'initialize:checkwazuhRegistry', + 'Migrated the registry file.', + 'info' + ); } - } catch (error) { - return Promise.reject(error); } }; - // Init function. Check for "wazuh-version" document existance. + // Init function. Check for wazuh-registry.json file exists. const init = async () => { await checkWazuhRegistry(); }; @@ -187,7 +221,6 @@ export function jobInitializeRun(context) { 'debug' ); await init(); - return; } catch (error) { return Promise.reject( new Error( @@ -208,7 +241,6 @@ export function jobInitializeRun(context) { 'debug' ); await createEmptyKibanaIndex(); - return; } catch (error) { return Promise.reject( new Error( @@ -231,7 +263,6 @@ export function jobInitializeRun(context) { 'debug' ); await createEmptyKibanaIndex(); - return; } catch (error) { log('initialize:getTemplateByName', error.message || error); return fixKibanaTemplate();