diff --git a/public/controllers/register-agent/config/os-commands-definitions.ts b/public/controllers/register-agent/config/os-commands-definitions.ts new file mode 100644 index 0000000000..dc69156571 --- /dev/null +++ b/public/controllers/register-agent/config/os-commands-definitions.ts @@ -0,0 +1,148 @@ +import { IOSDefinition, tOptionalParams } from '../core/register-commands/types'; + +// Defined OS combinations +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => + `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'rpm', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => + `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +const windowsDefinition: IOSDefinition = { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x86', + urlPackage: props => + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, + installCommand: props => + `Invoke-WebRequest -Uri ${props.urlPackage} -OutFile \${env.tmp}\\wazuh-agent.${props.extension}; msiexec.exe /i \${env.tmp}\\wazuh-agent.${props.extension} /q`, + startCommand: props => `Start-Service -Name wazuh-agent`, + }, + ], +}; + +const macDefinition: IOSDefinition = { + name: 'mac', + options: [ + { + extension: 'pkg', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, + installCommand: props => + `mac -so wazuh-agent.pkg ${props.urlPackage} && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target /`, + startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + }, + ], +}; + +export const osCommandsDefinitions = [ + linuxDefinition, + windowsDefinition, + macDefinition, +]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + agent_name: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + } +} diff --git a/public/controllers/register-agent/core/register-commands/README.md b/public/controllers/register-agent/core/register-commands/README.md new file mode 100644 index 0000000000..d53288e940 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/README.md @@ -0,0 +1,340 @@ +# Documentation + +- [Register Agent](#register-agent) + - [Solution details](#solution-details) + - [Configuration details](#configuration-details) + - [OS Definitions](#os-definitions) + - [Configuration example](#configuration-example) + - [Validations](#validations) + - [Optional Parameters Configuration](#optional-parameters-configuration) + - [Configuration example](#configuration-example-1) + - [Validations](#validations-1) + - [Command Generator](#command-generator) + - [Get install command](#get-install-command) + - [Get start command](#get-start-command) + - [Get url package](#get-url-package) + - [Get all commands](#get-all-commands) + +# Register Agent + +The register agent is a process that will allow the user to register an agent in the Wazuh Manager. The plugin will provide a form where the user will be able to select the OS and the package that he wants to install. The plugin will generate the registration commands and will show them to the user. + +# Solution details + +To optimize and make more easier the process to generate the registration commands we have created a class called `Command Generator` that given a set of parameters it will generate the registration commands. + +## Configuration + +To make the command generator works we need to configure the following parameters and pass them to the class: + +## OS Definitions + +The OS definitions are a set of parameters that will be used to generate the registration commands. The parameters are the following: + +```ts + + +// global types + +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +/// .... + +interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +export interface IOSCommandsDefinition { + extension: OS['extension']; + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + startCommand: (props: tOSEntryProps) => string; +} + +``` + +This configuration will define the different OS that we want to support and the different packages that we want to support for each OS. The `urlPackage` function will be used to generate the URL to download the package, the `installCommand` function will be used to generate the command to install the package and the `startCommand` function will be used to generate the command to start the agent. + +### Configuration example + +```ts + +const osDefinitions: IOSDefinition[] = [{ + name: 'linux', + options: [ + { + extension: 'rpm', + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + { + extension: 'deb', + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + } + ], +}, +{ + name: 'windows', + options: [ + { + extension: 'msi', + architecture: '32/64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + ], + } +}; +``` + +## Validations + +The `Command Generator` will validate the OS Definitions received and will throw an error if the configuration is not valid. The validations are the following: + +- The OS Definitions must not have duplicated OS names defined. +- The OS Definitions must not have duplicated options defined. +- The OS names would be defined in the `tOS` type. +- The Package Extensions would be defined in the `tPackageExtensions` type. + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Optional Parameters Configuration + +The optional parameters are a set of parameters that will be added to the registration commands. The parameters are the following: + +```ts + +export type tOptionalParamsName = + | 'server_address' + | 'agent_name' + | 'protocol' + | 'agent_group' + | 'wazuh_password'; + +export type tOptionalParams = { + [key in tOptionalParamsName]: { + property: string; + getParamCommand: (props) => string; + }; +} + +``` + +This configuration will define the different optional parameters that we want to support and the way how to we will process and show in the commands.The `getParamCommand` is the function that will process the props received and show the final command format. + +### Configuration example + +```ts + +export const optionalParameters: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => 'returns the optional param command' + } + }, + any_other: { + property: 'PARAM NAME IN THE COMMAND', + getParamCommand: props => 'returns the optional param command' + }, +} + +``` + +## Validations + +The `Command Generator` will validate the Optional Parameters received and will throw an error if the configuration is not valid. The validations are the following: + +- The Optional Parameters must not have duplicated names defined. +- The Optional Parameters name would be defined in the `tOptionalParamsName` type. + + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Command Generator + +To use the command generator we need to import the class and create a new instance of the class. The class will receive the OS Definitions and the Optional Parameters as parameters. + +```ts +import { CommandGenerator } from 'path/command-generator'; + +// Commange Generator interface/contract + +export interface ICommandGenerator extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams(props: IOptionalParameters): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +``` + +When the class is created the definitions provided will be validated and if the configuration is not valid an error will be thrown. The errors were mentioned in the configurations `Validations` section. + +### Get install command + +To generate the install command we need to call the `getInstallComand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// get install command +const installCommand = commandGenerator.getInstallCommand(); + +``` + +The `Command Generator` will search the OS provided and search in the OS Definitions and will process the command using the `installCommand` function defined in the OS Definition. If the OS is not found an error will be thrown. +If the `getInstallCommand` but the OS is not selected an error will be thrown. + +## Get start command + +To generate the install command we need to call the `getStartCommand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// get start command +const installCommand = commandGenerator.getStartCommand(); + +``` + +## Get url package + +To generate the install command we need to call the `getUrlPackage` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +const urlPackage = commandGenerator.getUrlPackage(); + +``` + +## Get all commands + +To generate the install command we need to call the `getAllCommands` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested commands. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// specify to the command generator the optional parameters that we want to use +commandGenerator.addOptionalParams({ + server_address: 'server-ip', + agent_name: 'agent-name', + any_parameter: 'any-value' +}); + +// get all commands +const installCommand = commandGenerator.getAllCommands(); + +``` + +If we specify the optional parameters the `Command Generator` will process the commands and will add the optional parameters to the commands. The optional parameters processing will be only applied to the commands that have the optional parameters defined in the Optional Parameters Definitions. If the OS Definition does not have the optional parameters defined the `Command Generator` will ignore the optional parameters. + +### getAllComands output + +```ts + +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + extension: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} + +``` \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts new file mode 100644 index 0000000000..71fd77ebd3 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -0,0 +1,398 @@ +import { CommandGenerator } from './command-generator'; +import { + IOSDefinition, + IOperationSystem, + IOptionalParameters, + tOptionalParams, +} from '../types'; +import { DuplicatedOSException, DuplicatedOSOptionException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; + +const mockedCommandValue = 'mocked command'; +const mockedCommandsResponse = jest.fn().mockReturnValue(mockedCommandValue); + +// Defined OS combinations +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +// Defined Optional Parameters + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + extension: 'msi', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, +]; + +const optionalParams: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_name: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => `${props.property}=${props.value}`, + }, + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => `${props.property}=${props.value}`, + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => `${props.property}=${props.value}`, + }, +}; + +const optionalValues: IOptionalParameters = { + server_address: '', + agent_name: '', + protocol: '', + agent_group: '', + wazuh_password: '', +}; + +describe('Command Generator', () => { + it('should create an valid instance', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + expect(commandGenerator).toBeDefined(); + }); + + it('should return the install command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getInstallCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return the start command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getStartCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return all the commands for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: 'linux', + architecture: 'x64', + extension: 'deb', + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: {}, + }); + }); + + it('should return commands with the filled optional params', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: '10.10.10.121', + agent_name: 'agent1', + protocol: 'tcp', + agent_group: '', + wazuh_password: '123456', + }; + commandGenerator.addOptionalParams(optionalValues); + + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: selectedOs.name, + architecture: selectedOs.architecture, + extension: selectedOs.extension, + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + agent_name: optionalParams.agent_name.getParamCommand({ + property: optionalParams.agent_name.property, + value: optionalValues.agent_name, + name: 'agent_name', + }), + protocol: optionalParams.protocol.getParamCommand({ + property: optionalParams.protocol.property, + value: optionalValues.protocol, + name: 'protocol', + }), + wazuh_password: optionalParams.wazuh_password.getParamCommand({ + property: optionalParams.wazuh_password.property, + value: optionalValues.wazuh_password, + name: 'wazuh_password', + }), + }, + }); + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSOptionException); + } + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSException); + } + }); + + it('should return an ERROR when we want to get commands and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getAllCommands(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the install command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getInstallCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the start command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getStartCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when receive an empty version', () => { + try { + new CommandGenerator(osDefinitions, optionalParams, ''); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + + it('should receives the solved optional params when the install command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getInstallCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); + + it('should receives the solved optional params when the start command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getStartCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); +}); \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts new file mode 100644 index 0000000000..8dd6b69889 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts @@ -0,0 +1,179 @@ +import { + ICommandsResponse, + IOSCommandsDefinition, + IOSDefinition, + IOperationSystem, + IOptionalParameters, + IOptionalParametersManager, + tOptionalParams, +} from '../types'; +import { ICommandGenerator } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from '../services/search-os-definitions.service'; +import { OptionalParametersManager } from '../optional-parameters-manager/optional-parameters-manager'; +import { NoArchitectureSelectedException, NoExtensionSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; +import { version } from '../../../../../../package.json'; + +export class CommandGenerator implements ICommandGenerator { + os: OS['name'] | null = null; + osDefinitionSelected: IOSCommandsDefinition | null = null; + optionalsManager: IOptionalParametersManager; + protected optionals: IOptionalParameters | object = {}; + constructor( + public osDefinitions: IOSDefinition[], + protected optionalParams: tOptionalParams, + public wazuhVersion: string = version, + ) { + // validate os definitions received + validateOSDefinitionsDuplicated(this.osDefinitions); + validateOSDefinitionHasDuplicatedOptions(this.osDefinitions); + if(wazuhVersion == ''){ + throw new WazuhVersionUndefinedException(); + } + this.optionalsManager = new OptionalParametersManager(optionalParams); + } + + /** + * This method selects the operating system to use based on the given parameters + * @param params - The operating system parameters to select + */ + selectOS(params: OS) { + try { + // Check if the selected operating system is valid + this.osDefinitionSelected = this.checkIfOSisValid(params); + // Set the selected operating system + this.os = params.name; + } catch (error) { + // If the selected operating system is not valid, reset the selected OS and OS definition + this.osDefinitionSelected = null; + this.os = null; + throw error; + } + } + + /** + * This method adds the optional parameters to use based on the given parameters + * @param props - The optional parameters to select + * @returns The selected optional parameters + */ + addOptionalParams(props: IOptionalParameters): void { + // Get all the optional parameters based on the given parameters + this.optionals = this.optionalsManager.getAllOptionalParams(props); + } + + /** + * This method checks if the selected operating system is valid + * @param params - The operating system parameters to check + * @returns The selected operating system definition + * @throws An error if the operating system is not valid + */ + private checkIfOSisValid(params: OS): IOSCommandsDefinition { + const { name, architecture, extension } = params; + if (!name) { + throw new NoOSSelectedException(); + } + if (!architecture) { + throw new NoArchitectureSelectedException(); + } + if (!extension) { + throw new NoExtensionSelectedException(); + } + + const option = searchOSDefinitions(this.osDefinitions, { + name, + architecture, + extension, + }); + return option; + } + + /** + * This method gets the URL package for the selected operating system + * @returns The URL package for the selected operating system + * @throws An error if the operating system is not selected + */ + getUrlPackage(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + return this.osDefinitionSelected.urlPackage({ + wazuhVersion: this.wazuhVersion, + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + name: this.os as OS['name'], + }); + } + + /** + * This method gets the install command for the selected operating system + * @returns The install command for the selected operating system + * @throws An error if the operating system is not selected + */ + getInstallCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.installCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + urlPackage: this.getUrlPackage(), + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets the start command for the selected operating system + * @returns The start command for the selected operating system + * @throws An error if the operating system is not selected + */ + getStartCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.startCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets all the commands for the selected operating system + * @returns An object containing all the commands for the selected operating system + * @throws An error if the operating system is not selected + */ + getAllCommands(): ICommandsResponse { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return { + wazuhVersion: this.wazuhVersion, + os: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + url_package: this.getUrlPackage(), + install_command: this.getInstallCommand(), + start_command: this.getStartCommand(), + optionals: this.optionals, + }; + } + + /** + * Returns the optional paramaters processed + * @returns optionals + */ + getOptionalParamsCommands(): IOptionalParameters | object { + return this.optionals; + } + +} diff --git a/public/controllers/register-agent/core/register-commands/exceptions/index.ts b/public/controllers/register-agent/core/register-commands/exceptions/index.ts new file mode 100644 index 0000000000..c84f513157 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/exceptions/index.ts @@ -0,0 +1,85 @@ +export class NoOptionFoundException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No OS option found for "${osName}" "${architecture}" "${extension}".Please check the OS definitions."`, + ); + } +} + +export class NoOSOptionFoundException extends Error { + constructor(osName: string) { + super( + `No OS option found for "${osName}".Please check the OS definitions."`, + ); + } +} + +export class NoStartCommandDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No start command definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoInstallCommandDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No install command definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoPackageURLDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No package URL definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoOptionalParamFoundException extends Error { + constructor(paramName: string) { + super( + `Optional parameter "${paramName}" not found. Please check the optional parameters definitions.`, + ); + } +} + +export class DuplicatedOSException extends Error { + constructor(osName: string) { + super(`Duplicate OS name found: ${osName}`); + } +} + +export class DuplicatedOSOptionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `Duplicate OS option found for "${osName}" "${architecture}" "${extension}"`, + ); + } +} + +export class WazuhVersionUndefinedException extends Error { + constructor() { + super(`Wazuh version not defined`); + } +} + +export class NoOSSelectedException extends Error { + constructor() { + super(`OS not selected. Please select`); + } +} + +export class NoArchitectureSelectedException extends Error { + constructor() { + super(`Architecture not selected. Please select`); + } +} + +export class NoExtensionSelectedException extends Error { + constructor() { + super(`Extension not selected. Please select`); + } +} diff --git a/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts new file mode 100644 index 0000000000..af6bef5b15 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts @@ -0,0 +1,229 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { + IOptionalParameters, + tOptionalParams, + tOptionalParamsCommandProps, +} from '../types'; +import { OptionalParametersManager } from './optional-parameters-manager'; + +type tOptionalParamsFieldname = + | 'server_address' + | 'protocol' + | 'agent_group' + | 'wazuh_password' + | 'another_valid_fieldname'; + +const returnOptionalParam = ( + props: tOptionalParamsCommandProps, +) => { + const { property, value } = props; + return `${property}=${value}`; +}; +const optionalParametersDefinition: tOptionalParams = + { + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: returnOptionalParam, + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: returnOptionalParam, + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: returnOptionalParam, + }, + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: returnOptionalParam, + }, + another_valid_fieldname: { + property: 'WAZUH_ANOTHER_PROPERTY', + getParamCommand: returnOptionalParam, + }, + }; + +describe('Optional Parameters Manager', () => { + it('should create an instance successfully', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + expect(optParamManager).toBeDefined(); + }); + + it.each([ + ['server_address', '10.10.10.27'], + ['protocol', 'TCP'], + ['agent_group', 'group1'], + ['wazuh_password', '123456'], + ['another_valid_fieldname', 'another_valid_value'] + ])( + `should return the corresponding command for "%s" param with "%s" value`, + (name, value) => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const commandParam = optParamManager.getOptionalParam({ + name: name as tOptionalParamsFieldname, + value, + }); + const defs = + optionalParametersDefinition[ + name as keyof typeof optionalParametersDefinition + ]; + expect(commandParam).toBe( + defs.getParamCommand({ + property: defs.property, + value, + name: name as tOptionalParamsFieldname, + }), + ); + }, + ); + + it('should return ERROR when the param received is not defined in the params definition', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const invalidParam = 'invalid_optional_param'; + try { + // @ts-ignore + optParamManager.getOptionalParam({ name: invalidParam, value: 'value' }); + } catch (error) { + expect(error).toBeInstanceOf(NoOptionalParamFoundException); + } + }); + + it('should return the corresponding command for all the params', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues: IOptionalParameters = { + protocol: 'TCP', + agent_group: 'group1', + wazuh_password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + expect(resolvedParams).toEqual({ + agent_group: optionalParametersDefinition.agent_group.getParamCommand({ + name: 'agent_group', + property: optionalParametersDefinition.agent_group.property, + value: paramsValues.agent_group, + }), + protocol: optionalParametersDefinition.protocol.getParamCommand({ + name: 'protocol', + property: optionalParametersDefinition.protocol.property, + value: paramsValues.protocol, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + wazuh_password: + optionalParametersDefinition.wazuh_password.getParamCommand({ + name: 'wazuh_password', + property: optionalParametersDefinition.wazuh_password.property, + value: paramsValues.wazuh_password, + }), + another_valid_fieldname: + optionalParametersDefinition.another_valid_fieldname.getParamCommand({ + name: 'another_valid_fieldname', + property: + optionalParametersDefinition.another_valid_fieldname.property, + value: paramsValues.another_valid_fieldname, + }), + } as IOptionalParameters); + }); + + it('should return the corresponse command for all the params with NOT empty values', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues: IOptionalParameters = { + protocol: 'TCP', + agent_group: 'group1', + wazuh_password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + expect(resolvedParams).toEqual({ + agent_group: optionalParametersDefinition.agent_group.getParamCommand({ + name: 'agent_group', + property: optionalParametersDefinition.agent_group.property, + value: paramsValues.agent_group, + }), + protocol: optionalParametersDefinition.protocol.getParamCommand({ + name: 'protocol', + property: optionalParametersDefinition.protocol.property, + value: paramsValues.protocol, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + wazuh_password: + optionalParametersDefinition.wazuh_password.getParamCommand({ + name: 'wazuh_password', + property: optionalParametersDefinition.wazuh_password.property, + value: paramsValues.wazuh_password, + }), + another_valid_fieldname: + optionalParametersDefinition.another_valid_fieldname.getParamCommand({ + name: 'another_valid_fieldname', + property: + optionalParametersDefinition.another_valid_fieldname.property, + value: paramsValues.another_valid_fieldname, + }), + } as IOptionalParameters); + }); + + it('should return ERROR when the param received is not defined in the params definition', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = { + serverAddress: 'invalid server address property value', + }; + + try { + // @ts-ignore + optParamManager.getAllOptionalParams(paramsValues); + } catch (error) { + expect(error).toBeInstanceOf(NoOptionalParamFoundException); + } + }); + + it('should return empty object response when receive an empty params object', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = {}; + // @ts-ignore + const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); + }); + + it('should return empty object response when receive all the params values with empty string ("")', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = { + server_address: '', + agent_name: '', + protocol: '', + agent_group: '', + wazuh_password: '', + }; + // @ts-ignore + const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); + }); +}); diff --git a/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts new file mode 100644 index 0000000000..34b943f797 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -0,0 +1,49 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { IOptionalParamInput, IOptionalParameters, IOptionalParametersManager, tOptionalParams } from '../types'; + +export class OptionalParametersManager implements IOptionalParametersManager { + constructor(private optionalParamsConfig: tOptionalParams) {} + + /** + * Returns the command string for a given optional parameter. + * @param props - An object containing the optional parameter name and value. + * @returns The command string for the given optional parameter. + * @throws NoOptionalParamFoundException if the given optional parameter name is not found in the configuration. + */ + getOptionalParam(props: IOptionalParamInput) { + const { value, name } = props; + if (!this.optionalParamsConfig[name]) { + throw new NoOptionalParamFoundException(name); + } + return this.optionalParamsConfig[name].getParamCommand({ + value, + property: this.optionalParamsConfig[name].property, + name + }); + } + + /** + * Returns an object containing the command strings for all optional parameters with non-empty values. + * @param paramsValues - An object containing the optional parameter names and values. + * @returns An object containing the command strings for all optional parameters with non-empty values. + * @throws NoOptionalParamFoundException if any of the given optional parameter names is not found in the configuration. + */ + getAllOptionalParams(paramsValues: IOptionalParameters){ + // get keys for only the optional params with values !== '' + const optionalParams = Object.keys(paramsValues).filter(key => paramsValues[key as keyof typeof paramsValues] !== '') as Array; + const resolvedOptionalParams: any = {}; + for(const param of optionalParams){ + if(!this.optionalParamsConfig[param]){ + throw new NoOptionalParamFoundException(param as string); + } + + const paramDef = this.optionalParamsConfig[param]; + resolvedOptionalParams[param as string] = paramDef.getParamCommand({ + name: param as Params, + value: paramsValues[param] as string, + property: paramDef.property + }) as string; + } + return resolvedOptionalParams; + } +} diff --git a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts new file mode 100644 index 0000000000..733737e8c5 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts @@ -0,0 +1,118 @@ +import { getInstallCommandByOS } from './get-install-command.service'; +import { IOSCommandsDefinition, IOSDefinition, IOptionalParameters } from '../types'; +import { + NoInstallCommandDefinitionException, + NoPackageURLDefinitionException, + WazuhVersionUndefinedException, +} from '../exceptions'; + + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password' | 'another_optional_parameter'; + +const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + installCommand: props => 'install command mocked', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', +}; +describe('getInstallCommandByOS', () => { + it('should return the correct install command for each OS', () => { + const installCommand = getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + expect(installCommand).toBe('install command mocked'); + }); + + it('should return ERROR when the version is not received', () => { + try { + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + it('should return ERROR when the OS has no install command', () => { + // @ts-ignore + const osDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + try { + getInstallCommandByOS( + osDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(NoInstallCommandDefinitionException); + } + }); + it('should return ERROR when the OS has no package url', () => { + try { + getInstallCommandByOS(validOsDefinition, '', '4.4', 'linux'); + } catch (error) { + expect(error).toBeInstanceOf(NoPackageURLDefinitionException); + } + }); + + it('should return install command with optional parameters', () => { + const mockedInstall = jest.fn(); + const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + installCommand: mockedInstall, + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + + const optionalParams: IOptionalParameters = { + agent_group: 'WAZUH_GROUP=agent_group', + agent_name: 'WAZUH_NAME=agent_name', + protocol: 'WAZUH_PROTOCOL=UDP', + server_address: 'WAZUH_MANAGER=server_address', + wazuh_password: 'WAZUH_PASSWORD=1231323', + another_optional_parameter: 'params value' + }; + + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + optionalParams + ); + expect(mockedInstall).toBeCalledTimes(1); + expect(mockedInstall).toBeCalledWith(expect.objectContaining({ optionals: optionalParams })); + }) +}); diff --git a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts new file mode 100644 index 0000000000..d66e75c8c0 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts @@ -0,0 +1,38 @@ +import { NoInstallCommandDefinitionException, NoPackageURLDefinitionException, WazuhVersionUndefinedException } from "../exceptions"; +import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from "../types"; + +/** + * Returns the installation command for a given operating system. + * @param {IOSCommandsDefinition} osDefinition - The definition of the operating system. + * @param {string} packageUrl - The URL of the package to install. + * @param {string} version - The version of Wazuh to install. + * @param {string} osName - The name of the operating system. + * @param {IOptionalParameters} [optionals] - Optional parameters to include in the command. + * @returns {string} The installation command for the given operating system. + * @throws {NoInstallCommandDefinitionException} If the installation command is not defined for the given operating system. + * @throws {NoPackageURLDefinitionException} If the package URL is not defined. + * @throws {WazuhVersionUndefinedException} If the Wazuh version is not defined. + */ +export function getInstallCommandByOS(osDefinition: IOSCommandsDefinition, packageUrl: string, version: string, osName: string, optionals?: IOptionalParameters) { + + if (!osDefinition.installCommand) { + throw new NoInstallCommandDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + } + + if(!packageUrl || packageUrl === ''){ + throw new NoPackageURLDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + } + + if(!version || version === ''){ + throw new WazuhVersionUndefinedException(); + } + + return osDefinition.installCommand({ + urlPackage: packageUrl, + wazuhVersion: version, + name: osName as OS['name'], + architecture: osDefinition.architecture, + extension: osDefinition.extension, + optionals, + }); +} \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts new file mode 100644 index 0000000000..f08b1f016d --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts @@ -0,0 +1,199 @@ +import { + NoOSOptionFoundException, + NoOptionFoundException, +} from '../exceptions'; +import { IOSDefinition } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from './search-os-definitions.service'; + +const mockedInstallCommand = (props: any) => 'install command mocked'; +const mockedStartCommand = (props: any) => 'start command mocked'; +const mockedUrlPackage = (props: any) => 'https://package-url.com'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const validOSDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, +]; + +describe('search OS definitions services', () => { + describe('searchOSDefinitions', () => { + it('should return the OS definition if the OS name is found', () => { + const result = searchOSDefinitions(validOSDefinitions, { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + expect(result).toMatchObject(validOSDefinitions[0].options[0]); + }); + + it('should throw an error if the OS name is not found', () => { + expect(() => + searchOSDefinitions(validOSDefinitions, { + // @ts-ignore + name: 'invalid-os', + architecture: 'x64', + extension: 'deb', + }), + ).toThrow(NoOSOptionFoundException); + }); + + it('should throw an error if the OS name is found but the architecture is not found', () => { + expect(() => + searchOSDefinitions(validOSDefinitions, { + name: 'linux', + architecture: 'invalid-architecture', + extension: 'deb', + }), + ).toThrow(NoOptionFoundException); + }); + }); + + describe('validateOSDefinitionsDuplicated', () => { + it('should not throw an error if there are no duplicated OS definitions', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionsDuplicated(osDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions', () => { + const osDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + // @ts-ignore + packageManager: 'aix', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }; + const osDefinitions: IOSDefinition[] = [osDefinition, osDefinition]; + + expect(() => validateOSDefinitionsDuplicated(osDefinitions)).toThrow(); + }); + }); + + describe('validateOSDefinitionHasDuplicatedOptions', () => { + it('should not throw an error if there are no duplicated OS definitions with different options', () => { + expect(() => + validateOSDefinitionHasDuplicatedOptions(validOSDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions with different options', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionHasDuplicatedOptions(osDefinitions), + ).toThrow(); + }); + }); +}); diff --git a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts new file mode 100644 index 0000000000..fcd5509681 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -0,0 +1,86 @@ +import { + DuplicatedOSException, + DuplicatedOSOptionException, + NoOSOptionFoundException, + NoOptionFoundException, +} from '../exceptions'; +import { IOSDefinition, IOperationSystem } from '../types'; + +/** + * Searches for the OS definition option that matches the given operation system parameters. + * Throws an exception if no matching option is found. + * + * @param osDefinitions - The list of OS definitions to search through. + * @param params - The operation system parameters to match against. + * @returns The matching OS definition option. + * @throws NoOSOptionFoundException - If no matching OS definition is found. + * @throws NoOptionFoundException - If no matching OS definition option is found. + */ +export function searchOSDefinitions( + osDefinitions: IOSDefinition[], + params: IOperationSystem, +){ + const { name, architecture, extension } = params; + + const osDefinition = osDefinitions.find(os => os.name === name); + if (!osDefinition) { + throw new NoOSOptionFoundException(name); + } + + const osDefinitionOption = osDefinition.options.find( + option => + option.architecture === architecture && option.extension === extension, + ); + + if (!osDefinitionOption) { + throw new NoOptionFoundException(name, architecture, extension); + } + + return osDefinitionOption; +}; + +/** + * Validates that there are no duplicated OS definitions in the given list. + * Throws an exception if a duplicated OS definition is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSException - If a duplicated OS definition is found. + */ +export function validateOSDefinitionsDuplicated( + osDefinitions: IOSDefinition[], +){ + const osNames = new Set(); + + for (const osDefinition of osDefinitions) { + if (osNames.has(osDefinition.name)) { + throw new DuplicatedOSException(osDefinition.name); + } + osNames.add(osDefinition.name); + } +}; + +/** + * Validates that there are no duplicated OS definition options in the given list. + * Throws an exception if a duplicated OS definition option is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSOptionException - If a duplicated OS definition option is found. + */ +export function validateOSDefinitionHasDuplicatedOptions( + osDefinitions: IOSDefinition[], +){ + for (const osDefinition of osDefinitions) { + const options = new Set(); + for (const option of osDefinition.options) { + let ext_arch_manager = `${option.extension}_${option.architecture}`; + if (options.has(ext_arch_manager)) { + throw new DuplicatedOSOptionException( + osDefinition.name, + option.extension, + option.architecture, + ); + } + options.add(ext_arch_manager); + } + } +}; diff --git a/public/controllers/register-agent/core/register-commands/types.ts b/public/controllers/register-agent/core/register-commands/types.ts new file mode 100644 index 0000000000..efd36433ff --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/types.ts @@ -0,0 +1,98 @@ +///////////////////////////////////////////////////////// +/// Domain +///////////////////////////////////////////////////////// +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +export type IOptionalParameters = { + [key in Params]: string; +}; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +interface IOptionalParamsWithValues { + optionals?: IOptionalParameters +} + + +type tOSEntryProps = IOSProps & IOptionalParamsWithValues; + +export interface IOSCommandsDefinition { + extension: OS['extension']; + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + startCommand: (props: tOSEntryProps) => string; +} + +export interface IOSProps extends IOperationSystem { + wazuhVersion: string; +} + +/////////////////////////////////////////////////////////////////// +//// Commands optional parameters +/////////////////////////////////////////////////////////////////// +interface IOptionalParamProps { + property: string; + value: string; +} + +export type tOptionalParamsCommandProps = IOptionalParamProps & { + name: T; +}; +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOptionalParamInput { + value: string; + name: T; +} +export interface IOptionalParametersManager { + getOptionalParam(props: IOptionalParamInput): string; + getAllOptionalParams(paramsValues: IOptionalParameters): object; +} + +/////////////////////////////////////////////////////////////////// +/// Command creator class +/////////////////////////////////////////////////////////////////// + +export type IOSInputs = IOperationSystem & IOptionalParameters; +export interface ICommandGenerator extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams(props: IOptionalParameters): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + extension: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} diff --git a/public/controllers/register-agent/hooks/README.md b/public/controllers/register-agent/hooks/README.md new file mode 100644 index 0000000000..0b06f1b93e --- /dev/null +++ b/public/controllers/register-agent/hooks/README.md @@ -0,0 +1,175 @@ +# Documentation + +- [useRegisterAgentCommand hook](#useregisteragentcommand-hook) +- [Advantages](#advantages) +- [Usage](#usage) +- [Types](#types) + - [Hook props](#hook-props) + - [Hook output](#hook-output) +- [Hook with Generic types](#hook-with-generic-types) + - [Operating systems types example](#operating-systems-types-example) + +## useRegisterAgentCommand hook + +This hook makes use of the `Command Generator class` to generate the commands to register agents in the manager and allows to use it in React components. + +## Advantages + +- Ease of use of the Command Generator class. +- The hook returns the methods envolved to create the register commands by the operating system and optionas specified. +- The commands generate are stored in the state of the hook and can be used in the component. + + +## Usage + +```ts + +import { useRegisterAgentCommands } from 'path/to/use-register-agent-commands'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + +/* + the props recived by the hook must implement types: + - OS: IOSDefinition[] + - optional parameters: tOptionalParams +*/ + +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(); + +// select OS depending on the specified OS defined in the hook configuration +selectOS({ + name: 'name-OS', + architecture: 'architecture-OS', + extension: 'extension-OS', +}) + +// add optionals params depending on the specified optional parameters in the hook configuration +setOptionalParams({ + field_1: 'value_1', + field_2: 'value_2', + ... +}) + +/** the commands and the optional params will be processed and stored in the hook state **/ + +// install command +console.log('install command for the selected OS with optionals params', installCommand); +// start command +console.log('start command for the selected OS with optionals params', startCommand); +// optionals params processed +console.log('optionals params processed', optionalParamsParsed); + +``` + +## Types + +### Hook props + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} +``` + +### Hook output + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +interface IUseRegisterCommandsOutput { + selectOS: (params: OS) => void; + setOptionalParams: (params: IOptionalParameters) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | {}; +} +``` + +## Hook with Generic types + +We can pass the types with the OS posibilities options and the optionals params defined. +And the hook will validate and show warning in compilation and development time. + +#### Operating systems types example + +```ts +// global types + +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +/// .... + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + + +// pass it to the hook and it will use the types when we are selecting the OS +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(OSdefintions, paramsDefinitions); + +// when the options are not valid depending on the types defined, the IDE will show a warning +selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'rpm', +}) + +```` + diff --git a/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts new file mode 100644 index 0000000000..ab0f6727df --- /dev/null +++ b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts @@ -0,0 +1,237 @@ +import React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { useRegisterAgentCommands } from './use-register-agent-commands'; +import { + IOSDefinition, + tOptionalParams, +} from '../core/register-commands/types'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +export const osCommandsDefinitions = [linuxDefinition]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = + { + optional1: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + optional2: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + }; + +describe('useRegisterAgentCommands hook', () => { + it('should return installCommand and startCommand null when the hook is initialized', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return ERROR when get installCommand and the OS received is NOT valid', () => { + const { + result: { + current: { selectOS }, + }, + } = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + try { + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + }); + } catch (error) { + if (error instanceof Error) + expect(error.message).toContain('No OS option found for'); + } + }); + + it('should change the commands when the OS is selected successfully', async () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS } = hook.result.current; + const { result } = hook; + + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64' && item.extension === 'deb', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + }); + expect(result.current.installCommand).not.toBe(''); + expect(result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(1); + expect(spyStart).toBeCalledTimes(1); + }); + + it('should return commands empty when set optional params and OS is NOT selected', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return optional params empty when optional params are not added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { optionalParamsParsed } = hook.result.current; + expect(optionalParamsParsed).toEqual({}); + }); + + it('should return optional params when optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + const spy1 = jest.spyOn( + optionalParamsDefinitions.optional1, + 'getParamCommand', + ); + const spy2 = jest.spyOn( + optionalParamsDefinitions.optional2, + 'getParamCommand', + ); + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(spy1).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(1); + }); + + it('should update the commands when the OS is selected and optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS, setOptionalParams } = hook.result.current; + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64' && item.extension === 'deb', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).not.toBe(''); + expect(hook.result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(2); + expect(spyStart).toBeCalledTimes(2); + }); +}); diff --git a/public/controllers/register-agent/hooks/use-register-agent-commands.ts b/public/controllers/register-agent/hooks/use-register-agent-commands.ts new file mode 100644 index 0000000000..800c198039 --- /dev/null +++ b/public/controllers/register-agent/hooks/use-register-agent-commands.ts @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { CommandGenerator } from '../core/register-commands/command-generator/command-generator'; +import { + IOSDefinition, + IOperationSystem, + IOptionalParameters, + tOptionalParams, +} from '../core/register-commands/types'; +import { version } from '../../../../package.json'; + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} + +interface IUseRegisterCommandsOutput { + selectOS: (params: OS) => void; + setOptionalParams: (params: IOptionalParameters) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | {}; +} + + +/** + * Custom hook that generates install and start commands based on the selected OS and optional parameters. + * + * @template T - The type of the selected OS. + * @param {IUseRegisterCommandsProps} props - The properties to configure the command generator. + * @returns {IUseRegisterCommandsOutput} - An object containing the generated commands and methods to update the selected OS and optional parameters. + */ +export function useRegisterAgentCommands(props: IUseRegisterCommandsProps): IUseRegisterCommandsOutput { + const { osDefinitions, optionalParamsDefinitions } = props; + // command generator settings + const wazuhVersion = version; + const osCommands: IOSDefinition[] = osDefinitions as IOSDefinition[]; + const optionalParams: tOptionalParams = optionalParamsDefinitions as tOptionalParams; + const commandGenerator = new CommandGenerator( + osCommands, + optionalParams, + wazuhVersion, + ); + + const [osSelected, setOsSelected] = useState(null); + const [optionalParamsValues, setOptionalParamsValues] = useState< + IOptionalParameters| {} + >({}); + const [optionalParamsParsed, setOptionalParamsParsed] = useState | {}>({}); + const [installCommand, setInstallCommand] = useState(''); + const [startCommand, setStartCommand] = useState(''); + + + /** + * Generates the install and start commands based on the selected OS and optional parameters. + * If no OS is selected, the method returns early without generating any commands. + * The generated commands are then set as state variables for later use. + */ + const generateCommands = () => { + if (!osSelected) return; + if (osSelected) { + commandGenerator.selectOS(osSelected); + } + if (optionalParamsValues) { + commandGenerator.addOptionalParams( + optionalParamsValues as IOptionalParameters, + ); + } + const installCommand = commandGenerator.getInstallCommand(); + const startCommand = commandGenerator.getStartCommand(); + setInstallCommand(installCommand); + setStartCommand(startCommand); + } + + useEffect(() => { + generateCommands(); + }, [osSelected, optionalParamsValues]); + + + /** + * Sets the selected OS for the command generator and updates the state variables accordingly. + * + * @param {T} params - The selected OS to be set. + * @returns {void} + */ + const selectOS = (params: OS) => { + commandGenerator.selectOS(params); + setOsSelected(params); + }; + + /** + * Sets the optional parameters for the command generator and updates the state variables accordingly. + * + * @param {IOptionalParameters} params - The optional parameters to be set. + * @returns {void} + */ + const setOptionalParams = (params: IOptionalParameters): void => { + commandGenerator.addOptionalParams(params); + setOptionalParamsValues(params); + setOptionalParamsParsed(commandGenerator.getOptionalParamsCommands()); + }; + + return { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } +};