From 659c0f207a892062d6bf01a9cfd3aee537bde648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 6 Feb 2025 10:14:00 +0100 Subject: [PATCH 01/22] feat: copy the register agent assistant to wazuh-fleet plugin - Copy the register-agent folder from wazuh plugin to wazuh-fleet - Add the webDocumentationLink and form components/hook to register-agent copy - Register minimal Fleet management app to display the register agent assistant view --- plugins/wazuh-fleet/common/constants.ts | 6 + .../public/application/application.tsx | 14 + .../wazuh-fleet/public/application/index.ts | 1 + .../wazuh-fleet/public/application/mount.ts | 28 + .../assets/images/themes/dark/icon.svg | 17 + .../assets/images/themes/dark/linux-icon.svg | 3 + .../assets/images/themes/dark/logo.svg | 32 + .../assets/images/themes/dark/mac-icon.svg | 4 + .../images/themes/dark/windows-icon.svg | 13 + .../assets/images/themes/light/icon.svg | 13 + .../assets/images/themes/light/linux-icon.svg | 3 + .../assets/images/themes/light/logo.svg | 23 + .../assets/images/themes/light/mac-icon.svg | 4 + .../images/themes/light/windows-icon.svg | 13 + .../command-output/command-output.tsx | 102 +++ .../components/command-output/os-warning.tsx | 69 ++ .../form/__snapshots__/index.test.tsx.snap | 466 +++++++++++++ .../components/form/hooks.test.tsx | 634 ++++++++++++++++++ .../register-agent/components/form/hooks.tsx | 303 +++++++++ .../components/form/index.test.tsx | 55 ++ .../register-agent/components/form/index.tsx | 99 +++ .../components/form/input-password.tsx | 23 + .../components/form/input_editor.tsx | 15 + .../components/form/input_filepicker.tsx | 20 + .../components/form/input_number.tsx | 15 + .../components/form/input_select.tsx | 21 + .../components/form/input_switch.tsx | 16 + .../components/form/input_text.tsx | 21 + .../components/form/input_text_area.tsx | 15 + .../register-agent/components/form/types.ts | 108 +++ .../components/group-input/group-input.scss | 5 + .../components/group-input/group-input.tsx | 102 +++ .../optionals-inputs/optionals-inputs.tsx | 116 ++++ .../checkbox-group/checkbox-group.scss | 46 ++ .../checkbox-group/checkbox-group.test.tsx | 59 ++ .../checkbox-group/checkbox-group.tsx | 53 ++ .../os-selector/os-card/os-card.scss | 55 ++ .../os-selector/os-card/os-card.test.tsx | 47 ++ .../os-selector/os-card/os-card.tsx | 71 ++ .../server-address/server-address.tsx | 172 +++++ .../register-agent/register-agent.scss | 5 + .../register-agent/register-agent.tsx | 226 +++++++ .../containers/steps/steps.scss | 51 ++ .../register-agent/containers/steps/steps.tsx | 265 ++++++++ .../core/config/os-commands-definitions.ts | 214 ++++++ .../core/register-commands/README.md | 339 ++++++++++ .../command-generator.test.ts | 380 +++++++++++ .../command-generator/command-generator.ts | 185 +++++ .../register-commands/exceptions/index.ts | 80 +++ .../optional-parameters-manager.test.ts | 229 +++++++ .../optional-parameters-manager.ts | 71 ++ .../get-install-command.service.test.ts | 112 ++++ .../services/get-install-command.service.ts | 37 + .../search-os-definitions.service.test.ts | 176 +++++ .../services/search-os-definitions.service.ts | 84 +++ .../core/register-commands/types.ts | 117 ++++ .../pages/register-agent/hooks/README.md | 167 +++++ .../hooks/use-register-agent-commands.test.ts | 229 +++++++ .../hooks/use-register-agent-commands.ts | 128 ++++ .../pages/register-agent/index.tsx | 1 + .../pages/register-agent/interfaces/types.ts | 18 + .../services/form-status-manager.test.tsx | 145 ++++ .../services/form-status-manager.tsx | 112 ++++ ...egister-agent-os-commands-services.test.ts | 227 +++++++ .../register-agent-os-commands-services.tsx | 176 +++++ .../services/register-agent-services.test.ts | 292 ++++++++ .../services/register-agent-services.tsx | 363 ++++++++++ .../register-agent-steps-status-services.tsx | 202 ++++++ .../services/wazuh-password-service.test.ts | 89 +++ .../services/wazuh-password-service.ts | 68 ++ .../services/web-documentation-link.test.ts | 25 + .../services/web-documentation-link.ts | 17 + .../utils/register-agent-data.tsx | 47 ++ .../register-agent/utils/validations.test.tsx | 45 ++ .../register-agent/utils/validations.tsx | 27 + .../public/application/render-app.tsx | 19 + .../wazuh-fleet/public/application/types.ts | 3 + plugins/wazuh-fleet/public/plugin.ts | 7 +- 78 files changed, 7859 insertions(+), 1 deletion(-) create mode 100644 plugins/wazuh-fleet/public/application/application.tsx create mode 100644 plugins/wazuh-fleet/public/application/index.ts create mode 100644 plugins/wazuh-fleet/public/application/mount.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx create mode 100644 plugins/wazuh-fleet/public/application/render-app.tsx create mode 100644 plugins/wazuh-fleet/public/application/types.ts diff --git a/plugins/wazuh-fleet/common/constants.ts b/plugins/wazuh-fleet/common/constants.ts index 4bcc9500f4..138722e9ff 100644 --- a/plugins/wazuh-fleet/common/constants.ts +++ b/plugins/wazuh-fleet/common/constants.ts @@ -1,2 +1,8 @@ +import { version } from '../package.json'; + export const PLUGIN_ID = 'wazuhFleet'; export const PLUGIN_NAME = 'wazuh_fleet'; +export const PLUGIN_VERSION_SHORT = version.split('.').splice(0, 2).join('.'); + +// Documentation +export const DOCUMENTATION_WEB_BASE_URL = 'https://documentation.wazuh.com'; diff --git a/plugins/wazuh-fleet/public/application/application.tsx b/plugins/wazuh-fleet/public/application/application.tsx new file mode 100644 index 0000000000..c06f58f981 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/application.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Router, Route, Switch, Redirect } from 'react-router-dom'; +import { RegisterAgent } from './pages/register-agent'; + +export function Application({ history }) { + return ( + + + + + + + ); +} diff --git a/plugins/wazuh-fleet/public/application/index.ts b/plugins/wazuh-fleet/public/application/index.ts new file mode 100644 index 0000000000..a0fab43d51 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/index.ts @@ -0,0 +1 @@ +export * from './mount'; diff --git a/plugins/wazuh-fleet/public/application/mount.ts b/plugins/wazuh-fleet/public/application/mount.ts new file mode 100644 index 0000000000..ce7499b650 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/mount.ts @@ -0,0 +1,28 @@ +import { AppSetup } from './types'; + +export function appSetup({ registerApp }: AppSetup) { + registerApp({ + id: 'wazuh-fleet', + title: 'Fleet management', + order: 1, + mount: async (params: AppMountParameters) => { + try { + // Load application bundle + const { renderApp } = await import('./render-app'); + + params.element.classList.add('dscAppWrapper', 'wz-app'); + + const unmount = await renderApp(params); + + return () => { + unmount(); + }; + } catch (error) { + console.debug(error); + } + }, + // category: Categories.find( + // ({ id: categoryID }) => categoryID === category, + // ), + }); +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg new file mode 100644 index 0000000000..966e74def8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg new file mode 100644 index 0000000000..c76c7d6328 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg new file mode 100644 index 0000000000..ea25e5d2f9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg new file mode 100644 index 0000000000..2eae996a06 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg new file mode 100644 index 0000000000..74d5b551f8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg new file mode 100644 index 0000000000..cc9df8577f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg new file mode 100644 index 0000000000..a931095bb7 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx new file mode 100644 index 0000000000..3137ba5831 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx @@ -0,0 +1,102 @@ +import { + EuiCodeBlock, + EuiCopy, + EuiIcon, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, + EuiText, +} from '@elastic/eui'; +import React, { Fragment, useEffect, useState } from 'react'; +import { tOperatingSystem } from '../../core/config/os-commands-definitions'; +import { osdfucatePasswordInCommand } from '../../services/wazuh-password-service'; + +interface ICommandSectionProps { + commandText: string; + showCommand: boolean; + onCopy: () => void; + os?: tOperatingSystem['name']; + password?: string; +} + +export default function CommandOutput(props: ICommandSectionProps) { + const { commandText, showCommand, onCopy, os, password } = props; + const [havePassword, setHavePassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + const onHandleCopy = (command: any) => { + onCopy && onCopy(); + return command; // the return is needed to avoid a bug in EuiCopy + }; + + const [commandToShow, setCommandToShow] = useState(commandText); + + useEffect(() => { + if (password) { + setHavePassword(true); + osdfucatePassword(password); + } else { + setHavePassword(false); + setCommandToShow(commandText); + } + }, [password, commandText, showPassword]); + + const osdfucatePassword = (password: string) => { + if (!password) return; + if (!commandText) return; + + if (showPassword) { + setCommandToShow(commandText); + } else { + setCommandToShow(osdfucatePasswordInCommand(password, commandText, os)); + } + }; + + const onChangeShowPassword = (event: EuiSwitchEvent) => { + setShowPassword(event.target.checked); + }; + return ( + + + +
+ + {showCommand ? commandToShow : ''} + + {showCommand && ( + + {copy => ( +
onHandleCopy(copy())} + > +

+ Copy command +

+
+ )} +
+ )} +
+ {showCommand && havePassword ? ( + <> + + + + ) : ( + + )} +
+
+ ); +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx new file mode 100644 index 0000000000..e9e17e0e65 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { tOperatingSystem } from '../../core/config/os-commands-definitions'; + +interface OsWarningProps { + os?: tOperatingSystem['name']; +} + +export default function OsCommandWarning(props: OsWarningProps) { + const osSelector = { + WINDOWS: ( + + +

+ Keep in mind you need to run this command in a Windows PowerShell + terminal. +

+
+ ), + LINUX: ( + + +

+ Keep in mind you need to run this command in a Shell Bash terminal. +

+
+ ), + macOS: ( + + +

+ Keep in mind you need to run this command in a Shell Bash terminal. +

+
+ ), + }; + + return osSelector[props?.os] || null; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..c7b6ee25cf --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap @@ -0,0 +1,466 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[component] InputForm Renders correctly to match the snapshot with validation errors. Input: number 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`[component] InputForm Renders correctly to match the snapshot with validation errors. Input: text 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ Validation error: string can not be empty +
+
+
+
+`; + +exports[`[component] InputForm Renders correctly to match the snapshot with validation errors. Input: text 2`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; + +exports[`[component] InputForm Renders correctly to match the snapshot: Input: editor 1`] = ` +
+
+ +
+ +
+`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx new file mode 100644 index 0000000000..e0d8eee60c --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx @@ -0,0 +1,634 @@ +import { fireEvent, render } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { renderHook, act } from '@testing-library/react-hooks'; +import React, { useState } from 'react'; +import { + enhanceFormFields, + getFormFields, + mapFormFields, + useForm, +} from './hooks'; +import { FormConfiguration, IInputForm } from './types'; + +function inspect(obj) { + return console.log( + require('util').inspect(obj, false, null, true /* enable colors */), + ); +} + +describe('[hook] useForm utils', () => { + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: '', + }, + }); + expect(result.text1.currentValue).toBe(''); + expect(result.text1.initialValue).toBe(''); + }); + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: 'text1', + }, + number1: { + type: 'number', + initialValue: 1, + }, + }); + expect(result.text1.currentValue).toBe('text1'); + expect(result.text1.initialValue).toBe('text1'); + expect(result.number1.currentValue).toBe(1); + expect(result.number1.initialValue).toBe(1); + }); + it('[utils] getFormFields', () => { + const result = getFormFields({ + text1: { + type: 'text', + initialValue: 'text1', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }, + }, + }); + expect(result.text1.currentValue).toBe('text1'); + expect(result.text1.initialValue).toBe('text1'); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].currentValue).toBe( + 'text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].initialValue).toBe( + 'text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].currentValue).toBe(10); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].initialValue).toBe(10); + }); + it.only('[utils] mapFormFields', () => { + const result = mapFormFields( + { + formDefinition: { + text1: { + type: 'text', + initialValue: 'text1', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }, + }, + }, + formState: { + text1: { + currentValue: 'changed1', + initialValue: 'text1', + }, + arrayOf1: { + fields: [ + { + 'arrayOf1.text1': { + currentValue: 'arrayOf1.text1.changed1', + initialValue: 'arrayOf1.text1', + }, + 'arrayOf1.number1': { + currentValue: 10, + initialValue: 0, + }, + }, + ], + }, + }, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, currentValue: state.initialValue }), + ); + expect(result.text1.currentValue).toBe('text1'); + expect(result.arrayOf1.fields[0]['arrayOf1.text1'].currentValue).toBe( + 'arrayOf1.text1', + ); + expect(result.arrayOf1.fields[0]['arrayOf1.number1'].currentValue).toBe(0); + }); +}); + +describe('[hook] useForm', () => { + it('[hook] enhanceFormFields', () => { + let state; + const setState = updateState => (state = updateState); + const references = { + current: {}, + }; + + const fields = { + text1: { + type: 'text', + initialValue: '', + }, + }; + + const formFields = getFormFields(fields); + const enhancedFields = enhanceFormFields(formFields, { + fields, + references, + setState, + }); + expect(enhancedFields.text1).toBeDefined(); + expect(enhancedFields.text1.type).toBe('text'); + expect(enhancedFields.text1.initialValue).toBe(''); + expect(enhancedFields.text1.value).toBe(''); + expect(enhancedFields.text1.changed).toBeDefined(); + expect(enhancedFields.text1.error).toBeUndefined(); + expect(enhancedFields.text1.setInputRef).toBeDefined(); + expect(enhancedFields.text1.inputRef).toBeUndefined(); + expect(enhancedFields.text1.onChange).toBeDefined(); + }); + + it('[hook] enhanceFormFields', () => { + let state; + const setState = updateState => (state = updateState); + const references = { + current: {}, + }; + + const arrayOfFields = { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + }, + }; + const fields = { + text1: { + type: 'text', + initialValue: '', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: arrayOfFields, + }, + }; + + const formFields = getFormFields(fields); + const enhancedFields = enhanceFormFields(formFields, { + fields, + references, + setState, + }); + expect(enhancedFields.text1).toBeDefined(); + expect(enhancedFields.text1.type).toBe('text'); + expect(enhancedFields.text1.initialValue).toBe(''); + expect(enhancedFields.text1.value).toBe(''); + expect(enhancedFields.text1.changed).toBeDefined(); + expect(enhancedFields.text1.error).toBeUndefined(); + expect(enhancedFields.text1.setInputRef).toBeDefined(); + expect(enhancedFields.text1.inputRef).toBeUndefined(); + expect(enhancedFields.text1.onChange).toBeDefined(); + expect(enhancedFields.arrayOf1).toBeDefined(); + expect(enhancedFields.arrayOf1.fields).toBeDefined(); + expect(enhancedFields.arrayOf1.fields).toHaveLength(1); + expect(enhancedFields.arrayOf1.fields[0]).toBeDefined(); + expect(enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].type).toBe( + 'text', + ); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + expect(enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].value).toBe( + 'text1', + ); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBeDefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].setInputRef, + ).toBeDefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].inputRef, + ).toBeUndefined(); + expect( + enhancedFields.arrayOf1.fields[0]['arrayOf1.text1'].onChange, + ).toBeDefined(); + }); +}); + +describe('[hook] useForm', () => { + it(`[hook] useForm. Verify the initial state`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + }); + + it(`[hook] useForm. Verify the initial state. Multiple fields.`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + number1: { + type: 'number', + initialValue: 1, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + + expect(result.current.fields.number1.changed).toBe(false); + expect(result.current.fields.number1.error).toBeUndefined(); + expect(result.current.fields.number1.type).toBe('number'); + expect(result.current.fields.number1.value).toBe(1); + expect(result.current.fields.number1.initialValue).toBe(1); + expect(result.current.fields.number1.onChange).toBeDefined(); + }); + + it(`[hook] useForm lifecycle. Set the initial value. Change the field value. Undo changes. Change the field. Do changes.`, async () => { + const initialFieldValue = ''; + const fieldType = 'text'; + + const initialFields: FormConfiguration = { + text1: { + type: fieldType, + initialValue: initialFieldValue, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + expect(result.current.fields.text1.onChange).toBeDefined(); + + // change the input + const changedValue = 't'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // undone changes + act(() => { + result.current.undoChanges(); + }); + + // assert undo changes state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // change the input + const changedValue2 = 'e'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue2, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue2); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // done changes + act(() => { + result.current.doChanges(); + }); + + // assert do changes state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue2); + expect(result.current.fields.text1.initialValue).toBe(changedValue2); + }); + + it(`[hook] useForm lifecycle. Set the initial value. Change the field value to invalid value`, async () => { + const initialFieldValue = 'test'; + const fieldType = 'text'; + + const initialFields: FormConfiguration = { + text1: { + type: fieldType, + initialValue: initialFieldValue, + validate: (value: string): string | undefined => + value.length ? undefined : `Validation error: string can be empty.`, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + expect(result.current.fields.text1.onChange).toBeDefined(); + + // change the input + const changedValue = ''; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeTruthy(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + }); + + it.only(`[hook] useForm. ArrayOf.`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + arrayOf1: { + type: 'arrayOf', + initialValue: [ + { + 'arrayOf1.text1': 'text1', + 'arrayOf1.number1': 10, + }, + ], + fields: { + 'arrayOf1.text1': { + type: 'text', + initialValue: 'default', + }, + 'arrayOf1.number1': { + type: 'number', + initialValue: 0, + options: { + min: 0, + max: 10, + integer: true, + }, + }, + }, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + + expect(result.current.fields.arrayOf1).toBeDefined(); + expect(result.current.fields.arrayOf1.fields).toBeDefined(); + expect(result.current.fields.arrayOf1.fields).toHaveLength(1); + expect(result.current.fields.arrayOf1.fields[0]).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].type, + ).toBe('text'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].setInputRef, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].inputRef, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].onChange, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].type, + ).toBe('number'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].initialValue, + ).toBe(10); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].value, + ).toBe(10); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].changed, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].setInputRef, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].inputRef, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].onChange, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options.min, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options.max, + ).toBeDefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.number1'].options + .integer, + ).toBeDefined(); + + // change the input + const changedValue = 'changed_text'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.initialValue).toBe(''); + + // change arrayOf input + const changedArrayOfValue = 'changed_arrayOf_field'; + act(() => { + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].onChange({ + target: { + value: changedArrayOfValue, + }, + }); + }); + + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBe(true); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].error, + ).toBeUndefined(); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].type, + ).toBe('text'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe(changedArrayOfValue); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].initialValue, + ).toBe('text1'); + + // Undo changes + act(() => { + result.current.undoChanges(); + }); + + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.changed).toBe(false); + + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].value, + ).toBe('text1'); + expect( + result.current.fields.arrayOf1.fields[0]['arrayOf1.text1'].changed, + ).toBe(false); + }); + + it('[hook] useForm. Verify the hook behavior when receives a custom field type', async () => { + const CustomComponent = (props: any) => { + const { onChange, field, initialValue } = props; + const [value, setValue] = useState(initialValue || ''); + + const handleOnChange = (e: any) => { + setValue(e.target.value); + onChange(e); + }; + + return ( + <> + {field} + + + ); + }; + + const formFields: FormConfiguration = { + customField: { + type: 'custom', + initialValue: 'default value', + component: props => CustomComponent(props), + }, + }; + + const { result } = renderHook(() => useForm(formFields)); + const { container, getByRole } = render( + , + ); + + expect(container).toBeInTheDocument(); + const input = getByRole('textbox'); + expect(input).toHaveValue('default value'); + fireEvent.change(input, { target: { value: 'new value' } }); + expect(result.current.fields.customField.component).toBeInstanceOf( + Function, + ); + expect(result.current.fields.customField.value).toBe('new value'); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx new file mode 100644 index 0000000000..1c2b3554d7 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx @@ -0,0 +1,303 @@ +import { useState, useRef } from 'react'; +import { isEqual, get, set, cloneDeep } from 'lodash'; +import { + CustomSettingType, + FormConfiguration, + SettingTypes, + UseFormReturn, +} from './types'; + +type IgetValueFromEventType = Record any>; + +// eslint-disable-next-line @typescript-eslint/naming-convention +enum EpluginSettingType { // eslint-disable-next-line @typescript-eslint/naming-convention + text = 'text', // eslint-disable-next-line @typescript-eslint/naming-convention + textarea = 'textarea', // eslint-disable-next-line @typescript-eslint/naming-convention + switch = 'switch', // eslint-disable-next-line @typescript-eslint/naming-convention + number = 'number', // eslint-disable-next-line @typescript-eslint/naming-convention + editor = 'editor', // eslint-disable-next-line @typescript-eslint/naming-convention + select = 'select', // eslint-disable-next-line @typescript-eslint/naming-convention + filepicker = 'filepicker', // eslint-disable-next-line @typescript-eslint/naming-convention + password = 'password', // eslint-disable-next-line @typescript-eslint/naming-convention + arrayOf = 'arrayOf', // eslint-disable-next-line @typescript-eslint/naming-convention + custom = 'custom', // eslint-disable-next-line @typescript-eslint/naming-convention +} + +/** + * Returns the value of the event according to the type of field + * When the type is not found, it returns the value defined in the default key + * + * @param event + * @param type + * @returns event value + */ +function getValueFromEvent( + event: any, + type: SettingTypes | CustomSettingType, +): any { + return (getValueFromEventType[type] || getValueFromEventType.default)(event); +} + +const getValueFromEventType: IgetValueFromEventType = { + [EpluginSettingType.switch]: (event: any) => event.target.checked, + [EpluginSettingType.editor]: (value: any) => value, + [EpluginSettingType.filepicker]: (value: any) => value, + [EpluginSettingType.select]: (event: any) => event.target.value, + [EpluginSettingType.text]: (event: any) => event.target.value, + [EpluginSettingType.textarea]: (event: any) => event.target.value, + [EpluginSettingType.number]: (event: any) => event.target.value, + [EpluginSettingType.password]: (event: any) => event.target.value, + custom: (event: any) => event.target.value, + default: (event: any) => event.target.value, +}; + +export function getFormFields(fields) { + return Object.fromEntries( + Object.entries(fields).map(([fieldKey, fieldConfiguration]) => [ + fieldKey, + { + ...(fieldConfiguration.type === 'arrayOf' + ? { + fields: fieldConfiguration.initialValue.map(item => + getFormFields( + Object.fromEntries( + Object.entries(fieldConfiguration.fields).map(([key]) => [ + key, + { + initialValue: item[key], + currentValue: item[key], + defaultValue: fieldConfiguration?.defaultValue, + }, + ]), + ), + ), + ), + } + : { + currentValue: fieldConfiguration.initialValue, + initialValue: fieldConfiguration.initialValue, + defaultValue: fieldConfiguration?.defaultValue, + }), + }, + ]), + ); +} + +export function enhanceFormFields( + formFields, + { + fields, + references, + setState, + pathFieldParent = [], + pathFormFieldParent = [], + }, +) { + return Object.entries(formFields).reduce( + (accum, [fieldKey, { currentValue: value, ...restFieldState }]) => { + // Define the path to fields object + const pathField = [...pathFieldParent, fieldKey]; + // Define the path to the form fields object + const pathFormField = [...pathFormFieldParent, fieldKey]; + // Get the field form the fields + const field = get(fields, pathField); + + return { + ...accum, + [fieldKey]: { + ...(field.type === 'arrayOf' + ? { + type: field.type, + fields: (() => + restFieldState.fields.map((fieldState, index) => + enhanceFormFields(fieldState, { + fields, + references, + setState, + pathFieldParent: [...pathField, 'fields'], + pathFormFieldParent: [...pathFormField, 'fields', index], + }), + ))(), + addNewItem: () => { + setState(state => { + const _state = get(state, [...pathField, 'fields']); + const newstate = set( + state, + [...pathField, 'fields', _state.length], + Object.fromEntries( + Object.entries(field.fields).map( + ([key, { defaultValue }]) => [ + key, + { + currentValue: cloneDeep(defaultValue), + initialValue: cloneDeep(defaultValue), + defaultValue: cloneDeep(defaultValue), + }, + ], + ), + ), + ); + + return cloneDeep(newstate); + }); + }, + } + : { + ...field, + ...restFieldState, + type: field.type, + value, + changed: !isEqual(restFieldState.initialValue, value), + error: field?.validate?.(value), + setInputRef: (reference: any) => { + set(references, pathFormField, reference); + }, + inputRef: get(references, pathFormField), + onChange: (event: any) => { + const inputValue = getValueFromEvent(event, field.type); + const currentValue = + field?.transformChangedInputValue?.(inputValue) ?? + inputValue; + + setState(state => { + const newState = set( + cloneDeep(state), + [...pathFormField, 'currentValue'], + currentValue, + ); + + return newState; + }); + }, + }), + }, + }; + }, + {}, + ); +} + +export function mapFormFields( + { + formDefinition, + formState, + pathFieldFormDefinition = [], + pathFormState = [], + }, + callbackFormField, +) { + return Object.entries(formState).reduce((accum, [key, value]) => { + const pathField = [...pathFieldFormDefinition, key]; + const fieldDefinition = get(formDefinition, pathField); + + return { + ...accum, + [key]: + fieldDefinition.type === 'arrayOf' + ? { + fields: value.fields.map((valueField, index) => + mapFormFields( + { + formDefinition, + formState: valueField, + pathFieldFormDefinition: [...pathField, 'fields'], + pathFormState: [...pathFormState, key, 'fields', index], + }, + callbackFormField, + ), + ), + } + : callbackFormField?.(value, key, { + formDefinition, + formState, + pathFieldFormDefinition, + pathFormState: [...pathFormState, key], + fieldDefinition, + }), + }; + }, {}); +} + +export const useForm = (fields: FormConfiguration): UseFormReturn => { + const [formFields, setFormFields] = useState< + Record + >(getFormFields(fields)); + const fieldRefs = useRef>({}); + const enhanceFields = enhanceFormFields(formFields, { + fields, + references: fieldRefs.current, + setState: setFormFields, + pathFieldParent: [], + pathFormFieldParent: [], + }); + const { changed, errors } = (() => { + const result = { + changed: {}, + errors: {}, + }; + + mapFormFields( + { + formDefinition: fields, + formState: enhanceFields, + pathFieldFormDefinition: [], + pathFormState: [], + }, + ({ changed, error, value }, _, { pathFormState, fieldDefinition }) => { + changed && (result.changed[pathFormState] = value); + error && (result.errors[pathFormState] = error); + }, + ); + + return result; + })(); + + function undoChanges() { + setFormFields(state => + mapFormFields( + { + formDefinition: fields, + formState: state, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, currentValue: state.initialValue }), + ), + ); + } + + function doChanges() { + setFormFields(state => + mapFormFields( + { + formDefinition: fields, + formState: state, + pathFieldFormDefinition: [], + pathFormState: [], + }, + state => ({ ...state, initialValue: state.currentValue }), + ), + ); + } + + function forEach(callback) { + return mapFormFields( + { + formDefinition: fields, + formState: enhanceFields, + pathFieldFormDefinition: [], + pathFormState: [], + }, + callback, + ); + } + + return { + fields: enhanceFields, + changed, + errors, + undoChanges, + doChanges, + forEach, + }; +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx new file mode 100644 index 0000000000..17e21ee8ec --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { InputForm } from './index'; +import { useForm } from './hooks'; + +jest.mock('../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'test-id', +})); + +describe('[component] InputForm', () => { + const optionsEditor = {editor: {language: 'json'}}; + const optionsFilepicker = {file: {type: 'image', extensions: ['.jpeg', '.jpg', '.png', '.svg']}}; + const optionsSelect = {select: [{text: 'Label1', value: 'value1'}, {text: 'Label2', value: 'value2'}]}; + const optionsSwitch = {switch: {values: {enabled: {label: 'Enabled', value: true}, disabled: {label: 'Disabled', value: false}}}}; + it.each` + inputType | value | options + ${'editor'} | ${'{}'} | ${optionsEditor} + ${'filepicker'} | ${'{}'} | ${optionsFilepicker} + ${'number'} | ${4} | ${undefined} + ${'select'} | ${'value1'} | ${optionsSelect} + ${'switch'} | ${true} | ${optionsSwitch} + ${'text'} | ${'test'} | ${undefined} + ${'textarea'} | ${'test'} | ${undefined} + `('Renders correctly to match the snapshot: Input: $inputType', ({ inputType, value, options }) => { + const wrapper = render( + {}} + options={options} + /> + ); + expect(wrapper.container).toMatchSnapshot(); + }); + + it.each` + inputType | initialValue | options | rest + ${'number'} | ${4} | ${{number: {min: 5}}} | ${{ validate: (value) => value > 3 ? undefined : 'Vaidation error: value is lower than 5'}} + ${'text'} | ${''} | ${undefined} | ${{ validate: (value) => value.length ? undefined : 'Validation error: string can not be empty' }} + ${'text'} | ${'test spaces'} | ${undefined} | ${{ validate: (value) => value.length ? undefined : 'Validation error: string can not contain spaces' }} + `('Renders correctly to match the snapshot with validation errors. Input: $inputType', async ({ inputType, initialValue, options, rest }) => { + const TestComponent = () => { + const { fields: { [inputType]: field } } = useForm({ [inputType]: { initialValue, type: inputType, options, ...rest } }); + return ( + + ); + }; + const wrapper = render(); + expect(wrapper.container).toMatchSnapshot(); + }); +}); + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx new file mode 100644 index 0000000000..08ef95f94d --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { InputFormEditor } from './input_editor'; +import { InputFormNumber } from './input_number'; +import { InputFormText } from './input_text'; +import { InputFormSelect } from './input_select'; +import { InputFormSwitch } from './input_switch'; +import { InputFormFilePicker } from './input_filepicker'; +import { InputFormTextArea } from './input_text_area'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { SettingTypes } from './types'; +import { InputFormPassword } from './input-password'; + +export interface InputFormProps { + type: SettingTypes; + value: any; + onChange: (event: React.ChangeEvent) => void; + error?: string; + label?: string | React.ReactNode; + header?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + footer?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + preInput?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + postInput?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); +} + +interface InputFormComponentProps extends InputFormProps { + rest: any; +} + +export const InputForm = ({ + type, + value, + onChange, + error, + label, + header, + footer, + preInput, + postInput, + ...rest +}: InputFormComponentProps) => { + const ComponentInput = Input[ + type as keyof typeof Input + ] as React.ComponentType; + + if (!ComponentInput) { + return null; + } + + const isInvalid = Boolean(error); + + const input = ( + + ); + + return label ? ( + + <> + {typeof header === 'function' ? header({ value, error }) : header} + + {typeof preInput === 'function' + ? preInput({ value, error }) + : preInput} + {input} + {typeof postInput === 'function' + ? postInput({ value, error }) + : postInput} + + {typeof footer === 'function' ? footer({ value, error }) : footer} + + + ) : ( + input + ); +}; + +const Input = { + switch: InputFormSwitch, + editor: InputFormEditor, + filepicker: InputFormFilePicker, + number: InputFormNumber, + select: InputFormSelect, + text: InputFormText, + textarea: InputFormTextArea, + password: InputFormPassword, + custom: ({ component, ...rest }) => component(rest), +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx new file mode 100644 index 0000000000..eebae6e517 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { EuiFieldPassword } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormPassword = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, + options, +}: IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx new file mode 100644 index 0000000000..ab38c89f6d --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormEditor = ({options, value, onChange}: IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx new file mode 100644 index 0000000000..5a46828a47 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { EuiFilePicker } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormFilePicker = ({onChange, options, setInputRef, key, ...rest} : IInputFormType) => ( + onChange( + // File was added. + fileList?.[0] + // File was removed. We set the initial value, so the useForm hook will not detect any change. */ + || rest.initialValue)} + display='default' + fullWidth + aria-label='Upload a file' + accept={options.file.extensions.join(',')} + ref={setInputRef} + /> +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx new file mode 100644 index 0000000000..f4db8e2a0b --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { EuiFieldNumber } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormNumber = ({ options, value, onChange }: IInputFormType) => { + const { integer, ...rest } = options?.number || {}; + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx new file mode 100644 index 0000000000..c610066136 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { EuiSelect } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormSelect = ({ + options, + value, + onChange, + placeholder, + dataTestSubj, +}: IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx new file mode 100644 index 0000000000..75a575d97f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { EuiSwitch } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormSwitch = ({ options, value, onChange }: IInputFormType) => { + const checked = Object.entries(options.switch.values) + .find(([, { value: statusValue }]) => value === statusValue)[0]; + + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx new file mode 100644 index 0000000000..c8e3d730d4 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { EuiFieldText } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormText = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, +}: IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx new file mode 100644 index 0000000000..90cbb2d2e1 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { EuiTextArea } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormTextArea = ({ value, isInvalid, onChange, options } : IInputFormType) => { + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts new file mode 100644 index 0000000000..2d009b5b88 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts @@ -0,0 +1,108 @@ +import { TPluginSettingWithKey } from '../../../../../wazuh-core/common/constants'; + +export interface IInputFormType { + field: TPluginSettingWithKey; + value: any; + onChange: (event: any) => void; + isInvalid?: boolean; + options: any; + setInputRef: (reference: any) => void; + placeholder: string; + dataTestSubj: string; +} + +export interface IInputForm { + field: TPluginSettingWithKey; + initialValue: any; + onChange: (event: any) => void; + label?: string; + preInput?: (options: { value: any; error: string | null }) => JSX.Element; + postInput?: (options: { value: any; error: string | null }) => JSX.Element; +} + +/// use form hook types + +export type SettingTypes = + | 'text' + | 'textarea' + | 'number' + | 'select' + | 'switch' + | 'editor' + | 'filepicker'; + +interface FieldConfiguration { + initialValue: any; + validate?: (value: any) => string | undefined; + transformChangedInputValue?: (value: any) => any; +} + +export interface DefaultFieldConfiguration extends FieldConfiguration { + type: SettingTypes; +} + +export type CustomSettingType = 'custom'; +interface CustomFieldConfiguration extends FieldConfiguration { + type: CustomSettingType; + component: (props: any) => JSX.Element; +} + +interface ArrayOfFieldConfiguration extends FieldConfiguration { + type: 'arrayOf'; + fields: { + [key: string]: any; // TODO: enhance this type + }; +} + +export interface FormConfiguration { + [key: string]: + | DefaultFieldConfiguration + | CustomFieldConfiguration + | ArrayOfFieldConfiguration; +} + +interface EnhancedField { + currentValue: any; + initialValue: any; + value: any; + changed: boolean; + error: string | null | undefined; + setInputRef: (reference: any) => void; + inputRef: any; + onChange: (event: any) => void; +} + +interface EnhancedDefaultField extends EnhancedField { + type: SettingTypes; +} + +interface EnhancedCustomField extends EnhancedField { + type: CustomSettingType; + component: (props: any) => JSX.Element; +} + +export type EnhancedFieldConfiguration = + | EnhancedDefaultField + | EnhancedCustomField; +export interface EnhancedFields { + [key: string]: EnhancedFieldConfiguration; +} + +export interface UseFormReturn { + fields: EnhancedFields; + changed: { [key: string]: any }; + errors: { [key: string]: string }; + undoChanges: () => void; + doChanges: () => void; + forEach: ( + value: any, + key: string, + form: { + formDefinition: any; + formState: any; + pathFieldFormDefinition: string[]; + pathFormState: string[]; + fieldDefinition: FormConfiguration; + }, + ) => { [key: string]: any }; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss new file mode 100644 index 0000000000..575880c792 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss @@ -0,0 +1,5 @@ +.registerAgentLabels { + font-weight: 700; + font-size: 12px; + line-height: 20px; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx new file mode 100644 index 0000000000..8b85de28c2 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx @@ -0,0 +1,102 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiComboBox, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import './group-input.scss'; +import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; + +const popoverAgentGroup = ( + + Learn about{' '} + + Select a group. + + +); + +const GroupInput = ({ value, options, onChange }) => { + const [isPopoverAgentGroup, setIsPopoverAgentGroup] = useState(false); + const onButtonAgentGroup = () => + setIsPopoverAgentGroup(isPopoverAgentGroup => !isPopoverAgentGroup); + const closeAgentGroup = () => setIsPopoverAgentGroup(false); + + return ( + <> + + +

+ Select one or more existing groups: +

+
+ + + } + isOpen={isPopoverAgentGroup} + closePopover={closeAgentGroup} + anchorPosition='rightCenter' + > + {popoverAgentGroup} + + +
+ { + onChange({ + target: { value: group }, + }); + }} + isDisabled={!options?.groups.length} + isClearable={true} + data-test-subj='demoComboBox' + data-testid='group-input-combobox' + /> + {!options?.groups.length && ( + <> + + + )} + + ); +}; + +export default GroupInput; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx new file mode 100644 index 0000000000..3d9bb7f58e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -0,0 +1,116 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiCallOut, + EuiLink, +} from '@elastic/eui'; +import { UseFormReturn } from '../form/types'; +import { InputForm } from '../form'; +import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/register-agent-data'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; +import '../group-input/group-input.scss'; + +interface OptionalsInputsProps { + formFields: UseFormReturn['fields']; +} + +const OptionalsInputs = (props: OptionalsInputsProps) => { + const { formFields } = props; + const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + const onButtonAgentName = () => + setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); + const closeAgentName = () => setIsPopoverAgentName(false); + const agentNameDocLink = webDocumentationLink( + 'user-manual/reference/ossec-conf/client.html#enrollment-agent-name', + PLUGIN_VERSION_SHORT, + ); + const popoverAgentName = ( + + Learn about{' '} + + Assigning an agent name. + + + ); + const warningForAgentName = + 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; + + return ( + + + {OPTIONAL_PARAMETERS_TEXT.map((data, index) => ( + + {data.subtitle} + + ))} + + + + +

Assign an agent name:

+
+ + + } + isOpen={isPopoverAgentName} + closePopover={closeAgentName} + anchorPosition='rightCenter' + > + {popoverAgentName} + + +
+ + } + placeholder='Agent name' + /> + + {warningForAgentName} + + + } + iconType='iInCircle' + className='warningForAgentName' + /> + +
+ ); +}; + +export default OptionalsInputs; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss new file mode 100644 index 0000000000..b8b985d165 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss @@ -0,0 +1,46 @@ +.checkbox-group-container { + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 26px; + justify-content: center; +} + +.checkbox-item { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: left; +} + +.checkbox-group-container.single-architecture { + margin-top: 44px; + display: flex; + justify-content: center; +} + +.checkbox-group-container.double-architecture { + margin-top: 24px; + display: flex; + flex-direction: column; + .checkbox-item:first-child { + margin-bottom: 13px; + } + .checkbox-item { + display: flex; + flex-direction: row-reverse; + justify-content: left; + align-self: baseline; + } +} + +.architecture-label { + margin-left: 8px; + font-style: normal; + font-weight: 400; + font-size: 12px; +} +.first-card-four-items { + .checkbox-item:nth-child(n + 3) { + padding-top: 16px; + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx new file mode 100644 index 0000000000..186fc0d240 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; + +describe('CheckboxGroupComponent', () => { + const data = ['Option 1', 'Option 2', 'Option 3']; + const cardIndex = 0; + const selectedOption = 'Option 1'; + const onOptionChange = jest.fn(); + + test('renders checkbox items with correct labels', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + expect(checkboxItems).toHaveLength(data.length); + + expect(checkboxItems[0]).toHaveAttribute('id', 'Option 1'); + expect(checkboxItems[1]).toHaveAttribute('id', 'Option 2'); + expect(checkboxItems[2]).toHaveAttribute('id', 'Option 3'); + + expect(checkboxItems[0]).toBeChecked(); + expect(checkboxItems[1]).not.toBeChecked(); + expect(checkboxItems[2]).not.toBeChecked(); + + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.getByText('Option 2')).toBeInTheDocument(); + expect(screen.getByText('Option 3')).toBeInTheDocument(); + }); + + test('calls onOptionChange when a checkbox is selected', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + + fireEvent.click(checkboxItems[1]); + + expect(onOptionChange).toHaveBeenCalledTimes(1); + expect(onOptionChange).toHaveBeenCalledWith( + expect.objectContaining({ + target: { value: `Option 2` }, + }), + ); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx new file mode 100644 index 0000000000..461e6e0943 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { EuiRadioGroup } from '@elastic/eui'; +import './checkbox-group.scss'; + +interface Props { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +const CheckboxGroupComponent: React.FC = ({ + data, + cardIndex, + selectedOption, + onOptionChange, +}) => { + const isSingleArchitecture = data.length === 1; + const isDoubleArchitecture = data.length === 2; + const isFirstCardWithFourItems = cardIndex === 0 && data.length === 4; + return ( +
+ {data.map((arch, idx) => ( +
+ + { + onOptionChange({ target: { value: id } }); + }} + /> +
+ ))} +
+ ); +}; + +export { CheckboxGroupComponent }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss new file mode 100644 index 0000000000..636996765b --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss @@ -0,0 +1,55 @@ +.card { + height: 183px; + + label { + cursor: pointer; + } +} + +.cardTitle { + display: flex; + align-items: center; + margin-top: 28px; + justify-content: center; + user-select: none; +} + +.cardIcon { + margin-right: 10px; +} + +.cardText { + font-style: normal; + font-weight: 700; + font-size: 18px; + display: flex; + align-items: center; + text-align: center; + letter-spacing: 0.6px; +} + +.hr { + border: 1px solid #d3dae6; +} + +.cardContent { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.checkboxGroupContainer { + flex-basis: 50%; +} + +.architectureItem { + margin-bottom: 8px; +} + +.last-card { + margin-right: 63px; +} + +.cardsCallOut { + margin-top: 16px; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx new file mode 100644 index 0000000000..dea9040ed0 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { OsCard } from './os-card'; + +jest.mock('../../../../../../kibana-services', () => ({ + ...(jest.requireActual('../../../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: url => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), + getUiSettings: jest.fn().mockReturnValue({ + get: name => { + return true; + }, + }), +})); + +describe('OsCard', () => { + test('renders three cards with different titles', () => { + render(); + + const cardTitles = screen.getAllByTestId('card-title'); + expect(cardTitles).toHaveLength(3); + + expect(cardTitles[0]).toHaveTextContent('LINUX'); + expect(cardTitles[1]).toHaveTextContent('WINDOWS'); + expect(cardTitles[2]).toHaveTextContent('macOS'); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx new file mode 100644 index 0000000000..70ff056db9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLink, + EuiCheckbox, +} from '@elastic/eui'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; +import './os-card.scss'; +import { webDocumentationLink } from '../../../services/web-documentation-link'; + +interface Props { + setStatusCheck: string; + onChange: React.ChangeEventHandler; + value: any; +} + +export const OsCard = ({ onChange, value }: Props) => ( +
+ + {OPERATING_SYSTEMS_OPTIONS.map((data, index) => ( + + + Icon + {data.title} +
+ } + display='plain' + hasBorder + className='card' + > + {data.hr &&
} + + + + ))} + + + For additional systems and architectures, please check our{' '} + + documentation + + . + + } + > +
+); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx new file mode 100644 index 0000000000..b33be98fa3 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx @@ -0,0 +1,172 @@ +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiLink, + EuiSwitch, +} from '@elastic/eui'; +import React, { Fragment, useEffect, useState } from 'react'; +import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; +import { EnhancedFieldConfiguration } from '../form/types'; +import { InputForm } from '../form'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; +import '../group-input/group-input.scss'; +import { getWazuhCore } from '../../../../../plugin-services'; + +interface ServerAddressInputProps { + formField: EnhancedFieldConfiguration; +} + +const popoverServerAddress = ( + + Learn about{' '} + + Server address. + + +); + +const ServerAddressInput = (props: ServerAddressInputProps) => { + const { formField } = props; + const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); + const onButtonServerAddress = () => + setIsPopoverServerAddress( + isPopoverServerAddress => !isPopoverServerAddress, + ); + const closeServerAddress = () => setIsPopoverServerAddress(false); + const [rememberServerAddress, setRememberServerAddress] = useState(false); + const [defaultServerAddress, setDefaultServerAddress] = useState( + formField?.initialValue ? formField?.initialValue : '', + ); + const appConfig = getWazuhCore().configuration.get(); // TODO: this should use a live state (that reacts to changes in the configuration) + + const handleToggleRememberAddress = async event => { + setRememberServerAddress(event.target.checked); + + if (event.target.checked) { + await saveServerAddress(); + setDefaultServerAddress(formField.value); + } + }; + + const saveServerAddress = async () => { + try { + const res = await getWazuhCore().http.server.request( + 'PUT', + '/utils/configuration', + { + 'enrollment.dns': formField.value, + }, + ); + } catch { + // TODO: use error handler + // ErrorHandler.handleError(error, { + // message: error.message, + // title: 'Error saving server address configuration', + // }); + setRememberServerAddress(false); + } + }; + + const rememberToggleIsDisabled = () => !formField.value || !!formField.error; + + const handleInputChange = value => { + if (value === defaultServerAddress) { + setRememberServerAddress(true); + } else { + setRememberServerAddress(false); + } + }; + + useEffect(() => { + handleInputChange(formField.value); + }, [formField.value]); + + const { ServerButtonPermissions } = getWazuhCore().ui; + + return ( + + + {SERVER_ADDRESS_TEXTS.map((data, index) => ( + + + {data.subtitle} + + + ))} + + + + + + + + Assign a server address + + + + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + + + } + fullWidth={false} + placeholder='Server address' + /> + + + {appConfig?.['configuration.ui_api_editable'] && ( + + + handleToggleRememberAddress(e)} + /> + + + )} + + ); +}; + +export default ServerAddressInput; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss new file mode 100644 index 0000000000..f54ef91389 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss @@ -0,0 +1,5 @@ +.register-agent-wizard-container { + box-sizing: border-box; + background: #ffffff; + border: 1px solid rgba(52, 55, 65, 0.2); +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx new file mode 100644 index 0000000000..46fa6b9c4a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx @@ -0,0 +1,226 @@ +import React, { useState, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiProgress, +} from '@elastic/eui'; +import { useSelector } from 'react-redux'; +import { compose } from 'redux'; +// import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +// import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +// import { ErrorHandler } from '../../../../../react-services/error-management'; +import './register-agent.scss'; +import { Steps } from '../steps/steps'; +import { InputForm } from '../../components/form'; +import { + getGroups, + getMasterConfiguration, +} from '../../services/register-agent-services'; +import { useForm } from '../../components/form/hooks'; +import { FormConfiguration } from '../../components/form/types'; +import { + withErrorBoundary, + withGlobalBreadcrumb, + withRouteResolvers, + withUserAuthorizationPrompt, +} from '../../../../common/hocs'; +import GroupInput from '../../components/group-input/group-input'; +import { OsCard } from '../../components/os-selector/os-card/os-card'; +import { validateAgentName } from '../../utils/validations'; +import { + enableMenu, + ip, + nestedResolve, + savedSearch, +} from '../../../../../services/resolves'; +import { getWazuhCore } from '../../../../../plugin-services'; + +export const RegisterAgent = compose( + // TODO: add HOCs + // withErrorBoundary, + // withRouteResolvers({ enableMenu, ip, nestedResolve, savedSearch }), + // withGlobalBreadcrumb([ + // { + // text: endpointSummary.breadcrumbLabel, + // href: `#${endpointSummary.redirectTo()}`, + // }, + // { text: 'Deploy new agent' }, + // ]), + // withUserAuthorizationPrompt([ + // [{ action: 'agent:create', resource: '*:*:*' }], + // ]), + WrappedComponent => props => , +)(() => { + const configuration = {}; // Use a live state (reacts to changes through some hook that provides the configuration); + const [wazuhVersion, setWazuhVersion] = useState(''); + const [haveUdpProtocol, setHaveUdpProtocol] = useState(false); + const [loading, setLoading] = useState(false); + const [wazuhPassword, setWazuhPassword] = useState(''); + const [groups, setGroups] = useState([]); + const [needsPassword, setNeedsPassword] = useState(false); + const initialFields: FormConfiguration = { + operatingSystemSelection: { + type: 'custom', + initialValue: '', + component: props => , + options: { + groups, + }, + }, + serverAddress: { + type: 'text', + initialValue: configuration['enrollment.dns'] || '', + // validate: + // getWazuhCore().configuration._settings.get('enrollment.dns').validate, + }, + agentName: { + type: 'text', + initialValue: '', + validate: validateAgentName, + }, + + agentGroups: { + type: 'custom', + initialValue: [], + component: props => , + options: { + groups, + }, + }, + }; + const form = useForm(initialFields); + + const getMasterConfig = async () => { + const masterConfig = await getMasterConfiguration(); + + if (masterConfig?.remote) { + setHaveUdpProtocol(masterConfig.remote.isUdp); + } + + return masterConfig; + }; + + const getWazuhVersion = async () => { + try { + const result = await getWazuhCore().http.server.request('GET', '/', {}); + + return result?.data?.data?.api_version; + } catch { + // TODO: manage error + // const options = { + // context: `RegisterAgent.getWazuhVersion`, + // level: UI_LOGGER_LEVELS.ERROR, + // severity: UI_ERROR_SEVERITIES.BUSINESS, + // error: { + // error: error, + // message: error.message || error, + // title: `Could not get the Wazuh version: ${error.message || error}`, + // }, + // }; + + // getErrorOrchestrator().handleError(options); + + return version; + } + }; + + useEffect(() => { + const fetchData = async () => { + try { + const wazuhVersion = await getWazuhVersion(); + const { auth: authConfig } = await getMasterConfig(); + // get wazuh password configuration + let wazuhPassword = ''; + const needsPassword = authConfig?.auth?.use_password === 'yes'; + + if (needsPassword) { + wazuhPassword = + configuration?.['enrollment.password'] || + authConfig?.['authd.pass'] || + ''; + } + + const groups = await getGroups(); + + setNeedsPassword(needsPassword); + setWazuhPassword(wazuhPassword); + setWazuhVersion(wazuhVersion); + setGroups(groups); + setLoading(false); + } catch { + setWazuhVersion(wazuhVersion); + setLoading(false); + + // TODO: manage error + // const options = { + // context: 'RegisterAgent', + // level: UI_LOGGER_LEVELS.ERROR, + // severity: UI_ERROR_SEVERITIES.BUSINESS, + // display: true, + // store: false, + // error: { + // error: error, + // message: error.message || error, + // title: error.name || error, + // }, + // }; + + // ErrorHandler.handleError(error, options); + } + }; + + fetchData(); + }, []); + + const osCard = ( + + ); + + return ( +
+ + + + + + + + +

Deploy new agent

+
+
+
+ + {loading ? ( + <> + + + + + + ) : ( + + + + )} +
+
+
+
+
+
+ ); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss new file mode 100644 index 0000000000..5ea8024f31 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss @@ -0,0 +1,51 @@ +.register-agent-wizard-container { + .euiStep__title { + font-style: normal; + font-weight: 700; + font-size: 16px; + letter-spacing: 0.6px; + flex-direction: row; + } + + .stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 9px; + } + + .stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 20px; + } + + .titleAndIcon { + display: flex; + flex-direction: row; + } + + .warningForAgentName { + margin-top: 10px; + } + + .subtitleAgentName { + flex-direction: 'row'; + font-style: 'normal'; + font-weight: 700; + font-size: '12px'; + line-height: '20px'; + color: '#343741'; + } + + .euiStep__titleWrapper { + align-items: center; + } + + .euiButtonEmpty .euiButtonEmpty__content { + padding: 0; + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx new file mode 100644 index 0000000000..4758e8261c --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx @@ -0,0 +1,265 @@ +import React, { Fragment, useEffect, useState } from 'react'; +import { EuiCallOut, EuiLink, EuiSteps } from '@elastic/eui'; +import './steps.scss'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; +import { + IParseRegisterFormValues, + getRegisterAgentFormValues, + parseRegisterAgentFormValues, +} from '../../services/register-agent-services'; +import { useRegisterAgentCommands } from '../../hooks/use-register-agent-commands'; +import { + osCommandsDefinitions, + optionalParamsDefinitions, + tOperatingSystem, + tOptionalParameters, +} from '../../core/config/os-commands-definitions'; +import { UseFormReturn } from '../../components/form/types'; +import CommandOutput from '../../components/command-output/command-output'; +import ServerAddress from '../../components/server-address/server-address'; +import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; +import { + getAgentCommandsStepStatus, + tFormStepsStatus, + getOSSelectorStepStatus, + getServerAddressStepStatus, + getOptionalParameterStepStatus, + showCommandsSections, + getPasswordStepStatus, + getIncompleteSteps, + getInvalidFields, + tFormFieldsLabel, + tFormStepsLabel, +} from '../../services/register-agent-steps-status-services'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import OsCommandWarning from '../../components/command-output/os-warning'; + +interface IStepsProps { + needsPassword: boolean; + form: UseFormReturn; + osCard: React.ReactElement; + connection: { + isUDP: boolean; + }; + wazuhPassword: string; +} + +export const Steps = ({ + needsPassword, + form, + osCard, + connection, + wazuhPassword, +}: IStepsProps) => { + const initialParsedFormValues = { + operatingSystem: { + name: '', + architecture: '', + }, + optionalParams: { + agentGroups: '', + agentName: '', + serverAddress: '', + wazuhPassword, + protocol: connection.isUDP ? 'UDP' : '', + }, + } as IParseRegisterFormValues; + const [missingStepsName, setMissingStepsName] = useState( + [], + ); + const [invalidFieldsName, setInvalidFieldsName] = useState< + tFormFieldsLabel[] + >([]); + const [registerAgentFormValues, setRegisterAgentFormValues] = + useState(initialParsedFormValues); + const FORM_MESSAGE_CONJUNTION = ' and '; + + useEffect(() => { + // get form values and parse them divided in OS and optional params + const registerAgentFormValuesParsed = parseRegisterAgentFormValues( + getRegisterAgentFormValues(form), + OPERATING_SYSTEMS_OPTIONS, + initialParsedFormValues, + ); + + setRegisterAgentFormValues(registerAgentFormValuesParsed); + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + setMissingStepsName(getIncompleteSteps(form.fields) || []); + setInvalidFieldsName(getInvalidFields(form.fields) || []); + }, [form.fields]); + + const { installCommand, startCommand, selectOS, setOptionalParams } = + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }); + // install - start commands step state + const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); + const [installCommandStepStatus, setInstallCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); + const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); + const [startCommandStepStatus, setStartCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); + + useEffect(() => { + if ( + registerAgentFormValues.operatingSystem.name !== '' && + registerAgentFormValues.operatingSystem.architecture !== '' + ) { + selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); + } + + setOptionalParams( + { ...registerAgentFormValues.optionalParams }, + registerAgentFormValues.operatingSystem as tOperatingSystem, + ); + setInstallCommandWasCopied(false); + setStartCommandWasCopied(false); + }, [registerAgentFormValues]); + + useEffect(() => { + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + }, [installCommandWasCopied]); + + useEffect(() => { + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + }, [startCommandWasCopied]); + + const registerAgentFormSteps = [ + { + title: 'Select the package to download and install on your system:', + children: osCard, + status: getOSSelectorStepStatus(form.fields), + }, + { + title: 'Server address:', + children: , + status: getServerAddressStepStatus(form.fields), + }, + ...(needsPassword && !wazuhPassword + ? [ + { + title: 'Password', + children: ( + + The password is required but wasn't defined. Please check + our{' '} + + documentation + + + } + iconType='iInCircle' + className='warningForAgentName' + /> + ), + status: getPasswordStepStatus(form.fields), + }, + ] + : []), + { + title: 'Optional settings:', + children: , + status: getOptionalParameterStepStatus( + form.fields, + installCommandWasCopied, + ), + }, + { + title: 'Run the following commands to download and install the agent:', + children: ( + <> + {missingStepsName?.length ? ( + + ) : null} + {invalidFieldsName?.length ? ( + + ) : null} + {!missingStepsName?.length && !invalidFieldsName?.length ? ( + <> + setInstallCommandWasCopied(true)} + password={registerAgentFormValues.optionalParams.wazuhPassword} + /> + + + ) : null} + + ), + status: installCommandStepStatus, + }, + { + title: 'Start the agent:', + children: ( + <> + {missingStepsName?.length ? ( + + ) : null} + {invalidFieldsName?.length ? ( + + ) : null} + {!missingStepsName?.length && !invalidFieldsName?.length ? ( + setStartCommandWasCopied(true)} + /> + ) : null} + + ), + status: startCommandStepStatus, + }, + ]; + + return ; +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts new file mode 100644 index 0000000000..0dc9f9e93f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts @@ -0,0 +1,214 @@ +import { + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getWindowsInstallCommand, + getWindowsStartCommand, + getDEBAMD64InstallCommand, + getRPMAMD64InstallCommand, + getRPMARM64InstallCommand, + getDEBARM64InstallCommand, +} from '../../services/register-agent-os-commands-services'; +import { + scapeSpecialCharsForLinux, + scapeSpecialCharsForMacOS, + scapeSpecialCharsForWindows, +} from '../../services/wazuh-password-service'; +import { IOSDefinition, tOptionalParams } from '../register-commands/types'; + +// Defined OS combinations + +/** Linux options **/ +export interface ILinuxAMDRPM { + name: 'LINUX'; + architecture: 'RPM amd64'; +} + +export interface ILinuxAARCHRPM { + name: 'LINUX'; + architecture: 'RPM aarch64'; +} + +export interface ILinuxAMDDEB { + name: 'LINUX'; + architecture: 'DEB amd64'; +} + +export interface ILinuxAARCHDEB { + name: 'LINUX'; + architecture: 'DEB aarch64'; +} + +type ILinuxOSTypes = + | ILinuxAMDRPM + | ILinuxAARCHRPM + | ILinuxAMDDEB + | ILinuxAARCHDEB; + +/** Windows options **/ +export interface IWindowsOSTypes { + name: 'WINDOWS'; + architecture: 'MSI 32/64 bits'; +} + +/** MacOS options **/ +export interface IMacOSIntel { + name: 'macOS'; + architecture: 'Intel'; +} + +export interface IMacOSApple { + name: 'macOS'; + architecture: 'Apple silicon'; +} + +type IMacOSTypes = IMacOSApple | IMacOSIntel; + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +export type tOptionalParameters = + | 'serverAddress' + | 'agentName' + | 'agentGroups' + | 'wazuhPassword' + | 'protocol'; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +const linuxDefinition: IOSDefinition = { + name: 'LINUX', + options: [ + { + architecture: 'DEB amd64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.deb`, + installCommand: props => getDEBAMD64InstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + { + architecture: 'RPM amd64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, + installCommand: props => getRPMAMD64InstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + { + architecture: 'DEB aarch64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_arm64.deb`, + installCommand: props => getDEBARM64InstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + { + architecture: 'RPM aarch64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.aarch64.rpm`, + installCommand: props => getRPMARM64InstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + ], +}; + +const windowsDefinition: IOSDefinition = { + name: 'WINDOWS', + options: [ + { + architecture: 'MSI 32/64 bits', + urlPackage: props => + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.msi`, + installCommand: props => getWindowsInstallCommand(props), + startCommand: props => getWindowsStartCommand(props), + }, + ], +}; + +const macDefinition: IOSDefinition = { + name: 'macOS', + options: [ + { + architecture: 'Intel', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.intel64.pkg`, + installCommand: props => getMacOsInstallCommand(props), + startCommand: props => getMacosStartCommand(props), + }, + { + architecture: 'Apple silicon', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.arm64.pkg`, + installCommand: props => getMacOsInstallCommand(props), + startCommand: props => getMacosStartCommand(props), + }, + ], +}; + +export const osCommandsDefinitions = [ + linuxDefinition, + windowsDefinition, + macDefinition, +]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = { + serverAddress: { + property: 'WAZUH_MANAGER', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + agentName: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + agentGroups: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + let parsedValue = value; + if (Array.isArray(value)) { + parsedValue = value.length > 0 ? value.join(',') : ''; + } + return parsedValue ? `${property}='${parsedValue}'` : ''; + }, + }, + protocol: { + property: 'WAZUH_PROTOCOL', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + wazuhPassword: { + property: 'WAZUH_REGISTRATION_PASSWORD', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + if (!value) { + return ''; + } + if (selectedOS) { + let osName = selectedOS.name.toLocaleLowerCase(); + switch (osName) { + case 'linux': + return `${property}=$'${scapeSpecialCharsForLinux(value)}'`; + case 'macos': + return `${property}='${scapeSpecialCharsForMacOS(value)}'`; + case 'windows': + return `${property}='${scapeSpecialCharsForWindows(value)}'`; + default: + return `${property}=$'${value}'`; + } + } + + return value !== '' ? `${property}=$'${value}'` : ''; + }, + }, +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md new file mode 100644 index 0000000000..72b92edde1 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md @@ -0,0 +1,339 @@ +# 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 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; +} + +/// .... + +interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options + +type tOptionalParameters = + | 'server_address' + | 'agent_name' + | 'agent_group' + | 'protocol' + | 'wazuh_password'; + +export interface IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +export interface IOSCommandsDefinition< + OS extends IOperationSystem, + Param extends string, +> { + 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: [ + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + } + ], +}, +{ + name: 'windows', + options: [ + { + 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< + OS extends IOperationSystem, + Params extends string, +> 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', +}); + +// 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', +}); + +// 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', +}); + +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', +}); + +// 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; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} +``` diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts new file mode 100644 index 0000000000..ae9af6d24e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -0,0 +1,380 @@ +import { CommandGenerator } from './command-generator'; +import { + IOSDefinition, + 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'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +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', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x86', + 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', + }); + 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', + }); + 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', + }); + commandGenerator.addOptionalParams(optionalValues); + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: 'linux', + architecture: 'x64', + 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', + }; + 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, + 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', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + 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', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + 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', + }; + 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', + }; + 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/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts new file mode 100644 index 0000000000..8194b88970 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts @@ -0,0 +1,185 @@ +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, + NoOSSelectedException, + WazuhVersionUndefinedException, +} from '../exceptions'; +import { version } from '../../../../../../../package.json'; + +export class CommandGenerator< + OS extends IOperationSystem, + Params extends string, +> 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, selectedOS?: OS): void { + // Get all the optional parameters based on the given parameters + this.optionals = this.optionalsManager.getAllOptionalParams( + props, + selectedOS, + ); + } + + /** + * 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 } = params; + if (!name) { + throw new NoOSSelectedException(); + } + if (!architecture) { + throw new NoArchitectureSelectedException(); + } + + const option = searchOSDefinitions(this.osDefinitions, { + name, + architecture, + }); + 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'], + 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'], + 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'], + 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'], + 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/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts new file mode 100644 index 0000000000..8ecf6d1be6 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts @@ -0,0 +1,80 @@ +export class NoOptionFoundException extends Error { + constructor(osName: string, architecture: string) { + super( + `No OS option found for "${osName}" "${architecture}". 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) { + super( + `No start command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, + ); + } +} + +export class NoInstallCommandDefinitionException extends Error { + constructor(osName: string, architecture: string) { + super( + `No install command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, + ); + } +} + +export class NoPackageURLDefinitionException extends Error { + constructor(osName: string, architecture: string) { + super( + `No package URL definition found for "${osName}" "${architecture}". 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) { + super( + `Duplicate OS option found for "${osName}" "${architecture}"`, + ); + } +} + +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`); + } +} + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts new file mode 100644 index 0000000000..af6bef5b15 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/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/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts new file mode 100644 index 0000000000..881894186a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -0,0 +1,71 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { + IOperationSystem, + 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, + selectedOS?: IOperationSystem, + ) { + const { value, name } = props; + if (!this.optionalParamsConfig[name]) { + throw new NoOptionalParamFoundException(name); + } + return this.optionalParamsConfig[name].getParamCommand( + { + value, + property: this.optionalParamsConfig[name].property, + name, + }, + selectedOS, + ); + } + + /** + * 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, + selectedOS: IOperationSystem, + ) { + // 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, + }, + selectedOS, + ) as string; + } + return resolvedOptionalParams; + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts new file mode 100644 index 0000000000..a4d16fcf32 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts @@ -0,0 +1,112 @@ +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'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +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', + 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', + 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', + 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/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts new file mode 100644 index 0000000000..c8fabc3ebf --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts @@ -0,0 +1,37 @@ +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); + } + + if(!packageUrl || packageUrl === ''){ + throw new NoPackageURLDefinitionException(osName, osDefinition.architecture); + } + + if(!version || version === ''){ + throw new WazuhVersionUndefinedException(); + } + + return osDefinition.installCommand({ + urlPackage: packageUrl, + wazuhVersion: version, + name: osName as OS['name'], + architecture: osDefinition.architecture, + optionals, + }); +} \ No newline at end of file diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts new file mode 100644 index 0000000000..73412b9fdb --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts @@ -0,0 +1,176 @@ +import { + NoOSOptionFoundException, +} 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'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const validOSDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + 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', + }); + 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', + }), + ).toThrow(NoOSOptionFoundException); + }); + + }); + + describe('validateOSDefinitionsDuplicated', () => { + it('should not throw an error if there are no duplicated OS definitions', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + 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: [ + { + 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: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionHasDuplicatedOptions(osDefinitions), + ).toThrow(); + }); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts new file mode 100644 index 0000000000..0ceefada65 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -0,0 +1,84 @@ +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. + */ +export function searchOSDefinitions( + osDefinitions: IOSDefinition[], + params: IOperationSystem, +){ + const { name, architecture } = params; + + const osDefinition = osDefinitions.find(os => os.name === name); + if (!osDefinition) { + throw new NoOSOptionFoundException(name); + } + + const osDefinitionOption = osDefinition.options.find( + option => + option.architecture === architecture, + ); + + if (!osDefinitionOption) { + throw new NoOptionFoundException(name, architecture); + } + + 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.architecture}`; + if (options.has(ext_arch_manager)) { + throw new DuplicatedOSOptionException( + osDefinition.name, + option.architecture, + ); + } + options.add(ext_arch_manager); + } + } +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts new file mode 100644 index 0000000000..c09e828a78 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts @@ -0,0 +1,117 @@ +///////////////////////////////////////////////////////// +/// Domain +///////////////////////////////////////////////////////// +export interface IOperationSystem { + name: string; + architecture: string; +} + +export type IOptionalParameters = { + [key in Params]: string; +}; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +export interface IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +interface IOptionalParamsWithValues { + optionals?: IOptionalParameters; +} + +export type tOSEntryProps = IOSProps & + IOptionalParamsWithValues; +export type tOSEntryInstallCommand = + tOSEntryProps & { urlPackage: string }; + +export interface IOSCommandsDefinition< + OS extends IOperationSystem, + Param extends string, +> { + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryInstallCommand) => 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, + selectedOS?: IOperationSystem, + ) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOptionalParamInput { + value: any; + name: T; +} +export interface IOptionalParametersManager { + getOptionalParam(props: IOptionalParamInput): string; + getAllOptionalParams( + paramsValues: IOptionalParameters, + selectedOs?: IOperationSystem, + ): object; +} + +/////////////////////////////////////////////////////////////////// +/// Command creator class +/////////////////////////////////////////////////////////////////// + +export type IOSInputs = IOperationSystem & + IOptionalParameters; +export interface ICommandGenerator< + OS extends IOperationSystem, + Params extends string, +> extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams( + props: IOptionalParameters, + osSelected?: IOperationSystem, + ): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md new file mode 100644 index 0000000000..d3ec96adc1 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md @@ -0,0 +1,167 @@ +# 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', +}) + +// 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; +} + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} +``` + +### Hook output + +```ts + +export interface IOperationSystem { + name: string; + architecture: 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; +} + +/// .... + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +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', +}) + +```` + diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts new file mode 100644 index 0000000000..20a4de7b32 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts @@ -0,0 +1,229 @@ +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'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, + installCommand: props => `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}`, + 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', + }); + }); + } 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', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + }); + }); + 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', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + }); + + 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/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts new file mode 100644 index 0000000000..414f2ea41f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts @@ -0,0 +1,128 @@ +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< + OS extends IOperationSystem, + Params extends string, +> { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} + +interface IUseRegisterCommandsOutput< + OS extends IOperationSystem, + Params extends string, +> { + selectOS: (params: OS) => void; + setOptionalParams: ( + params: IOptionalParameters, + selectedOS?: OS, + ) => 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< + OS extends IOperationSystem, + Params extends string, +>( + 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< + IOptionalParameters | {} + >({}); + 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, + osSelected, + ); + } + 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, + selectedOS?: OS, + ): void => { + commandGenerator.addOptionalParams(params, selectedOS); + setOptionalParamsValues(params); + setOptionalParamsParsed(commandGenerator.getOptionalParamsCommands()); + }; + + return { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed, + }; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx new file mode 100644 index 0000000000..146589950a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx @@ -0,0 +1 @@ +export { RegisterAgent } from './containers/register-agent/register-agent'; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts new file mode 100644 index 0000000000..f9fe6c02fc --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts @@ -0,0 +1,18 @@ +import { tOperatingSystem } from '../config/os-commands-definitions'; + +interface RegisterAgentData { + icon: string; + title: tOperatingSystem['name']; + hr: boolean; + architecture: tOperatingSystem['architecture'][] +} + +interface CheckboxGroupComponentProps { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +export type { RegisterAgentData, CheckboxGroupComponentProps }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx new file mode 100644 index 0000000000..db512362f5 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx @@ -0,0 +1,145 @@ +import { + EnhancedFieldConfiguration, + UseFormReturn, +} from '../../../components/common/form/types'; +import { + FormStepsDependencies, + RegisterAgentFormStatusManager, +} from './form-status-manager'; + +const defaultFormFieldData: EnhancedFieldConfiguration = { + changed: true, + value: 'value1', + error: '', + currentValue: '', + initialValue: '', + type: 'text', + onChange: () => { + console.log('onChange'); + }, + setInputRef: () => { + console.log('setInputRef'); + }, + inputRef: null, +}; + +const formFieldsDefault: UseFormReturn['fields'] = { + field1: { + ...defaultFormFieldData, + value: '', + error: null, + }, + field2: { + ...defaultFormFieldData, + value: '', + error: 'error message', + }, + field3: { + ...defaultFormFieldData, + value: 'value valid', + error: null, + }, +}; + +describe('RegisterAgentFormStatusManager', () => { + it('should create a instance', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + }); + + it('should return the form status', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + const formStatus = registerAgentFormStatusManager.getFormStatus(); + expect(formStatus).toEqual({ + field1: 'empty', + field2: 'invalid', + field3: 'complete', + }); + }); + + it('should return the field status', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + const fieldStatus = registerAgentFormStatusManager.getFieldStatus('field1'); + expect(fieldStatus).toEqual('empty'); + }); + + it('should return error if fieldname not found', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + expect(() => + registerAgentFormStatusManager.getFieldStatus('field4'), + ).toThrowError('Fieldname not found'); + }); + + it('should return a INVALID when the step have an error', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1', 'field2'], + step2: ['field3'], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + 'invalid', + ); + }); + + it('should return COMPLETE when the step have no errors and is not empty', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1', 'field2'], + step2: ['field3'], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step2')).toEqual( + 'complete', + ); + }); + + it('should return EMPTY when the step all fields empty', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: [ 'field2', + 'field3' ], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + 'empty', + ); + }); + + it('should return all the steps status', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: [ 'field2', + 'field3' ], + step3: ['field3'] + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getFormStepsStatus()).toEqual({ + step1: 'empty', + step2: 'invalid', + step3: 'complete' + }); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx new file mode 100644 index 0000000000..45423e7524 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx @@ -0,0 +1,112 @@ +import { UseFormReturn } from '../components/form/types'; + +type FieldStatus = 'invalid' | 'empty' | 'complete'; +type FormStatus = Record; + +type FormFields = UseFormReturn['fields']; +type FormFieldName = keyof FormFields; + +export type FormStepsDependencies = Record; + +type FormStepsStatus = Record; + +interface FormFieldsStatusManager { + getFieldStatus: (fieldname: FormFieldName) => FieldStatus; + getFormStatus: () => FormStatus; + getStepStatus: (stepName: string) => FieldStatus; + getFormStepsStatus: () => FormStepsStatus; +} + +export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { + constructor( + private readonly formFields: FormFields, + private readonly formSteps?: FormStepsDependencies, + ) {} + + getFieldStatus = (fieldname: FormFieldName): FieldStatus => { + const field = this.formFields[fieldname]; + + if (!field) { + throw new Error('Fieldname not found'); + } + + if (field.error) { + return 'invalid'; + } + + if (field.value?.length === 0) { + return 'empty'; + } + + return 'complete'; + }; + getFormStatus = (): FormStatus => { + const fieldNames = Object.keys(this.formFields); + const formStatus: FormStatus | object = {}; + + fieldNames.forEach((fieldName: string) => { + formStatus[fieldName] = this.getFieldStatus(fieldName); + }); + + return formStatus as FormStatus; + }; + getStepStatus = (stepName: string): FieldStatus => { + if (!this.formSteps) { + throw new Error('Form steps not defined'); + } + + const stepFields = this.formSteps[stepName]; + + if (!stepFields) { + throw new Error('Step name not found'); + } + + const formStepStatus: FormStepsStatus | object = {}; + + stepFields.forEach((fieldName: FormFieldName) => { + formStepStatus[fieldName] = this.getFieldStatus(fieldName); + }); + + const stepStatus = Object.values(formStepStatus); + + // if any is invalid + if (stepStatus.includes('invalid')) { + return 'invalid'; + } else if (stepStatus.includes('empty')) { + // if all are empty + return 'empty'; + } else { + // if all are complete + return 'complete'; + } + }; + getFormStepsStatus = (): FormStepsStatus => { + if (!this.formSteps) { + throw new Error('Form steps not defined'); + } + + const formStepsStatus: FormStepsStatus | object = {}; + + Object.keys(this.formSteps).forEach((stepName: string) => { + formStepsStatus[stepName] = this.getStepStatus(stepName); + }); + + return formStepsStatus as FormStepsStatus; + }; + getIncompleteSteps = (): string[] => { + const formStepsStatus = this.getFormStepsStatus(); + const notCompleteSteps = Object.entries(formStepsStatus).filter( + ([_, status]) => status === 'empty', + ); + + return notCompleteSteps.map(([stepName, _]) => stepName); + }; + getInvalidFields = (): string[] => { + const formStatus = this.getFormStatus(); + const invalidFields = Object.entries(formStatus).filter( + ([_, status]) => status === 'invalid', + ); + + return invalidFields.map(([fieldName, _]) => fieldName); + }; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts new file mode 100644 index 0000000000..48eaafd660 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts @@ -0,0 +1,227 @@ +import { + getAllOptionals, + getAllOptionalsMacos, + getDEBAMD64InstallCommand, + getDEBARM64InstallCommand, + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getRPMAMD64InstallCommand, + getRPMARM64InstallCommand, + getWindowsInstallCommand, + getWindowsStartCommand, + transformOptionalsParamatersMacOSCommand, +} from './register-agent-os-commands-services'; + +let test: any; + +beforeEach(() => { + test = { + optionals: { + agentGroups: "WAZUH_AGENT_GROUP='default'", + agentName: "WAZUH_AGENT_NAME='test'", + serverAddress: "WAZUH_MANAGER='1.1.1.1'", + wazuhPassword: "WAZUH_REGISTRATION_PASSWORD=''", + }, + urlPackage: 'https://test.com/agent.deb', + wazuhVersion: '4.8.0', + }; +}); + +describe('getAllOptionals', () => { + it('should return empty string if optionals is falsy', () => { + const result = getAllOptionals(null); + expect(result).toBe(''); + }); + + it('should return the correct paramsText', () => { + const optionals = { + serverAddress: 'localhost', + wazuhPassword: 'password', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + }; + const result = getAllOptionals(optionals, 'linux'); + expect(result).toBe('localhost password group1 agent1 http '); + }); +}); + +describe('getDEBAMD64InstallCommand', () => { + it('should return the correct install command', () => { + const props = { + optionals: { + serverAddress: 'localhost', + wazuhPassword: 'password', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + }, + urlPackage: 'https://example.com/package.deb', + wazuhVersion: '4.0.0', + }; + const result = getDEBAMD64InstallCommand(props); + expect(result).toBe( + 'wget https://example.com/package.deb && sudo localhost password group1 agent1 http dpkg -i ./wazuh-agent_4.0.0-1_amd64.deb', + ); + }); +}); + +describe('getDEBAMD64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + const withAllOptionals = getDEBAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + const withServerAddresAndAgentGroupsOptions = + getDEBAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getDEBARM64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + const withAllOptionals = getDEBARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + const withServerAddresAndAgentGroupsOptions = + getDEBARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getRPMAMD64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + const withAllOptionals = getRPMAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + const withServerAddresAndAgentGroupsOptions = + getRPMAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getRPMARM64InstallCommand', () => { + it('should return the correct command', () => { + let expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + const withAllOptionals = getRPMARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + const withServerAddresAndAgentGroupsOptions = + getRPMARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getLinuxStartCommand', () => { + it('returns the correct start command for Linux', () => { + const startCommand = getLinuxStartCommand({}); + const expectedCommand = + 'sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent'; + + expect(startCommand).toEqual(expectedCommand); + }); +}); + +// Windows + +describe('getWindowsInstallCommand', () => { + it('should return the correct install command', () => { + let expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} `; + + const withAllOptionals = getWindowsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + + expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.agentGroups} `; + const withServerAddresAndAgentGroupsOptions = + getWindowsInstallCommand(test); + + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getWindowsStartCommand', () => { + it('should return the correct start command', () => { + const expectedCommand = 'NET START WazuhSvc'; + + const result = getWindowsStartCommand({}); + + expect(result).toEqual(expectedCommand); + }); +}); + +// MacOS + +describe('getAllOptionalsMacos', () => { + it('should return empty string if optionals is falsy', () => { + const result = getAllOptionalsMacos(null); + expect(result).toBe(''); + }); + + it('should return the correct paramsValueList', () => { + const optionals = { + serverAddress: 'localhost', + agentGroups: 'group1', + agentName: 'agent1', + protocol: 'http', + wazuhPassword: 'password', + }; + const result = getAllOptionalsMacos(optionals); + expect(result).toBe('localhost && group1 && agent1 && http && password'); + }); +}); + +describe('transformOptionalsParamatersMacOSCommand', () => { + it('should transform the command correctly', () => { + const command = + "' serverAddress && agentGroups && agentName && protocol && wazuhPassword"; + const result = transformOptionalsParamatersMacOSCommand(command); + expect(result).toBe( + "' && serverAddress && agentGroups && agentName && protocol && wazuhPassword", + ); + }); +}); + +describe('getMacOsInstallCommand', () => { + it('should return the correct macOS installation script', () => { + let expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups} && ${test.optionals.agentName} && ${test.optionals.wazuhPassword}\" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; + + const withAllOptionals = getMacOsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); + + delete test.optionals.wazuhPassword; + delete test.optionals.agentName; + expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups}" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; + + const withServerAddresAndAgentGroupsOptions = getMacOsInstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getMacosStartCommand', () => { + it('returns the correct start command for macOS', () => { + const startCommand = getMacosStartCommand({}); + expect(startCommand).toEqual('sudo /Library/Ossec/bin/wazuh-control start'); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx new file mode 100644 index 0000000000..e5f96c8c4e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx @@ -0,0 +1,176 @@ +import { tOptionalParameters } from '../core/config/os-commands-definitions'; +import { + IOptionalParameters, + tOSEntryInstallCommand, + tOSEntryProps, +} from '../core/register-commands/types'; +import { tOperatingSystem } from '../hooks/use-register-agent-commands.test'; + +export const getAllOptionals = ( + optionals: IOptionalParameters, + osName?: tOperatingSystem['name'], +) => { + // create paramNameOrderList, which is an array of the keys of optionals add interface + const paramNameOrderList: (keyof IOptionalParameters)[] = + ['serverAddress', 'wazuhPassword', 'agentGroups', 'agentName', 'protocol']; + + if (!optionals) return ''; + let paramsText = Object.entries(paramNameOrderList).reduce( + (acc, [key, value]) => { + if (optionals[value]) { + acc += `${optionals[value]} `; + } + return acc; + }, + '', + ); + + return paramsText; +}; + +export const getAllOptionalsMacos = ( + optionals: IOptionalParameters, +) => { + // create paramNameOrderList, which is an array of the keys of optionals add interface + const paramNameOrderList: (keyof IOptionalParameters)[] = + ['serverAddress', 'agentGroups', 'agentName', 'protocol', 'wazuhPassword']; + + if (!optionals) return ''; + + const paramsValueList = []; + + paramNameOrderList.forEach(paramName => { + if (optionals[paramName] && optionals[paramName] !== '') { + paramsValueList.push(optionals[paramName]); + } + }); + + if (paramsValueList.length) { + return paramsValueList.join(' && '); + } + + return ''; +}; + +/******* DEB *******/ + +export const getDEBAMD64InstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb`; + return `wget ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }dpkg -i ./${packageName}`; +}; + +export const getDEBARM64InstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_arm64.deb`; + return `wget ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }dpkg -i ./${packageName}`; +}; + +/******* RPM *******/ + +export const getRPMAMD64InstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, wazuhVersion, architecture } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm`; + return `curl -o ${packageName} ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }rpm -ihv ${packageName}`; +}; + +export const getRPMARM64InstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, wazuhVersion, architecture } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.aarch64.rpm`; + return `curl -o ${packageName} ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }rpm -ihv ${packageName}`; +}; + +/******* Linux *******/ + +// Start command +export const getLinuxStartCommand = ( + _props: tOSEntryProps, +) => { + return `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; +}; + +/******** Windows ********/ + +export const getWindowsInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, name } = props; + return `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${ + optionals && getAllOptionals(optionals, name) + }`; +}; + +export const getWindowsStartCommand = ( + _props: tOSEntryProps, +) => { + return `NET START WazuhSvc`; +}; + +/******** MacOS ********/ + +export const transformOptionalsParamatersMacOSCommand = (command: string) => { + return command + .replace(/\' ([a-zA-Z])/g, "' && $1") // Separate environment variables with && + .replace(/\"/g, '\\"') // Escape double quotes + .trim(); +}; + +export const getMacOsInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage } = props; + + let optionalsForCommand = { ...optionals }; + if (optionalsForCommand?.wazuhPassword) { + /** + * We use the JSON.stringify to prevent that the scaped specials characters will be removed + * and maintain the format of the password + The JSON.stringify maintain the password format but adds " to wrap the characters + */ + const scapedPasswordLength = JSON.stringify( + optionalsForCommand?.wazuhPassword, + ).length; + // We need to remove the " added by JSON.stringify + optionalsForCommand.wazuhPassword = `${JSON.stringify( + optionalsForCommand?.wazuhPassword, + ).substring(1, scapedPasswordLength - 1)}`; + } + + // Set macOS installation script with environment variables + const optionalsText = + optionalsForCommand && getAllOptionalsMacos(optionalsForCommand); + const macOSInstallationOptions = transformOptionalsParamatersMacOSCommand( + optionalsText || '', + ); + + // If no variables are set, the echo will be empty + const macOSInstallationSetEnvVariablesScript = macOSInstallationOptions + ? `echo "${macOSInstallationOptions}" > /tmp/wazuh_envs && ` + : ``; + + // Merge environment variables with installation script + const macOSInstallationScript = `curl -so wazuh-agent.pkg ${urlPackage} && ${macOSInstallationSetEnvVariablesScript}sudo installer -pkg ./wazuh-agent.pkg -target /`; + return macOSInstallationScript; +}; + +export const getMacosStartCommand = ( + _props: tOSEntryProps, +) => { + return `sudo /Library/Ossec/bin/wazuh-control start`; +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts new file mode 100644 index 0000000000..ecc68fb6b9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts @@ -0,0 +1,292 @@ +import * as RegisterAgentService from './register-agent-services'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { ServerAddressOptions } from './register-agent-services'; + +jest.mock('../../../../react-services', () => ({ + ...(jest.requireActual('../../../../react-services') as object), + WzRequest: () => ({ + apiReq: jest.fn(), + }), +})); + +describe('Register agent service', () => { + beforeEach(() => jest.clearAllMocks()); + describe('getRemoteConfiguration', () => { + it('should return secure connection = TRUE when have connection secure', async () => { + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['UDP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['UDP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + const nodeName = 'example-node'; + const res = await RegisterAgentService.getRemoteConfiguration( + nodeName, + false, + ); + expect(res.name).toBe(nodeName); + expect(res.haveSecureConnection).toBe(true); + }); + + it('should return secure connection = FALSE available when dont have connection secure', async () => { + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['UDP', 'TCP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + const nodeName = 'example-node'; + const res = await RegisterAgentService.getRemoteConfiguration( + nodeName, + false, + ); + expect(res.name).toBe(nodeName); + expect(res.haveSecureConnection).toBe(false); + }); + + it('should return protocols UDP when is the only connection protocol available', async () => { + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['UDP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['UDP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + const nodeName = 'example-node'; + const res = await RegisterAgentService.getRemoteConfiguration( + nodeName, + false, + ); + expect(res.name).toBe(nodeName); + expect(res.isUdp).toEqual(true); + }); + + it('should return protocols TCP when is the only connection protocol available', async () => { + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['TCP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['TCP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + const nodeName = 'example-node'; + const res = await RegisterAgentService.getRemoteConfiguration( + nodeName, + false, + ); + expect(res.name).toBe(nodeName); + expect(res.isUdp).toEqual(false); + }); + + it('should return is not UDP when have UDP and TCP protocols available', async () => { + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['TCP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['UDP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + const nodeName = 'example-node'; + const res = await RegisterAgentService.getRemoteConfiguration( + nodeName, + false, + ); + expect(res.name).toBe(nodeName); + expect(res.isUdp).toEqual(false); + }); + }); + + describe('getConnectionConfig', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + + it('should return IS NOT UDP when the server address is typed manually (custom)', async () => { + const nodeSelected: ServerAddressOptions = { + label: 'node-selected', + value: 'node-selected', + nodetype: 'master', + }; + + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['UDP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['UDP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + + const config = await RegisterAgentService.getConnectionConfig( + nodeSelected, + 'default-dns-address', + ); + expect(config.udpProtocol).toEqual(false); + expect(config.serverAddress).toBe('default-dns-address'); + }); + + it('should return IS NOT UDP when the server address is received like default server address dns (custom)', async () => { + const nodeSelected: ServerAddressOptions = { + label: 'node-selected', + value: 'node-selected', + nodetype: 'master', + }; + + const remoteWithSecureAndNoSecure = [ + { + connection: 'syslog', + ipv6: 'no', + protocol: ['UDP'], + port: '514', + 'allowed-ips': ['0.0.0.0/0'], + }, + { + connection: 'secure', + ipv6: 'no', + protocol: ['UDP'], + port: '1514', + queue_size: '131072', + }, + ]; + const mockedResponse = { + data: { + data: { + affected_items: [ + { + remote: remoteWithSecureAndNoSecure, + }, + ], + }, + }, + }; + WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); + + const config = await RegisterAgentService.getConnectionConfig( + nodeSelected, + 'custom-server-address', + ); + expect(config.udpProtocol).toEqual(false); + }); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx new file mode 100644 index 0000000000..c9b14664e5 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx @@ -0,0 +1,363 @@ +import { UseFormReturn } from '../components/form/types'; +import { + tOperatingSystem, + tOptionalParameters, +} from '../core/config/os-commands-definitions'; +import { RegisterAgentData } from '../interfaces/types'; + +type Protocol = 'TCP' | 'UDP'; + +interface RemoteItem { + connection: 'syslog' | 'secure'; + ipv6: 'yes' | 'no'; + protocol: Protocol[]; + allowed_ips?: string[]; + queue_size?: string; +} + +interface RemoteConfig { + name: string; + isUdp: boolean | null; + haveSecureConnection: boolean | null; +} + +export interface ServerAddressOptions { + label: string; + value: string; + nodetype: string; +} + +/** + * Get the cluster status + */ +export const clusterStatusResponse = async (): Promise => { + const clusterStatus = await getWazuhCore().http.server.request( + 'GET', + '/cluster/status', + {}, + ); + + if ( + clusterStatus.data.data.enabled === 'yes' && + clusterStatus.data.data.running === 'yes' + ) { + // Cluster mode + return true; + } else { + // Manager mode + return false; + } +}; + +/** + * Get the remote configuration from api + */ +async function getRemoteConfiguration( + nodeName: string, + clusterStatus: boolean, +): Promise { + const config: RemoteConfig = { + name: nodeName, + isUdp: false, + haveSecureConnection: false, + }; + + try { + let result; + + if (clusterStatus) { + result = await getWazuhCore().http.server.request( + 'GET', + `/cluster/${nodeName}/configuration/request/remote`, + {}, + ); + } else { + result = await getWazuhCore().http.server.request( + 'GET', + '/manager/configuration/request/remote', + {}, + ); + } + + const items = result?.data?.data?.affected_items || []; + const remote = items[0]?.remote; + + if (remote) { + const remoteFiltered = remote.filter( + (item: RemoteItem) => item.connection === 'secure', + ); + + remoteFiltered.length > 0 + ? (config.haveSecureConnection = true) + : (config.haveSecureConnection = false); + + let protocolsAvailable: Protocol[] = []; + + remote.forEach((item: RemoteItem) => { + // get all protocols available + for (const protocol of item.protocol) { + protocolsAvailable = protocolsAvailable.concat(protocol); + } + }); + + config.isUdp = + getRemoteProtocol(protocolsAvailable) === 'UDP' ? true : false; + } + + return config; + } catch { + return config; + } +} + +/** + * Get the manager/cluster auth configuration from Wazuh API + * @param node + * @returns + */ +async function getAuthConfiguration(node: string, clusterStatus: boolean) { + const authConfigUrl = clusterStatus + ? `/cluster/${node}/configuration/auth/auth` + : '/manager/configuration/auth/auth'; + const result = await getWazuhCore().http.server.request( + 'GET', + authConfigUrl, + {}, + ); + const auth = result?.data?.data?.affected_items?.[0]; + + return auth; +} + +/** + * Get the remote protocol available from list of protocols + * @param protocols + */ +function getRemoteProtocol(protocols: Protocol[]) { + if (protocols.length === 1) { + return protocols[0]; + } else { + return protocols.includes('TCP') ? 'TCP' : 'UDP'; + } +} + +/** + * Get the remote configuration from nodes registered in the cluster and decide the protocol to setting up in deploy agent param + * @param nodeSelected + * @param defaultServerAddress + */ +async function getConnectionConfig( + nodeSelected: ServerAddressOptions, + defaultServerAddress?: string, +) { + const nodeName = nodeSelected?.label; + const nodeIp = nodeSelected?.value; + + if (defaultServerAddress) { + return { + serverAddress: defaultServerAddress, + udpProtocol: false, + connectionSecure: true, + }; + } else { + if (nodeSelected.nodetype === 'custom') { + return { + serverAddress: nodeName, + udpProtocol: false, + connectionSecure: true, + }; + } else { + const clusterStatus = await clusterStatusResponse(); + const remoteConfig = await getRemoteConfiguration( + nodeName, + clusterStatus, + ); + + return { + serverAddress: nodeIp, + udpProtocol: remoteConfig.isUdp, + connectionSecure: remoteConfig.haveSecureConnection, + }; + } + } +} + +interface NodeItem { + name: string; + ip: string; + type: string; +} + +interface NodeResponse { + data: { + data: { + affected_items: NodeItem[]; + }; + }; +} + +/** + * Get the list of the cluster nodes and parse it into a list of options + */ +export const getNodeIPs = async (): Promise => + await getWazuhCore().http.server.request('GET', '/cluster/nodes', {}); + +/** + * Get the list of the manager and parse it into a list of options + */ +export const getManagerNode = async (): Promise => { + const managerNode = await getWazuhCore().http.server.request( + 'GET', + '/manager/api/config', + {}, + ); + + return ( + managerNode?.data?.data?.affected_items?.map(item => ({ + label: item.node_name, + value: item.node_api_config.host, + nodetype: 'master', + })) || [] + ); +}; + +/** + * Parse the nodes list from the API response to a format that can be used by the EuiComboBox + * @param nodes + */ +export const parseNodesInOptions = ( + nodes: NodeResponse, +): ServerAddressOptions[] => + nodes.data.data.affected_items.map((item: NodeItem) => ({ + label: item.name, + value: item.ip, + nodetype: item.type, + })); + +/** + * Get the list of the cluster nodes from API and parse it into a list of options + */ +export const fetchClusterNodesOptions = async (): Promise< + ServerAddressOptions[] +> => { + const clusterStatus = await clusterStatusResponse(); + + if (clusterStatus) { + // Cluster mode + // Get the cluster nodes + const nodes = await getNodeIPs(); + + return parseNodesInOptions(nodes); + } else { + // Manager mode + // Get the manager node + return await getManagerNode(); + } +}; + +/** + * Get the master node data from the list of cluster nodes + * @param nodeIps + */ +export const getMasterNode = ( + nodeIps: ServerAddressOptions[], +): ServerAddressOptions[] => + nodeIps.filter(nodeIp => nodeIp.nodetype === 'master'); + +/** + * Get the remote and the auth configuration from manager + * This function get the config from manager mode or cluster mode + */ +export const getMasterConfiguration = async () => { + const nodes = await fetchClusterNodesOptions(); + const masterNode = getMasterNode(nodes); + const clusterStatus = await clusterStatusResponse(); + const remote = await getRemoteConfiguration( + masterNode[0].label, + clusterStatus, + ); + const auth = await getAuthConfiguration(masterNode[0].label, clusterStatus); + + return { + remote, + auth, + }; +}; + +export { getConnectionConfig, getRemoteConfiguration }; + +export const getGroups = async () => { + try { + const result = await getWazuhCore().http.server.request( + 'GET', + '/groups', + {}, + ); + + return result.data.data.affected_items.map(item => ({ + label: item.name, + id: item.name, + })); + } catch (error) { + throw new Error(error); + } +}; + +export const getRegisterAgentFormValues = (form: UseFormReturn) => + // return the values form the formFields and the value property + Object.keys(form.fields).map(key => ({ + name: key, + value: form.fields[key].value, + })); + +export interface IParseRegisterFormValues { + operatingSystem: { + name: tOperatingSystem['name'] | ''; + architecture: tOperatingSystem['architecture'] | ''; + }; + // optionalParams is an object that their key is defined in tOptionalParameters and value must be string + optionalParams: Record; +} + +export const parseRegisterAgentFormValues = ( + formValues: { name: keyof UseFormReturn['fields']; value: any }[], + OSOptionsDefined: RegisterAgentData[], + initialValues?: IParseRegisterFormValues, +) => { + // return the values form the formFields and the value property + const parsedForm = + initialValues || + ({ + operatingSystem: { + architecture: '', + name: '', + }, + optionalParams: {}, + } as IParseRegisterFormValues); + + for (const field of formValues) { + if (field.name === 'operatingSystemSelection') { + // search the architecture defined in architecture array and get the os name defined in title array in the same index + const operatingSystem = OSOptionsDefined.find(os => + os.architecture.includes(field.value), + ); + + if (operatingSystem) { + parsedForm.operatingSystem = { + name: operatingSystem.title, + architecture: field.value, + }; + } + } else { + if (field.name === 'agentGroups') { + parsedForm.optionalParams[field.name as any] = field.value.map( + item => item.id, + ); + } else { + parsedForm.optionalParams[field.name as any] = field.value; + } + } + } + + return parsedForm; +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx new file mode 100644 index 0000000000..88e69f375e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx @@ -0,0 +1,202 @@ +import { EuiStepStatus } from '@elastic/eui'; +import { UseFormReturn } from '../components/form/types'; +import { + FormStepsDependencies, + RegisterAgentFormStatusManager, +} from './form-status-manager'; + +const fieldsHaveErrors = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw new Error('fields to check are not defined in formFields'); + } + + const haveError = fieldsToCheck.some(key => formFields[key]?.error); + + return haveError; +}; + +const fieldsAreEmpty = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw new Error('fields to check are not defined in formFields'); + } + + const notEmpty = fieldsToCheck.some( + key => formFields[key]?.value?.length > 0, + ); + + return !notEmpty; +}; + +const anyFieldIsComplete = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw new Error('fields to check are not defined in formFields'); + } + + if (fieldsHaveErrors(fieldsToCheck, formFields)) { + return false; + } + + if (fieldsAreEmpty(fieldsToCheck, formFields)) { + return false; + } + + return true; +}; + +export const showCommandsSections = ( + formFields: UseFormReturn['fields'], +): boolean => { + if ( + !formFields.operatingSystemSelection.value || + formFields.serverAddress.value === '' || + formFields.serverAddress.error + ) { + return false; + } else if ( + formFields.serverAddress.value === '' && + formFields.agentName.value === '' + ) { + return true; + } else if (fieldsHaveErrors(['agentGroups', 'agentName'], formFields)) { + return false; + } else { + return true; + } +}; + +/** ****** Form Steps status getters ********/ + +export type tFormStepsStatus = EuiStepStatus | 'current' | 'disabled' | ''; + +export const getOSSelectorStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => + formFields.operatingSystemSelection.value ? 'complete' : 'current'; + +export const getAgentCommandsStepStatus = ( + formFields: UseFormReturn['fields'], + wasCopied: boolean, +): tFormStepsStatus | 'disabled' => { + if (!showCommandsSections(formFields)) { + return 'disabled'; + } else if (showCommandsSections(formFields) && wasCopied) { + return 'complete'; + } else { + return 'current'; + } +}; + +export const getServerAddressStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error + ) { + return 'disabled'; + } else if ( + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'current'; + } else { + return 'complete'; + } +}; + +export const getOptionalParameterStepStatus = ( + formFields: UseFormReturn['fields'], + installCommandWasCopied: boolean, +): tFormStepsStatus => { + // when previous step are not complete + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error || + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'disabled'; + } else if ( + installCommandWasCopied || + anyFieldIsComplete(['agentName', 'agentGroups'], formFields) + ) { + return 'complete'; + } else { + return 'current'; + } +}; + +export const getPasswordStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error || + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'disabled'; + } else { + return 'complete'; + } +}; + +export enum tFormStepsLabel { + operatingSystemSelection = 'operating system', + serverAddress = 'server address', +} + +export const getIncompleteSteps = ( + formFields: UseFormReturn['fields'], +): tFormStepsLabel[] => { + const steps: FormStepsDependencies = { + operatingSystemSelection: ['operatingSystemSelection'], + serverAddress: ['serverAddress'], + }; + const statusManager = new RegisterAgentFormStatusManager(formFields, steps); + + // replace fields array using label names + return statusManager + .getIncompleteSteps() + .map(field => tFormStepsLabel[field] || field); +}; + +export enum tFormFieldsLabel { + agentName = 'agent name', + agentGroups = 'agent groups', + serverAddress = 'server address', +} + +export const getInvalidFields = ( + formFields: UseFormReturn['fields'], +): tFormFieldsLabel[] => { + const statusManager = new RegisterAgentFormStatusManager(formFields); + + return statusManager + .getInvalidFields() + .map(field => tFormFieldsLabel[field] || field); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts new file mode 100644 index 0000000000..f1be9e62c3 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts @@ -0,0 +1,89 @@ +import { scapeSpecialCharsForLinux, scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows } from './wazuh-password-service'; +describe('Wazuh Password Service', () => { + // NOTE: + // The password constant must be written as it comes from the backend + // The expectedPassword variable must be written taking into account how the \ will be escaped + describe('For Linux shell' , () => { + it.each` + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'\"'\"'sds\\\"es"} + ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\\\usds\\\"\\\\es"} + `( + ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', + ({ passwordFromAPI, expectedScapedPassword }) => { + const passwordScaped = scapeSpecialCharsForLinux(passwordFromAPI); + /* log to compare passwords + console.log( + 'PASSWORD REAL: ', + passwordFromAPI, + '\nPASSWORD BACKEND: ', + JSON.stringify(passwordFromAPI), + '\nRESULT PASSWORD SCAPED REAL IN COMMAND: ', + passwordScaped, + '\nPASSWORD SCAPED REAL IN COMMAND EXPECTED: ', + expectedScapedPassword + );*/ + expect(passwordScaped).toEqual(expectedScapedPassword); + } + ); + }) + + describe('For Windows shell' , () => { + it.each` + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'\"'\"'sds\\\"es"} + ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\usds\\\"\\es"} + `( + ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', + ({ passwordFromAPI, expectedScapedPassword }) => { + const passwordScaped = scapeSpecialCharsForWindows(passwordFromAPI); + /* log to compare passwords + console.log( + 'PASSWORD REAL: ', + passwordFromAPI, + '\nPASSWORD BACKEND: ', + JSON.stringify(passwordFromAPI), + '\nRESULT PASSWORD SCAPED REAL IN COMMAND: ', + passwordScaped, + '\nPASSWORD SCAPED REAL IN COMMAND EXPECTED: ', + expectedScapedPassword + );*/ + expect(passwordScaped).toEqual(expectedScapedPassword); + } + ); + }); + + describe('For macOS shell' , () => { + it.each` + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'with'special'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'sds\\\"es"} + ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} + ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\usds\\\"\\es"} + `( + ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', + ({ passwordFromAPI, expectedScapedPassword }) => { + const passwordScaped = scapeSpecialCharsForMacOS(passwordFromAPI); + /* log to compare passwords + console.log( + 'PASSWORD REAL: ', + passwordFromAPI, + '\nPASSWORD BACKEND: ', + JSON.stringify(passwordFromAPI), + '\nRESULT PASSWORD SCAPED REAL IN COMMAND: ', + passwordScaped, + '\nPASSWORD SCAPED REAL IN COMMAND EXPECTED: ', + expectedScapedPassword + );*/ + expect(passwordScaped).toEqual(expectedScapedPassword); + } + ); +}) +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts new file mode 100644 index 0000000000..a42e35afb9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts @@ -0,0 +1,68 @@ +import { tOperatingSystem } from "../hooks/use-register-agent-commands.test"; + +export const scapeSpecialCharsForLinux = (password: string) => { + let passwordScaped = password; + // the " characters is scaped by default in the password retrieved from the API + const specialCharsList = ["'"]; + const regex = new RegExp(`([${specialCharsList.join('')}])`, 'g'); + // the single quote is escaped first, and then any unescaped backslashes are escaped + return passwordScaped.replace(regex, `\'\"$&\"\'`).replace(/(? { + let passwordScaped = password; + // The double quote is escaped first and then the backslash followed by a single quote + return passwordScaped.replace(/\\"/g, '\\\"').replace(/\\'/g, `\\'\"'\"'`); +} + +export const scapeSpecialCharsForWindows = (password: string) => { + let passwordScaped = password; + // the " characters is scaped by default in the password retrieved from the API + const specialCharsList = ["'"]; + const regex = new RegExp(`([${specialCharsList.join('')}])`, 'g'); + // the single quote is escaped first, and then any unescaped backslashes are escaped + return passwordScaped.replace(regex, `\'\"$&\"\'`).replace(/(? { + let command = commandText; + const osName = os?.toLocaleLowerCase(); + switch (osName){ + case 'macos': + { + const regex = /WAZUH_REGISTRATION_PASSWORD=\'((?:\\'|[^']|[\"'])*)'/g; + const replacedString = command.replace( + regex, + (match, capturedGroup) => { + return match.replace( + capturedGroup, + '*'.repeat(capturedGroup.length), + ); + } + ); + return replacedString; + } + case 'windows': + { + const replacedString = command.replace( + `WAZUH_REGISTRATION_PASSWORD=\'${scapeSpecialCharsForWindows(password)}'`, + () => { + return `WAZUH_REGISTRATION_PASSWORD=\'${'*'.repeat(scapeSpecialCharsForWindows(password).length)}\'`; + } + ); + return replacedString; + } + case 'linux': + { + const replacedString = command.replace( + `WAZUH_REGISTRATION_PASSWORD=\$'${scapeSpecialCharsForLinux(password)}'`, + () => { + return `WAZUH_REGISTRATION_PASSWORD=\$\'${'*'.repeat(scapeSpecialCharsForLinux(password).length)}\'`; + } + ); + return replacedString; + } + default: + return commandText; + } +} \ No newline at end of file diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts new file mode 100644 index 0000000000..ef286990c0 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts @@ -0,0 +1,25 @@ +import { + DOCUMENTATION_WEB_BASE_URL, + PLUGIN_VERSION_SHORT, +} from '../../../../../common/constants'; +import { webDocumentationLink } from './web-documentation-link'; + +test(`Generate a web documentation URL using to the plugin short version`, () => { + expect(webDocumentationLink('user-manual/agent-enrollment/index.html')).toBe( + `${DOCUMENTATION_WEB_BASE_URL}/${PLUGIN_VERSION_SHORT}/user-manual/agent-enrollment/index.html`, + ); +}); + +test(`Generate a web documentation URL to the base URL using to the plugin short version`, () => { + expect(webDocumentationLink('')).toBe( + `${DOCUMENTATION_WEB_BASE_URL}/${PLUGIN_VERSION_SHORT}/`, + ); +}); + +test(`Generate a web documentation URL using a specific version`, () => { + expect( + webDocumentationLink('user-manual/agent-enrollment/index.html', '4.6'), + ).toBe( + `${DOCUMENTATION_WEB_BASE_URL}/4.6/user-manual/agent-enrollment/index.html`, + ); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts new file mode 100644 index 0000000000..cc4f825f01 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts @@ -0,0 +1,17 @@ +import { + DOCUMENTATION_WEB_BASE_URL, + PLUGIN_VERSION_SHORT, +} from '../../../../../common/constants'; + +/** + * Generate a URL to the web documentation taking in account the plugin short version or specified version. + * @param urlPath Relative path to the base URL + version. + * @param version version. Optional. It will use the plugin short version by default. + * @returns + */ +export function webDocumentationLink( + urlPath: string, + version: string = PLUGIN_VERSION_SHORT, +): string { + return `${DOCUMENTATION_WEB_BASE_URL}/${version}/${urlPath}`; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx new file mode 100644 index 0000000000..09d87fd86f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx @@ -0,0 +1,47 @@ +import { RegisterAgentData } from '../interfaces/types'; +import LinuxDarkIcon from '../assets/images/themes/dark/linux-icon.svg'; +import LinuxLightIcon from '../assets/images/themes/light/linux-icon.svg'; +import WindowsDarkIcon from '../assets/images/themes/dark/windows-icon.svg'; +import WindowsLightIcon from '../assets/images/themes/light/windows-icon.svg'; +import MacDarkIcon from '../assets/images/themes/dark/mac-icon.svg'; +import MacLightIcon from '../assets/images/themes/light/mac-icon.svg'; +import { getCore } from '../../../../plugin-services'; + +const darkMode = getCore()?.uiSettings?.get('theme:darkMode'); + +export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ + { + icon: darkMode ? LinuxDarkIcon : LinuxLightIcon, + title: 'LINUX', + hr: true, + architecture: ['RPM amd64', 'RPM aarch64', 'DEB amd64', 'DEB aarch64'], + }, + { + icon: darkMode ? WindowsDarkIcon : WindowsLightIcon, + title: 'WINDOWS', + hr: true, + architecture: ['MSI 32/64 bits'], + }, + { + icon: darkMode ? MacDarkIcon : MacLightIcon, + title: 'macOS', + hr: true, + architecture: ['Intel', 'Apple silicon'], + }, +]; + +export const SERVER_ADDRESS_TEXTS = [ + { + title: 'Server address', + subtitle: + 'This is the address the agent uses to communicate with the server. Enter an IP address or a fully qualified domain name (FQDN).', + }, +]; + +export const OPTIONAL_PARAMETERS_TEXT = [ + { + title: 'Optional settings', + subtitle: + 'By default, the deployment uses the hostname as the agent name. Optionally, you can use a different agent name in the field below.', + }, +]; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx new file mode 100644 index 0000000000..1d457ec2b4 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx @@ -0,0 +1,45 @@ +import { validateAgentName } from './validations'; + +describe('Validations', () => { + test('should return undefined for an empty value', () => { + const emptyValue = ''; + const result = validateAgentName(emptyValue); + expect(result).toBeUndefined(); + }); + + test('should return an error message for invalid format and length', () => { + const invalidAgentName = '?'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The minimum length is 2 characters. The character "?" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid format of 1 character', () => { + const invalidAgentName = 'agent$name'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The character "$" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid format of more than 1 character', () => { + const invalidAgentName = 'agent$?name'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The characters "$,?" are not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid length', () => { + const invalidAgentName = 'a'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe('The minimum length is 2 characters.'); + }); + + test('should return an empty string for a valid agent name', () => { + const validAgentName = 'agent_name'; + const result = validateAgentName(validAgentName); + expect(result).toBe(''); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx new file mode 100644 index 0000000000..06d9aaf940 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx @@ -0,0 +1,27 @@ +export const validateAgentName = (value: any) => { + if (value.length === 0) { + return undefined; + } + let invalidCharacters = validateCharacters(value); + if (value.length < 2) { + return `The minimum length is 2 characters.${ + invalidCharacters && ` ${invalidCharacters}` + }`; + } + return `${invalidCharacters}`; +}; + +const validateCharacters = (value: any) => { + const regex = /^[a-z0-9.\-_,]+$/i; + const invalidCharacters = [ + ...new Set(value.split('').filter(char => !regex.test(char))), + ]; + if (invalidCharacters.length > 1) { + return `The characters "${invalidCharacters.join( + ',', + )}" are not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; + } else if (invalidCharacters.length === 1) { + return `The character "${invalidCharacters[0]}" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; + } + return ''; +}; diff --git a/plugins/wazuh-fleet/public/application/render-app.tsx b/plugins/wazuh-fleet/public/application/render-app.tsx new file mode 100644 index 0000000000..69225dec7f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/render-app.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { I18nProvider } from '@osd/i18n/react'; +import { createHashHistory } from 'history'; +import { Application } from './application'; + +export async function renderApp(params) { + const history = createHashHistory(); + const deps = { /* coreStart, navigation, */ params /* config */, history }; + + ReactDOM.render( + + + , + params.element, + ); + + return () => ReactDOM.unmountComponentAtNode(params.element); +} diff --git a/plugins/wazuh-fleet/public/application/types.ts b/plugins/wazuh-fleet/public/application/types.ts new file mode 100644 index 0000000000..84c00e239f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/types.ts @@ -0,0 +1,3 @@ +export interface AppSetup { + registerApp: (app: any) => void; +} diff --git a/plugins/wazuh-fleet/public/plugin.ts b/plugins/wazuh-fleet/public/plugin.ts index 3253a55bff..073e769cc0 100644 --- a/plugins/wazuh-fleet/public/plugin.ts +++ b/plugins/wazuh-fleet/public/plugin.ts @@ -6,11 +6,16 @@ import { } from './types'; import { FleetManagement } from './components'; import { setCore, setPlugins, setWazuhCore } from './plugin-services'; +import { appSetup } from './application'; export class WazuhFleetPlugin implements Plugin { - public setup(core: CoreSetup): WazuhFleetPluginSetup { + public setup(core: CoreSetup, plugins): WazuhFleetPluginSetup { + appSetup({ + registerApp: app => core.application.register(app), + }); + return {}; } From f258346256916d29e4e996d68498dca79afa776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Feb 2025 09:21:39 +0100 Subject: [PATCH 02/22] feat: add inputs to enrollment agent assistant Added: - Added inputs to match with the `wazuh-agent` cli: - Input to manage the username - Input to manage the password - Input to manage the SSL verification mode - Input to manage the key (This key must have 32 alphanumeric characters validation) - Added new step "Server credentials" to define the username and password - Added tests for the new inputs Changed: - Changed the generation of command to install and enroll the agent - Changed the validation of "Assign a server address" input - Changed the placeholder of "Assign a server address" input - Changed the description of "Server address" step to indicate the usage of URL instead of raw IP or FQDNS Removed: - Groups management was removed because the current Wazuh agent enrollment does not support the definition of these. - Removed groups form input - Removed fetch information about available groups - Removed fetch information about authentication password provided through the Wazuh server API - Removed fetch information about protocol provided through the Wazuh server API - Temporally: - The command to download the package were temporaly removed because the packages are not publically hosted. A warning message was added to inform about this. --- plugins/wazuh-fleet/package.json | 8 +- .../command-output/command-output.scss | 47 ++ .../command-output/command-output.tsx | 42 +- .../components/command-output/os-warning.tsx | 4 +- .../form/__snapshots__/index.test.tsx.snap | 2 +- .../components/form/hooks.test.tsx | 43 +- .../components/form/index.test.tsx | 117 ++-- .../enrollment-key-input.test.tsx.snap | 99 +++ .../optionals-inputs.test.tsx.snap | 374 +++++++++++ .../verification-mode-input.test.tsx.snap | 119 ++++ .../enrollment-key-input.test.tsx | 26 + .../optionals-inputs/enrollment-key-input.tsx | 94 +++ .../optionals-inputs.test.tsx | 56 ++ .../optionals-inputs/optionals-inputs.tsx | 5 +- .../verification-mode-input.test.tsx | 36 ++ .../verification-mode-input.tsx | 94 +++ .../checkbox-group/checkbox-group.tsx | 1 + .../os-selector/os-card/os-card.test.tsx | 34 +- .../__snapshots__/index.test.tsx.snap | 267 ++++++++ .../password-input.test.tsx.snap | 143 +++++ .../username-input.test.tsx.snap | 99 +++ .../components/security/index.test.tsx | 28 + .../components/security/index.tsx | 32 + .../security/password-input.test.tsx | 26 + .../components/security/password-input.tsx | 94 +++ .../security/username-input.test.tsx | 26 + .../components/security/username-input.tsx | 94 +++ .../server-address/server-address.tsx | 73 +-- .../register-agent/register-agent.tsx | 138 ++--- .../register-agent/containers/steps/steps.tsx | 122 ++-- .../core/config/os-commands-definitions.ts | 121 ++-- .../core/register-commands/README.md | 24 +- .../command-generator.test.ts | 217 ++++--- .../optional-parameters-manager.test.ts | 125 ++-- .../optional-parameters-manager.ts | 9 +- .../get-install-command.service.test.ts | 74 ++- .../search-os-definitions.service.test.ts | 37 +- .../pages/register-agent/hooks/README.md | 61 +- .../hooks/use-register-agent-commands.test.ts | 60 +- .../pages/register-agent/interfaces/types.ts | 6 +- ...egister-agent-os-commands-services.test.ts | 243 ++++++-- .../register-agent-os-commands-services.tsx | 210 ++++--- .../services/register-agent-services.test.ts | 292 --------- .../services/register-agent-services.tsx | 285 +-------- .../register-agent-steps-status-services.tsx | 161 +++-- .../services/wazuh-password-service.ts | 94 +-- .../utils/register-agent-data.tsx | 2 +- .../register-agent/utils/validations.test.tsx | 49 +- .../register-agent/utils/validations.tsx | 81 ++- plugins/wazuh-fleet/yarn.lock | 582 +----------------- 50 files changed, 3003 insertions(+), 2073 deletions(-) create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts diff --git a/plugins/wazuh-fleet/package.json b/plugins/wazuh-fleet/package.json index e2d6f6ce29..da97684903 100644 --- a/plugins/wazuh-fleet/package.json +++ b/plugins/wazuh-fleet/package.json @@ -22,16 +22,10 @@ "md5": "^2.3.0" }, "devDependencies": { - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.5.2", - "@types/": "testing-library/user-event", "@types/jest": "^29.5.14", "@types/md5": "^2.3.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "jest": "^29.7.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "jest": "^29.7.0" } } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss new file mode 100644 index 0000000000..d2f2d6bac3 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss @@ -0,0 +1,47 @@ +.register-agent-wizard-container .copy-codeblock-wrapper { + position: relative; + + .euiToolTipAnchor { + opacity: 0; + transition: 150ms opacity ease-in-out; + position: absolute; + top: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(255, 255, 255, 0.75); + + &:hover { + opacity: 1; + } + .copy-overlay { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + cursor: pointer; + width: 100%; + p { + margin: 0; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + } + } + + code.euiCodeBlock__code { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx index 3137ba5831..dc0e31600b 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx @@ -8,14 +8,15 @@ import { EuiText, } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; -import { tOperatingSystem } from '../../core/config/os-commands-definitions'; -import { osdfucatePasswordInCommand } from '../../services/wazuh-password-service'; +import { TOperatingSystem } from '../../core/config/os-commands-definitions'; +import { obfuscatePasswordInCommand } from '../../services/wazuh-password-service'; +import './command-output.scss'; interface ICommandSectionProps { commandText: string; showCommand: boolean; onCopy: () => void; - os?: tOperatingSystem['name']; + os?: TOperatingSystem['name']; password?: string; } @@ -25,36 +26,45 @@ export default function CommandOutput(props: ICommandSectionProps) { const [showPassword, setShowPassword] = useState(false); const onHandleCopy = (command: any) => { - onCopy && onCopy(); + if (onCopy) { + onCopy(); + } + return command; // the return is needed to avoid a bug in EuiCopy }; const [commandToShow, setCommandToShow] = useState(commandText); - useEffect(() => { - if (password) { - setHavePassword(true); - osdfucatePassword(password); - } else { - setHavePassword(false); - setCommandToShow(commandText); + const obfuscatePassword = (password: string) => { + if (!password) { + return; } - }, [password, commandText, showPassword]); - const osdfucatePassword = (password: string) => { - if (!password) return; - if (!commandText) return; + if (!commandText) { + return; + } if (showPassword) { setCommandToShow(commandText); } else { - setCommandToShow(osdfucatePasswordInCommand(password, commandText, os)); + setCommandToShow(obfuscatePasswordInCommand(password, commandText, os)); } }; + useEffect(() => { + if (password) { + setHavePassword(true); + obfuscatePassword(password); + } else { + setHavePassword(false); + setCommandToShow(commandText); + } + }, [password, commandText, showPassword]); + const onChangeShowPassword = (event: EuiSwitchEvent) => { setShowPassword(event.target.checked); }; + return ( diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx index e9e17e0e65..5c4da92c99 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { EuiCallOut } from '@elastic/eui'; -import { tOperatingSystem } from '../../core/config/os-commands-definitions'; +import { TOperatingSystem } from '../../core/config/os-commands-definitions'; interface OsWarningProps { - os?: tOperatingSystem['name']; + os?: TOperatingSystem['name']; } export default function OsCommandWarning(props: OsWarningProps) { diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap index c7b6ee25cf..c5edbf210e 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap @@ -376,7 +376,7 @@ exports[`[component] InputForm Renders correctly to match the snapshot: Input: s exports[`[component] InputForm Renders correctly to match the snapshot: Input: switch 1`] = `
+
+
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+ + +`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap new file mode 100644 index 0000000000..aad3dcb562 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap @@ -0,0 +1,374 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Enrollment key input match the snapshopt 1`] = ` +
+
+
+
+ By default, the deployment uses the hostname as the agent name. Optionally, you can use a different agent name in the field below. +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + The agent name must be unique. It can’t be changed once the agent has been enrolled. + + + + (opens in a new tab or window) + + + + +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap new file mode 100644 index 0000000000..2c39e79f6e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verification mode input match the snapshopt 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+
+ + +`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap new file mode 100644 index 0000000000..0d2ad30f7c --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`password input match the snapshopt 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+
+`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap new file mode 100644 index 0000000000..e12f665e54 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`password input match the snapshopt 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx new file mode 100644 index 0000000000..ad86f97fe9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SecurityInputs } from './index'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'test-id', +})); + +describe('credentials input', () => { + it('match the snapshopt', () => { + const wrapper = render( + {}, + }} + password={{ + type: 'password', + value: '', + onChange: () => {}, + }} + />, + ); + + expect(wrapper.container).toMatchSnapshot(); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx new file mode 100644 index 0000000000..f30f8aa08f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EnhancedFieldConfiguration } from '../form/types'; +import { UsernameInput } from './username-input'; +import { PasswordInput } from './password-input'; + +export const SecurityInputs = (props: { + username: EnhancedFieldConfiguration; + password: EnhancedFieldConfiguration; +}) => ( + <> + + + + + + + + + + + + + + + + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx new file mode 100644 index 0000000000..f66c8f43bb --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; +import { PasswordInput } from './password-input'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'test-id', +})); + +describe('password input', () => { + it('match the snapshopt', () => { + const wrapper = render( + + {}, + }} + /> + , + ); + + expect(wrapper.container).toMatchSnapshot(); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx new file mode 100644 index 0000000000..ab27117f9d --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +import { InputForm } from '../form'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import { EnhancedFieldConfiguration } from '../form/types'; + +export const PasswordInput = (props: { + formField: EnhancedFieldConfiguration; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onClickButtonPopoverOpen = () => + setIsPopoverOpen(isPopoverServerAddress => !isPopoverServerAddress); + const { formField } = props; + + return ( + <> + + + + + + + + + + } + isOpen={isPopoverOpen} + closePopover={onClickButtonPopoverOpen} + anchorPosition='rightCenter' + > + + + + + + + + + + + } + fullWidth={false} + placeholder={i18n.translate( + 'wzFleet.enrollmentAssistant.steps.credentials.password.placeholder', + { + defaultMessage: 'Server API password', + }, + )} + /> + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx new file mode 100644 index 0000000000..e26a8fa7b5 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; +import { UsernameInput } from './username-input'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'test-id', +})); + +describe('password input', () => { + it('match the snapshopt', () => { + const wrapper = render( + + {}, + }} + /> + , + ); + + expect(wrapper.container).toMatchSnapshot(); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx new file mode 100644 index 0000000000..9fc7eb5cfe --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +import { InputForm } from '../form'; +import { webDocumentationLink } from '../../services/web-documentation-link'; +import { EnhancedFieldConfiguration } from '../form/types'; + +export const UsernameInput = (props: { + formField: EnhancedFieldConfiguration; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onClickButtonPopoverOpen = () => + setIsPopoverOpen(isPopoverServerAddress => !isPopoverServerAddress); + const { formField } = props; + + return ( + <> + + + + + + + + + + } + isOpen={isPopoverOpen} + closePopover={onClickButtonPopoverOpen} + anchorPosition='rightCenter' + > + + + + + + + + + + + } + fullWidth={false} + placeholder={i18n.translate( + 'wzFleet.enrollmentAssistant.steps.credentials.username.placeholder', + { + defaultMessage: 'Server API username', + }, + )} + /> + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx index b33be98fa3..cd9dae7eba 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx @@ -5,16 +5,14 @@ import { EuiPopover, EuiButtonEmpty, EuiLink, - EuiSwitch, } from '@elastic/eui'; -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; import { EnhancedFieldConfiguration } from '../form/types'; import { InputForm } from '../form'; import { webDocumentationLink } from '../../services/web-documentation-link'; import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; import '../group-input/group-input.scss'; -import { getWazuhCore } from '../../../../../plugin-services'; interface ServerAddressInputProps { formField: EnhancedFieldConfiguration; @@ -44,58 +42,9 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { isPopoverServerAddress => !isPopoverServerAddress, ); const closeServerAddress = () => setIsPopoverServerAddress(false); - const [rememberServerAddress, setRememberServerAddress] = useState(false); - const [defaultServerAddress, setDefaultServerAddress] = useState( - formField?.initialValue ? formField?.initialValue : '', - ); - const appConfig = getWazuhCore().configuration.get(); // TODO: this should use a live state (that reacts to changes in the configuration) - - const handleToggleRememberAddress = async event => { - setRememberServerAddress(event.target.checked); - - if (event.target.checked) { - await saveServerAddress(); - setDefaultServerAddress(formField.value); - } - }; - - const saveServerAddress = async () => { - try { - const res = await getWazuhCore().http.server.request( - 'PUT', - '/utils/configuration', - { - 'enrollment.dns': formField.value, - }, - ); - } catch { - // TODO: use error handler - // ErrorHandler.handleError(error, { - // message: error.message, - // title: 'Error saving server address configuration', - // }); - setRememberServerAddress(false); - } - }; - - const rememberToggleIsDisabled = () => !formField.value || !!formField.error; - - const handleInputChange = value => { - if (value === defaultServerAddress) { - setRememberServerAddress(true); - } else { - setRememberServerAddress(false); - } - }; - - useEffect(() => { - handleInputChange(formField.value); - }, [formField.value]); - - const { ServerButtonPermissions } = getWazuhCore().ui; return ( - + <> {SERVER_ADDRESS_TEXTS.map((data, index) => ( @@ -147,25 +96,11 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { } fullWidth={false} - placeholder='Server address' + placeholder='https://server-address:55000' /> - {appConfig?.['configuration.ui_api_editable'] && ( - - - handleToggleRememberAddress(e)} - /> - - - )} - + ); }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx index 46fa6b9c4a..731cb573fb 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx @@ -9,36 +9,20 @@ import { EuiSpacer, EuiProgress, } from '@elastic/eui'; -import { useSelector } from 'react-redux'; import { compose } from 'redux'; -// import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; -// import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; -// import { ErrorHandler } from '../../../../../react-services/error-management'; import './register-agent.scss'; import { Steps } from '../steps/steps'; import { InputForm } from '../../components/form'; -import { - getGroups, - getMasterConfiguration, -} from '../../services/register-agent-services'; import { useForm } from '../../components/form/hooks'; import { FormConfiguration } from '../../components/form/types'; -import { - withErrorBoundary, - withGlobalBreadcrumb, - withRouteResolvers, - withUserAuthorizationPrompt, -} from '../../../../common/hocs'; -import GroupInput from '../../components/group-input/group-input'; import { OsCard } from '../../components/os-selector/os-card/os-card'; -import { validateAgentName } from '../../utils/validations'; import { - enableMenu, - ip, - nestedResolve, - savedSearch, -} from '../../../../../services/resolves'; + validateAgentName, + validateEnrollmentKey, + validateServerAddress, +} from '../../utils/validations'; import { getWazuhCore } from '../../../../../plugin-services'; +import { version } from '../../../../../../package.json'; export const RegisterAgent = compose( // TODO: add HOCs @@ -54,57 +38,60 @@ export const RegisterAgent = compose( // withUserAuthorizationPrompt([ // [{ action: 'agent:create', resource: '*:*:*' }], // ]), + // eslint-disable-next-line react/display-name WrappedComponent => props => , )(() => { const configuration = {}; // Use a live state (reacts to changes through some hook that provides the configuration); const [wazuhVersion, setWazuhVersion] = useState(''); - const [haveUdpProtocol, setHaveUdpProtocol] = useState(false); const [loading, setLoading] = useState(false); - const [wazuhPassword, setWazuhPassword] = useState(''); - const [groups, setGroups] = useState([]); - const [needsPassword, setNeedsPassword] = useState(false); const initialFields: FormConfiguration = { operatingSystemSelection: { type: 'custom', initialValue: '', component: props => , - options: { - groups, - }, }, serverAddress: { type: 'text', - initialValue: configuration['enrollment.dns'] || '', - // validate: - // getWazuhCore().configuration._settings.get('enrollment.dns').validate, + initialValue: configuration['enrollment.dns'] || '', // TODO: use the setting value as default value + validate: validateServerAddress, }, - agentName: { + username: { type: 'text', initialValue: '', - validate: validateAgentName, }, - - agentGroups: { - type: 'custom', - initialValue: [], - component: props => , + password: { + type: 'password', + initialValue: '', + }, + verificationMode: { + type: 'select', + initialValue: 'none', options: { - groups, + select: [ + { + text: 'none', + value: 'none', + }, + { + text: 'full', + value: 'full', + }, + ], }, }, + agentName: { + type: 'text', + initialValue: '', + validate: validateAgentName, + }, + enrollmentKey: { + type: 'text', + initialValue: '', + validate: validateEnrollmentKey, + }, }; const form = useForm(initialFields); - const getMasterConfig = async () => { - const masterConfig = await getMasterConfiguration(); - - if (masterConfig?.remote) { - setHaveUdpProtocol(masterConfig.remote.isUdp); - } - - return masterConfig; - }; - const getWazuhVersion = async () => { try { const result = await getWazuhCore().http.server.request('GET', '/', {}); @@ -112,18 +99,6 @@ export const RegisterAgent = compose( return result?.data?.data?.api_version; } catch { // TODO: manage error - // const options = { - // context: `RegisterAgent.getWazuhVersion`, - // level: UI_LOGGER_LEVELS.ERROR, - // severity: UI_ERROR_SEVERITIES.BUSINESS, - // error: { - // error: error, - // message: error.message || error, - // title: `Could not get the Wazuh version: ${error.message || error}`, - // }, - // }; - - // getErrorOrchestrator().handleError(options); return version; } @@ -133,44 +108,15 @@ export const RegisterAgent = compose( const fetchData = async () => { try { const wazuhVersion = await getWazuhVersion(); - const { auth: authConfig } = await getMasterConfig(); - // get wazuh password configuration - let wazuhPassword = ''; - const needsPassword = authConfig?.auth?.use_password === 'yes'; - - if (needsPassword) { - wazuhPassword = - configuration?.['enrollment.password'] || - authConfig?.['authd.pass'] || - ''; - } - const groups = await getGroups(); - - setNeedsPassword(needsPassword); - setWazuhPassword(wazuhPassword); + // get wazuh password configuration setWazuhVersion(wazuhVersion); - setGroups(groups); setLoading(false); } catch { setWazuhVersion(wazuhVersion); setLoading(false); // TODO: manage error - // const options = { - // context: 'RegisterAgent', - // level: UI_LOGGER_LEVELS.ERROR, - // severity: UI_ERROR_SEVERITIES.BUSINESS, - // display: true, - // store: false, - // error: { - // error: error, - // message: error.message || error, - // title: error.name || error, - // }, - // }; - - // ErrorHandler.handleError(error, options); } }; @@ -205,15 +151,7 @@ export const RegisterAgent = compose( ) : ( - + )} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx index 4758e8261c..d0099f8b92 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx @@ -1,6 +1,7 @@ -import React, { Fragment, useEffect, useState } from 'react'; -import { EuiCallOut, EuiLink, EuiSteps } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { EuiCallOut, EuiSteps, EuiSpacer } from '@elastic/eui'; import './steps.scss'; +import { FormattedMessage } from '@osd/i18n/react'; import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; import { IParseRegisterFormValues, @@ -11,68 +12,70 @@ import { useRegisterAgentCommands } from '../../hooks/use-register-agent-command import { osCommandsDefinitions, optionalParamsDefinitions, - tOperatingSystem, - tOptionalParameters, + TOperatingSystem, + TOptionalParameters, } from '../../core/config/os-commands-definitions'; import { UseFormReturn } from '../../components/form/types'; import CommandOutput from '../../components/command-output/command-output'; import ServerAddress from '../../components/server-address/server-address'; import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; +import { SecurityInputs } from '../../components/security'; import { getAgentCommandsStepStatus, - tFormStepsStatus, + TFormStepsStatus, getOSSelectorStepStatus, getServerAddressStepStatus, getOptionalParameterStepStatus, showCommandsSections, - getPasswordStepStatus, getIncompleteSteps, getInvalidFields, - tFormFieldsLabel, - tFormStepsLabel, + FORM_FIELDS_LABEL, + FORM_STEPS_LABELS, + getServerCredentialsStepStatus, } from '../../services/register-agent-steps-status-services'; -import { webDocumentationLink } from '../../services/web-documentation-link'; import OsCommandWarning from '../../components/command-output/os-warning'; interface IStepsProps { - needsPassword: boolean; form: UseFormReturn; osCard: React.ReactElement; connection: { isUDP: boolean; }; - wazuhPassword: string; } -export const Steps = ({ - needsPassword, - form, - osCard, - connection, - wazuhPassword, -}: IStepsProps) => { +const FORM_MESSAGE_CONJUNTION = ' and '; + +export const Steps = ({ form, osCard }: IStepsProps) => { const initialParsedFormValues = { operatingSystem: { name: '', architecture: '', }, optionalParams: { - agentGroups: '', agentName: '', serverAddress: '', - wazuhPassword, - protocol: connection.isUDP ? 'UDP' : '', }, } as IParseRegisterFormValues; - const [missingStepsName, setMissingStepsName] = useState( + const [missingStepsName, setMissingStepsName] = useState( [], ); const [invalidFieldsName, setInvalidFieldsName] = useState< - tFormFieldsLabel[] + FORM_FIELDS_LABEL[] >([]); const [registerAgentFormValues, setRegisterAgentFormValues] = useState(initialParsedFormValues); - const FORM_MESSAGE_CONJUNTION = ' and '; + const { installCommand, startCommand, selectOS, setOptionalParams } = + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }); + // install - start commands step state + const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); + const [installCommandStepStatus, setInstallCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); + const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); + const [startCommandStepStatus, setStartCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); useEffect(() => { // get form values and parse them divided in OS and optional params @@ -93,30 +96,17 @@ export const Steps = ({ setInvalidFieldsName(getInvalidFields(form.fields) || []); }, [form.fields]); - const { installCommand, startCommand, selectOS, setOptionalParams } = - useRegisterAgentCommands({ - osDefinitions: osCommandsDefinitions, - optionalParamsDefinitions: optionalParamsDefinitions, - }); - // install - start commands step state - const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); - const [installCommandStepStatus, setInstallCommandStepStatus] = - useState(getAgentCommandsStepStatus(form.fields, false)); - const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); - const [startCommandStepStatus, setStartCommandStepStatus] = - useState(getAgentCommandsStepStatus(form.fields, false)); - useEffect(() => { if ( registerAgentFormValues.operatingSystem.name !== '' && registerAgentFormValues.operatingSystem.architecture !== '' ) { - selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); + selectOS(registerAgentFormValues.operatingSystem as TOperatingSystem); } setOptionalParams( { ...registerAgentFormValues.optionalParams }, - registerAgentFormValues.operatingSystem as tOperatingSystem, + registerAgentFormValues.operatingSystem as TOperatingSystem, ); setInstallCommandWasCopied(false); setStartCommandWasCopied(false); @@ -145,36 +135,21 @@ export const Steps = ({ children: , status: getServerAddressStepStatus(form.fields), }, - ...(needsPassword && !wazuhPassword - ? [ - { - title: 'Password', - children: ( - - The password is required but wasn't defined. Please check - our{' '} - - documentation - - - } - iconType='iInCircle' - className='warningForAgentName' - /> - ), - status: getPasswordStepStatus(form.fields), - }, - ] - : []), + { + title: ( + + ), + children: ( + + ), + status: getServerCredentialsStepStatus(form.fields), + }, { title: 'Optional settings:', children: , @@ -208,12 +183,19 @@ export const Steps = ({ ) : null} {!missingStepsName?.length && !invalidFieldsName?.length ? ( <> + {/* TODO: remove the warning and spacer when the packages are publically hosted */} + + setInstallCommandWasCopied(true)} - password={registerAgentFormValues.optionalParams.wazuhPassword} + password={registerAgentFormValues.optionalParams.password} /> = { +const linuxDefinition: IOSDefinition = { name: 'LINUX', options: [ { @@ -110,8 +111,7 @@ const linuxDefinition: IOSDefinition = { }, ], }; - -const windowsDefinition: IOSDefinition = { +const windowsDefinition: IOSDefinition = { name: 'WINDOWS', options: [ { @@ -123,8 +123,7 @@ const windowsDefinition: IOSDefinition = { }, ], }; - -const macDefinition: IOSDefinition = { +const macDefinition: IOSDefinition = { name: 'macOS', options: [ { @@ -150,65 +149,79 @@ export const osCommandsDefinitions = [ macDefinition, ]; -/////////////////////////////////////////////////////////////////// -/// Optional parameters definitions -/////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////// +// / Optional parameters definitions +// ///////////////////////////////////////////////////////////////// -export const optionalParamsDefinitions: tOptionalParams = { +export const optionalParamsDefinitions: tOptionalParams = { serverAddress: { - property: 'WAZUH_MANAGER', - getParamCommand: (props, selectedOS) => { + property: '--url', + getParamCommand: props => { const { property, value } = props; - return value !== '' ? `${property}='${value}'` : ''; + + return value === '' ? '' : `${property} '${value}'`; }, }, - agentName: { - property: 'WAZUH_AGENT_NAME', - getParamCommand: (props, selectedOS) => { + username: { + property: '--user', + getParamCommand: props => { const { property, value } = props; - return value !== '' ? `${property}='${value}'` : ''; + + return value === '' ? '' : `${property} '${value}'`; }, }, - agentGroups: { - property: 'WAZUH_AGENT_GROUP', + password: { + property: '--password', getParamCommand: (props, selectedOS) => { const { property, value } = props; - let parsedValue = value; - if (Array.isArray(value)) { - parsedValue = value.length > 0 ? value.join(',') : ''; + + if (selectedOS) { + const osName = selectedOS.name.toLocaleLowerCase(); + + switch (osName) { + case 'linux': { + return `${property} $'${scapeSpecialCharsForLinux(value)}'`; + } + + case 'macos': { + return `${property} '${scapeSpecialCharsForMacOS(value)}'`; + } + + case 'windows': { + return `${property} '${scapeSpecialCharsForWindows(value)}'`; + } + + default: { + return `${property} '${value}'`; + } + } } - return parsedValue ? `${property}='${parsedValue}'` : ''; + + return value === '' ? '' : `${property} '${value}'`; }, }, - protocol: { - property: 'WAZUH_PROTOCOL', - getParamCommand: (props, selectedOS) => { + verificationMode: { + property: '--verification-mode', + getParamCommand: props => { const { property, value } = props; - return value !== '' ? `${property}='${value}'` : ''; + + return value === '' ? '' : `${property} '${value}'`; }, }, - wazuhPassword: { - property: 'WAZUH_REGISTRATION_PASSWORD', - getParamCommand: (props, selectedOS) => { + agentName: { + property: '--name', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, + enrollmentKey: { + property: '--key', + getParamCommand: props => { const { property, value } = props; - if (!value) { - return ''; - } - if (selectedOS) { - let osName = selectedOS.name.toLocaleLowerCase(); - switch (osName) { - case 'linux': - return `${property}=$'${scapeSpecialCharsForLinux(value)}'`; - case 'macos': - return `${property}='${scapeSpecialCharsForMacOS(value)}'`; - case 'windows': - return `${property}='${scapeSpecialCharsForWindows(value)}'`; - default: - return `${property}=$'${value}'`; - } - } - return value !== '' ? `${property}=$'${value}'` : ''; + return value === '' ? '' : `${property} '${value}'`; }, }, }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md index 72b92edde1..9d1d558684 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md @@ -64,14 +64,15 @@ interface IMacOSTypes { architecture: '32/64'; } -type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options +type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options -type tOptionalParameters = +type TOptionalParameters = | 'server_address' | 'agent_name' - | 'agent_group' - | 'protocol' - | 'wazuh_password'; + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; export interface IOSDefinition< OS extends IOperationSystem, @@ -100,7 +101,7 @@ This configuration will define the different OS that we want to support and the ```ts -const osDefinitions: IOSDefinition[] = [{ +const osDefinitions: IOSDefinition[] = [{ name: 'linux', options: [ { @@ -150,9 +151,10 @@ The optional parameters are a set of parameters that will be added to the regist export type tOptionalParamsName = | 'server_address' | 'agent_name' - | 'protocol' - | 'agent_group' - | 'wazuh_password'; + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; export type tOptionalParams = { [key in tOptionalParamsName]: { @@ -168,9 +170,9 @@ This configuration will define the different optional parameters that we want to ```ts -export const optionalParameters: tOptionalParams = { +export const optionalParameters: tOptionalParams = { server_address: { - property: 'WAZUH_MANAGER', + property: '--url', getParamCommand: props => 'returns the optional param command' } }, diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts index ae9af6d24e..a8fbd90a35 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -1,13 +1,14 @@ -import { CommandGenerator } from './command-generator'; +import { IOSDefinition, IOptionalParameters, tOptionalParams } from '../types'; import { - IOSDefinition, - IOptionalParameters, - tOptionalParams, -} from '../types'; -import { DuplicatedOSException, DuplicatedOSOptionException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; + DuplicatedOSException, + DuplicatedOSOptionException, + NoOSSelectedException, + WazuhVersionUndefinedException, +} from '../exceptions'; +import { CommandGenerator } from './command-generator'; -const mockedCommandValue = 'mocked command'; -const mockedCommandsResponse = jest.fn().mockReturnValue(mockedCommandValue); +const MOCKED_COMMAND_VALUE = 'mocked command'; +const mockedCommandsResponse = jest.fn().mockReturnValue(MOCKED_COMMAND_VALUE); // Defined OS combinations export interface ILinuxOSTypes { @@ -24,14 +25,19 @@ export interface IMacOSTypes { architecture: '32/64'; } -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; +export type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // Defined Optional Parameters +export type TOptionalParameters = + | 'server_address' + | 'agent_name' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; -export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; - -const osDefinitions: IOSDefinition[] = [ +const osDefinitions: IOSDefinition[] = [ { name: 'linux', options: [ @@ -50,36 +56,39 @@ const osDefinitions: IOSDefinition[] = [ ], }, ]; - -const optionalParams: tOptionalParams = { +const optionalParams: tOptionalParams = { server_address: { - property: 'WAZUH_MANAGER', - getParamCommand: props => `${props.property}=${props.value}`, + property: '--url', + getParamCommand: props => `${props.property} '${props.value}'`, }, agent_name: { - property: 'WAZUH_AGENT_NAME', - getParamCommand: props => `${props.property}=${props.value}`, + property: '--name', + getParamCommand: props => `${props.property} '${props.value}'`, }, - protocol: { - property: 'WAZUH_MANAGER_PROTOCOL', - getParamCommand: props => `${props.property}=${props.value}`, + username: { + property: '--user', + getParamCommand: props => `${props.property} '${props.value}'`, }, - agent_group: { - property: 'WAZUH_AGENT_GROUP', - getParamCommand: props => `${props.property}=${props.value}`, + password: { + property: '--password', + getParamCommand: props => `${props.property} '${props.value}'`, }, - wazuh_password: { - property: 'WAZUH_PASSWORD', - getParamCommand: props => `${props.property}=${props.value}`, + verificationMode: { + property: '--verification-mode', + getParamCommand: props => `${props.property} '${props.value}'`, + }, + enrollmentKey: { + property: '--key', + getParamCommand: props => `${props.property} '${props.value}'`, }, }; - -const optionalValues: IOptionalParameters = { +const optionalValues: IOptionalParameters = { server_address: '', agent_name: '', - protocol: '', - agent_group: '', - wazuh_password: '', + username: '', + password: '', + verificationMode: '', + enrollmentKey: '', }; describe('Command Generator', () => { @@ -87,8 +96,9 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + expect(commandGenerator).toBeDefined(); }); @@ -96,51 +106,60 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + commandGenerator.selectOS({ name: 'linux', architecture: 'x64', }); commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getInstallCommand(); - expect(command).toBe(mockedCommandValue); + + expect(command).toBe(MOCKED_COMMAND_VALUE); }); it('should return the start command for the os selected', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + commandGenerator.selectOS({ name: 'linux', architecture: 'x64', }); commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getStartCommand(); - expect(command).toBe(mockedCommandValue); + + expect(command).toBe(MOCKED_COMMAND_VALUE); }); it('should return all the commands for the os selected', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + commandGenerator.selectOS({ name: 'linux', architecture: 'x64', }); commandGenerator.addOptionalParams(optionalValues); + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ os: 'linux', architecture: 'x64', - wazuhVersion: '4.4', - install_command: mockedCommandValue, - start_command: mockedCommandValue, - url_package: mockedCommandValue, + wazuhVersion: '5.0', + install_command: MOCKED_COMMAND_VALUE, + start_command: MOCKED_COMMAND_VALUE, + url_package: MOCKED_COMMAND_VALUE, optionals: {}, }); }); @@ -149,32 +168,35 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); - - const selectedOs: tOperatingSystem = { + const selectedOs: TOperatingSystem = { name: 'linux', architecture: 'x64', }; + commandGenerator.selectOS(selectedOs); const optionalValues = { server_address: '10.10.10.121', agent_name: 'agent1', - protocol: 'tcp', - agent_group: '', - wazuh_password: '123456', + username: 'user', + password: '1234', + verificationMode: 'none', + enrollmentKey: '00000000000000000000000000000000', }; + commandGenerator.addOptionalParams(optionalValues); const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ os: selectedOs.name, architecture: selectedOs.architecture, - wazuhVersion: '4.4', - install_command: mockedCommandValue, - start_command: mockedCommandValue, - url_package: mockedCommandValue, + wazuhVersion: '5.0', + install_command: MOCKED_COMMAND_VALUE, + start_command: MOCKED_COMMAND_VALUE, + url_package: MOCKED_COMMAND_VALUE, optionals: { server_address: optionalParams.server_address.getParamCommand({ property: optionalParams.server_address.property, @@ -186,22 +208,35 @@ describe('Command Generator', () => { value: optionalValues.agent_name, name: 'agent_name', }), - protocol: optionalParams.protocol.getParamCommand({ - property: optionalParams.protocol.property, - value: optionalValues.protocol, - name: 'protocol', + username: optionalParams.username.getParamCommand({ + property: optionalParams.username.property, + value: optionalValues.username, + name: 'username', }), - wazuh_password: optionalParams.wazuh_password.getParamCommand({ - property: optionalParams.wazuh_password.property, - value: optionalValues.wazuh_password, - name: 'wazuh_password', + password: optionalParams.password.getParamCommand({ + property: optionalParams.password.property, + value: optionalValues.password, + name: 'password', + }), + verificationMode: optionalParams.verificationMode.getParamCommand({ + property: optionalParams.verificationMode.property, + value: optionalValues.verificationMode, + name: 'verificationMode', + }), + enrollmentKey: optionalParams.enrollmentKey.getParamCommand({ + property: optionalParams.enrollmentKey.property, + value: optionalValues.enrollmentKey, + name: 'enrollmentKey', }), }, }); }); it('should return an ERROR when the os definitions received has a os with options duplicated', () => { - const osDefinitions: IOSDefinition[] = [ + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParameters + >[] = [ { name: 'linux', options: [ @@ -222,15 +257,19 @@ describe('Command Generator', () => { ]; try { - new CommandGenerator(osDefinitions, optionalParams, '4.4'); + new CommandGenerator(osDefinitions, optionalParams, '5.0'); } catch (error) { - if (error instanceof 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[] = [ + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParameters + >[] = [ { name: 'linux', options: [ @@ -256,10 +295,11 @@ describe('Command Generator', () => { ]; try { - new CommandGenerator(osDefinitions, optionalParams, '4.4'); + new CommandGenerator(osDefinitions, optionalParams, '5.0'); } catch (error) { - if (error instanceof Error) + if (error instanceof Error) { expect(error).toBeInstanceOf(DuplicatedOSException); + } } }); @@ -267,13 +307,15 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + try { commandGenerator.getAllCommands(); } catch (error) { - if (error instanceof Error) + if (error instanceof Error) { expect(error).toBeInstanceOf(NoOSSelectedException); + } } }); @@ -281,13 +323,15 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + try { commandGenerator.getInstallCommand(); } catch (error) { - if (error instanceof Error) + if (error instanceof Error) { expect(error).toBeInstanceOf(NoOSSelectedException); + } } }); @@ -295,13 +339,15 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); + try { commandGenerator.getStartCommand(); } catch (error) { - if (error instanceof Error) + if (error instanceof Error) { expect(error).toBeInstanceOf(NoOSSelectedException); + } } }); @@ -309,8 +355,9 @@ describe('Command Generator', () => { try { new CommandGenerator(osDefinitions, optionalParams, ''); } catch (error) { - if (error instanceof Error) + if (error instanceof Error) { expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } } }); @@ -318,20 +365,22 @@ describe('Command Generator', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); - - const selectedOs: tOperatingSystem = { + const selectedOs: TOperatingSystem = { name: 'linux', architecture: 'x64', }; + commandGenerator.selectOS(selectedOs); const optionalValues = { server_address: 'wazuh-ip', }; - commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.addOptionalParams( + optionalValues as IOptionalParameters, + ); commandGenerator.getInstallCommand(); expect(mockedCommandsResponse).toHaveBeenCalledWith( expect.objectContaining({ @@ -346,24 +395,26 @@ describe('Command Generator', () => { ); }); - it('should receives the solved optional params when the start command is called', () => { + it('should receive the solved optional params when the start command is called', () => { const commandGenerator = new CommandGenerator( osDefinitions, optionalParams, - '4.4', + '5.0', ); - - const selectedOs: tOperatingSystem = { + const selectedOs: TOperatingSystem = { name: 'linux', architecture: 'x64', }; + commandGenerator.selectOS(selectedOs); const optionalValues = { server_address: 'wazuh-ip', }; - commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.addOptionalParams( + optionalValues as IOptionalParameters, + ); commandGenerator.getStartCommand(); expect(mockedCommandsResponse).toHaveBeenCalledWith( expect.objectContaining({ @@ -377,4 +428,4 @@ describe('Command Generator', () => { }), ); }); -}); \ No newline at end of file +}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts index af6bef5b15..cf5090ecf5 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts @@ -6,39 +6,36 @@ import { } from '../types'; import { OptionalParametersManager } from './optional-parameters-manager'; -type tOptionalParamsFieldname = +type TOptionalParamsFieldname = | 'server_address' - | 'protocol' - | 'agent_group' - | 'wazuh_password' + | 'username' + | 'password' | 'another_valid_fieldname'; const returnOptionalParam = ( - props: tOptionalParamsCommandProps, + props: tOptionalParamsCommandProps, ) => { const { property, value } = props; - return `${property}=${value}`; + + return `${property} '${value}'`; }; -const optionalParametersDefinition: tOptionalParams = + +const optionalParametersDefinition: tOptionalParams = { - protocol: { - property: 'WAZUH_MANAGER_PROTOCOL', + username: { + property: '--user', getParamCommand: returnOptionalParam, }, - agent_group: { - property: 'WAZUH_AGENT_GROUP', - getParamCommand: returnOptionalParam, - }, - wazuh_password: { - property: 'WAZUH_PASSWORD', + password: { + property: '--password', getParamCommand: returnOptionalParam, }, server_address: { - property: 'WAZUH_MANAGER', + property: '--url', getParamCommand: returnOptionalParam, }, another_valid_fieldname: { - property: 'WAZUH_ANOTHER_PROPERTY', + property: '--another-field', getParamCommand: returnOptionalParam, }, }; @@ -48,15 +45,15 @@ describe('Optional Parameters Manager', () => { 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'] + ['username', 'user'], + ['password', '123456'], + ['another_valid_fieldname', 'another_valid_value'], ])( `should return the corresponding command for "%s" param with "%s" value`, (name, value) => { @@ -64,18 +61,19 @@ describe('Optional Parameters Manager', () => { optionalParametersDefinition, ); const commandParam = optParamManager.getOptionalParam({ - name: name as tOptionalParamsFieldname, + 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, + name: name as TOptionalParamsFieldname, }), ); }, @@ -86,8 +84,8 @@ describe('Optional Parameters Manager', () => { optionalParametersDefinition, ); const invalidParam = 'invalid_optional_param'; + try { - // @ts-ignore optParamManager.getOptionalParam({ name: invalidParam, value: 'value' }); } catch (error) { expect(error).toBeInstanceOf(NoOptionalParamFoundException); @@ -98,24 +96,19 @@ describe('Optional Parameters Manager', () => { const optParamManager = new OptionalParametersManager( optionalParametersDefinition, ); - const paramsValues: IOptionalParameters = { - protocol: 'TCP', - agent_group: 'group1', - wazuh_password: '123456', + const paramsValues: IOptionalParameters = { + username: 'user', + 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, + username: optionalParametersDefinition.username.getParamCommand({ + name: 'username', + property: optionalParametersDefinition.username.property, + value: paramsValues.username, }), server_address: optionalParametersDefinition.server_address.getParamCommand({ @@ -123,12 +116,11 @@ describe('Optional Parameters Manager', () => { 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, - }), + password: optionalParametersDefinition.password.getParamCommand({ + name: 'password', + property: optionalParametersDefinition.password.property, + value: paramsValues.password, + }), another_valid_fieldname: optionalParametersDefinition.another_valid_fieldname.getParamCommand({ name: 'another_valid_fieldname', @@ -136,32 +128,26 @@ describe('Optional Parameters Manager', () => { optionalParametersDefinition.another_valid_fieldname.property, value: paramsValues.another_valid_fieldname, }), - } as IOptionalParameters); + } 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', + const paramsValues: IOptionalParameters = { + username: 'user', + 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, + username: optionalParametersDefinition.username.getParamCommand({ + name: 'username', + property: optionalParametersDefinition.username.property, + value: paramsValues.username, }), server_address: optionalParametersDefinition.server_address.getParamCommand({ @@ -169,12 +155,11 @@ describe('Optional Parameters Manager', () => { 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, - }), + password: optionalParametersDefinition.password.getParamCommand({ + name: 'password', + property: optionalParametersDefinition.password.property, + value: paramsValues.password, + }), another_valid_fieldname: optionalParametersDefinition.another_valid_fieldname.getParamCommand({ name: 'another_valid_fieldname', @@ -182,7 +167,7 @@ describe('Optional Parameters Manager', () => { optionalParametersDefinition.another_valid_fieldname.property, value: paramsValues.another_valid_fieldname, }), - } as IOptionalParameters); + } as IOptionalParameters); }); it('should return ERROR when the param received is not defined in the params definition', () => { @@ -194,7 +179,6 @@ describe('Optional Parameters Manager', () => { }; try { - // @ts-ignore optParamManager.getAllOptionalParams(paramsValues); } catch (error) { expect(error).toBeInstanceOf(NoOptionalParamFoundException); @@ -206,8 +190,8 @@ describe('Optional Parameters Manager', () => { optionalParametersDefinition, ); const paramsValues = {}; - // @ts-ignore const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); }); @@ -218,12 +202,11 @@ describe('Optional Parameters Manager', () => { const paramsValues = { server_address: '', agent_name: '', - protocol: '', - agent_group: '', - wazuh_password: '', + username: '', + password: '', }; - // @ts-ignore const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); }); }); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts index 881894186a..c4fac9d107 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -10,7 +10,7 @@ import { export class OptionalParametersManager implements IOptionalParametersManager { - constructor(private optionalParamsConfig: tOptionalParams) {} + constructor(private readonly optionalParamsConfig: tOptionalParams) {} /** * Returns the command string for a given optional parameter. @@ -23,9 +23,11 @@ export class OptionalParametersManager selectedOS?: IOperationSystem, ) { const { value, name } = props; + if (!this.optionalParamsConfig[name]) { throw new NoOptionalParamFoundException(name); } + return this.optionalParamsConfig[name].getParamCommand( { value, @@ -49,14 +51,16 @@ export class OptionalParametersManager // get keys for only the optional params with values !== '' const optionalParams = Object.keys(paramsValues).filter( key => paramsValues[key as keyof typeof paramsValues] !== '', - ) as Array; + ) as (keyof typeof paramsValues)[]; 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, @@ -66,6 +70,7 @@ export class OptionalParametersManager selectedOS, ) as string; } + return resolvedOptionalParams; } } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts index a4d16fcf32..f505496b71 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts @@ -1,11 +1,14 @@ -import { getInstallCommandByOS } from './get-install-command.service'; -import { IOSCommandsDefinition, IOSDefinition, IOptionalParameters } from '../types'; +import { + IOSCommandsDefinition, + IOSDefinition, + IOptionalParameters, +} from '../types'; import { NoInstallCommandDefinitionException, NoPackageURLDefinitionException, WazuhVersionUndefinedException, } from '../exceptions'; - +import { getInstallCommandByOS } from './get-install-command.service'; export interface ILinuxOSTypes { name: 'linux'; @@ -21,25 +24,36 @@ export interface IMacOSTypes { architecture: '32/64'; } -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; - +export type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; -export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password' | 'another_optional_parameter'; +export type TOptionalParameters = + | 'server_address' + | 'agent_name' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey' + | 'another_optional_parameter'; -const validOsDefinition: IOSCommandsDefinition = { +const validOsDefinition: IOSCommandsDefinition< + TOperatingSystem, + TOptionalParameters +> = { architecture: 'x64', 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', + '5.0', 'linux', ); + expect(installCommand).toBe('install command mocked'); }); @@ -56,17 +70,20 @@ describe('getInstallCommandByOS', () => { } }); it('should return ERROR when the OS has no install command', () => { - // @ts-ignore - const osDefinition: IOSCommandsDefinition = { + const osDefinition: IOSCommandsDefinition< + TOperatingSystem, + TOptionalParameters + > = { architecture: 'x64', startCommand: props => 'start command mocked', urlPackage: props => 'https://package-url.com', }; + try { getInstallCommandByOS( osDefinition, 'https://package-url.com', - '4.4', + '5.0', 'linux', ); } catch (error) { @@ -75,38 +92,43 @@ describe('getInstallCommandByOS', () => { }); it('should return ERROR when the OS has no package url', () => { try { - getInstallCommandByOS(validOsDefinition, '', '4.4', 'linux'); + getInstallCommandByOS(validOsDefinition, '', '5.0', 'linux'); } catch (error) { expect(error).toBeInstanceOf(NoPackageURLDefinitionException); } }); it('should return install command with optional parameters', () => { - const mockedInstall = jest.fn(); - const validOsDefinition: IOSCommandsDefinition = { + const mockedInstall = jest.fn(); + const validOsDefinition: IOSCommandsDefinition< + TOperatingSystem, + TOptionalParameters + > = { architecture: 'x64', 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' + const optionalParams: IOptionalParameters = { + username: "--user 'user'", + agent_name: "--name 'agent_name'", + server_address: "--url 'server_address'", + password: "--password '1231323'", + verificationMode: "--verification-mode '1231323'", + enrollmentKey: "--key '1231323'", + another_optional_parameter: 'params value', }; getInstallCommandByOS( validOsDefinition, 'https://package-url.com', - '4.4', + '5.0', 'linux', - optionalParams + optionalParams, ); expect(mockedInstall).toBeCalledTimes(1); - expect(mockedInstall).toBeCalledWith(expect.objectContaining({ optionals: optionalParams })); - }) + expect(mockedInstall).toBeCalledWith( + expect.objectContaining({ optionals: optionalParams }), + ); + }); }); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts index 73412b9fdb..4ccb0424c4 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts @@ -1,6 +1,4 @@ -import { - NoOSOptionFoundException, -} from '../exceptions'; +import { NoOSOptionFoundException } from '../exceptions'; import { IOSDefinition } from '../types'; import { searchOSDefinitions, @@ -12,7 +10,7 @@ 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'; +type TOptionalParamsNames = 'optional1' | 'optional2'; export interface ILinuxOSTypes { name: 'linux'; @@ -28,9 +26,12 @@ export interface IMacOSTypes { architecture: '32/64'; } -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; +export type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; -const validOSDefinitions: IOSDefinition[] = [ +const validOSDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames +>[] = [ { name: 'linux', options: [ @@ -62,24 +63,26 @@ describe('search OS definitions services', () => { name: 'linux', architecture: 'x64', }); + 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', }), ).toThrow(NoOSOptionFoundException); }); - }); describe('validateOSDefinitionsDuplicated', () => { it('should not throw an error if there are no duplicated OS definitions', () => { - const osDefinitions: IOSDefinition[] = [ + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames + >[] = [ { name: 'linux', options: [ @@ -110,12 +113,14 @@ describe('search OS definitions services', () => { }); it('should throw an error if there are duplicated OS definitions', () => { - const osDefinition: IOSDefinition = { + const osDefinition: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames + > = { name: 'linux', options: [ { architecture: 'x64', - // @ts-ignore packageManager: 'aix', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -123,7 +128,10 @@ describe('search OS definitions services', () => { }, ], }; - const osDefinitions: IOSDefinition[] = [osDefinition, osDefinition]; + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames + >[] = [osDefinition, osDefinition]; expect(() => validateOSDefinitionsDuplicated(osDefinitions)).toThrow(); }); @@ -137,7 +145,10 @@ describe('search OS definitions services', () => { }); it('should throw an error if there are duplicated OS definitions with different options', () => { - const osDefinitions: IOSDefinition[] = [ + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames + >[] = [ { name: 'linux', options: [ diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md index d3ec96adc1..5ccb42f367 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md @@ -4,10 +4,10 @@ - [Advantages](#advantages) - [Usage](#usage) - [Types](#types) - - [Hook props](#hook-props) - - [Hook output](#hook-output) + - [Hook props](#hook-props) + - [Hook output](#hook-output) - [Hook with Generic types](#hook-with-generic-types) - - [Operating systems types example](#operating-systems-types-example) + - [Operating systems types example](#operating-systems-types-example) ## useRegisterAgentCommand hook @@ -19,7 +19,6 @@ This hook makes use of the `Command Generator class` to generate the commands to - 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 @@ -28,19 +27,19 @@ 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 + - OS: IOSDefinition[] + - optional parameters: tOptionalParams */ -const { +const { selectOS, setOptionalParams, installCommand, startCommand, optionalParamsParsed - } = useRegisterAgentCommands(); + } = useRegisterAgentCommands(); // select OS depending on the specified OS defined in the hook configuration selectOS({ @@ -71,13 +70,15 @@ console.log('optionals params processed', optionalParamsParsed); ### Hook props ```ts - export interface IOperationSystem { name: string; architecture: string; } -interface IUseRegisterCommandsProps { +interface IUseRegisterCommandsProps< + OS extends IOperationSystem, + Params extends string, +> { osDefinitions: IOSDefinition[]; optionalParamsDefinitions: tOptionalParams; } @@ -86,13 +87,15 @@ interface IUseRegisterCommandsProps { +interface IUseRegisterCommandsOutput< + OS extends IOperationSystem, + Params extends string, +> { selectOS: (params: OS) => void; setOptionalParams: (params: IOptionalParameters) => void; installCommand: string; @@ -141,27 +144,33 @@ export interface IMacOSTypes { architecture: '32/64'; } -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; +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'; +type TOptionalParameters = + | 'server_address' + | 'agent_name' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; +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 { +const { selectOS, setOptionalParams, installCommand, startCommand, - optionalParamsParsed - } = useRegisterAgentCommands(OSdefintions, paramsDefinitions); + 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', -}) - -```` - + name: 'linux', + architecture: 'x64', +}); +``` diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts index 20a4de7b32..faefd57b6f 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts @@ -1,12 +1,12 @@ 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'; +import { useRegisterAgentCommands } from './use-register-agent-commands'; -type tOptionalParamsNames = 'optional1' | 'optional2'; +type TOptionalParamsNames = 'optional1' | 'optional2'; export interface ILinuxOSTypes { name: 'linux'; @@ -22,9 +22,9 @@ export interface IMacOSTypes { architecture: '32/64'; } -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; +export type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; -const linuxDefinition: IOSDefinition = { +const linuxDefinition: IOSDefinition = { name: 'linux', options: [ { @@ -47,24 +47,26 @@ const linuxDefinition: IOSDefinition = { export const osCommandsDefinitions = [linuxDefinition]; -/////////////////////////////////////////////////////////////////// -/// Optional parameters definitions -/////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////// +// / Optional parameters definitions +// ///////////////////////////////////////////////////////////////// -export const optionalParamsDefinitions: tOptionalParams = +export const optionalParamsDefinitions: tOptionalParams = { optional1: { - property: 'WAZUH_MANAGER', + property: '--url', getParamCommand: props => { const { property, value } = props; - return `${property}=${value}`; + + return `${property} '${value}'`; }, }, optional2: { - property: 'WAZUH_AGENT_NAME', + property: '--name', getParamCommand: props => { const { property, value } = props; - return `${property}=${value}`; + + return `${property} '${value}'`; }, }, }; @@ -77,6 +79,7 @@ describe('useRegisterAgentCommands hook', () => { optionalParamsDefinitions: optionalParamsDefinitions, }), ); + expect(hook.result.current.installCommand).toBe(''); expect(hook.result.current.startCommand).toBe(''); }); @@ -92,6 +95,7 @@ describe('useRegisterAgentCommands hook', () => { optionalParamsDefinitions: optionalParamsDefinitions, }), ); + try { act(() => { selectOS({ @@ -100,28 +104,26 @@ describe('useRegisterAgentCommands hook', () => { }); }); } catch (error) { - if (error instanceof 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({ + 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', - ); - const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); - const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + ?.options.find(item => item.architecture === 'x64'); + const spyInstall = jest.spyOn(optionSelected, 'installCommand'); + const spyStart = jest.spyOn(optionSelected, 'startCommand'); act(() => { selectOS({ @@ -137,7 +139,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return commands empty when set optional params and OS is NOT selected', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useRegisterAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -157,18 +159,19 @@ describe('useRegisterAgentCommands hook', () => { it('should return optional params empty when optional params are not added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + 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({ + useRegisterAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -182,6 +185,7 @@ describe('useRegisterAgentCommands hook', () => { optionalParamsDefinitions.optional2, 'getParamCommand', ); + act(() => { setOptionalParams({ optional1: 'value 1', @@ -195,7 +199,7 @@ describe('useRegisterAgentCommands hook', () => { it('should update the commands when the OS is selected and optional params are added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useRegisterAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -203,11 +207,9 @@ describe('useRegisterAgentCommands hook', () => { const { selectOS, setOptionalParams } = hook.result.current; const optionSelected = osCommandsDefinitions .find(os => os.name === 'linux') - ?.options.find( - item => item.architecture === 'x64', - ); - const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); - const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + ?.options.find(item => item.architecture === 'x64'); + const spyInstall = jest.spyOn(optionSelected, 'installCommand'); + const spyStart = jest.spyOn(optionSelected, 'startCommand'); act(() => { selectOS({ diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts index f9fe6c02fc..3ae633b6eb 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts @@ -1,10 +1,10 @@ -import { tOperatingSystem } from '../config/os-commands-definitions'; +import { TOperatingSystem } from '../config/os-commands-definitions'; interface RegisterAgentData { icon: string; - title: tOperatingSystem['name']; + title: TOperatingSystem['name']; hr: boolean; - architecture: tOperatingSystem['architecture'][] + architecture: TOperatingSystem['architecture'][]; } interface CheckboxGroupComponentProps { diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts index 48eaafd660..ff63336dd8 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts @@ -18,32 +18,39 @@ let test: any; beforeEach(() => { test = { optionals: { - agentGroups: "WAZUH_AGENT_GROUP='default'", - agentName: "WAZUH_AGENT_NAME='test'", - serverAddress: "WAZUH_MANAGER='1.1.1.1'", - wazuhPassword: "WAZUH_REGISTRATION_PASSWORD=''", + serverAddress: "--url '1.1.1.1'", + username: "--username 'user'", + password: "--password 'pass'", + agentName: "--name 'test'", + verificationMode: "--verification-mode 'none'", + enrollmentKey: "--key '00000000000000000000000000000000'", }, urlPackage: 'https://test.com/agent.deb', - wazuhVersion: '4.8.0', + wazuhVersion: '5.0.0', }; }); describe('getAllOptionals', () => { it('should return empty string if optionals is falsy', () => { const result = getAllOptionals(null); + expect(result).toBe(''); }); it('should return the correct paramsText', () => { const optionals = { - serverAddress: 'localhost', - wazuhPassword: 'password', - agentGroups: 'group1', - agentName: 'agent1', - protocol: 'http', + serverAddress: "--url '1.1.1.1'", + username: "--username 'user'", + password: "--password 'pass'", + agentName: "--name 'test'", + verificationMode: "--verification-mode 'none'", + enrollmentKey: "--key '00000000000000000000000000000000'", }; const result = getAllOptionals(optionals, 'linux'); - expect(result).toBe('localhost password group1 agent1 http '); + + expect(result).toBe( + "--url '1.1.1.1' --username 'user' --password 'pass' --verification-mode 'none' --name 'test' --key '00000000000000000000000000000000'", + ); }); }); @@ -51,82 +58,176 @@ describe('getDEBAMD64InstallCommand', () => { it('should return the correct install command', () => { const props = { optionals: { - serverAddress: 'localhost', - wazuhPassword: 'password', - agentGroups: 'group1', - agentName: 'agent1', - protocol: 'http', + serverAddress: "--url 'localhost'", + username: "--username 'user'", + password: "--password 'pass'", + agentName: "--name 'agent1'", + verificationMode: "--verification-mode 'none'", + enrollmentKey: "--key '00000000000000000000000000000000'", }, urlPackage: 'https://example.com/package.deb', - wazuhVersion: '4.0.0', + wazuhVersion: '5.0.0', }; const result = getDEBAMD64InstallCommand(props); + expect(result).toBe( - 'wget https://example.com/package.deb && sudo localhost password group1 agent1 http dpkg -i ./wazuh-agent_4.0.0-1_amd64.deb', + "sudo dpkg -i ./wazuh-agent_5.0.0-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent --url 'localhost' --username 'user' --password 'pass' --verification-mode 'none' --name 'agent1' --key '00000000000000000000000000000000'", ); }); }); describe('getDEBAMD64InstallCommand', () => { it('should return the correct command', () => { - let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withAllOptionals = getDEBAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); - delete test.optionals.wazuhPassword; + delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb`; + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + const withServerAddresAndAgentGroupsOptions = getDEBAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); }); }); describe('getDEBARM64InstallCommand', () => { it('should return the correct command', () => { - let expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withAllOptionals = getDEBARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); - delete test.optionals.wazuhPassword; + delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `wget ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb`; + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + const withServerAddresAndAgentGroupsOptions = getDEBARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); }); }); describe('getRPMAMD64InstallCommand', () => { it('should return the correct command', () => { - let expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withAllOptionals = getRPMAMD64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); - delete test.optionals.wazuhPassword; + delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `curl -o wazuh-agent-4.8.0-1.x86_64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm`; + expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + const withServerAddresAndAgentGroupsOptions = getRPMAMD64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); }); }); describe('getRPMARM64InstallCommand', () => { it('should return the correct command', () => { - let expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withAllOptionals = getRPMARM64InstallCommand(test); + expect(withAllOptionals).toEqual(expected); - delete test.optionals.wazuhPassword; + delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `curl -o wazuh-agent-4.8.0-1.aarch64.rpm ${test.urlPackage} && sudo ${test.optionals.serverAddress} ${test.optionals.agentGroups} rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm`; + expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + const withServerAddresAndAgentGroupsOptions = getRPMARM64InstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); }); }); @@ -145,15 +246,36 @@ describe('getLinuxStartCommand', () => { describe('getWindowsInstallCommand', () => { it('should return the correct install command', () => { - let expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.wazuhPassword} ${test.optionals.agentGroups} ${test.optionals.agentName} `; - + let expected = `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')} /q`; const withAllOptionals = getWindowsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `Invoke-WebRequest -Uri ${test.urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${test.optionals.serverAddress} ${test.optionals.agentGroups} `; + expected = `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')} /q`; + const withServerAddresAndAgentGroupsOptions = getWindowsInstallCommand(test); @@ -164,7 +286,6 @@ describe('getWindowsInstallCommand', () => { describe('getWindowsStartCommand', () => { it('should return the correct start command', () => { const expectedCommand = 'NET START WazuhSvc'; - const result = getWindowsStartCommand({}); expect(result).toEqual(expectedCommand); @@ -176,19 +297,26 @@ describe('getWindowsStartCommand', () => { describe('getAllOptionalsMacos', () => { it('should return empty string if optionals is falsy', () => { const result = getAllOptionalsMacos(null); + expect(result).toBe(''); }); it('should return the correct paramsValueList', () => { - const optionals = { - serverAddress: 'localhost', - agentGroups: 'group1', - agentName: 'agent1', - protocol: 'http', - wazuhPassword: 'password', - }; - const result = getAllOptionalsMacos(optionals); - expect(result).toBe('localhost && group1 && agent1 && http && password'); + const result = getAllOptionalsMacos(test.optionals); + + expect(result).toBe( + [ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' '), + ); }); }); @@ -197,6 +325,7 @@ describe('transformOptionalsParamatersMacOSCommand', () => { const command = "' serverAddress && agentGroups && agentName && protocol && wazuhPassword"; const result = transformOptionalsParamatersMacOSCommand(command); + expect(result).toBe( "' && serverAddress && agentGroups && agentName && protocol && wazuhPassword", ); @@ -205,16 +334,37 @@ describe('transformOptionalsParamatersMacOSCommand', () => { describe('getMacOsInstallCommand', () => { it('should return the correct macOS installation script', () => { - let expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups} && ${test.optionals.agentName} && ${test.optionals.wazuhPassword}\" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; - + let expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withAllOptionals = getMacOsInstallCommand(test); + expect(withAllOptionals).toEqual(expected); delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `curl -so wazuh-agent.pkg ${test.urlPackage} && echo "${test.optionals.serverAddress} && ${test.optionals.agentGroups}" > /tmp/wazuh_envs && sudo installer -pkg ./wazuh-agent.pkg -target /`; + expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; const withServerAddresAndAgentGroupsOptions = getMacOsInstallCommand(test); + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); }); }); @@ -222,6 +372,7 @@ describe('getMacOsInstallCommand', () => { describe('getMacosStartCommand', () => { it('returns the correct start command for macOS', () => { const startCommand = getMacosStartCommand({}); + expect(startCommand).toEqual('sudo /Library/Ossec/bin/wazuh-control start'); }); }); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx index e5f96c8c4e..7f0ab02ce5 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx @@ -1,176 +1,190 @@ -import { tOptionalParameters } from '../core/config/os-commands-definitions'; +import { TOptionalParameters } from '../core/config/os-commands-definitions'; import { IOptionalParameters, tOSEntryInstallCommand, tOSEntryProps, } from '../core/register-commands/types'; -import { tOperatingSystem } from '../hooks/use-register-agent-commands.test'; +import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; export const getAllOptionals = ( - optionals: IOptionalParameters, - osName?: tOperatingSystem['name'], + optionals: IOptionalParameters, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + osName?: TOperatingSystem['name'], ) => { // create paramNameOrderList, which is an array of the keys of optionals add interface - const paramNameOrderList: (keyof IOptionalParameters)[] = - ['serverAddress', 'wazuhPassword', 'agentGroups', 'agentName', 'protocol']; - - if (!optionals) return ''; - let paramsText = Object.entries(paramNameOrderList).reduce( - (acc, [key, value]) => { - if (optionals[value]) { - acc += `${optionals[value]} `; - } - return acc; - }, - '', - ); + const paramNameOrderList: (keyof IOptionalParameters)[] = + [ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ]; + + if (!optionals) { + return ''; + } - return paramsText; + return Object.values(paramNameOrderList) + .map(key => optionals[key]) + .filter(Boolean) + .join(' '); }; export const getAllOptionalsMacos = ( - optionals: IOptionalParameters, + optionals: IOptionalParameters, ) => { // create paramNameOrderList, which is an array of the keys of optionals add interface - const paramNameOrderList: (keyof IOptionalParameters)[] = - ['serverAddress', 'agentGroups', 'agentName', 'protocol', 'wazuhPassword']; - - if (!optionals) return ''; - - const paramsValueList = []; - - paramNameOrderList.forEach(paramName => { - if (optionals[paramName] && optionals[paramName] !== '') { - paramsValueList.push(optionals[paramName]); - } - }); - - if (paramsValueList.length) { - return paramsValueList.join(' && '); + const paramNameOrderList: (keyof IOptionalParameters)[] = + [ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ]; + + if (!optionals) { + return ''; } - return ''; + return Object.values(paramNameOrderList) + .map(key => optionals[key]) + .filter(Boolean) + .join(' '); }; -/******* DEB *******/ +/** ***** DEB *******/ export const getDEBAMD64InstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage, wazuhVersion } = props; + const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb`; - return `wget ${urlPackage} && sudo ${ - optionals && getAllOptionals(optionals) - }dpkg -i ./${packageName}`; + + return [ + // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo dpkg -i ./${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); }; export const getDEBARM64InstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage, wazuhVersion } = props; + const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent_${wazuhVersion}-1_arm64.deb`; - return `wget ${urlPackage} && sudo ${ - optionals && getAllOptionals(optionals) - }dpkg -i ./${packageName}`; + + return [ + // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo dpkg -i ./${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); }; -/******* RPM *******/ +/** ***** RPM *******/ export const getRPMAMD64InstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage, wazuhVersion, architecture } = props; + const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm`; - return `curl -o ${packageName} ${urlPackage} && sudo ${ - optionals && getAllOptionals(optionals) - }rpm -ihv ${packageName}`; + + return [ + // `curl -o ${packageName} ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo rpm -ihv ${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); }; export const getRPMARM64InstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage, wazuhVersion, architecture } = props; + const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent-${wazuhVersion}-1.aarch64.rpm`; - return `curl -o ${packageName} ${urlPackage} && sudo ${ - optionals && getAllOptionals(optionals) - }rpm -ihv ${packageName}`; + + return [ + // `curl -o ${packageName} ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo rpm -ihv ${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); }; -/******* Linux *******/ +/** ***** Linux *******/ // Start command export const getLinuxStartCommand = ( - _props: tOSEntryProps, -) => { - return `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; -}; + _props: tOSEntryProps, +) => + `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; -/******** Windows ********/ +/** ****** Windows ********/ export const getWindowsInstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage, name } = props; - return `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent; msiexec.exe /i \$env:tmp\\wazuh-agent /q ${ - optionals && getAllOptionals(optionals, name) - }`; + const { optionals, name } = props; + + return [ + // `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent;`, // TODO: enable when the packages are publically hosted + `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${ + optionals && getAllOptionals(optionals, name) + } /q`, + ].join(' '); }; export const getWindowsStartCommand = ( - _props: tOSEntryProps, -) => { - return `NET START WazuhSvc`; -}; + _props: tOSEntryProps, +) => `NET START WazuhSvc`; -/******** MacOS ********/ +/** ****** MacOS ********/ -export const transformOptionalsParamatersMacOSCommand = (command: string) => { - return command - .replace(/\' ([a-zA-Z])/g, "' && $1") // Separate environment variables with && - .replace(/\"/g, '\\"') // Escape double quotes +export const transformOptionalsParamatersMacOSCommand = (command: string) => + command + .replaceAll(/' ([A-Za-z])/g, "' && $1") // Separate environment variables with && + .replaceAll('"', String.raw`\"`) // Escape double quotes .trim(); -}; export const getMacOsInstallCommand = ( - props: tOSEntryInstallCommand, + props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage } = props; + const { optionals } = props; + const optionalsForCommand = { ...optionals }; - let optionalsForCommand = { ...optionals }; - if (optionalsForCommand?.wazuhPassword) { + if (optionalsForCommand?.password) { /** * We use the JSON.stringify to prevent that the scaped specials characters will be removed * and maintain the format of the password The JSON.stringify maintain the password format but adds " to wrap the characters */ const scapedPasswordLength = JSON.stringify( - optionalsForCommand?.wazuhPassword, + optionalsForCommand?.password, ).length; + // We need to remove the " added by JSON.stringify - optionalsForCommand.wazuhPassword = `${JSON.stringify( - optionalsForCommand?.wazuhPassword, - ).substring(1, scapedPasswordLength - 1)}`; + optionalsForCommand.password = `${JSON.stringify( + optionalsForCommand?.password, + ).slice(1, scapedPasswordLength - 1)}`; } // Set macOS installation script with environment variables const optionalsText = - optionalsForCommand && getAllOptionalsMacos(optionalsForCommand); + optionalsForCommand && getAllOptionals(optionalsForCommand); const macOSInstallationOptions = transformOptionalsParamatersMacOSCommand( optionalsText || '', ); - - // If no variables are set, the echo will be empty - const macOSInstallationSetEnvVariablesScript = macOSInstallationOptions - ? `echo "${macOSInstallationOptions}" > /tmp/wazuh_envs && ` - : ``; - // Merge environment variables with installation script - const macOSInstallationScript = `curl -so wazuh-agent.pkg ${urlPackage} && ${macOSInstallationSetEnvVariablesScript}sudo installer -pkg ./wazuh-agent.pkg -target /`; + const macOSInstallationScript = [ + // `curl -so wazuh-agent.pkg ${urlPackage}`, + 'sudo installer -pkg ./wazuh-agent.pkg -target /', + `/Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${macOSInstallationOptions}`, + ].join(' && '); + return macOSInstallationScript; }; export const getMacosStartCommand = ( - _props: tOSEntryProps, -) => { - return `sudo /Library/Ossec/bin/wazuh-control start`; -}; + _props: tOSEntryProps, +) => `sudo /Library/Ossec/bin/wazuh-control start`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts deleted file mode 100644 index ecc68fb6b9..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import * as RegisterAgentService from './register-agent-services'; -import { WzRequest } from '../../../../react-services/wz-request'; -import { ServerAddressOptions } from './register-agent-services'; - -jest.mock('../../../../react-services', () => ({ - ...(jest.requireActual('../../../../react-services') as object), - WzRequest: () => ({ - apiReq: jest.fn(), - }), -})); - -describe('Register agent service', () => { - beforeEach(() => jest.clearAllMocks()); - describe('getRemoteConfiguration', () => { - it('should return secure connection = TRUE when have connection secure', async () => { - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['UDP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['UDP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - const nodeName = 'example-node'; - const res = await RegisterAgentService.getRemoteConfiguration( - nodeName, - false, - ); - expect(res.name).toBe(nodeName); - expect(res.haveSecureConnection).toBe(true); - }); - - it('should return secure connection = FALSE available when dont have connection secure', async () => { - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['UDP', 'TCP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - const nodeName = 'example-node'; - const res = await RegisterAgentService.getRemoteConfiguration( - nodeName, - false, - ); - expect(res.name).toBe(nodeName); - expect(res.haveSecureConnection).toBe(false); - }); - - it('should return protocols UDP when is the only connection protocol available', async () => { - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['UDP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['UDP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - const nodeName = 'example-node'; - const res = await RegisterAgentService.getRemoteConfiguration( - nodeName, - false, - ); - expect(res.name).toBe(nodeName); - expect(res.isUdp).toEqual(true); - }); - - it('should return protocols TCP when is the only connection protocol available', async () => { - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['TCP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['TCP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - const nodeName = 'example-node'; - const res = await RegisterAgentService.getRemoteConfiguration( - nodeName, - false, - ); - expect(res.name).toBe(nodeName); - expect(res.isUdp).toEqual(false); - }); - - it('should return is not UDP when have UDP and TCP protocols available', async () => { - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['TCP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['UDP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - const nodeName = 'example-node'; - const res = await RegisterAgentService.getRemoteConfiguration( - nodeName, - false, - ); - expect(res.name).toBe(nodeName); - expect(res.isUdp).toEqual(false); - }); - }); - - describe('getConnectionConfig', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - - it('should return IS NOT UDP when the server address is typed manually (custom)', async () => { - const nodeSelected: ServerAddressOptions = { - label: 'node-selected', - value: 'node-selected', - nodetype: 'master', - }; - - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['UDP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['UDP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - - const config = await RegisterAgentService.getConnectionConfig( - nodeSelected, - 'default-dns-address', - ); - expect(config.udpProtocol).toEqual(false); - expect(config.serverAddress).toBe('default-dns-address'); - }); - - it('should return IS NOT UDP when the server address is received like default server address dns (custom)', async () => { - const nodeSelected: ServerAddressOptions = { - label: 'node-selected', - value: 'node-selected', - nodetype: 'master', - }; - - const remoteWithSecureAndNoSecure = [ - { - connection: 'syslog', - ipv6: 'no', - protocol: ['UDP'], - port: '514', - 'allowed-ips': ['0.0.0.0/0'], - }, - { - connection: 'secure', - ipv6: 'no', - protocol: ['UDP'], - port: '1514', - queue_size: '131072', - }, - ]; - const mockedResponse = { - data: { - data: { - affected_items: [ - { - remote: remoteWithSecureAndNoSecure, - }, - ], - }, - }, - }; - WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse); - - const config = await RegisterAgentService.getConnectionConfig( - nodeSelected, - 'custom-server-address', - ); - expect(config.udpProtocol).toEqual(false); - }); - }); -}); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx index c9b14664e5..a89d470aef 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx @@ -1,187 +1,16 @@ import { UseFormReturn } from '../components/form/types'; import { - tOperatingSystem, - tOptionalParameters, + TOperatingSystem, + TOptionalParameters, } from '../core/config/os-commands-definitions'; import { RegisterAgentData } from '../interfaces/types'; -type Protocol = 'TCP' | 'UDP'; - -interface RemoteItem { - connection: 'syslog' | 'secure'; - ipv6: 'yes' | 'no'; - protocol: Protocol[]; - allowed_ips?: string[]; - queue_size?: string; -} - -interface RemoteConfig { - name: string; - isUdp: boolean | null; - haveSecureConnection: boolean | null; -} - export interface ServerAddressOptions { label: string; value: string; nodetype: string; } -/** - * Get the cluster status - */ -export const clusterStatusResponse = async (): Promise => { - const clusterStatus = await getWazuhCore().http.server.request( - 'GET', - '/cluster/status', - {}, - ); - - if ( - clusterStatus.data.data.enabled === 'yes' && - clusterStatus.data.data.running === 'yes' - ) { - // Cluster mode - return true; - } else { - // Manager mode - return false; - } -}; - -/** - * Get the remote configuration from api - */ -async function getRemoteConfiguration( - nodeName: string, - clusterStatus: boolean, -): Promise { - const config: RemoteConfig = { - name: nodeName, - isUdp: false, - haveSecureConnection: false, - }; - - try { - let result; - - if (clusterStatus) { - result = await getWazuhCore().http.server.request( - 'GET', - `/cluster/${nodeName}/configuration/request/remote`, - {}, - ); - } else { - result = await getWazuhCore().http.server.request( - 'GET', - '/manager/configuration/request/remote', - {}, - ); - } - - const items = result?.data?.data?.affected_items || []; - const remote = items[0]?.remote; - - if (remote) { - const remoteFiltered = remote.filter( - (item: RemoteItem) => item.connection === 'secure', - ); - - remoteFiltered.length > 0 - ? (config.haveSecureConnection = true) - : (config.haveSecureConnection = false); - - let protocolsAvailable: Protocol[] = []; - - remote.forEach((item: RemoteItem) => { - // get all protocols available - for (const protocol of item.protocol) { - protocolsAvailable = protocolsAvailable.concat(protocol); - } - }); - - config.isUdp = - getRemoteProtocol(protocolsAvailable) === 'UDP' ? true : false; - } - - return config; - } catch { - return config; - } -} - -/** - * Get the manager/cluster auth configuration from Wazuh API - * @param node - * @returns - */ -async function getAuthConfiguration(node: string, clusterStatus: boolean) { - const authConfigUrl = clusterStatus - ? `/cluster/${node}/configuration/auth/auth` - : '/manager/configuration/auth/auth'; - const result = await getWazuhCore().http.server.request( - 'GET', - authConfigUrl, - {}, - ); - const auth = result?.data?.data?.affected_items?.[0]; - - return auth; -} - -/** - * Get the remote protocol available from list of protocols - * @param protocols - */ -function getRemoteProtocol(protocols: Protocol[]) { - if (protocols.length === 1) { - return protocols[0]; - } else { - return protocols.includes('TCP') ? 'TCP' : 'UDP'; - } -} - -/** - * Get the remote configuration from nodes registered in the cluster and decide the protocol to setting up in deploy agent param - * @param nodeSelected - * @param defaultServerAddress - */ -async function getConnectionConfig( - nodeSelected: ServerAddressOptions, - defaultServerAddress?: string, -) { - const nodeName = nodeSelected?.label; - const nodeIp = nodeSelected?.value; - - if (defaultServerAddress) { - return { - serverAddress: defaultServerAddress, - udpProtocol: false, - connectionSecure: true, - }; - } else { - if (nodeSelected.nodetype === 'custom') { - return { - serverAddress: nodeName, - udpProtocol: false, - connectionSecure: true, - }; - } else { - const clusterStatus = await clusterStatusResponse(); - const remoteConfig = await getRemoteConfiguration( - nodeName, - clusterStatus, - ); - - return { - serverAddress: nodeIp, - udpProtocol: remoteConfig.isUdp, - connectionSecure: remoteConfig.haveSecureConnection, - }; - } - } -} - interface NodeItem { name: string; ip: string; @@ -196,31 +25,6 @@ interface NodeResponse { }; } -/** - * Get the list of the cluster nodes and parse it into a list of options - */ -export const getNodeIPs = async (): Promise => - await getWazuhCore().http.server.request('GET', '/cluster/nodes', {}); - -/** - * Get the list of the manager and parse it into a list of options - */ -export const getManagerNode = async (): Promise => { - const managerNode = await getWazuhCore().http.server.request( - 'GET', - '/manager/api/config', - {}, - ); - - return ( - managerNode?.data?.data?.affected_items?.map(item => ({ - label: item.node_name, - value: item.node_api_config.host, - nodetype: 'master', - })) || [] - ); -}; - /** * Parse the nodes list from the API response to a format that can be used by the EuiComboBox * @param nodes @@ -234,75 +38,6 @@ export const parseNodesInOptions = ( nodetype: item.type, })); -/** - * Get the list of the cluster nodes from API and parse it into a list of options - */ -export const fetchClusterNodesOptions = async (): Promise< - ServerAddressOptions[] -> => { - const clusterStatus = await clusterStatusResponse(); - - if (clusterStatus) { - // Cluster mode - // Get the cluster nodes - const nodes = await getNodeIPs(); - - return parseNodesInOptions(nodes); - } else { - // Manager mode - // Get the manager node - return await getManagerNode(); - } -}; - -/** - * Get the master node data from the list of cluster nodes - * @param nodeIps - */ -export const getMasterNode = ( - nodeIps: ServerAddressOptions[], -): ServerAddressOptions[] => - nodeIps.filter(nodeIp => nodeIp.nodetype === 'master'); - -/** - * Get the remote and the auth configuration from manager - * This function get the config from manager mode or cluster mode - */ -export const getMasterConfiguration = async () => { - const nodes = await fetchClusterNodesOptions(); - const masterNode = getMasterNode(nodes); - const clusterStatus = await clusterStatusResponse(); - const remote = await getRemoteConfiguration( - masterNode[0].label, - clusterStatus, - ); - const auth = await getAuthConfiguration(masterNode[0].label, clusterStatus); - - return { - remote, - auth, - }; -}; - -export { getConnectionConfig, getRemoteConfiguration }; - -export const getGroups = async () => { - try { - const result = await getWazuhCore().http.server.request( - 'GET', - '/groups', - {}, - ); - - return result.data.data.affected_items.map(item => ({ - label: item.name, - id: item.name, - })); - } catch (error) { - throw new Error(error); - } -}; - export const getRegisterAgentFormValues = (form: UseFormReturn) => // return the values form the formFields and the value property Object.keys(form.fields).map(key => ({ @@ -312,11 +47,11 @@ export const getRegisterAgentFormValues = (form: UseFormReturn) => export interface IParseRegisterFormValues { operatingSystem: { - name: tOperatingSystem['name'] | ''; - architecture: tOperatingSystem['architecture'] | ''; + name: TOperatingSystem['name'] | ''; + architecture: TOperatingSystem['architecture'] | ''; }; - // optionalParams is an object that their key is defined in tOptionalParameters and value must be string - optionalParams: Record; + // optionalParams is an object that their key is defined in TOptionalParameters and value must be string + optionalParams: Record; } export const parseRegisterAgentFormValues = ( @@ -349,13 +84,7 @@ export const parseRegisterAgentFormValues = ( }; } } else { - if (field.name === 'agentGroups') { - parsedForm.optionalParams[field.name as any] = field.value.map( - item => item.id, - ); - } else { - parsedForm.optionalParams[field.name as any] = field.value; - } + parsedForm.optionalParams[field.name as any] = field.value; } } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx index 88e69f375e..b77189ba57 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx @@ -36,11 +36,11 @@ const fieldsAreEmpty = ( throw new Error('fields to check are not defined in formFields'); } - const notEmpty = fieldsToCheck.some( - key => formFields[key]?.value?.length > 0, + const someFieldisEmptyValue = fieldsToCheck.some( + key => formFields[key]?.value?.length === 0, ); - return !notEmpty; + return someFieldisEmptyValue; }; const anyFieldIsComplete = ( @@ -71,36 +71,44 @@ export const showCommandsSections = ( formFields: UseFormReturn['fields'], ): boolean => { if ( - !formFields.operatingSystemSelection.value || - formFields.serverAddress.value === '' || - formFields.serverAddress.error + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + [ + 'operatingSystemSelection', + 'serverAddress', + 'username', + 'password', + 'agentName', + 'verificationMode', + 'enrollmentKey', + ], + formFields, + ) ) { return false; - } else if ( - formFields.serverAddress.value === '' && - formFields.agentName.value === '' - ) { - return true; - } else if (fieldsHaveErrors(['agentGroups', 'agentName'], formFields)) { - return false; - } else { - return true; } + + return true; }; /** ****** Form Steps status getters ********/ -export type tFormStepsStatus = EuiStepStatus | 'current' | 'disabled' | ''; +export type TFormStepsStatus = EuiStepStatus | 'current' | 'disabled' | ''; export const getOSSelectorStepStatus = ( formFields: UseFormReturn['fields'], -): tFormStepsStatus => +): TFormStepsStatus => formFields.operatingSystemSelection.value ? 'complete' : 'current'; export const getAgentCommandsStepStatus = ( formFields: UseFormReturn['fields'], wasCopied: boolean, -): tFormStepsStatus | 'disabled' => { +): TFormStepsStatus | 'disabled' => { if (!showCommandsSections(formFields)) { return 'disabled'; } else if (showCommandsSections(formFields) && wasCopied) { @@ -112,15 +120,31 @@ export const getAgentCommandsStepStatus = ( export const getServerAddressStepStatus = ( formFields: UseFormReturn['fields'], -): tFormStepsStatus => { +): TFormStepsStatus => { if ( - !formFields.operatingSystemSelection.value || - formFields.operatingSystemSelection.error + fieldsAreEmpty( + // required fields + ['operatingSystemSelection'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection'], + formFields, + ) ) { return 'disabled'; } else if ( - !formFields.serverAddress.value || - formFields.serverAddress.error + fieldsAreEmpty( + // required fields + ['serverAddress'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['serverAddress'], + formFields, + ) ) { return 'current'; } else { @@ -128,75 +152,114 @@ export const getServerAddressStepStatus = ( } }; -export const getOptionalParameterStepStatus = ( +export const getServerCredentialsStepStatus = ( formFields: UseFormReturn['fields'], - installCommandWasCopied: boolean, -): tFormStepsStatus => { - // when previous step are not complete +): TFormStepsStatus => { if ( - !formFields.operatingSystemSelection.value || - formFields.operatingSystemSelection.error || - !formFields.serverAddress.value || - formFields.serverAddress.error + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection', 'serverAddress'], + formFields, + ) ) { return 'disabled'; } else if ( - installCommandWasCopied || - anyFieldIsComplete(['agentName', 'agentGroups'], formFields) + fieldsAreEmpty( + // required fields + ['username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['username', 'password'], + formFields, + ) ) { - return 'complete'; - } else { return 'current'; + } else { + return 'complete'; } }; -export const getPasswordStepStatus = ( +export const getOptionalParameterStepStatus = ( formFields: UseFormReturn['fields'], -): tFormStepsStatus => { + installCommandWasCopied: boolean, +): TFormStepsStatus => { + // when previous step are not complete if ( - !formFields.operatingSystemSelection.value || - formFields.operatingSystemSelection.error || - !formFields.serverAddress.value || - formFields.serverAddress.error + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) ) { return 'disabled'; - } else { + } else if ( + installCommandWasCopied || + anyFieldIsComplete( + ['agentName', 'verificationMode', 'enrollmentKey'], + formFields, + ) + ) { return 'complete'; + } else { + return 'current'; } }; -export enum tFormStepsLabel { +export enum FORM_STEPS_LABELS { + // eslint-disable-next-line @typescript-eslint/naming-convention operatingSystemSelection = 'operating system', + // eslint-disable-next-line @typescript-eslint/naming-convention serverAddress = 'server address', } export const getIncompleteSteps = ( formFields: UseFormReturn['fields'], -): tFormStepsLabel[] => { +): FORM_STEPS_LABELS[] => { const steps: FormStepsDependencies = { operatingSystemSelection: ['operatingSystemSelection'], serverAddress: ['serverAddress'], + username: ['username'], + password: ['password'], }; const statusManager = new RegisterAgentFormStatusManager(formFields, steps); // replace fields array using label names return statusManager .getIncompleteSteps() - .map(field => tFormStepsLabel[field] || field); + .map(field => FORM_STEPS_LABELS[field] || field); }; -export enum tFormFieldsLabel { +export enum FORM_FIELDS_LABEL { + // eslint-disable-next-line @typescript-eslint/naming-convention agentName = 'agent name', - agentGroups = 'agent groups', + // eslint-disable-next-line @typescript-eslint/naming-convention + username = 'username', + // eslint-disable-next-line @typescript-eslint/naming-convention + password = 'password', + // eslint-disable-next-line @typescript-eslint/naming-convention + enrollmentKey = 'enrollment key', + // eslint-disable-next-line @typescript-eslint/naming-convention serverAddress = 'server address', } export const getInvalidFields = ( formFields: UseFormReturn['fields'], -): tFormFieldsLabel[] => { +): FORM_FIELDS_LABEL[] => { const statusManager = new RegisterAgentFormStatusManager(formFields); return statusManager .getInvalidFields() - .map(field => tFormFieldsLabel[field] || field); + .map(field => FORM_FIELDS_LABEL[field] || field); }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts index a42e35afb9..9ba9d4100d 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts @@ -1,68 +1,82 @@ -import { tOperatingSystem } from "../hooks/use-register-agent-commands.test"; +import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; export const scapeSpecialCharsForLinux = (password: string) => { - let passwordScaped = password; + const passwordScaped = password; // the " characters is scaped by default in the password retrieved from the API const specialCharsList = ["'"]; const regex = new RegExp(`([${specialCharsList.join('')}])`, 'g'); + // the single quote is escaped first, and then any unescaped backslashes are escaped - return passwordScaped.replace(regex, `\'\"$&\"\'`).replace(/(? { - let passwordScaped = password; + const passwordScaped = password; + // The double quote is escaped first and then the backslash followed by a single quote - return passwordScaped.replace(/\\"/g, '\\\"').replace(/\\'/g, `\\'\"'\"'`); -} + return ( + passwordScaped + // eslint-disable-next-line no-useless-escape + .replaceAll(String.raw`\"`, '\\\"') + // eslint-disable-next-line no-useless-escape + .replaceAll(String.raw`\'`, `\\'\"'\"'`) + ); +}; export const scapeSpecialCharsForWindows = (password: string) => { - let passwordScaped = password; + const passwordScaped = password; // the " characters is scaped by default in the password retrieved from the API const specialCharsList = ["'"]; const regex = new RegExp(`([${specialCharsList.join('')}])`, 'g'); + // the single quote is escaped first, and then any unescaped backslashes are escaped - return passwordScaped.replace(regex, `\'\"$&\"\'`).replace(/(? { - let command = commandText; +export const obfuscatePasswordInCommand = ( + password: string, + commandText: string, + os: TOperatingSystem['name'], +): string => { + const command = commandText; const osName = os?.toLocaleLowerCase(); - switch (osName){ - case 'macos': - { - const regex = /WAZUH_REGISTRATION_PASSWORD=\'((?:\\'|[^']|[\"'])*)'/g; + + switch (osName) { + case 'macos': { + const regex = /--password\s'((?:\\'|[^']|["'])*)'/g; + const replacedString = command.replaceAll(regex, (match, capturedGroup) => + match.replace(capturedGroup, '*'.repeat(capturedGroup.length)), + ); + + return replacedString; + } + + case 'windows': { const replacedString = command.replace( - regex, - (match, capturedGroup) => { - return match.replace( - capturedGroup, - '*'.repeat(capturedGroup.length), - ); - } + `--password '${scapeSpecialCharsForWindows(password)}'`, + () => + `--password '${'*'.repeat(scapeSpecialCharsForWindows(password).length)}'`, ); - return replacedString; + + return replacedString; } - case 'windows': - { - const replacedString = command.replace( - `WAZUH_REGISTRATION_PASSWORD=\'${scapeSpecialCharsForWindows(password)}'`, - () => { - return `WAZUH_REGISTRATION_PASSWORD=\'${'*'.repeat(scapeSpecialCharsForWindows(password).length)}\'`; - } - ); - return replacedString; - } - case 'linux': - { + + case 'linux': { const replacedString = command.replace( - `WAZUH_REGISTRATION_PASSWORD=\$'${scapeSpecialCharsForLinux(password)}'`, - () => { - return `WAZUH_REGISTRATION_PASSWORD=\$\'${'*'.repeat(scapeSpecialCharsForLinux(password).length)}\'`; - } + `--password $'${scapeSpecialCharsForLinux(password)}'`, + () => + `--password $'${'*'.repeat(scapeSpecialCharsForLinux(password).length)}'`, ); + return replacedString; } - default: + + default: { return commandText; + } } -} \ No newline at end of file +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx index 09d87fd86f..0030188a91 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx @@ -34,7 +34,7 @@ export const SERVER_ADDRESS_TEXTS = [ { title: 'Server address', subtitle: - 'This is the address the agent uses to communicate with the server. Enter an IP address or a fully qualified domain name (FQDN).', + 'This is the address the agent uses to communicate with the server. Enter an valid URL including the port, the address can be an IP address or a fully qualified domain name (FQDN).', }, ]; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx index 1d457ec2b4..e4764d7039 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx @@ -1,15 +1,21 @@ -import { validateAgentName } from './validations'; +import { + validateAgentName, + validateServerAddress, + validateEnrollmentKey, +} from './validations'; describe('Validations', () => { test('should return undefined for an empty value', () => { const emptyValue = ''; const result = validateAgentName(emptyValue); + expect(result).toBeUndefined(); }); test('should return an error message for invalid format and length', () => { const invalidAgentName = '?'; const result = validateAgentName(invalidAgentName); + expect(result).toBe( 'The minimum length is 2 characters. The character "?" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', ); @@ -18,6 +24,7 @@ describe('Validations', () => { test('should return an error message for invalid format of 1 character', () => { const invalidAgentName = 'agent$name'; const result = validateAgentName(invalidAgentName); + expect(result).toBe( 'The character "$" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', ); @@ -26,6 +33,7 @@ describe('Validations', () => { test('should return an error message for invalid format of more than 1 character', () => { const invalidAgentName = 'agent$?name'; const result = validateAgentName(invalidAgentName); + expect(result).toBe( 'The characters "$,?" are not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"', ); @@ -34,12 +42,51 @@ describe('Validations', () => { test('should return an error message for invalid length', () => { const invalidAgentName = 'a'; const result = validateAgentName(invalidAgentName); + expect(result).toBe('The minimum length is 2 characters.'); }); test('should return an empty string for a valid agent name', () => { const validAgentName = 'agent_name'; const result = validateAgentName(validAgentName); + expect(result).toBe(''); }); + + it.each` + input | result + ${'test'} | ${'Value is not a valid URL.'} + ${'http'} | ${'Value is not a valid URL.'} + ${'http://server:'} | ${'Value is not a valid URL.'} + ${'https://server:'} | ${'Value is not a valid URL.'} + ${'http://server:55000'} | ${undefined} + ${'https://server:55000'} | ${undefined} + ${'http://server:1000000'} | ${'The port has an invalid value.'} + ${'https://server:1000000'} | ${'The port has an invalid value.'} + ${'https://example.fqdn.valid:55000'} | ${undefined} + ${'https://192.168.1.1:55000'} | ${undefined} + ${'https://2001:0db8:85a3:0000:0000:8a2e:0370:7334:55000'} | ${undefined} + ${'https://2001:db8:85a3::8a2e:370:7334:55000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6.'} + ${'https://2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12:55000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6.'} + ${'https://example.:55000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6.'} + ${'https://example.fqdn.valid:1000000'} | ${'The port has an invalid value.'} + ${'https://192.168.1.1:1000000'} | ${'The port has an invalid value.'} + ${'https://2001:0db8:85a3:0000:0000:8a2e:0370:7334:1000000'} | ${'The port has an invalid value.'} + ${'https://2001:db8:85a3::8a2e:370:7334:1000000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6. The port has an invalid value.'} + ${'https://2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12:1000000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6. The port has an invalid value.'} + ${'https://example.:1000000'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6. The port has an invalid value.'} + `('Validate server URL input: $input', ({ input, result }) => { + expect(validateServerAddress(input)).toBe(result); + }); + + it.each` + input | result + ${'test'} | ${'It should be a 32 alphanumeric characters.'} + ${'test?-dsa'} | ${'It should be a 32 alphanumeric characters.'} + ${'00000000000000000000000000000000'} | ${undefined} + ${'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'} | ${undefined} + ${'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0'} | ${undefined} + `('Validate server URL input: $input', ({ input, result }) => { + expect(validateEnrollmentKey(input)).toBe(result); + }); }); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx index 06d9aaf940..2558aaa72c 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx @@ -1,27 +1,80 @@ +const validateCharacters = (value: any) => { + const regex = /^[\w,.-]+$/i; + const invalidCharacters = [ + ...new Set([...value].filter(char => !regex.test(char))), + ]; + + if (invalidCharacters.length > 1) { + return `The characters "${invalidCharacters.join( + ',', + )}" are not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; + } else if (invalidCharacters.length === 1) { + return `The character "${invalidCharacters[0]}" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; + } + + return ''; +}; + export const validateAgentName = (value: any) => { if (value.length === 0) { - return undefined; + return; } - let invalidCharacters = validateCharacters(value); + + const invalidCharacters = validateCharacters(value); + if (value.length < 2) { return `The minimum length is 2 characters.${ invalidCharacters && ` ${invalidCharacters}` }`; } + return `${invalidCharacters}`; }; -const validateCharacters = (value: any) => { - const regex = /^[a-z0-9.\-_,]+$/i; - const invalidCharacters = [ - ...new Set(value.split('').filter(char => !regex.test(char))), - ]; - if (invalidCharacters.length > 1) { - return `The characters "${invalidCharacters.join( - ',', - )}" are not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; - } else if (invalidCharacters.length === 1) { - return `The character "${invalidCharacters[0]}" is not valid. Allowed characters are A-Z, a-z, 0-9, ".", "-", "_"`; +type TValidateOnDefinedCallback = ( + value: T | undefined, +) => string | undefined; + +function validateOnDefined(callback: TValidateOnDefinedCallback) { + return (value: T) => (value ? callback(value) : undefined); +} + +const serverAddressHostnameFQDNIPv4IPv6 = (value: string) => { + const isFQDNOrHostname = + /^(?!-)(?!.*--)[\dA-Za-záéíñóúü-]{0,62}[\dA-Za-záéíñóúü](?:\.[\dA-Za-záéíñóúü-]{0,62}[\dA-Za-záéíñóúü])*$/; + const isIPv6 = /^(?:[\dA-Fa-f]{4}:){7}[\dA-Fa-f]{4}$/; + + if ( + value.length > 255 || + (value.length > 0 && !isFQDNOrHostname.test(value) && !isIPv6.test(value)) + ) { + return 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6.'; } - return ''; }; + +const validateServerAddressPort = (value: number) => + value >= 0 && value <= 65535 ? undefined : 'The port has an invalid value.'; + +export const validateServerAddress = validateOnDefined((value: string) => { + const URLRegex = /^https?:\/\/(.+):(\d+)$/; + const matches = value.match(URLRegex); + + if (!matches) { + return 'Value is not a valid URL.'; + } + + const [_, address, port] = matches; + const addressValidation = serverAddressHostnameFQDNIPv4IPv6(address); + const portValidation = validateServerAddressPort(Number(port)); + const validationError = [addressValidation, portValidation] + .filter(Boolean) + .join(' '); + + return validationError || undefined; +}); + +export const validateEnrollmentKey = validateOnDefined((value: string) => + /^[\da-z]{32}$/i.test(value) + ? undefined + : 'It should be a 32 alphanumeric characters.', +); diff --git a/plugins/wazuh-fleet/yarn.lock b/plugins/wazuh-fleet/yarn.lock index d577aa548d..d35ad24b68 100644 --- a/plugins/wazuh-fleet/yarn.lock +++ b/plugins/wazuh-fleet/yarn.lock @@ -2,11 +2,6 @@ # yarn lockfile v1 -"@adobe/css-tools@^4.4.0": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.1.tgz#2447a230bfe072c1659e6815129c03cf170710e3" - integrity sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ== - "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -15,7 +10,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -243,13 +238,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" -"@babel/runtime@^7.12.5": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" - integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.25.9", "@babel/template@^7.3.3": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" @@ -544,56 +532,6 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@testing-library/dom@^8.5.0": - version "8.20.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" - integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.1.3" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - -"@testing-library/jest-dom@^6.6.3": - version "6.6.3" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" - integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== - dependencies: - "@adobe/css-tools" "^4.4.0" - aria-query "^5.0.0" - chalk "^3.0.0" - css.escape "^1.5.1" - dom-accessibility-api "^0.6.3" - lodash "^4.17.21" - redent "^3.0.0" - -"@testing-library/react@^13.0.0": - version "13.4.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.4.0.tgz#6a31e3bf5951615593ad984e96b9e5e2d9380966" - integrity sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw== - dependencies: - "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^8.5.0" - "@types/react-dom" "^18.0.0" - -"@testing-library/user-event@^14.5.0": - version "14.5.0" - resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.0.tgz#4036add379525b635a64bce4d727820d4ba516a7" - integrity sha512-nQRCteEZvULJJrlcGQuNhwGekz25TOUILA+sTWI9PB/vNKKivS+7K7XRTwoikw/2fmJPaM4pPKy+hLWEGg9+JA== - -"@types/@testing-library/user-event": - version "0.0.0-semantically-released" - resolved "https://codeload.github.com/testing-library/user-event/tar.gz/4be87b3452f524bcc256d43cfb891ba1f0e236d6" - -"@types/aria-query@^5.0.1": - version "5.0.4" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" - integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== - "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -661,10 +599,10 @@ expect "^29.0.0" pretty-format "^29.0.0" -"@types/md5@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.2.tgz#529bb3f8a7e9e9f621094eb76a443f585d882528" - integrity sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og== +"@types/md5@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.5.tgz#481cef0a896e3a5dcbfc5a8a8b02c05958af48a5" + integrity sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw== "@types/node@*": version "22.10.0" @@ -678,7 +616,7 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== -"@types/react-dom@^18.0.0", "@types/react-dom@^18.3.1": +"@types/react-dom@^18.3.1": version "18.3.1" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07" integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ== @@ -749,38 +687,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -aria-query@^5.0.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" - integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== - -array-buffer-byte-length@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - axios@^1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.4.tgz#4c8ded1b43683c8dd362973c393f3ede24052aa2" @@ -895,17 +806,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -926,15 +826,7 @@ caniuse-lite@^1.0.30001669: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1037,11 +929,6 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -css.escape@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" - integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== - csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -1059,53 +946,11 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1121,16 +966,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -dom-accessibility-api@^0.5.9: - version "0.5.16" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" - integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== - -dom-accessibility-api@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" - integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== - electron-to-chromium@^1.5.41: version "1.5.65" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.65.tgz#e2b9d84d31e187a847e3ccdcfb415ddd4a3d1ea7" @@ -1153,33 +988,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -1258,13 +1066,6 @@ follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -1289,11 +1090,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -1304,17 +1100,6 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -1342,53 +1127,17 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -has-bigints@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.0, hasown@^2.0.2: +hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -1418,11 +1167,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1436,61 +1180,16 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -internal-slot@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - is-core-module@^2.13.0: version "2.15.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" @@ -1498,13 +1197,6 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.2" -is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -1515,80 +1207,16 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-map@^2.0.2, is-map@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" - integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-set@^2.0.2, is-set@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" - integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== - -is-shared-array-buffer@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== - dependencies: - call-bind "^1.0.7" - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-weakmap@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" - integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== - -is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2005,7 +1633,7 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -2055,18 +1683,6 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -2074,11 +1690,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lz-string@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" - integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== - make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -2132,11 +1743,6 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2176,34 +1782,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-inspect@^1.13.1: - version "1.13.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a" - integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2296,20 +1874,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -pretty-format@^27.0.2: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -2337,54 +1901,11 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== -react-dom@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regexp.prototype.flags@^1.5.1: - version "1.5.3" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42" - integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -2416,13 +1937,6 @@ resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -2433,28 +1947,6 @@ semver@^7.5.3, semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -set-function-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2467,16 +1959,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -side-channel@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -2517,13 +1999,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -2558,13 +2033,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -2649,38 +2117,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-collection@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" - integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== - dependencies: - is-map "^2.0.3" - is-set "^2.0.3" - is-weakmap "^2.0.2" - is-weakset "^2.0.3" - -which-typed-array@^1.1.13: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" From c8edfb9a13b4f2d76ac58d85eaa528bb08b32f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Feb 2025 13:27:57 +0100 Subject: [PATCH 03/22] fix: prettier and eslint --- .../register-agent/components/form/hooks.tsx | 38 +++--- .../register-agent/components/form/index.tsx | 39 +++--- .../components/form/input-editor.tsx | 17 +++ .../components/form/input-filepicker.tsx | 29 +++++ .../components/form/input-number.tsx | 16 +++ .../components/form/input-password.tsx | 22 ++-- .../components/form/input-select.tsx | 19 +++ .../components/form/input-switch.tsx | 21 ++++ .../components/form/input-text.tsx | 19 +++ .../components/form/input-textarea.tsx | 18 +++ .../components/form/input_editor.tsx | 15 --- .../components/form/input_filepicker.tsx | 20 --- .../components/form/input_number.tsx | 15 --- .../components/form/input_select.tsx | 21 ---- .../components/form/input_switch.tsx | 16 --- .../components/form/input_text.tsx | 21 ---- .../components/form/input_text_area.tsx | 15 --- .../register-agent/components/form/types.ts | 28 ++--- .../components/group-input/group-input.tsx | 102 --------------- .../group-input.scss => inputs/styles.scss} | 2 +- .../enrollment-key-input.test.tsx.snap | 2 +- .../optionals-inputs.test.tsx.snap | 6 +- .../verification-mode-input.test.tsx.snap | 2 +- .../optionals-inputs/enrollment-key-input.tsx | 2 +- .../optionals-inputs/optionals-inputs.tsx | 6 +- .../verification-mode-input.tsx | 2 +- .../checkbox-group/checkbox-group.test.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 4 +- .../password-input.test.tsx.snap | 2 +- .../username-input.test.tsx.snap | 2 +- .../components/security/password-input.tsx | 2 +- .../components/security/username-input.tsx | 2 +- .../server-address/server-address.tsx | 4 +- .../core/config/os-commands-definitions.ts | 4 +- .../core/register-commands/README.md | 20 +-- .../command-generator.test.ts | 4 +- .../command-generator/command-generator.ts | 15 ++- .../register-commands/exceptions/index.ts | 29 ++++- .../optional-parameters-manager.test.ts | 8 +- .../optional-parameters-manager.ts | 4 +- .../services/get-install-command.service.ts | 68 ++++++---- .../services/search-os-definitions.service.ts | 43 ++++--- .../core/register-commands/types.ts | 116 +++++++++--------- .../pages/register-agent/hooks/README.md | 8 +- .../hooks/use-register-agent-commands.test.ts | 4 +- .../hooks/use-register-agent-commands.ts | 25 ++-- .../services/form-status-manager.test.tsx | 19 +-- .../services/form-status-manager.tsx | 32 +++-- .../register-agent-os-commands-services.tsx | 22 ++-- .../services/wazuh-password-service.test.ts | 83 +++++++------ 50 files changed, 515 insertions(+), 519 deletions(-) create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx create mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx delete mode 100644 plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx rename plugins/wazuh-fleet/public/application/pages/register-agent/components/{group-input/group-input.scss => inputs/styles.scss} (62%) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx index 1c2b3554d7..75a7de5909 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx @@ -23,6 +23,19 @@ enum EpluginSettingType { // eslint-disable-next-line @typescript-eslint/naming- custom = 'custom', // eslint-disable-next-line @typescript-eslint/naming-convention } +const getValueFromEventType: IgetValueFromEventType = { + [EpluginSettingType.switch]: (event: any) => event.target.checked, + [EpluginSettingType.editor]: (value: any) => value, + [EpluginSettingType.filepicker]: (value: any) => value, + [EpluginSettingType.select]: (event: any) => event.target.value, + [EpluginSettingType.text]: (event: any) => event.target.value, + [EpluginSettingType.textarea]: (event: any) => event.target.value, + [EpluginSettingType.number]: (event: any) => event.target.value, + [EpluginSettingType.password]: (event: any) => event.target.value, + custom: (event: any) => event.target.value, + default: (event: any) => event.target.value, +}; + /** * Returns the value of the event according to the type of field * When the type is not found, it returns the value defined in the default key @@ -38,19 +51,6 @@ function getValueFromEvent( return (getValueFromEventType[type] || getValueFromEventType.default)(event); } -const getValueFromEventType: IgetValueFromEventType = { - [EpluginSettingType.switch]: (event: any) => event.target.checked, - [EpluginSettingType.editor]: (value: any) => value, - [EpluginSettingType.filepicker]: (value: any) => value, - [EpluginSettingType.select]: (event: any) => event.target.value, - [EpluginSettingType.text]: (event: any) => event.target.value, - [EpluginSettingType.textarea]: (event: any) => event.target.value, - [EpluginSettingType.number]: (event: any) => event.target.value, - [EpluginSettingType.password]: (event: any) => event.target.value, - custom: (event: any) => event.target.value, - default: (event: any) => event.target.value, -}; - export function getFormFields(fields) { return Object.fromEntries( Object.entries(fields).map(([fieldKey, fieldConfiguration]) => [ @@ -93,6 +93,7 @@ export function enhanceFormFields( pathFormFieldParent = [], }, ) { + // eslint-disable-next-line unicorn/no-array-reduce return Object.entries(formFields).reduce( (accum, [fieldKey, { currentValue: value, ...restFieldState }]) => { // Define the path to fields object @@ -186,6 +187,7 @@ export function mapFormFields( }, callbackFormField, ) { + // eslint-disable-next-line unicorn/no-array-reduce return Object.entries(formState).reduce((accum, [key, value]) => { const pathField = [...pathFieldFormDefinition, key]; const fieldDefinition = get(formDefinition, pathField); @@ -243,9 +245,15 @@ export const useForm = (fields: FormConfiguration): UseFormReturn => { pathFieldFormDefinition: [], pathFormState: [], }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars ({ changed, error, value }, _, { pathFormState, fieldDefinition }) => { - changed && (result.changed[pathFormState] = value); - error && (result.errors[pathFormState] = error); + if (changed) { + result.changed[pathFormState] = value; + } + + if (error) { + result.errors[pathFormState] = error; + } }, ); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx index 08ef95f94d..fa5ab2a643 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx @@ -1,15 +1,27 @@ import React from 'react'; -import { InputFormEditor } from './input_editor'; -import { InputFormNumber } from './input_number'; -import { InputFormText } from './input_text'; -import { InputFormSelect } from './input_select'; -import { InputFormSwitch } from './input_switch'; -import { InputFormFilePicker } from './input_filepicker'; -import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { InputFormEditor } from './input-editor'; +import { InputFormNumber } from './input-number'; +import { InputFormText } from './input-text'; +import { InputFormSelect } from './input-select'; +import { InputFormSwitch } from './input-switch'; +import { InputFormFilePicker } from './input-filepicker'; +import { InputFormTextArea } from './input-textarea'; import { SettingTypes } from './types'; import { InputFormPassword } from './input-password'; +const Input = { + switch: InputFormSwitch, + editor: InputFormEditor, + filepicker: InputFormFilePicker, + number: InputFormNumber, + select: InputFormSelect, + text: InputFormText, + textarea: InputFormTextArea, + password: InputFormPassword, + custom: ({ component, ...rest }) => component(rest), +}; + export interface InputFormProps { type: SettingTypes; value: any; @@ -55,7 +67,6 @@ export const InputForm = ({ } const isInvalid = Boolean(error); - const input = ( component(rest), -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx new file mode 100644 index 0000000000..a8d49d4d1b --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormEditor = ({ + options, + value, + onChange, +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx new file mode 100644 index 0000000000..d76ea2d5a9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { EuiFilePicker } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormFilePicker = ({ + onChange, + options, + setInputRef, + key, + ...rest +}: IInputFormType) => ( + + onChange( + // File was added. + fileList?.[0] || + // File was removed. We set the initial value, so the useForm hook will not detect any change. */ + rest.initialValue, + ) + } + display='default' + fullWidth + aria-label='Upload a file' + accept={options.file.extensions.join(',')} + ref={setInputRef} + /> +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx new file mode 100644 index 0000000000..c06e8f0cf8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { EuiFieldNumber } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormNumber = ({ + options, + value, + onChange, +}: IInputFormType) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { integer, ...rest } = options?.number || {}; + + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx index eebae6e517..bad109fe35 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx @@ -9,15 +9,13 @@ export const InputFormPassword = ({ placeholder, fullWidth, options, -}: IInputFormType) => { - return ( - - ); -}; +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx new file mode 100644 index 0000000000..1c7c3e8d86 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { EuiSelect } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormSelect = ({ + options, + value, + onChange, + placeholder, + dataTestSubj, +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx new file mode 100644 index 0000000000..faf3e7d642 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { EuiSwitch } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormSwitch = ({ + options, + value, + onChange, +}: IInputFormType) => { + const checked = Object.entries(options.switch.values).find( + ([, { value: statusValue }]) => value === statusValue, + )[0]; + + return ( + + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx new file mode 100644 index 0000000000..9faf2ea577 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { EuiFieldText } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormText = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx new file mode 100644 index 0000000000..0f28418341 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { EuiTextArea } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormTextArea = ({ + value, + isInvalid, + onChange, + options, +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx deleted file mode 100644 index ab38c89f6d..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_editor.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormEditor = ({options, value, onChange}: IInputFormType) => { - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx deleted file mode 100644 index 5a46828a47..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_filepicker.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { EuiFilePicker } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormFilePicker = ({onChange, options, setInputRef, key, ...rest} : IInputFormType) => ( - onChange( - // File was added. - fileList?.[0] - // File was removed. We set the initial value, so the useForm hook will not detect any change. */ - || rest.initialValue)} - display='default' - fullWidth - aria-label='Upload a file' - accept={options.file.extensions.join(',')} - ref={setInputRef} - /> -); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx deleted file mode 100644 index f4db8e2a0b..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_number.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { EuiFieldNumber } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormNumber = ({ options, value, onChange }: IInputFormType) => { - const { integer, ...rest } = options?.number || {}; - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx deleted file mode 100644 index c610066136..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_select.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { EuiSelect } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormSelect = ({ - options, - value, - onChange, - placeholder, - dataTestSubj, -}: IInputFormType) => { - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx deleted file mode 100644 index 75a575d97f..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_switch.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { EuiSwitch } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormSwitch = ({ options, value, onChange }: IInputFormType) => { - const checked = Object.entries(options.switch.values) - .find(([, { value: statusValue }]) => value === statusValue)[0]; - - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx deleted file mode 100644 index c8e3d730d4..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { EuiFieldText } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormText = ({ - value, - isInvalid, - onChange, - placeholder, - fullWidth, -}: IInputFormType) => { - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx deleted file mode 100644 index 90cbb2d2e1..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input_text_area.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { EuiTextArea } from '@elastic/eui'; -import { IInputFormType } from './types'; - -export const InputFormTextArea = ({ value, isInvalid, onChange, options } : IInputFormType) => { - return ( - - ); -}; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts index 2d009b5b88..06ee400f84 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts @@ -20,7 +20,7 @@ export interface IInputForm { postInput?: (options: { value: any; error: string | null }) => JSX.Element; } -/// use form hook types +// / use form hook types export type SettingTypes = | 'text' @@ -49,17 +49,15 @@ interface CustomFieldConfiguration extends FieldConfiguration { interface ArrayOfFieldConfiguration extends FieldConfiguration { type: 'arrayOf'; - fields: { - [key: string]: any; // TODO: enhance this type - }; + fields: Record; } -export interface FormConfiguration { - [key: string]: - | DefaultFieldConfiguration - | CustomFieldConfiguration - | ArrayOfFieldConfiguration; -} +export type FormConfiguration = Record< + string, + | DefaultFieldConfiguration + | CustomFieldConfiguration + | ArrayOfFieldConfiguration +>; interface EnhancedField { currentValue: any; @@ -84,14 +82,12 @@ interface EnhancedCustomField extends EnhancedField { export type EnhancedFieldConfiguration = | EnhancedDefaultField | EnhancedCustomField; -export interface EnhancedFields { - [key: string]: EnhancedFieldConfiguration; -} +export type EnhancedFields = Record; export interface UseFormReturn { fields: EnhancedFields; - changed: { [key: string]: any }; - errors: { [key: string]: string }; + changed: Record; + errors: Record; undoChanges: () => void; doChanges: () => void; forEach: ( @@ -104,5 +100,5 @@ export interface UseFormReturn { pathFormState: string[]; fieldDefinition: FormConfiguration; }, - ) => { [key: string]: any }; + ) => Record; } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx deleted file mode 100644 index 8b85de28c2..0000000000 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { Fragment, useState } from 'react'; -import { - EuiComboBox, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiButtonEmpty, - EuiLink, -} from '@elastic/eui'; -import { webDocumentationLink } from '../../services/web-documentation-link'; -import './group-input.scss'; -import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; - -const popoverAgentGroup = ( - - Learn about{' '} - - Select a group. - - -); - -const GroupInput = ({ value, options, onChange }) => { - const [isPopoverAgentGroup, setIsPopoverAgentGroup] = useState(false); - const onButtonAgentGroup = () => - setIsPopoverAgentGroup(isPopoverAgentGroup => !isPopoverAgentGroup); - const closeAgentGroup = () => setIsPopoverAgentGroup(false); - - return ( - <> - - -

- Select one or more existing groups: -

-
- - - } - isOpen={isPopoverAgentGroup} - closePopover={closeAgentGroup} - anchorPosition='rightCenter' - > - {popoverAgentGroup} - - -
- { - onChange({ - target: { value: group }, - }); - }} - isDisabled={!options?.groups.length} - isClearable={true} - data-test-subj='demoComboBox' - data-testid='group-input-combobox' - /> - {!options?.groups.length && ( - <> - - - )} - - ); -}; - -export default GroupInput; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss b/plugins/wazuh-fleet/public/application/pages/register-agent/components/inputs/styles.scss similarity index 62% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss rename to plugins/wazuh-fleet/public/application/pages/register-agent/components/inputs/styles.scss index 575880c792..b77ac3057a 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/group-input/group-input.scss +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/inputs/styles.scss @@ -1,4 +1,4 @@ -.registerAgentLabels { +.enrollment-agent-form-input-label { font-weight: 700; font-size: 12px; line-height: 20px; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap index ecb1b7bc0b..5e618f6573 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Enrollment key input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Assign an enrollment key diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap index aad3dcb562..6d6e221ab0 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap @@ -34,7 +34,7 @@ exports[`Enrollment key input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >

Assign an agent name:

@@ -183,7 +183,7 @@ exports[`Enrollment key input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > SSL verification mode @@ -296,7 +296,7 @@ exports[`Enrollment key input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Assign an enrollment key diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap index 2c39e79f6e..5a9fc37e83 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Verification mode input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > SSL verification mode diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx index 868818e34a..e90826f73f 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx @@ -33,7 +33,7 @@ export const EnrollmentKeyInput = (props: { gutterSize='s' > - + { gutterSize='s' > -

Assign an agent name:

+

+ Assign an agent name: +

- + { ); const checkboxItems = screen.getAllByRole('radio'); + expect(checkboxItems).toHaveLength(data.length); expect(checkboxItems[0]).toHaveAttribute('id', 'Option 1'); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap index faa6a62f4a..c1a91f8ff4 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap @@ -42,7 +42,7 @@ exports[`credentials input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Define the server API username @@ -141,7 +141,7 @@ exports[`credentials input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Define the server API password diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap index 0d2ad30f7c..a71df1a16a 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap @@ -21,7 +21,7 @@ exports[`password input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Define the server API password diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap index e12f665e54..8cbaa9a08d 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap @@ -21,7 +21,7 @@ exports[`password input match the snapshopt 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" > Define the server API username diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx index ab27117f9d..383feaebae 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx @@ -33,7 +33,7 @@ export const PasswordInput = (props: { gutterSize='s' > - + - + { gutterSize='s' > - + Assign a server address diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts index 5a71d15a32..2ba0dc2165 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts @@ -14,7 +14,7 @@ import { scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows, } from '../../services/wazuh-password-service'; -import { IOSDefinition, tOptionalParams } from '../register-commands/types'; +import { IOSDefinition, TOptionalParams } from '../register-commands/types'; // Defined OS combinations @@ -153,7 +153,7 @@ export const osCommandsDefinitions = [ // / Optional parameters definitions // ///////////////////////////////////////////////////////////////// -export const optionalParamsDefinitions: tOptionalParams = { +export const optionalParamsDefinitions: TOptionalParams = { serverAddress: { property: '--url', getParamCommand: props => { diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md index 9d1d558684..0fc4678a00 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md @@ -36,10 +36,10 @@ The OS definitions are a set of parameters that will be used to generate the reg export interface IOptionsParamConfig { property: string; - getParamCommand: (props: tOptionalParamsCommandProps) => string; + getParamCommand: (props: TOptionalParamsCommandProps) => string; } -export type tOptionalParams = { +export type TOptionalParams = { [key in T]: IOptionsParamConfig; }; @@ -87,11 +87,11 @@ export interface IOSCommandsDefinition< Param extends string, > { architecture: OS['architecture']; - urlPackage: (props: tOSEntryProps) => string; + urlPackage: (props: TOSEntryProps) => string; installCommand: ( - props: tOSEntryProps & { urlPackage: string }, + props: TOSEntryProps & { urlPackage: string }, ) => string; - startCommand: (props: tOSEntryProps) => string; + startCommand: (props: TOSEntryProps) => string; } ``` @@ -148,7 +148,7 @@ Another validations will be provided in development time and will be provided by 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 = +export type TOptionalParamsName = | 'server_address' | 'agent_name' | 'username' @@ -156,8 +156,8 @@ export type tOptionalParamsName = | 'verificationMode' | 'enrollmentKey'; -export type tOptionalParams = { - [key in tOptionalParamsName]: { +export type TOptionalParams = { + [key in TOptionalParamsName]: { property: string; getParamCommand: (props) => string; }; @@ -170,7 +170,7 @@ This configuration will define the different optional parameters that we want to ```ts -export const optionalParameters: tOptionalParams = { +export const optionalParameters: TOptionalParams = { server_address: { property: '--url', getParamCommand: props => 'returns the optional param command' @@ -189,7 +189,7 @@ export const optionalParameters: tOptionalParams = { 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. +- 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. diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts index a8fbd90a35..55aad0899b 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -1,4 +1,4 @@ -import { IOSDefinition, IOptionalParameters, tOptionalParams } from '../types'; +import { IOSDefinition, IOptionalParameters, TOptionalParams } from '../types'; import { DuplicatedOSException, DuplicatedOSOptionException, @@ -56,7 +56,7 @@ const osDefinitions: IOSDefinition[] = [ ], }, ]; -const optionalParams: tOptionalParams = { +const optionalParams: TOptionalParams = { server_address: { property: '--url', getParamCommand: props => `${props.property} '${props.value}'`, diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts index 8194b88970..2c2b029516 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts @@ -5,9 +5,9 @@ import { IOperationSystem, IOptionalParameters, IOptionalParametersManager, - tOptionalParams, + TOptionalParams, + ICommandGenerator, } from '../types'; -import { ICommandGenerator } from '../types'; import { searchOSDefinitions, validateOSDefinitionHasDuplicatedOptions, @@ -30,17 +30,20 @@ export class CommandGenerator< osDefinitionSelected: IOSCommandsDefinition | null = null; optionalsManager: IOptionalParametersManager; protected optionals: IOptionalParameters | object = {}; + constructor( public osDefinitions: IOSDefinition[], - protected optionalParams: tOptionalParams, + protected optionalParams: TOptionalParams, public wazuhVersion: string = version, ) { // validate os definitions received validateOSDefinitionsDuplicated(this.osDefinitions); validateOSDefinitionHasDuplicatedOptions(this.osDefinitions); - if (wazuhVersion == '') { + + if (wazuhVersion === '') { throw new WazuhVersionUndefinedException(); } + this.optionalsManager = new OptionalParametersManager(optionalParams); } @@ -83,9 +86,11 @@ export class CommandGenerator< */ private checkIfOSisValid(params: OS): IOSCommandsDefinition { const { name, architecture } = params; + if (!name) { throw new NoOSSelectedException(); } + if (!architecture) { throw new NoArchitectureSelectedException(); } @@ -94,6 +99,7 @@ export class CommandGenerator< name, architecture, }); + return option; } @@ -106,6 +112,7 @@ export class CommandGenerator< if (!this.osDefinitionSelected) { throw new NoOSSelectedException(); } + return this.osDefinitionSelected.urlPackage({ wazuhVersion: this.wazuhVersion, architecture: this.osDefinitionSelected diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts index 8ecf6d1be6..5d7c4419f2 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts @@ -1,4 +1,6 @@ +// eslint-disable-next-line unicorn/custom-error-definition export class NoOptionFoundException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string, architecture: string) { super( `No OS option found for "${osName}" "${architecture}". Please check the OS definitions."`, @@ -6,7 +8,9 @@ export class NoOptionFoundException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoOSOptionFoundException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string) { super( `No OS option found for "${osName}". Please check the OS definitions."`, @@ -14,7 +18,9 @@ export class NoOSOptionFoundException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoStartCommandDefinitionException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string, architecture: string) { super( `No start command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, @@ -22,7 +28,9 @@ export class NoStartCommandDefinitionException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoInstallCommandDefinitionException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string, architecture: string) { super( `No install command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, @@ -30,7 +38,9 @@ export class NoInstallCommandDefinitionException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoPackageURLDefinitionException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string, architecture: string) { super( `No package URL definition found for "${osName}" "${architecture}". Please check the OS definitions.`, @@ -38,7 +48,9 @@ export class NoPackageURLDefinitionException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoOptionalParamFoundException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(paramName: string) { super( `Optional parameter "${paramName}" not found. Please check the optional parameters definitions.`, @@ -46,35 +58,42 @@ export class NoOptionalParamFoundException extends Error { } } +// eslint-disable-next-line unicorn/custom-error-definition export class DuplicatedOSException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string) { super(`Duplicate OS name found: ${osName}`); } } +// eslint-disable-next-line unicorn/custom-error-definition export class DuplicatedOSOptionException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor(osName: string, architecture: string) { - super( - `Duplicate OS option found for "${osName}" "${architecture}"`, - ); + super(`Duplicate OS option found for "${osName}" "${architecture}"`); } } +// eslint-disable-next-line unicorn/custom-error-definition export class WazuhVersionUndefinedException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor() { super(`Wazuh version not defined`); } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoOSSelectedException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor() { super(`OS not selected. Please select`); } } +// eslint-disable-next-line unicorn/custom-error-definition export class NoArchitectureSelectedException extends Error { + // eslint-disable-next-line unicorn/custom-error-definition constructor() { - super(`Architecture not selected. Please select`); + super(`Architecture not selected. Please select`); } } - diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts index cf5090ecf5..28bd2769fb 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts @@ -1,8 +1,8 @@ import { NoOptionalParamFoundException } from '../exceptions'; import { IOptionalParameters, - tOptionalParams, - tOptionalParamsCommandProps, + TOptionalParams, + TOptionalParamsCommandProps, } from '../types'; import { OptionalParametersManager } from './optional-parameters-manager'; @@ -13,14 +13,14 @@ type TOptionalParamsFieldname = | 'another_valid_fieldname'; const returnOptionalParam = ( - props: tOptionalParamsCommandProps, + props: TOptionalParamsCommandProps, ) => { const { property, value } = props; return `${property} '${value}'`; }; -const optionalParametersDefinition: tOptionalParams = +const optionalParametersDefinition: TOptionalParams = { username: { property: '--user', diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts index c4fac9d107..a2c01c80bd 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -4,13 +4,13 @@ import { IOptionalParamInput, IOptionalParameters, IOptionalParametersManager, - tOptionalParams, + TOptionalParams, } from '../types'; export class OptionalParametersManager implements IOptionalParametersManager { - constructor(private readonly optionalParamsConfig: tOptionalParams) {} + constructor(private readonly optionalParamsConfig: TOptionalParams) {} /** * Returns the command string for a given optional parameter. diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts index c8fabc3ebf..d323668320 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts @@ -1,5 +1,13 @@ -import { NoInstallCommandDefinitionException, NoPackageURLDefinitionException, WazuhVersionUndefinedException } from "../exceptions"; -import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from "../types"; +import { + NoInstallCommandDefinitionException, + NoPackageURLDefinitionException, + WazuhVersionUndefinedException, +} from '../exceptions'; +import { + IOSCommandsDefinition, + IOperationSystem, + IOptionalParameters, +} from '../types'; /** * Returns the installation command for a given operating system. @@ -13,25 +21,39 @@ import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from ".. * @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); - } - - if(!packageUrl || packageUrl === ''){ - throw new NoPackageURLDefinitionException(osName, osDefinition.architecture); - } +export function getInstallCommandByOS< + OS extends IOperationSystem, + Params extends string, +>( + osDefinition: IOSCommandsDefinition, + packageUrl: string, + version: string, + osName: string, + optionals?: IOptionalParameters, +) { + if (!osDefinition.installCommand) { + throw new NoInstallCommandDefinitionException( + osName, + osDefinition.architecture, + ); + } - if(!version || version === ''){ - throw new WazuhVersionUndefinedException(); - } - - return osDefinition.installCommand({ - urlPackage: packageUrl, - wazuhVersion: version, - name: osName as OS['name'], - architecture: osDefinition.architecture, - optionals, - }); -} \ No newline at end of file + if (!packageUrl || packageUrl === '') { + throw new NoPackageURLDefinitionException( + osName, + osDefinition.architecture, + ); + } + + if (!version || version === '') { + throw new WazuhVersionUndefinedException(); + } + + return osDefinition.installCommand({ + urlPackage: packageUrl, + wazuhVersion: version, + name: osName as OS['name'], + architecture: osDefinition.architecture, + optionals, + }); +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts index 0ceefada65..b468466075 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -15,20 +15,19 @@ import { IOSDefinition, IOperationSystem } from '../types'; * @returns The matching OS definition option. * @throws NoOSOptionFoundException - If no matching OS definition is found. */ -export function searchOSDefinitions( - osDefinitions: IOSDefinition[], - params: IOperationSystem, -){ +export function searchOSDefinitions< + OS extends IOperationSystem, + Params extends string, +>(osDefinitions: IOSDefinition[], params: IOperationSystem) { const { name, architecture } = 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 => option.architecture === architecture, ); if (!osDefinitionOption) { @@ -36,7 +35,7 @@ export function searchOSDefinitions( - osDefinitions: IOSDefinition[], -){ +export function validateOSDefinitionsDuplicated< + OS extends IOperationSystem, + Params extends string, +>(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. @@ -65,20 +66,24 @@ export function validateOSDefinitionsDuplicated( - osDefinitions: IOSDefinition[], -){ +export function validateOSDefinitionHasDuplicatedOptions< + OS extends IOperationSystem, + Params extends string, +>(osDefinitions: IOSDefinition[]) { for (const osDefinition of osDefinitions) { const options = new Set(); + for (const option of osDefinition.options) { - let ext_arch_manager = `${option.architecture}`; - if (options.has(ext_arch_manager)) { + const managerArchitecture = `${option.architecture}`; + + if (options.has(managerArchitecture)) { throw new DuplicatedOSOptionException( osDefinition.name, option.architecture, ); } - options.add(ext_arch_manager); + + options.add(managerArchitecture); } } -}; +} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts index c09e828a78..33f067d5b7 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts @@ -1,89 +1,108 @@ -///////////////////////////////////////////////////////// -/// Domain -///////////////////////////////////////////////////////// +// /////////////////////////////////////////////////////// +// / Domain +// /////////////////////////////////////////////////////// export interface IOperationSystem { name: string; architecture: string; } -export type IOptionalParameters = { - [key in Params]: string; -}; +export type IOptionalParameters = Record; -/////////////////////////////////////////////////////////////////// -/// Operating system commands definitions -/////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////// +// / Operating system commands definitions +// ///////////////////////////////////////////////////////////////// -export interface IOSDefinition< - OS extends IOperationSystem, - Params extends string, -> { - name: OS['name']; - options: IOSCommandsDefinition[]; +export interface IOSProps extends IOperationSystem { + wazuhVersion: string; } interface IOptionalParamsWithValues { optionals?: IOptionalParameters; } -export type tOSEntryProps = IOSProps & +export type TOSEntryProps = IOSProps & IOptionalParamsWithValues; -export type tOSEntryInstallCommand = - tOSEntryProps & { urlPackage: string }; +export type TOSEntryInstallCommand = + TOSEntryProps & { urlPackage: string }; export interface IOSCommandsDefinition< OS extends IOperationSystem, Param extends string, > { architecture: OS['architecture']; - urlPackage: (props: tOSEntryProps) => string; - installCommand: (props: tOSEntryInstallCommand) => string; - startCommand: (props: tOSEntryProps) => string; + urlPackage: (props: TOSEntryProps) => string; + installCommand: (props: TOSEntryInstallCommand) => string; + startCommand: (props: TOSEntryProps) => string; } - -export interface IOSProps extends IOperationSystem { - wazuhVersion: string; +export interface IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { + name: OS['name']; + options: IOSCommandsDefinition[]; } -/////////////////////////////////////////////////////////////////// -//// Commands optional parameters -/////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////// +// // Commands optional parameters +// ///////////////////////////////////////////////////////////////// interface IOptionalParamProps { property: string; value: string; } -export type tOptionalParamsCommandProps = +export type TOptionalParamsCommandProps = IOptionalParamProps & { name: T; }; export interface IOptionsParamConfig { property: string; getParamCommand: ( - props: tOptionalParamsCommandProps, + props: TOptionalParamsCommandProps, selectedOS?: IOperationSystem, ) => string; } -export type tOptionalParams = { - [key in T]: IOptionsParamConfig; -}; +export type TOptionalParams = Record< + T, + IOptionsParamConfig +>; export interface IOptionalParamInput { value: any; name: T; } export interface IOptionalParametersManager { - getOptionalParam(props: IOptionalParamInput): string; - getAllOptionalParams( + getOptionalParam: (props: IOptionalParamInput) => string; + getAllOptionalParams: ( paramsValues: IOptionalParameters, selectedOs?: IOperationSystem, - ): object; + ) => object; } -/////////////////////////////////////////////////////////////////// -/// Command creator class -/////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////// +// / Command creator class +// ///////////////////////////////////////////////////////////////// +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} + +export interface ICommandGeneratorMethods { + selectOS: (params: IOperationSystem) => void; + addOptionalParams: ( + props: IOptionalParameters, + osSelected?: IOperationSystem, + ) => void; + getInstallCommand: () => string; + getStartCommand: () => string; + getUrlPackage: () => string; + getAllCommands: () => ICommandsResponse; +} export type IOSInputs = IOperationSystem & IOptionalParameters; @@ -94,24 +113,3 @@ export interface ICommandGenerator< osDefinitions: IOSDefinition[]; wazuhVersion: string; } - -export interface ICommandGeneratorMethods { - selectOS(params: IOperationSystem): void; - addOptionalParams( - props: IOptionalParameters, - osSelected?: IOperationSystem, - ): void; - getInstallCommand(): string; - getStartCommand(): string; - getUrlPackage(): string; - getAllCommands(): ICommandsResponse; -} -export interface ICommandsResponse { - wazuhVersion: string; - os: string; - architecture: string; - url_package: string; - install_command: string; - start_command: string; - optionals: IOptionalParameters | object; -} diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md index 5ccb42f367..472d662339 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md @@ -30,7 +30,7 @@ import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; /* the props recived by the hook must implement types: - OS: IOSDefinition[] - - optional parameters: tOptionalParams + - optional parameters: TOptionalParams */ const { @@ -80,7 +80,7 @@ interface IUseRegisterCommandsProps< Params extends string, > { osDefinitions: IOSDefinition[]; - optionalParamsDefinitions: tOptionalParams; + optionalParamsDefinitions: TOptionalParams; } ``` @@ -116,10 +116,10 @@ And the hook will validate and show warning in compilation and development time. export interface IOptionsParamConfig { property: string; - getParamCommand: (props: tOptionalParamsCommandProps) => string; + getParamCommand: (props: TOptionalParamsCommandProps) => string; } -export type tOptionalParams = { +export type TOptionalParams = { [key in T]: IOptionsParamConfig; }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts index faefd57b6f..9d86d89dc6 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts @@ -2,7 +2,7 @@ import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { IOSDefinition, - tOptionalParams, + TOptionalParams, } from '../core/register-commands/types'; import { useRegisterAgentCommands } from './use-register-agent-commands'; @@ -51,7 +51,7 @@ export const osCommandsDefinitions = [linuxDefinition]; // / Optional parameters definitions // ///////////////////////////////////////////////////////////////// -export const optionalParamsDefinitions: tOptionalParams = +export const optionalParamsDefinitions: TOptionalParams = { optional1: { property: '--url', diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts index 414f2ea41f..9c39b82b53 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { CommandGenerator } from '../core/register-commands/command-generator/command-generator'; import { IOSDefinition, IOperationSystem, IOptionalParameters, - tOptionalParams, + TOptionalParams, } from '../core/register-commands/types'; import { version } from '../../../../../package.json'; @@ -13,7 +13,7 @@ interface IUseRegisterCommandsProps< Params extends string, > { osDefinitions: IOSDefinition[]; - optionalParamsDefinitions: tOptionalParams; + optionalParamsDefinitions: TOptionalParams; } interface IUseRegisterCommandsOutput< @@ -27,7 +27,7 @@ interface IUseRegisterCommandsOutput< ) => void; installCommand: string; startCommand: string; - optionalParamsParsed: IOptionalParameters | {}; + optionalParamsParsed: IOptionalParameters | object; } /** @@ -48,20 +48,19 @@ export function useRegisterAgentCommands< const wazuhVersion = version; const osCommands: IOSDefinition[] = osDefinitions as IOSDefinition[]; - const optionalParams: tOptionalParams = - optionalParamsDefinitions as tOptionalParams; + const optionalParams: TOptionalParams = + optionalParamsDefinitions as TOptionalParams; const commandGenerator = new CommandGenerator( osCommands, optionalParams, wazuhVersion, ); - const [osSelected, setOsSelected] = useState(null); const [optionalParamsValues, setOptionalParamsValues] = useState< - IOptionalParameters | {} + IOptionalParameters | object >({}); const [optionalParamsParsed, setOptionalParamsParsed] = useState< - IOptionalParameters | {} + IOptionalParameters | object >({}); const [installCommand, setInstallCommand] = useState(''); const [startCommand, setStartCommand] = useState(''); @@ -72,18 +71,24 @@ export function useRegisterAgentCommands< * The generated commands are then set as state variables for later use. */ const generateCommands = () => { - if (!osSelected) return; + if (!osSelected) { + return; + } + if (osSelected) { commandGenerator.selectOS(osSelected); } + if (optionalParamsValues) { commandGenerator.addOptionalParams( optionalParamsValues as IOptionalParameters, osSelected, ); } + const installCommand = commandGenerator.getInstallCommand(); const startCommand = commandGenerator.getStartCommand(); + setInstallCommand(installCommand); setStartCommand(startCommand); }; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx index db512362f5..64ade460af 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx @@ -22,7 +22,6 @@ const defaultFormFieldData: EnhancedFieldConfiguration = { }, inputRef: null, }; - const formFieldsDefault: UseFormReturn['fields'] = { field1: { ...defaultFormFieldData, @@ -46,6 +45,7 @@ describe('RegisterAgentFormStatusManager', () => { const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( formFieldsDefault, ); + expect(registerAgentFormStatusManager).toBeDefined(); }); @@ -54,6 +54,7 @@ describe('RegisterAgentFormStatusManager', () => { formFieldsDefault, ); const formStatus = registerAgentFormStatusManager.getFormStatus(); + expect(formStatus).toEqual({ field1: 'empty', field2: 'invalid', @@ -66,6 +67,7 @@ describe('RegisterAgentFormStatusManager', () => { formFieldsDefault, ); const fieldStatus = registerAgentFormStatusManager.getFieldStatus('field1'); + expect(fieldStatus).toEqual('empty'); }); @@ -73,6 +75,7 @@ describe('RegisterAgentFormStatusManager', () => { const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( formFieldsDefault, ); + expect(() => registerAgentFormStatusManager.getFieldStatus('field4'), ).toThrowError('Fieldname not found'); @@ -87,6 +90,7 @@ describe('RegisterAgentFormStatusManager', () => { formFieldsDefault, formSteps, ); + expect(registerAgentFormStatusManager).toBeDefined(); expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( 'invalid', @@ -102,6 +106,7 @@ describe('RegisterAgentFormStatusManager', () => { formFieldsDefault, formSteps, ); + expect(registerAgentFormStatusManager).toBeDefined(); expect(registerAgentFormStatusManager.getStepStatus('step2')).toEqual( 'complete', @@ -111,13 +116,13 @@ describe('RegisterAgentFormStatusManager', () => { it('should return EMPTY when the step all fields empty', () => { const formSteps: FormStepsDependencies = { step1: ['field1'], - step2: [ 'field2', - 'field3' ], + step2: ['field2', 'field3'], }; const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( formFieldsDefault, formSteps, ); + expect(registerAgentFormStatusManager).toBeDefined(); expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( 'empty', @@ -127,19 +132,19 @@ describe('RegisterAgentFormStatusManager', () => { it('should return all the steps status', () => { const formSteps: FormStepsDependencies = { step1: ['field1'], - step2: [ 'field2', - 'field3' ], - step3: ['field3'] + step2: ['field2', 'field3'], + step3: ['field3'], }; const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( formFieldsDefault, formSteps, ); + expect(registerAgentFormStatusManager).toBeDefined(); expect(registerAgentFormStatusManager.getFormStepsStatus()).toEqual({ step1: 'empty', step2: 'invalid', - step3: 'complete' + step3: 'complete', }); }); }); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx index 45423e7524..75bed9dc2e 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx @@ -23,7 +23,7 @@ export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { private readonly formSteps?: FormStepsDependencies, ) {} - getFieldStatus = (fieldname: FormFieldName): FieldStatus => { + getFieldStatus(fieldname: FormFieldName): FieldStatus { const field = this.formFields[fieldname]; if (!field) { @@ -39,18 +39,21 @@ export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { } return 'complete'; - }; - getFormStatus = (): FormStatus => { + } + + getFormStatus(): FormStatus { const fieldNames = Object.keys(this.formFields); const formStatus: FormStatus | object = {}; + // eslint-disable-next-line unicorn/no-array-for-each fieldNames.forEach((fieldName: string) => { formStatus[fieldName] = this.getFieldStatus(fieldName); }); return formStatus as FormStatus; - }; - getStepStatus = (stepName: string): FieldStatus => { + } + + getStepStatus(stepName: string): FieldStatus { if (!this.formSteps) { throw new Error('Form steps not defined'); } @@ -63,6 +66,7 @@ export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { const formStepStatus: FormStepsStatus | object = {}; + // eslint-disable-next-line unicorn/no-array-for-each stepFields.forEach((fieldName: FormFieldName) => { formStepStatus[fieldName] = this.getFieldStatus(fieldName); }); @@ -79,34 +83,38 @@ export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { // if all are complete return 'complete'; } - }; - getFormStepsStatus = (): FormStepsStatus => { + } + + getFormStepsStatus(): FormStepsStatus { if (!this.formSteps) { throw new Error('Form steps not defined'); } const formStepsStatus: FormStepsStatus | object = {}; + // eslint-disable-next-line unicorn/no-array-for-each Object.keys(this.formSteps).forEach((stepName: string) => { formStepsStatus[stepName] = this.getStepStatus(stepName); }); return formStepsStatus as FormStepsStatus; - }; - getIncompleteSteps = (): string[] => { + } + + getIncompleteSteps(): string[] { const formStepsStatus = this.getFormStepsStatus(); const notCompleteSteps = Object.entries(formStepsStatus).filter( ([_, status]) => status === 'empty', ); return notCompleteSteps.map(([stepName, _]) => stepName); - }; - getInvalidFields = (): string[] => { + } + + getInvalidFields(): string[] { const formStatus = this.getFormStatus(); const invalidFields = Object.entries(formStatus).filter( ([_, status]) => status === 'invalid', ); return invalidFields.map(([fieldName, _]) => fieldName); - }; + } } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx index 7f0ab02ce5..f9fb1b6493 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx @@ -1,8 +1,8 @@ import { TOptionalParameters } from '../core/config/os-commands-definitions'; import { IOptionalParameters, - tOSEntryInstallCommand, - tOSEntryProps, + TOSEntryInstallCommand, + TOSEntryProps, } from '../core/register-commands/types'; import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; @@ -59,7 +59,7 @@ export const getAllOptionalsMacos = ( /** ***** DEB *******/ export const getDEBAMD64InstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb`; @@ -72,7 +72,7 @@ export const getDEBAMD64InstallCommand = ( }; export const getDEBARM64InstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent_${wazuhVersion}-1_arm64.deb`; @@ -87,7 +87,7 @@ export const getDEBARM64InstallCommand = ( /** ***** RPM *******/ export const getRPMAMD64InstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm`; @@ -100,7 +100,7 @@ export const getRPMAMD64InstallCommand = ( }; export const getRPMARM64InstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals, wazuhVersion } = props; const packageName = `wazuh-agent-${wazuhVersion}-1.aarch64.rpm`; @@ -116,14 +116,14 @@ export const getRPMARM64InstallCommand = ( // Start command export const getLinuxStartCommand = ( - _props: tOSEntryProps, + _props: TOSEntryProps, ) => `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; /** ****** Windows ********/ export const getWindowsInstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals, name } = props; @@ -136,7 +136,7 @@ export const getWindowsInstallCommand = ( }; export const getWindowsStartCommand = ( - _props: tOSEntryProps, + _props: TOSEntryProps, ) => `NET START WazuhSvc`; /** ****** MacOS ********/ @@ -148,7 +148,7 @@ export const transformOptionalsParamatersMacOSCommand = (command: string) => .trim(); export const getMacOsInstallCommand = ( - props: tOSEntryInstallCommand, + props: TOSEntryInstallCommand, ) => { const { optionals } = props; const optionalsForCommand = { ...optionals }; @@ -186,5 +186,5 @@ export const getMacOsInstallCommand = ( }; export const getMacosStartCommand = ( - _props: tOSEntryProps, + _props: TOSEntryProps, ) => `sudo /Library/Ossec/bin/wazuh-control start`; diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts index f1be9e62c3..20432f850a 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts @@ -1,21 +1,28 @@ -import { scapeSpecialCharsForLinux, scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows } from './wazuh-password-service'; +/* eslint-disable unicorn/prefer-string-raw */ +import { + scapeSpecialCharsForLinux, + scapeSpecialCharsForMacOS, + scapeSpecialCharsForWindows, +} from './wazuh-password-service'; + describe('Wazuh Password Service', () => { // NOTE: // The password constant must be written as it comes from the backend // The expectedPassword variable must be written taking into account how the \ will be escaped - describe('For Linux shell' , () => { - it.each` - passwordFromAPI | expectedScapedPassword - ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} - ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'\"'\"'sds\\\"es"} - ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\\\usds\\\"\\\\es"} + describe('For Linux shell', () => { + it.each` + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${'password"with"doublequ\'"\'"\'sds\\"es'} + ${'password"with"doubleq\\\'usds\\"es'} | ${'password"with"doubleq\\\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'usds\\"es'} | ${'password"with"doubleq\\\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'\\usds\\"\\es'} | ${'password"with"doubleq\\\\\'"\'"\'\\\\usds\\"\\\\es'} `( ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', ({ passwordFromAPI, expectedScapedPassword }) => { const passwordScaped = scapeSpecialCharsForLinux(passwordFromAPI); - /* log to compare passwords + + /* log to compare passwords console.log( 'PASSWORD REAL: ', passwordFromAPI, @@ -27,23 +34,24 @@ describe('Wazuh Password Service', () => { expectedScapedPassword );*/ expect(passwordScaped).toEqual(expectedScapedPassword); - } + }, ); - }) + }); - describe('For Windows shell' , () => { + describe('For Windows shell', () => { it.each` - passwordFromAPI | expectedScapedPassword - ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} - ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'\"'\"'sds\\\"es"} - ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\usds\\\"\\es"} + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'\"'\"'with'\"'\"'special'\"'\"'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${'password"with"doublequ\'"\'"\'sds\\"es'} + ${'password"with"doubleq\\\'usds\\"es'} | ${'password"with"doubleq\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'usds\\"es'} | ${'password"with"doubleq\\\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'\\usds\\"\\es'} | ${'password"with"doubleq\\\\\'"\'"\'\\usds\\"\\es'} `( ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', ({ passwordFromAPI, expectedScapedPassword }) => { const passwordScaped = scapeSpecialCharsForWindows(passwordFromAPI); - /* log to compare passwords + + /* log to compare passwords console.log( 'PASSWORD REAL: ', passwordFromAPI, @@ -55,23 +63,24 @@ describe('Wazuh Password Service', () => { expectedScapedPassword );*/ expect(passwordScaped).toEqual(expectedScapedPassword); - } + }, ); }); - describe('For macOS shell' , () => { + describe('For macOS shell', () => { it.each` - passwordFromAPI | expectedScapedPassword - ${"password'with'special'characters"} | ${"password'with'special'characters"} - ${'password"with"doublequ\'sds\\"es'} | ${"password\"with\"doublequ'sds\\\"es"} - ${"password\"with\"doubleq\\'usds\\\"es"} | ${"password\"with\"doubleq\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'usds\\\"es"} | ${"password\"with\"doubleq\\\\'\"'\"'usds\\\"es"} - ${"password\"with\"doubleq\\\\'\\usds\\\"\\es"} | ${"password\"with\"doubleq\\\\'\"'\"'\\usds\\\"\\es"} - `( - ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', - ({ passwordFromAPI, expectedScapedPassword }) => { - const passwordScaped = scapeSpecialCharsForMacOS(passwordFromAPI); - /* log to compare passwords + passwordFromAPI | expectedScapedPassword + ${"password'with'special'characters"} | ${"password'with'special'characters"} + ${'password"with"doublequ\'sds\\"es'} | ${'password"with"doublequ\'sds\\"es'} + ${'password"with"doubleq\\\'usds\\"es'} | ${'password"with"doubleq\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'usds\\"es'} | ${'password"with"doubleq\\\\\'"\'"\'usds\\"es'} + ${'password"with"doubleq\\\\\'\\usds\\"\\es'} | ${'password"with"doubleq\\\\\'"\'"\'\\usds\\"\\es'} + `( + ' should return password received with scaped characters: $passwordFromAPI | $scapedPassword | $expectedScapedPassword', + ({ passwordFromAPI, expectedScapedPassword }) => { + const passwordScaped = scapeSpecialCharsForMacOS(passwordFromAPI); + + /* log to compare passwords console.log( 'PASSWORD REAL: ', passwordFromAPI, @@ -82,8 +91,8 @@ describe('Wazuh Password Service', () => { '\nPASSWORD SCAPED REAL IN COMMAND EXPECTED: ', expectedScapedPassword );*/ - expect(passwordScaped).toEqual(expectedScapedPassword); - } - ); -}) + expect(passwordScaped).toEqual(expectedScapedPassword); + }, + ); + }); }); From bbb92925f07abbea23a9c7c648765c86720f7834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 11 Feb 2025 13:34:56 +0100 Subject: [PATCH 04/22] fix: eslint --- plugins/wazuh-fleet/public/application/application.tsx | 3 ++- plugins/wazuh-fleet/public/plugin.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/application.tsx b/plugins/wazuh-fleet/public/application/application.tsx index c06f58f981..2ee3e9143d 100644 --- a/plugins/wazuh-fleet/public/application/application.tsx +++ b/plugins/wazuh-fleet/public/application/application.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { Router, Route, Switch, Redirect } from 'react-router-dom'; +import { History } from 'history'; import { RegisterAgent } from './pages/register-agent'; -export function Application({ history }) { +export function Application({ history }: { history: History }) { return ( diff --git a/plugins/wazuh-fleet/public/plugin.ts b/plugins/wazuh-fleet/public/plugin.ts index 073e769cc0..aeb8f0b2e9 100644 --- a/plugins/wazuh-fleet/public/plugin.ts +++ b/plugins/wazuh-fleet/public/plugin.ts @@ -11,7 +11,7 @@ import { appSetup } from './application'; export class WazuhFleetPlugin implements Plugin { - public setup(core: CoreSetup, plugins): WazuhFleetPluginSetup { + public setup(core: CoreSetup): WazuhFleetPluginSetup { appSetup({ registerApp: app => core.application.register(app), }); From a05efa701e2f45576001a95fd199cf16de48d3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 12 Feb 2025 09:33:18 +0100 Subject: [PATCH 05/22] fix: install agent on Windows command --- .../register-agent-os-commands-services.test.ts | 10 +++++----- .../services/register-agent-os-commands-services.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts index ff63336dd8..20e8083aa4 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts @@ -246,7 +246,7 @@ describe('getLinuxStartCommand', () => { describe('getWindowsInstallCommand', () => { it('should return the correct install command', () => { - let expected = `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${[ + let expected = `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ 'serverAddress', 'username', 'password', @@ -256,7 +256,7 @@ describe('getWindowsInstallCommand', () => { ] .map(key => test.optionals[key]) .filter(Boolean) - .join(' ')} /q`; + .join(' ')}`; const withAllOptionals = getWindowsInstallCommand(test); expect(withAllOptionals).toEqual(expected); @@ -264,7 +264,7 @@ describe('getWindowsInstallCommand', () => { delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${[ + expected = `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ 'serverAddress', 'username', 'password', @@ -274,7 +274,7 @@ describe('getWindowsInstallCommand', () => { ] .map(key => test.optionals[key]) .filter(Boolean) - .join(' ')} /q`; + .join(' ')}`; const withServerAddresAndAgentGroupsOptions = getWindowsInstallCommand(test); @@ -285,7 +285,7 @@ describe('getWindowsInstallCommand', () => { describe('getWindowsStartCommand', () => { it('should return the correct start command', () => { - const expectedCommand = 'NET START WazuhSvc'; + const expectedCommand = "NET START 'Wazuh Agent'"; const result = getWindowsStartCommand({}); expect(result).toEqual(expectedCommand); diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx index f9fb1b6493..219df73778 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx @@ -129,15 +129,16 @@ export const getWindowsInstallCommand = ( return [ // `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent;`, // TODO: enable when the packages are publically hosted - `msiexec.exe /i $env:tmp\\wazuh-agent --register-agent ${ + // https://stackoverflow.com/questions/1673967/how-to-run-an-exe-file-in-powershell-with-parameters-with-spaces-and-quotes + `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${ optionals && getAllOptionals(optionals, name) - } /q`, + }`, ].join(' '); }; export const getWindowsStartCommand = ( _props: TOSEntryProps, -) => `NET START WazuhSvc`; +) => `NET START 'Wazuh Agent'`; /** ****** MacOS ********/ From 591c975dd94236069ba70d1791b377d59836a7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 13 Feb 2025 17:01:39 +0100 Subject: [PATCH 06/22] feat(enrollment): add ability to remember the server address --- plugins/wazuh-core/common/constants.ts | 9 +- .../wazuh-fleet/public/application/mount.ts | 6 +- .../server-address/server-address.tsx | 91 ++++++++++++++++++- .../wazuh-fleet/public/application/types.ts | 4 + plugins/wazuh-fleet/public/plugin-services.ts | 4 + plugins/wazuh-fleet/public/plugin.ts | 12 ++- 6 files changed, 117 insertions(+), 9 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index af0f9feb0b..a03fe620a7 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -594,10 +594,11 @@ export const PLUGIN_SETTINGS: Record = { category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: '', - validate: SettingsValidator.compose( - SettingsValidator.isString, - SettingsValidator.serverAddressHostnameFQDNIPv4IPv6, - ), + // TODO: this should be enabled when the configuration service of core plugin provides a mechanism to retrieve this definition to be used in the enrollment agent wizard. See https://github.com/wazuh/wazuh-dashboard/issues/514#issuecomment-2656602679 + // validate: SettingsValidator.compose( + // SettingsValidator.isString, + // SettingsValidator.serverAddressHostnameFQDNIPv4IPv6, + // ), }, 'enrollment.password': { title: 'Enrollment password', diff --git a/plugins/wazuh-fleet/public/application/mount.ts b/plugins/wazuh-fleet/public/application/mount.ts index ce7499b650..91be0dc054 100644 --- a/plugins/wazuh-fleet/public/application/mount.ts +++ b/plugins/wazuh-fleet/public/application/mount.ts @@ -1,6 +1,7 @@ +import { setEnrollAgentManagement } from '../plugin-services'; import { AppSetup } from './types'; -export function appSetup({ registerApp }: AppSetup) { +export function appSetup({ registerApp, enrollmentAgentManagement }: AppSetup) { registerApp({ id: 'wazuh-fleet', title: 'Fleet management', @@ -25,4 +26,7 @@ export function appSetup({ registerApp }: AppSetup) { // ({ id: categoryID }) => categoryID === category, // ), }); + + // TODO: This setter should be local to fleet management instead of using the related to the plugin itself. This approach was done because the integration of FleetManagement is using another setter from plugin-services. + setEnrollAgentManagement(enrollmentAgentManagement); } diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx index 43c143a938..214136532e 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx @@ -1,18 +1,23 @@ import { + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiPopover, EuiButtonEmpty, EuiLink, + EuiToolTip, } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; import { EnhancedFieldConfiguration } from '../form/types'; import { InputForm } from '../form'; import { webDocumentationLink } from '../../services/web-documentation-link'; import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; import '../inputs/styles.scss'; +import { getEnrollAgentManagement } from '../../../../../plugin-services'; interface ServerAddressInputProps { formField: EnhancedFieldConfiguration; @@ -42,6 +47,42 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { isPopoverServerAddress => !isPopoverServerAddress, ); const closeServerAddress = () => setIsPopoverServerAddress(false); + const [defaultServerAddress, setDefaultServerAddress] = useState( + formField?.initialValue ?? '', + ); + + const saveServerAddress = async () => { + try { + await getEnrollAgentManagement().setServerAddress(formField.value); + setDefaultServerAddress(formField.value); + } catch { + /* empty */ + } + }; + + const rememberServerAddressIsDisabled = + !formField.value || + !!formField.error || + formField.value === defaultServerAddress; + + // TODO: this retrieves the value and redefines the form field value, but this should be obtained before defining the form and set this as the initial value + useEffect(() => { + async function fetchServerAddresFromConfiguration() { + const userValue = await getEnrollAgentManagement().getServerAddress(); + + if (userValue) { + setDefaultServerAddress(userValue); + formField.onChange({ + // simulate the input text interface expected by the onChange method + target: { + value: userValue, + }, + }); + } + } + + fetchServerAddresFromConfiguration(); + }, []); return ( <> @@ -68,7 +109,10 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { > - Assign a server address + @@ -95,8 +139,49 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { } - fullWidth={false} + fullWidth={true} placeholder='https://server-address:55000' + postInput={ + <> + + + ) : ( + + ) + } + > + + + + + + } /> diff --git a/plugins/wazuh-fleet/public/application/types.ts b/plugins/wazuh-fleet/public/application/types.ts index 84c00e239f..44e247c265 100644 --- a/plugins/wazuh-fleet/public/application/types.ts +++ b/plugins/wazuh-fleet/public/application/types.ts @@ -1,3 +1,7 @@ export interface AppSetup { registerApp: (app: any) => void; + enrollmentAgentManagement: { + getServerAddress: () => Promise; + setServerAddress: (url: string) => Promise; + }; } diff --git a/plugins/wazuh-fleet/public/plugin-services.ts b/plugins/wazuh-fleet/public/plugin-services.ts index 978706134a..14d8de135f 100644 --- a/plugins/wazuh-fleet/public/plugin-services.ts +++ b/plugins/wazuh-fleet/public/plugin-services.ts @@ -8,3 +8,7 @@ export const [getPlugins, setPlugins] = export const [getCore, setCore] = createGetterSetter('Core'); export const [getWazuhCore, setWazuhCore] = createGetterSetter('WazuhCore'); +export const [getEnrollAgentManagement, setEnrollAgentManagement] = + createGetterSetter( + 'fleetManagementEnrollmentAgentManagement', + ); diff --git a/plugins/wazuh-fleet/public/plugin.ts b/plugins/wazuh-fleet/public/plugin.ts index aeb8f0b2e9..4db40731ca 100644 --- a/plugins/wazuh-fleet/public/plugin.ts +++ b/plugins/wazuh-fleet/public/plugin.ts @@ -5,7 +5,7 @@ import { WazuhFleetPluginStart, } from './types'; import { FleetManagement } from './components'; -import { setCore, setPlugins, setWazuhCore } from './plugin-services'; +import { getCore, setCore, setPlugins, setWazuhCore } from './plugin-services'; import { appSetup } from './application'; export class WazuhFleetPlugin @@ -14,6 +14,16 @@ export class WazuhFleetPlugin public setup(core: CoreSetup): WazuhFleetPluginSetup { appSetup({ registerApp: app => core.application.register(app), + enrollmentAgentManagement: { + async getServerAddress() { + // TODO: this should be replaced by getWazuhCore().configuration.get that in the current state does not return the setting because this is filtering by settings with the category 'wazuhCore'. + return getCore().uiSettings.get('enrollment.dns'); + }, + async setServerAddress(url) { + // TODO: this should be replaced by getWazuhCore().configuration.set that is not implemented + return await getCore().uiSettings.set('enrollment.dns', url); + }, + }, }); return {}; From beb18ff6e77fa2df506a7f0b2b7fe1979cdccf21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 13 Feb 2025 17:16:37 +0100 Subject: [PATCH 07/22] feat(enrollment): rename Deploy new agent by Enroll new agent --- .../containers/register-agent/register-agent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx index 731cb573fb..57cc8680ea 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx @@ -33,7 +33,7 @@ export const RegisterAgent = compose( // text: endpointSummary.breadcrumbLabel, // href: `#${endpointSummary.redirectTo()}`, // }, - // { text: 'Deploy new agent' }, + // { text: 'Enroll new agent' }, // ]), // withUserAuthorizationPrompt([ // [{ action: 'agent:create', resource: '*:*:*' }], @@ -137,7 +137,7 @@ export const RegisterAgent = compose( -

Deploy new agent

+

Enroll new agent

From 22b8646dac69a5ae13e18d425cdbb29f2a685c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 13 Feb 2025 17:18:15 +0100 Subject: [PATCH 08/22] remove(enrollment): remove enrollment.password setting by securiy reasons The setting is saved in the advanced settings (tenant configuration) and is visible to each user with read permissions in the tenant. So we remove the setting to avoid other users can see the value. The user should indicate the username and password each time the enroll agent assistant is used. --- plugins/wazuh-core/common/constants.ts | 11 ----------- plugins/wazuh-core/common/plugin-settings.test.ts | 3 --- 2 files changed, 14 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index a03fe620a7..5966f177c8 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -600,16 +600,6 @@ export const PLUGIN_SETTINGS: Record = { // SettingsValidator.serverAddressHostnameFQDNIPv4IPv6, // ), }, - 'enrollment.password': { - title: 'Enrollment password', - description: - 'Specifies the password used to authenticate during the agent enrollment.', - source: EConfigurationProviders.PLUGIN_UI_SETTINGS, - category: SettingCategory.GENERAL, - type: EpluginSettingType.text, - defaultValue: '', - validate: SettingsValidator.compose(SettingsValidator.isString), - }, hideManagerAlerts: { title: 'Hide manager alerts', description: 'Hide the alerts of the manager in every dashboard.', @@ -1021,7 +1011,6 @@ export const CRON_PREFIX = 'cron.prefix'; export const CUSTOMIZATION_ENABLED = 'customization.enabled'; export const ENROLLMENT_DNS = 'enrollment.dns'; -export const ENROLLMENT_PASSWORD = 'enrollment.password'; export const IP_IGNORE = 'ip.ignore'; export const IP_SELECTOR = 'ip.selector'; diff --git a/plugins/wazuh-core/common/plugin-settings.test.ts b/plugins/wazuh-core/common/plugin-settings.test.ts index a611fab1e9..d001125432 100644 --- a/plugins/wazuh-core/common/plugin-settings.test.ts +++ b/plugins/wazuh-core/common/plugin-settings.test.ts @@ -36,9 +36,6 @@ describe.skip('[settings] Input validation', () => { ${'enrollment.dns'} | ${'2001:0db8:85a3:0000:0000:8a2e:0370:7334:KL12'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'} ${'enrollment.dns'} | ${'example.'} | ${'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'} ${'enrollment.dns'} | ${'127.0.0.1'} | ${undefined} - ${'enrollment.password'} | ${'test'} | ${undefined} - ${'enrollment.password'} | ${''} | ${'Value can not be empty.'} - ${'enrollment.password'} | ${'test space'} | ${undefined} ${'ip.ignore'} | ${'["test"]'} | ${undefined} ${'ip.ignore'} | ${'["test*"]'} | ${undefined} ${'ip.ignore'} | ${'[""]'} | ${'Value can not be empty.'} From 4d80439475e65a195b51f8afb022f28fb4419aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 14 Feb 2025 14:39:20 +0100 Subject: [PATCH 09/22] fix(enrollment): windows command to install and enroll the agent --- .../components/server-address/server-address.tsx | 4 +++- .../containers/register-agent/register-agent.tsx | 8 ++++++-- .../services/register-agent-os-commands-services.test.ts | 4 ++-- .../services/register-agent-os-commands-services.tsx | 2 +- plugins/wazuh-fleet/public/application/types.ts | 1 + plugins/wazuh-fleet/public/plugin-services.ts | 3 ++- plugins/wazuh-fleet/public/plugin.ts | 1 + 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx index 214136532e..c203667781 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx @@ -159,7 +159,9 @@ const ServerAddressInput = (props: ServerAddressInputProps) => { id='wzFleet.enrollmentAssistant.steps.serverAddress.serverAddress.rememberValue.setValue' defaultMessage='Save the {setting} setting' values={{ - setting: 'enrollment.dns', + setting: + getEnrollAgentManagement() + .serverAddresSettingName, }} /> ) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx index 57cc8680ea..f74cc69d1b 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx @@ -21,7 +21,10 @@ import { validateEnrollmentKey, validateServerAddress, } from '../../utils/validations'; -import { getWazuhCore } from '../../../../../plugin-services'; +import { + getEnrollAgentManagement, + getWazuhCore, +} from '../../../../../plugin-services'; import { version } from '../../../../../../package.json'; export const RegisterAgent = compose( @@ -52,7 +55,8 @@ export const RegisterAgent = compose( }, serverAddress: { type: 'text', - initialValue: configuration['enrollment.dns'] || '', // TODO: use the setting value as default value + initialValue: + configuration[getEnrollAgentManagement().serverAddresSettingName] || '', // TODO: use the setting value as default value validate: validateServerAddress, }, username: { diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts index 20e8083aa4..dadfdd1c60 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts @@ -246,7 +246,7 @@ describe('getLinuxStartCommand', () => { describe('getWindowsInstallCommand', () => { it('should return the correct install command', () => { - let expected = `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ + let expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ 'serverAddress', 'username', 'password', @@ -264,7 +264,7 @@ describe('getWindowsInstallCommand', () => { delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ + expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ 'serverAddress', 'username', 'password', diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx index 219df73778..789d37f09f 100644 --- a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx @@ -130,7 +130,7 @@ export const getWindowsInstallCommand = ( return [ // `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent;`, // TODO: enable when the packages are publically hosted // https://stackoverflow.com/questions/1673967/how-to-run-an-exe-file-in-powershell-with-parameters-with-spaces-and-quotes - `msiexec.exe /i $env:tmp\\wazuh-agent /q;& 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${ + `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${ optionals && getAllOptionals(optionals, name) }`, ].join(' '); diff --git a/plugins/wazuh-fleet/public/application/types.ts b/plugins/wazuh-fleet/public/application/types.ts index 44e247c265..eb6c9fa28a 100644 --- a/plugins/wazuh-fleet/public/application/types.ts +++ b/plugins/wazuh-fleet/public/application/types.ts @@ -1,6 +1,7 @@ export interface AppSetup { registerApp: (app: any) => void; enrollmentAgentManagement: { + serverAddresSettingName: string; getServerAddress: () => Promise; setServerAddress: (url: string) => Promise; }; diff --git a/plugins/wazuh-fleet/public/plugin-services.ts b/plugins/wazuh-fleet/public/plugin-services.ts index 14d8de135f..ea85d5b57b 100644 --- a/plugins/wazuh-fleet/public/plugin-services.ts +++ b/plugins/wazuh-fleet/public/plugin-services.ts @@ -2,6 +2,7 @@ import { CoreStart } from 'opensearch-dashboards/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; import { WazuhCorePluginStart } from '../../wazuh-core/public'; import { AppPluginStartDependencies } from './types'; +import { AppSetup } from './application/types'; export const [getPlugins, setPlugins] = createGetterSetter('Plugins'); @@ -9,6 +10,6 @@ export const [getCore, setCore] = createGetterSetter('Core'); export const [getWazuhCore, setWazuhCore] = createGetterSetter('WazuhCore'); export const [getEnrollAgentManagement, setEnrollAgentManagement] = - createGetterSetter( + createGetterSetter( 'fleetManagementEnrollmentAgentManagement', ); diff --git a/plugins/wazuh-fleet/public/plugin.ts b/plugins/wazuh-fleet/public/plugin.ts index 4db40731ca..3e854be393 100644 --- a/plugins/wazuh-fleet/public/plugin.ts +++ b/plugins/wazuh-fleet/public/plugin.ts @@ -15,6 +15,7 @@ export class WazuhFleetPlugin appSetup({ registerApp: app => core.application.register(app), enrollmentAgentManagement: { + serverAddresSettingName: 'enrollment.dns', async getServerAddress() { // TODO: this should be replaced by getWazuhCore().configuration.get that in the current state does not return the setting because this is filtering by settings with the category 'wazuhCore'. return getCore().uiSettings.get('enrollment.dns'); From 8ace81e8017f71f8368c840b8472862cb3733a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 14 Feb 2025 14:47:18 +0100 Subject: [PATCH 10/22] fix: rename register-agent direcotry to enroll-agent --- .../assets/images/themes/dark/icon.svg | 0 .../assets/images/themes/dark/linux-icon.svg | 0 .../assets/images/themes/dark/logo.svg | 0 .../assets/images/themes/dark/mac-icon.svg | 0 .../assets/images/themes/dark/windows-icon.svg | 0 .../assets/images/themes/light/icon.svg | 0 .../assets/images/themes/light/linux-icon.svg | 0 .../assets/images/themes/light/logo.svg | 0 .../assets/images/themes/light/mac-icon.svg | 0 .../assets/images/themes/light/windows-icon.svg | 0 .../components/command-output/command-output.scss | 0 .../components/command-output/command-output.tsx | 0 .../components/command-output/os-warning.tsx | 0 .../components/form/__snapshots__/index.test.tsx.snap | 0 .../components/form/hooks.test.tsx | 0 .../{register-agent => enroll-agent}/components/form/hooks.tsx | 0 .../components/form/index.test.tsx | 0 .../{register-agent => enroll-agent}/components/form/index.tsx | 0 .../components/form/input-editor.tsx | 0 .../components/form/input-filepicker.tsx | 0 .../components/form/input-number.tsx | 0 .../components/form/input-password.tsx | 0 .../components/form/input-select.tsx | 0 .../components/form/input-switch.tsx | 0 .../components/form/input-text.tsx | 0 .../components/form/input-textarea.tsx | 0 .../{register-agent => enroll-agent}/components/form/types.ts | 0 .../components/inputs/styles.scss | 0 .../__snapshots__/enrollment-key-input.test.tsx.snap | 0 .../optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap | 0 .../__snapshots__/verification-mode-input.test.tsx.snap | 0 .../components/optionals-inputs/enrollment-key-input.test.tsx | 0 .../components/optionals-inputs/enrollment-key-input.tsx | 0 .../components/optionals-inputs/optionals-inputs.test.tsx | 0 .../components/optionals-inputs/optionals-inputs.tsx | 0 .../components/optionals-inputs/verification-mode-input.test.tsx | 0 .../components/optionals-inputs/verification-mode-input.tsx | 0 .../components/os-selector/checkbox-group/checkbox-group.scss | 0 .../components/os-selector/checkbox-group/checkbox-group.test.tsx | 0 .../components/os-selector/checkbox-group/checkbox-group.tsx | 0 .../components/os-selector/os-card/os-card.scss | 0 .../components/os-selector/os-card/os-card.test.tsx | 0 .../components/os-selector/os-card/os-card.tsx | 0 .../components/security/__snapshots__/index.test.tsx.snap | 0 .../security/__snapshots__/password-input.test.tsx.snap | 0 .../security/__snapshots__/username-input.test.tsx.snap | 0 .../components/security/index.test.tsx | 0 .../components/security/index.tsx | 0 .../components/security/password-input.test.tsx | 0 .../components/security/password-input.tsx | 0 .../components/security/username-input.test.tsx | 0 .../components/security/username-input.tsx | 0 .../components/server-address/server-address.tsx | 0 .../containers/register-agent/register-agent.scss | 0 .../containers/register-agent/register-agent.tsx | 0 .../{register-agent => enroll-agent}/containers/steps/steps.scss | 0 .../{register-agent => enroll-agent}/containers/steps/steps.tsx | 0 .../core/config/os-commands-definitions.ts | 0 .../core/register-commands/README.md | 0 .../register-commands/command-generator/command-generator.test.ts | 0 .../core/register-commands/command-generator/command-generator.ts | 0 .../core/register-commands/exceptions/index.ts | 0 .../optional-parameters-manager.test.ts | 0 .../optional-parameters-manager/optional-parameters-manager.ts | 0 .../services/get-install-command.service.test.ts | 0 .../register-commands/services/get-install-command.service.ts | 0 .../services/search-os-definitions.service.test.ts | 0 .../register-commands/services/search-os-definitions.service.ts | 0 .../core/register-commands/types.ts | 0 .../pages/{register-agent => enroll-agent}/hooks/README.md | 0 .../hooks/use-register-agent-commands.test.ts | 0 .../hooks/use-register-agent-commands.ts | 0 .../application/pages/{register-agent => enroll-agent}/index.tsx | 0 .../pages/{register-agent => enroll-agent}/interfaces/types.ts | 0 .../services/form-status-manager.test.tsx | 0 .../services/form-status-manager.tsx | 0 .../services/register-agent-os-commands-services.test.ts | 0 .../services/register-agent-os-commands-services.tsx | 0 .../services/register-agent-services.tsx | 0 .../services/register-agent-steps-status-services.tsx | 0 .../services/wazuh-password-service.test.ts | 0 .../services/wazuh-password-service.ts | 0 .../services/web-documentation-link.test.ts | 0 .../services/web-documentation-link.ts | 0 .../utils/register-agent-data.tsx | 0 .../{register-agent => enroll-agent}/utils/validations.test.tsx | 0 .../pages/{register-agent => enroll-agent}/utils/validations.tsx | 0 87 files changed, 0 insertions(+), 0 deletions(-) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/dark/icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/dark/linux-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/dark/logo.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/dark/mac-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/dark/windows-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/light/icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/light/linux-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/light/logo.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/light/mac-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/assets/images/themes/light/windows-icon.svg (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/command-output/command-output.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/command-output/command-output.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/command-output/os-warning.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/__snapshots__/index.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/hooks.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/hooks.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/index.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/index.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-editor.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-filepicker.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-number.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-password.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-select.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-switch.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-text.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/input-textarea.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/form/types.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/inputs/styles.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/enrollment-key-input.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/enrollment-key-input.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/optionals-inputs.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/optionals-inputs.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/verification-mode-input.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/optionals-inputs/verification-mode-input.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/checkbox-group/checkbox-group.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/checkbox-group/checkbox-group.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/checkbox-group/checkbox-group.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/os-card/os-card.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/os-card/os-card.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/os-selector/os-card/os-card.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/__snapshots__/index.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/__snapshots__/password-input.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/__snapshots__/username-input.test.tsx.snap (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/index.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/index.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/password-input.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/password-input.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/username-input.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/security/username-input.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/components/server-address/server-address.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/containers/register-agent/register-agent.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/containers/register-agent/register-agent.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/containers/steps/steps.scss (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/containers/steps/steps.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/config/os-commands-definitions.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/README.md (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/command-generator/command-generator.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/command-generator/command-generator.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/exceptions/index.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/services/get-install-command.service.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/services/get-install-command.service.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/services/search-os-definitions.service.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/services/search-os-definitions.service.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/core/register-commands/types.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/hooks/README.md (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/hooks/use-register-agent-commands.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/hooks/use-register-agent-commands.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/index.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/interfaces/types.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/form-status-manager.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/form-status-manager.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/register-agent-os-commands-services.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/register-agent-os-commands-services.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/register-agent-services.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/register-agent-steps-status-services.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/wazuh-password-service.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/wazuh-password-service.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/web-documentation-link.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/services/web-documentation-link.ts (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/utils/register-agent-data.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/utils/validations.test.tsx (100%) rename plugins/wazuh-fleet/public/application/pages/{register-agent => enroll-agent}/utils/validations.tsx (100%) diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/linux-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/linux-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/linux-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/logo.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/logo.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/logo.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/mac-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/mac-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/mac-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/windows-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/dark/windows-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/windows-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/linux-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/linux-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/linux-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/logo.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/logo.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/logo.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/mac-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/mac-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/mac-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/windows-icon.svg similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/assets/images/themes/light/windows-icon.svg rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/windows-icon.svg diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/command-output.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/os-warning.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/command-output/os-warning.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/os-warning.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/__snapshots__/index.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/__snapshots__/index.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/__snapshots__/index.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/hooks.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/index.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-editor.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-editor.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-editor.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-filepicker.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-filepicker.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-filepicker.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-number.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-number.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-number.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-password.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-password.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-password.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-select.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-select.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-select.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-switch.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-switch.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-switch.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-text.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-text.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-text.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-textarea.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/input-textarea.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-textarea.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/form/types.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/inputs/styles.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/inputs/styles.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/inputs/styles.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/inputs/styles.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/optionals-inputs.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/verification-mode-input.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/enrollment-key-input.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/enrollment-key-input.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/enrollment-key-input.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/enrollment-key-input.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/enrollment-key-input.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/optionals-inputs.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/verification-mode-input.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/verification-mode-input.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/verification-mode-input.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/optionals-inputs/verification-mode-input.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/verification-mode-input.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/checkbox-group/checkbox-group.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/os-selector/os-card/os-card.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/index.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/index.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/index.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/password-input.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/password-input.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/password-input.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/username-input.test.tsx.snap similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/__snapshots__/username-input.test.tsx.snap rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/__snapshots__/username-input.test.tsx.snap diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/index.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/password-input.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/security/username-input.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/components/server-address/server-address.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/containers/register-agent/register-agent.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/containers/steps/steps.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/config/os-commands-definitions.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/README.md rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/command-generator/command-generator.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/exceptions/index.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/exceptions/index.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/exceptions/index.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/get-install-command.service.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/services/search-os-definitions.service.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/types.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/core/register-commands/types.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/types.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/hooks/README.md rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/hooks/use-register-agent-commands.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/index.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/interfaces/types.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/form-status-manager.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-os-commands-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/register-agent-steps-status-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/wazuh-password-service.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/services/web-documentation-link.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.ts diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/utils/register-agent-data.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.test.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.test.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.tsx similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/register-agent/utils/validations.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.tsx From 8f692e9ffaa802641d2b4a42ca0f4b8045e65761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 14 Feb 2025 15:57:59 +0100 Subject: [PATCH 11/22] fix(enrollment): rename register to enroll references --- .../command-output/command-output.scss | 2 +- .../optionals-inputs/optionals-inputs.tsx | 2 +- .../os-selector/os-card/os-card.tsx | 2 +- .../server-address/server-address.tsx | 2 +- .../enroll-agent.scss} | 2 +- .../enroll-agent.tsx} | 6 +-- .../enroll-agent/containers/steps/steps.scss | 2 +- .../enroll-agent/containers/steps/steps.tsx | 52 +++++++++---------- .../core/config/os-commands-definitions.ts | 4 +- .../core/register-commands/README.md | 6 +-- .../pages/enroll-agent/hooks/README.md | 18 +++---- ...t.ts => use-enroll-agent-commands.test.ts} | 23 ++++---- ...mmands.ts => use-enroll-agent-commands.ts} | 18 +++---- .../application/pages/enroll-agent/index.tsx | 2 +- .../pages/enroll-agent/interfaces/types.ts | 4 +- ...enroll-agent-os-commands-services.test.ts} | 2 +- ... => enroll-agent-os-commands-services.tsx} | 4 +- ...services.tsx => enroll-agent-services.tsx} | 14 ++--- ...=> enroll-agent-steps-status-services.tsx} | 6 +-- .../services/form-status-manager.test.tsx | 44 ++++++++-------- .../services/form-status-manager.tsx | 2 +- .../services/wazuh-password-service.ts | 2 +- ...r-agent-data.tsx => enroll-agent-data.tsx} | 4 +- 23 files changed, 110 insertions(+), 113 deletions(-) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/{register-agent/register-agent.scss => enroll-agent/enroll-agent.scss} (72%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/{register-agent/register-agent.tsx => enroll-agent/enroll-agent.tsx} (96%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/{use-register-agent-commands.test.ts => use-enroll-agent-commands.test.ts} (90%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/{use-register-agent-commands.ts => use-enroll-agent-commands.ts} (85%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-os-commands-services.test.ts => enroll-agent-os-commands-services.test.ts} (99%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-os-commands-services.tsx => enroll-agent-os-commands-services.tsx} (98%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-services.tsx => enroll-agent-services.tsx} (86%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-steps-status-services.tsx => enroll-agent-steps-status-services.tsx} (97%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/{register-agent-data.tsx => enroll-agent-data.tsx} (92%) diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss index d2f2d6bac3..6ac780c794 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container .copy-codeblock-wrapper { +.enroll-agent-wizard-container .copy-codeblock-wrapper { position: relative; .euiToolTipAnchor { diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx index 00546e5227..5884ce69f9 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx @@ -10,7 +10,7 @@ import { } from '@elastic/eui'; import { UseFormReturn } from '../form/types'; import { InputForm } from '../form'; -import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/register-agent-data'; +import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/enroll-agent-data'; import { webDocumentationLink } from '../../services/web-documentation-link'; import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; import '../inputs/styles.scss'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx index 70ff056db9..d224f4918d 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx @@ -7,7 +7,7 @@ import { EuiLink, EuiCheckbox, } from '@elastic/eui'; -import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/register-agent-data'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/enroll-agent-data'; import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; import './os-card.scss'; import { webDocumentationLink } from '../../../services/web-documentation-link'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx index c203667781..3ef2b16a1a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx @@ -11,7 +11,7 @@ import { import React, { useState, useEffect } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; -import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; +import { SERVER_ADDRESS_TEXTS } from '../../utils/enroll-agent-data'; import { EnhancedFieldConfiguration } from '../form/types'; import { InputForm } from '../form'; import { webDocumentationLink } from '../../services/web-documentation-link'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss similarity index 72% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss index f54ef91389..2994f56ef4 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container { +.enroll-agent-wizard-container { box-sizing: border-box; background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx similarity index 96% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx index f74cc69d1b..b3f047a12f 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx @@ -10,7 +10,7 @@ import { EuiProgress, } from '@elastic/eui'; import { compose } from 'redux'; -import './register-agent.scss'; +import './enroll-agent.scss'; import { Steps } from '../steps/steps'; import { InputForm } from '../../components/form'; import { useForm } from '../../components/form/hooks'; @@ -27,7 +27,7 @@ import { } from '../../../../../plugin-services'; import { version } from '../../../../../../package.json'; -export const RegisterAgent = compose( +export const EnrollAgent = compose( // TODO: add HOCs // withErrorBoundary, // withRouteResolvers({ enableMenu, ip, nestedResolve, savedSearch }), @@ -137,7 +137,7 @@ export const RegisterAgent = compose( - + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss index 5ea8024f31..f00657fc1c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container { +.enroll-agent-wizard-container { .euiStep__title { font-style: normal; font-weight: 700; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx index d0099f8b92..ffa0acd811 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react'; import { EuiCallOut, EuiSteps, EuiSpacer } from '@elastic/eui'; import './steps.scss'; import { FormattedMessage } from '@osd/i18n/react'; -import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/enroll-agent-data'; import { - IParseRegisterFormValues, - getRegisterAgentFormValues, - parseRegisterAgentFormValues, -} from '../../services/register-agent-services'; -import { useRegisterAgentCommands } from '../../hooks/use-register-agent-commands'; + IParseEnrollFormValues, + getEnrollAgentFormValues, + parseEnrollAgentFormValues, +} from '../../services/enroll-agent-services'; +import { useEnrollAgentCommands } from '../../hooks/use-enroll-agent-commands'; import { osCommandsDefinitions, optionalParamsDefinitions, @@ -32,7 +32,7 @@ import { FORM_FIELDS_LABEL, FORM_STEPS_LABELS, getServerCredentialsStepStatus, -} from '../../services/register-agent-steps-status-services'; +} from '../../services/enroll-agent-steps-status-services'; import OsCommandWarning from '../../components/command-output/os-warning'; interface IStepsProps { @@ -55,17 +55,17 @@ export const Steps = ({ form, osCard }: IStepsProps) => { agentName: '', serverAddress: '', }, - } as IParseRegisterFormValues; + } as IParseEnrollFormValues; const [missingStepsName, setMissingStepsName] = useState( [], ); const [invalidFieldsName, setInvalidFieldsName] = useState< FORM_FIELDS_LABEL[] >([]); - const [registerAgentFormValues, setRegisterAgentFormValues] = - useState(initialParsedFormValues); + const [enrollAgentFormValues, setEnrollAgentFormValues] = + useState(initialParsedFormValues); const { installCommand, startCommand, selectOS, setOptionalParams } = - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }); @@ -79,13 +79,13 @@ export const Steps = ({ form, osCard }: IStepsProps) => { useEffect(() => { // get form values and parse them divided in OS and optional params - const registerAgentFormValuesParsed = parseRegisterAgentFormValues( - getRegisterAgentFormValues(form), + const enrollAgentFormValuesParsed = parseEnrollAgentFormValues( + getEnrollAgentFormValues(form), OPERATING_SYSTEMS_OPTIONS, initialParsedFormValues, ); - setRegisterAgentFormValues(registerAgentFormValuesParsed); + setEnrollAgentFormValues(enrollAgentFormValuesParsed); setInstallCommandStepStatus( getAgentCommandsStepStatus(form.fields, installCommandWasCopied), ); @@ -98,19 +98,19 @@ export const Steps = ({ form, osCard }: IStepsProps) => { useEffect(() => { if ( - registerAgentFormValues.operatingSystem.name !== '' && - registerAgentFormValues.operatingSystem.architecture !== '' + enrollAgentFormValues.operatingSystem.name !== '' && + enrollAgentFormValues.operatingSystem.architecture !== '' ) { - selectOS(registerAgentFormValues.operatingSystem as TOperatingSystem); + selectOS(enrollAgentFormValues.operatingSystem as TOperatingSystem); } setOptionalParams( - { ...registerAgentFormValues.optionalParams }, - registerAgentFormValues.operatingSystem as TOperatingSystem, + { ...enrollAgentFormValues.optionalParams }, + enrollAgentFormValues.operatingSystem as TOperatingSystem, ); setInstallCommandWasCopied(false); setStartCommandWasCopied(false); - }, [registerAgentFormValues]); + }, [enrollAgentFormValues]); useEffect(() => { setInstallCommandStepStatus( @@ -124,7 +124,7 @@ export const Steps = ({ form, osCard }: IStepsProps) => { ); }, [startCommandWasCopied]); - const registerAgentFormSteps = [ + const enrollAgentFormSteps = [ { title: 'Select the package to download and install on your system:', children: osCard, @@ -193,12 +193,12 @@ export const Steps = ({ form, osCard }: IStepsProps) => { setInstallCommandWasCopied(true)} - password={registerAgentFormValues.optionalParams.password} + password={enrollAgentFormValues.optionalParams.password} /> ) : null} @@ -233,7 +233,7 @@ export const Steps = ({ form, osCard }: IStepsProps) => { setStartCommandWasCopied(true)} /> ) : null} @@ -243,5 +243,5 @@ export const Steps = ({ form, osCard }: IStepsProps) => { }, ]; - return ; + return ; }; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts index 2ba0dc2165..2eaeade29e 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts @@ -8,13 +8,13 @@ import { getRPMAMD64InstallCommand, getRPMARM64InstallCommand, getDEBARM64InstallCommand, -} from '../../services/register-agent-os-commands-services'; +} from '../../services/enroll-agent-os-commands-services'; import { scapeSpecialCharsForLinux, scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows, } from '../../services/wazuh-password-service'; -import { IOSDefinition, TOptionalParams } from '../register-commands/types'; +import { IOSDefinition, TOptionalParams } from '../enroll-commands/types'; // Defined OS combinations diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md index 0fc4678a00..2a4bbb9a85 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md @@ -1,6 +1,6 @@ # Documentation -- [Register Agent](#register-agent) +- [Enroll Agent](#enroll-agent) - [Solution details](#solution-details) - [Configuration details](#configuration-details) - [OS Definitions](#os-definitions) @@ -15,9 +15,9 @@ - [Get url package](#get-url-package) - [Get all commands](#get-all-commands) -# Register Agent +# Enroll Agent -The register agent is a process that will allow the user to register an agent in the 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. +The agent enrollment is a process that will allow the user to enroll an agent in the 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 diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md index 472d662339..18c98cea42 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md @@ -1,6 +1,6 @@ # Documentation -- [useRegisterAgentCommand hook](#useregisteragentcommand-hook) +- [useEnrollAgentCommand hook](#useenrollagentcommand-hook) - [Advantages](#advantages) - [Usage](#usage) - [Types](#types) @@ -9,21 +9,21 @@ - [Hook with Generic types](#hook-with-generic-types) - [Operating systems types example](#operating-systems-types-example) -## useRegisterAgentCommand hook +## useEnrollAgentCommand 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. +This hook makes use of the `Command Generator class` to generate the commands to enroll 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 hook returns the methods envolved to create the enroll 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 { useEnrollAgentCommands } from 'path/to/use-enroll-agent-commands'; import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; @@ -39,7 +39,7 @@ const { installCommand, startCommand, optionalParamsParsed - } = useRegisterAgentCommands(); + } = useEnrollAgentCommands(); // select OS depending on the specified OS defined in the hook configuration selectOS({ @@ -75,7 +75,7 @@ export interface IOperationSystem { architecture: string; } -interface IUseRegisterCommandsProps< +interface IUseEnrollCommandsProps< OS extends IOperationSystem, Params extends string, > { @@ -92,7 +92,7 @@ export interface IOperationSystem { architecture: string; } -interface IUseRegisterCommandsOutput< +interface IUseEnrollCommandsOutput< OS extends IOperationSystem, Params extends string, > { @@ -163,7 +163,7 @@ const { installCommand, startCommand, optionalParamsParsed, -} = useRegisterAgentCommands( +} = useEnrollAgentCommands( OSdefintions, paramsDefinitions, ); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts similarity index 90% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts index 9d86d89dc6..51d6d6472a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts @@ -1,10 +1,7 @@ import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; -import { - IOSDefinition, - TOptionalParams, -} from '../core/register-commands/types'; -import { useRegisterAgentCommands } from './use-register-agent-commands'; +import { IOSDefinition, TOptionalParams } from '../core/enroll-commands/types'; +import { useEnrollAgentCommands } from './use-enroll-agent-commands'; type TOptionalParamsNames = 'optional1' | 'optional2'; @@ -71,10 +68,10 @@ export const optionalParamsDefinitions: TOptionalParams = }, }; -describe('useRegisterAgentCommands hook', () => { +describe('useEnrollAgentCommands hook', () => { it('should return installCommand and startCommand null when the hook is initialized', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -90,7 +87,7 @@ describe('useRegisterAgentCommands hook', () => { current: { selectOS }, }, } = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -112,7 +109,7 @@ describe('useRegisterAgentCommands hook', () => { it('should change the commands when the OS is selected successfully', async () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -139,7 +136,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return commands empty when set optional params and OS is NOT selected', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -159,7 +156,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return optional params empty when optional params are not added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -171,7 +168,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return optional params when optional params are added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -199,7 +196,7 @@ describe('useRegisterAgentCommands hook', () => { it('should update the commands when the OS is selected and optional params are added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts similarity index 85% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts index 9c39b82b53..0348735545 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts @@ -1,14 +1,14 @@ import { useEffect, useState } from 'react'; -import { CommandGenerator } from '../core/register-commands/command-generator/command-generator'; +import { CommandGenerator } from '../core/enroll-commands/command-generator/command-generator'; import { IOSDefinition, IOperationSystem, IOptionalParameters, TOptionalParams, -} from '../core/register-commands/types'; +} from '../core/enroll-commands/types'; import { version } from '../../../../../package.json'; -interface IUseRegisterCommandsProps< +interface IUseEnrollCommandsProps< OS extends IOperationSystem, Params extends string, > { @@ -16,7 +16,7 @@ interface IUseRegisterCommandsProps< optionalParamsDefinitions: TOptionalParams; } -interface IUseRegisterCommandsOutput< +interface IUseEnrollCommandsOutput< OS extends IOperationSystem, Params extends string, > { @@ -34,15 +34,15 @@ interface IUseRegisterCommandsOutput< * 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. + * @param {IUseEnrollCommandsProps} props - The properties to configure the command generator. + * @returns {IUseEnrollCommandsOutput} - An object containing the generated commands and methods to update the selected OS and optional parameters. */ -export function useRegisterAgentCommands< +export function useEnrollAgentCommands< OS extends IOperationSystem, Params extends string, >( - props: IUseRegisterCommandsProps, -): IUseRegisterCommandsOutput { + props: IUseEnrollCommandsProps, +): IUseEnrollCommandsOutput { const { osDefinitions, optionalParamsDefinitions } = props; // command generator settings const wazuhVersion = version; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx index 146589950a..d6a02b2ee2 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx @@ -1 +1 @@ -export { RegisterAgent } from './containers/register-agent/register-agent'; +export { EnrollAgent } from './containers/enroll-agent/enroll-agent'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts index 3ae633b6eb..853400d253 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts @@ -1,6 +1,6 @@ import { TOperatingSystem } from '../config/os-commands-definitions'; -interface RegisterAgentData { +interface EnrollAgentData { icon: string; title: TOperatingSystem['name']; hr: boolean; @@ -15,4 +15,4 @@ interface CheckboxGroupComponentProps { onChange: (id: string) => void; } -export type { RegisterAgentData, CheckboxGroupComponentProps }; +export type { EnrollAgentData, CheckboxGroupComponentProps }; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts similarity index 99% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts index dadfdd1c60..bfb3b59c1c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts @@ -11,7 +11,7 @@ import { getWindowsInstallCommand, getWindowsStartCommand, transformOptionalsParamatersMacOSCommand, -} from './register-agent-os-commands-services'; +} from './enroll-agent-os-commands-services'; let test: any; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx similarity index 98% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx index 789d37f09f..b21f29af2c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx @@ -3,8 +3,8 @@ import { IOptionalParameters, TOSEntryInstallCommand, TOSEntryProps, -} from '../core/register-commands/types'; -import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; +} from '../core/enroll-commands/types'; +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; export const getAllOptionals = ( optionals: IOptionalParameters, diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx similarity index 86% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx index a89d470aef..8766bbacd9 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx @@ -3,7 +3,7 @@ import { TOperatingSystem, TOptionalParameters, } from '../core/config/os-commands-definitions'; -import { RegisterAgentData } from '../interfaces/types'; +import { EnrollAgentData } from '../interfaces/types'; export interface ServerAddressOptions { label: string; @@ -38,14 +38,14 @@ export const parseNodesInOptions = ( nodetype: item.type, })); -export const getRegisterAgentFormValues = (form: UseFormReturn) => +export const getEnrollAgentFormValues = (form: UseFormReturn) => // return the values form the formFields and the value property Object.keys(form.fields).map(key => ({ name: key, value: form.fields[key].value, })); -export interface IParseRegisterFormValues { +export interface IParseEnrollFormValues { operatingSystem: { name: TOperatingSystem['name'] | ''; architecture: TOperatingSystem['architecture'] | ''; @@ -54,10 +54,10 @@ export interface IParseRegisterFormValues { optionalParams: Record; } -export const parseRegisterAgentFormValues = ( +export const parseEnrollAgentFormValues = ( formValues: { name: keyof UseFormReturn['fields']; value: any }[], - OSOptionsDefined: RegisterAgentData[], - initialValues?: IParseRegisterFormValues, + OSOptionsDefined: EnrollAgentData[], + initialValues?: IParseEnrollFormValues, ) => { // return the values form the formFields and the value property const parsedForm = @@ -68,7 +68,7 @@ export const parseRegisterAgentFormValues = ( name: '', }, optionalParams: {}, - } as IParseRegisterFormValues); + } as IParseEnrollFormValues); for (const field of formValues) { if (field.name === 'operatingSystemSelection') { diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx similarity index 97% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx index b77189ba57..3aa4d57aea 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx @@ -2,7 +2,7 @@ import { EuiStepStatus } from '@elastic/eui'; import { UseFormReturn } from '../components/form/types'; import { FormStepsDependencies, - RegisterAgentFormStatusManager, + EnrollAgentFormStatusManager, } from './form-status-manager'; const fieldsHaveErrors = ( @@ -233,7 +233,7 @@ export const getIncompleteSteps = ( username: ['username'], password: ['password'], }; - const statusManager = new RegisterAgentFormStatusManager(formFields, steps); + const statusManager = new EnrollAgentFormStatusManager(formFields, steps); // replace fields array using label names return statusManager @@ -257,7 +257,7 @@ export enum FORM_FIELDS_LABEL { export const getInvalidFields = ( formFields: UseFormReturn['fields'], ): FORM_FIELDS_LABEL[] => { - const statusManager = new RegisterAgentFormStatusManager(formFields); + const statusManager = new EnrollAgentFormStatusManager(formFields); return statusManager .getInvalidFields() diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx index 64ade460af..49fd32c466 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx @@ -4,7 +4,7 @@ import { } from '../../../components/common/form/types'; import { FormStepsDependencies, - RegisterAgentFormStatusManager, + EnrollAgentFormStatusManager, } from './form-status-manager'; const defaultFormFieldData: EnhancedFieldConfiguration = { @@ -40,20 +40,20 @@ const formFieldsDefault: UseFormReturn['fields'] = { }, }; -describe('RegisterAgentFormStatusManager', () => { +describe('EnrollAgentFormStatusManager', () => { it('should create a instance', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - expect(registerAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager).toBeDefined(); }); it('should return the form status', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - const formStatus = registerAgentFormStatusManager.getFormStatus(); + const formStatus = enrollAgentFormStatusManager.getFormStatus(); expect(formStatus).toEqual({ field1: 'empty', @@ -63,21 +63,21 @@ describe('RegisterAgentFormStatusManager', () => { }); it('should return the field status', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - const fieldStatus = registerAgentFormStatusManager.getFieldStatus('field1'); + const fieldStatus = enrollAgentFormStatusManager.getFieldStatus('field1'); expect(fieldStatus).toEqual('empty'); }); it('should return error if fieldname not found', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); expect(() => - registerAgentFormStatusManager.getFieldStatus('field4'), + enrollAgentFormStatusManager.getFieldStatus('field4'), ).toThrowError('Fieldname not found'); }); @@ -86,13 +86,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1', 'field2'], step2: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step1')).toEqual( 'invalid', ); }); @@ -102,13 +102,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1', 'field2'], step2: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step2')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step2')).toEqual( 'complete', ); }); @@ -118,13 +118,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1'], step2: ['field2', 'field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step1')).toEqual( 'empty', ); }); @@ -135,13 +135,13 @@ describe('RegisterAgentFormStatusManager', () => { step2: ['field2', 'field3'], step3: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getFormStepsStatus()).toEqual({ + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getFormStepsStatus()).toEqual({ step1: 'empty', step2: 'invalid', step3: 'complete', diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx index 75bed9dc2e..dc190b04fa 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx @@ -17,7 +17,7 @@ interface FormFieldsStatusManager { getFormStepsStatus: () => FormStepsStatus; } -export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { +export class EnrollAgentFormStatusManager implements FormFieldsStatusManager { constructor( private readonly formFields: FormFields, private readonly formSteps?: FormStepsDependencies, diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts index 9ba9d4100d..fb9bc9d904 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts @@ -1,4 +1,4 @@ -import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; export const scapeSpecialCharsForLinux = (password: string) => { const passwordScaped = password; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx similarity index 92% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx index 0030188a91..4ec57f0e49 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx @@ -1,4 +1,4 @@ -import { RegisterAgentData } from '../interfaces/types'; +import { EnrollAgentData } from '../interfaces/types'; import LinuxDarkIcon from '../assets/images/themes/dark/linux-icon.svg'; import LinuxLightIcon from '../assets/images/themes/light/linux-icon.svg'; import WindowsDarkIcon from '../assets/images/themes/dark/windows-icon.svg'; @@ -9,7 +9,7 @@ import { getCore } from '../../../../plugin-services'; const darkMode = getCore()?.uiSettings?.get('theme:darkMode'); -export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ +export const OPERATING_SYSTEMS_OPTIONS: EnrollAgentData[] = [ { icon: darkMode ? LinuxDarkIcon : LinuxLightIcon, title: 'LINUX', From fa5023d437b4985ae2437b69829679e9cab1bfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 14 Feb 2025 15:57:59 +0100 Subject: [PATCH 12/22] fix(enrollment): rename register to enroll references --- .../public/application/application.tsx | 4 +- .../command-output/command-output.dark.scss | 3 + .../command-output/command-output.scss | 2 +- .../optionals-inputs/optionals-inputs.tsx | 2 +- .../os-selector/os-card/os-card.tsx | 2 +- .../server-address/server-address.tsx | 2 +- .../enroll-agent.scss} | 2 +- .../enroll-agent.tsx} | 8 +- .../enroll-agent/containers/steps/steps.scss | 2 +- .../enroll-agent/containers/steps/steps.tsx | 52 +-- .../core/config/os-commands-definitions.ts | 4 +- .../core/enroll-commands/README.md | 341 ++++++++++++++++++ .../command-generator.test.ts | 0 .../command-generator/command-generator.ts | 0 .../exceptions/index.ts | 0 .../optional-parameters-manager.test.ts | 0 .../optional-parameters-manager.ts | 0 .../get-install-command.service.test.ts | 0 .../services/get-install-command.service.ts | 0 .../search-os-definitions.service.test.ts | 0 .../services/search-os-definitions.service.ts | 0 .../types.ts | 0 .../core/register-commands/README.md | 12 +- .../pages/enroll-agent/hooks/README.md | 18 +- ...t.ts => use-enroll-agent-commands.test.ts} | 23 +- ...mmands.ts => use-enroll-agent-commands.ts} | 18 +- .../application/pages/enroll-agent/index.tsx | 2 +- .../pages/enroll-agent/interfaces/types.ts | 4 +- ...enroll-agent-os-commands-services.test.ts} | 2 +- ... => enroll-agent-os-commands-services.tsx} | 4 +- ...services.tsx => enroll-agent-services.tsx} | 14 +- ...=> enroll-agent-steps-status-services.tsx} | 6 +- .../services/form-status-manager.test.tsx | 44 +-- .../services/form-status-manager.tsx | 2 +- .../services/wazuh-password-service.ts | 2 +- ...r-agent-data.tsx => enroll-agent-data.tsx} | 4 +- 36 files changed, 460 insertions(+), 119 deletions(-) create mode 100644 plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.dark.scss rename plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/{register-agent/register-agent.scss => enroll-agent/enroll-agent.scss} (72%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/{register-agent/register-agent.tsx => enroll-agent/enroll-agent.tsx} (94%) create mode 100644 plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/README.md rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/command-generator/command-generator.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/command-generator/command-generator.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/exceptions/index.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/optional-parameters-manager/optional-parameters-manager.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/optional-parameters-manager/optional-parameters-manager.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/services/get-install-command.service.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/services/get-install-command.service.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/services/search-os-definitions.service.test.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/services/search-os-definitions.service.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/core/{register-commands => enroll-commands}/types.ts (100%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/{use-register-agent-commands.test.ts => use-enroll-agent-commands.test.ts} (90%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/{use-register-agent-commands.ts => use-enroll-agent-commands.ts} (85%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-os-commands-services.test.ts => enroll-agent-os-commands-services.test.ts} (99%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-os-commands-services.tsx => enroll-agent-os-commands-services.tsx} (98%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-services.tsx => enroll-agent-services.tsx} (86%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/services/{register-agent-steps-status-services.tsx => enroll-agent-steps-status-services.tsx} (97%) rename plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/{register-agent-data.tsx => enroll-agent-data.tsx} (92%) diff --git a/plugins/wazuh-fleet/public/application/application.tsx b/plugins/wazuh-fleet/public/application/application.tsx index 2ee3e9143d..9a5dfe2764 100644 --- a/plugins/wazuh-fleet/public/application/application.tsx +++ b/plugins/wazuh-fleet/public/application/application.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { Router, Route, Switch, Redirect } from 'react-router-dom'; import { History } from 'history'; -import { RegisterAgent } from './pages/register-agent'; +import { EnrollAgent } from './pages/enroll-agent'; export function Application({ history }: { history: History }) { return ( - + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.dark.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.dark.scss new file mode 100644 index 0000000000..1a7f99fd47 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.dark.scss @@ -0,0 +1,3 @@ +.enroll-agent-wizard-container .copy-codeblock-wrapper .euiToolTipAnchor { + background-color: rgba(0, 0, 0, 0.7); +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss index d2f2d6bac3..6ac780c794 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container .copy-codeblock-wrapper { +.enroll-agent-wizard-container .copy-codeblock-wrapper { position: relative; .euiToolTipAnchor { diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx index 00546e5227..5884ce69f9 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/optionals-inputs.tsx @@ -10,7 +10,7 @@ import { } from '@elastic/eui'; import { UseFormReturn } from '../form/types'; import { InputForm } from '../form'; -import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/register-agent-data'; +import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/enroll-agent-data'; import { webDocumentationLink } from '../../services/web-documentation-link'; import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; import '../inputs/styles.scss'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx index 70ff056db9..d224f4918d 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/os-selector/os-card/os-card.tsx @@ -7,7 +7,7 @@ import { EuiLink, EuiCheckbox, } from '@elastic/eui'; -import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/register-agent-data'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/enroll-agent-data'; import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; import './os-card.scss'; import { webDocumentationLink } from '../../../services/web-documentation-link'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx index c203667781..3ef2b16a1a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx @@ -11,7 +11,7 @@ import { import React, { useState, useEffect } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; -import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; +import { SERVER_ADDRESS_TEXTS } from '../../utils/enroll-agent-data'; import { EnhancedFieldConfiguration } from '../form/types'; import { InputForm } from '../form'; import { webDocumentationLink } from '../../services/web-documentation-link'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss similarity index 72% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss index f54ef91389..2994f56ef4 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container { +.enroll-agent-wizard-container { box-sizing: border-box; background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx similarity index 94% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx index f74cc69d1b..121b5edbe6 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/register-agent/register-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx @@ -10,7 +10,7 @@ import { EuiProgress, } from '@elastic/eui'; import { compose } from 'redux'; -import './register-agent.scss'; +import './enroll-agent.scss'; import { Steps } from '../steps/steps'; import { InputForm } from '../../components/form'; import { useForm } from '../../components/form/hooks'; @@ -27,7 +27,7 @@ import { } from '../../../../../plugin-services'; import { version } from '../../../../../../package.json'; -export const RegisterAgent = compose( +export const EnrollAgent = compose( // TODO: add HOCs // withErrorBoundary, // withRouteResolvers({ enableMenu, ip, nestedResolve, savedSearch }), @@ -44,7 +44,7 @@ export const RegisterAgent = compose( // eslint-disable-next-line react/display-name WrappedComponent => props => , )(() => { - const configuration = {}; // Use a live state (reacts to changes through some hook that provides the configuration); + const configuration = {}; // TODO: Use a live state (reacts to changes through some hook that provides the configuration); const [wazuhVersion, setWazuhVersion] = useState(''); const [loading, setLoading] = useState(false); const initialFields: FormConfiguration = { @@ -137,7 +137,7 @@ export const RegisterAgent = compose( - + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss index 5ea8024f31..f00657fc1c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss @@ -1,4 +1,4 @@ -.register-agent-wizard-container { +.enroll-agent-wizard-container { .euiStep__title { font-style: normal; font-weight: 700; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx index d0099f8b92..ffa0acd811 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx @@ -2,13 +2,13 @@ import React, { useEffect, useState } from 'react'; import { EuiCallOut, EuiSteps, EuiSpacer } from '@elastic/eui'; import './steps.scss'; import { FormattedMessage } from '@osd/i18n/react'; -import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/enroll-agent-data'; import { - IParseRegisterFormValues, - getRegisterAgentFormValues, - parseRegisterAgentFormValues, -} from '../../services/register-agent-services'; -import { useRegisterAgentCommands } from '../../hooks/use-register-agent-commands'; + IParseEnrollFormValues, + getEnrollAgentFormValues, + parseEnrollAgentFormValues, +} from '../../services/enroll-agent-services'; +import { useEnrollAgentCommands } from '../../hooks/use-enroll-agent-commands'; import { osCommandsDefinitions, optionalParamsDefinitions, @@ -32,7 +32,7 @@ import { FORM_FIELDS_LABEL, FORM_STEPS_LABELS, getServerCredentialsStepStatus, -} from '../../services/register-agent-steps-status-services'; +} from '../../services/enroll-agent-steps-status-services'; import OsCommandWarning from '../../components/command-output/os-warning'; interface IStepsProps { @@ -55,17 +55,17 @@ export const Steps = ({ form, osCard }: IStepsProps) => { agentName: '', serverAddress: '', }, - } as IParseRegisterFormValues; + } as IParseEnrollFormValues; const [missingStepsName, setMissingStepsName] = useState( [], ); const [invalidFieldsName, setInvalidFieldsName] = useState< FORM_FIELDS_LABEL[] >([]); - const [registerAgentFormValues, setRegisterAgentFormValues] = - useState(initialParsedFormValues); + const [enrollAgentFormValues, setEnrollAgentFormValues] = + useState(initialParsedFormValues); const { installCommand, startCommand, selectOS, setOptionalParams } = - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }); @@ -79,13 +79,13 @@ export const Steps = ({ form, osCard }: IStepsProps) => { useEffect(() => { // get form values and parse them divided in OS and optional params - const registerAgentFormValuesParsed = parseRegisterAgentFormValues( - getRegisterAgentFormValues(form), + const enrollAgentFormValuesParsed = parseEnrollAgentFormValues( + getEnrollAgentFormValues(form), OPERATING_SYSTEMS_OPTIONS, initialParsedFormValues, ); - setRegisterAgentFormValues(registerAgentFormValuesParsed); + setEnrollAgentFormValues(enrollAgentFormValuesParsed); setInstallCommandStepStatus( getAgentCommandsStepStatus(form.fields, installCommandWasCopied), ); @@ -98,19 +98,19 @@ export const Steps = ({ form, osCard }: IStepsProps) => { useEffect(() => { if ( - registerAgentFormValues.operatingSystem.name !== '' && - registerAgentFormValues.operatingSystem.architecture !== '' + enrollAgentFormValues.operatingSystem.name !== '' && + enrollAgentFormValues.operatingSystem.architecture !== '' ) { - selectOS(registerAgentFormValues.operatingSystem as TOperatingSystem); + selectOS(enrollAgentFormValues.operatingSystem as TOperatingSystem); } setOptionalParams( - { ...registerAgentFormValues.optionalParams }, - registerAgentFormValues.operatingSystem as TOperatingSystem, + { ...enrollAgentFormValues.optionalParams }, + enrollAgentFormValues.operatingSystem as TOperatingSystem, ); setInstallCommandWasCopied(false); setStartCommandWasCopied(false); - }, [registerAgentFormValues]); + }, [enrollAgentFormValues]); useEffect(() => { setInstallCommandStepStatus( @@ -124,7 +124,7 @@ export const Steps = ({ form, osCard }: IStepsProps) => { ); }, [startCommandWasCopied]); - const registerAgentFormSteps = [ + const enrollAgentFormSteps = [ { title: 'Select the package to download and install on your system:', children: osCard, @@ -193,12 +193,12 @@ export const Steps = ({ form, osCard }: IStepsProps) => { setInstallCommandWasCopied(true)} - password={registerAgentFormValues.optionalParams.password} + password={enrollAgentFormValues.optionalParams.password} /> ) : null} @@ -233,7 +233,7 @@ export const Steps = ({ form, osCard }: IStepsProps) => { setStartCommandWasCopied(true)} /> ) : null} @@ -243,5 +243,5 @@ export const Steps = ({ form, osCard }: IStepsProps) => { }, ]; - return ; + return ; }; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts index 2ba0dc2165..2eaeade29e 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts @@ -8,13 +8,13 @@ import { getRPMAMD64InstallCommand, getRPMARM64InstallCommand, getDEBARM64InstallCommand, -} from '../../services/register-agent-os-commands-services'; +} from '../../services/enroll-agent-os-commands-services'; import { scapeSpecialCharsForLinux, scapeSpecialCharsForMacOS, scapeSpecialCharsForWindows, } from '../../services/wazuh-password-service'; -import { IOSDefinition, TOptionalParams } from '../register-commands/types'; +import { IOSDefinition, TOptionalParams } from '../enroll-commands/types'; // Defined OS combinations diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/README.md new file mode 100644 index 0000000000..8b66ec38e2 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/README.md @@ -0,0 +1,341 @@ +# Documentation + +- [Enroll Agent](#enroll-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) + +# Enroll Agent + +The agent enrollment is a process that will allow the user to enroll an agent in the 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 enrollment commands and will show them to the user. + +# Solution details + +To optimize and make more easier the process to generate the enrollment commands we have created a class called `Command Generator` that given a set of parameters it will generate the enrollment 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 enrollment 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; +} + +/// .... + +interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +type TOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options + +type TOptionalParameters = + | 'server_address' + | 'agent_name' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; + +export interface IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +export interface IOSCommandsDefinition< + OS extends IOperationSystem, + Param extends string, +> { + 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: [ + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + } + ], +}, +{ + name: 'windows', + options: [ + { + 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 enrollment commands. The parameters are the following: + +```ts +export type TOptionalParamsName = + | 'server_address' + | 'agent_name' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; + +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: '--url', + 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< + OS extends IOperationSystem, + Params extends string, +> 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', +}); + +// 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', +}); + +// 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', +}); + +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', +}); + +// 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; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} +``` diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/command-generator/command-generator.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/exceptions/index.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/exceptions/index.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/exceptions/index.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/exceptions/index.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/get-install-command.service.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.test.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.test.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/services/search-os-definitions.service.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/types.ts similarity index 100% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/types.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/types.ts diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md index 0fc4678a00..8b66ec38e2 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-commands/README.md @@ -1,6 +1,6 @@ # Documentation -- [Register Agent](#register-agent) +- [Enroll Agent](#enroll-agent) - [Solution details](#solution-details) - [Configuration details](#configuration-details) - [OS Definitions](#os-definitions) @@ -15,13 +15,13 @@ - [Get url package](#get-url-package) - [Get all commands](#get-all-commands) -# Register Agent +# Enroll Agent -The register agent is a process that will allow the user to register an agent in the 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. +The agent enrollment is a process that will allow the user to enroll an agent in the 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 enrollment 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. +To optimize and make more easier the process to generate the enrollment commands we have created a class called `Command Generator` that given a set of parameters it will generate the enrollment commands. ## Configuration @@ -29,7 +29,7 @@ To make the command generator works we need to configure the following parameter ## OS Definitions -The OS definitions are a set of parameters that will be used to generate the registration commands. The parameters are the following: +The OS definitions are a set of parameters that will be used to generate the enrollment commands. The parameters are the following: ```ts // global types @@ -145,7 +145,7 @@ Another validations will be provided in development time and will be provided by ## Optional Parameters Configuration -The optional parameters are a set of parameters that will be added to the registration commands. The parameters are the following: +The optional parameters are a set of parameters that will be added to the enrollment commands. The parameters are the following: ```ts export type TOptionalParamsName = diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md index 472d662339..18c98cea42 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md @@ -1,6 +1,6 @@ # Documentation -- [useRegisterAgentCommand hook](#useregisteragentcommand-hook) +- [useEnrollAgentCommand hook](#useenrollagentcommand-hook) - [Advantages](#advantages) - [Usage](#usage) - [Types](#types) @@ -9,21 +9,21 @@ - [Hook with Generic types](#hook-with-generic-types) - [Operating systems types example](#operating-systems-types-example) -## useRegisterAgentCommand hook +## useEnrollAgentCommand 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. +This hook makes use of the `Command Generator class` to generate the commands to enroll 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 hook returns the methods envolved to create the enroll 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 { useEnrollAgentCommands } from 'path/to/use-enroll-agent-commands'; import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; @@ -39,7 +39,7 @@ const { installCommand, startCommand, optionalParamsParsed - } = useRegisterAgentCommands(); + } = useEnrollAgentCommands(); // select OS depending on the specified OS defined in the hook configuration selectOS({ @@ -75,7 +75,7 @@ export interface IOperationSystem { architecture: string; } -interface IUseRegisterCommandsProps< +interface IUseEnrollCommandsProps< OS extends IOperationSystem, Params extends string, > { @@ -92,7 +92,7 @@ export interface IOperationSystem { architecture: string; } -interface IUseRegisterCommandsOutput< +interface IUseEnrollCommandsOutput< OS extends IOperationSystem, Params extends string, > { @@ -163,7 +163,7 @@ const { installCommand, startCommand, optionalParamsParsed, -} = useRegisterAgentCommands( +} = useEnrollAgentCommands( OSdefintions, paramsDefinitions, ); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts similarity index 90% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts index 9d86d89dc6..51d6d6472a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts @@ -1,10 +1,7 @@ import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; -import { - IOSDefinition, - TOptionalParams, -} from '../core/register-commands/types'; -import { useRegisterAgentCommands } from './use-register-agent-commands'; +import { IOSDefinition, TOptionalParams } from '../core/enroll-commands/types'; +import { useEnrollAgentCommands } from './use-enroll-agent-commands'; type TOptionalParamsNames = 'optional1' | 'optional2'; @@ -71,10 +68,10 @@ export const optionalParamsDefinitions: TOptionalParams = }, }; -describe('useRegisterAgentCommands hook', () => { +describe('useEnrollAgentCommands hook', () => { it('should return installCommand and startCommand null when the hook is initialized', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -90,7 +87,7 @@ describe('useRegisterAgentCommands hook', () => { current: { selectOS }, }, } = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -112,7 +109,7 @@ describe('useRegisterAgentCommands hook', () => { it('should change the commands when the OS is selected successfully', async () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -139,7 +136,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return commands empty when set optional params and OS is NOT selected', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -159,7 +156,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return optional params empty when optional params are not added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -171,7 +168,7 @@ describe('useRegisterAgentCommands hook', () => { it('should return optional params when optional params are added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), @@ -199,7 +196,7 @@ describe('useRegisterAgentCommands hook', () => { it('should update the commands when the OS is selected and optional params are added', () => { const hook = renderHook(() => - useRegisterAgentCommands({ + useEnrollAgentCommands({ osDefinitions: osCommandsDefinitions, optionalParamsDefinitions: optionalParamsDefinitions, }), diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts similarity index 85% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts index 9c39b82b53..0348735545 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-register-agent-commands.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts @@ -1,14 +1,14 @@ import { useEffect, useState } from 'react'; -import { CommandGenerator } from '../core/register-commands/command-generator/command-generator'; +import { CommandGenerator } from '../core/enroll-commands/command-generator/command-generator'; import { IOSDefinition, IOperationSystem, IOptionalParameters, TOptionalParams, -} from '../core/register-commands/types'; +} from '../core/enroll-commands/types'; import { version } from '../../../../../package.json'; -interface IUseRegisterCommandsProps< +interface IUseEnrollCommandsProps< OS extends IOperationSystem, Params extends string, > { @@ -16,7 +16,7 @@ interface IUseRegisterCommandsProps< optionalParamsDefinitions: TOptionalParams; } -interface IUseRegisterCommandsOutput< +interface IUseEnrollCommandsOutput< OS extends IOperationSystem, Params extends string, > { @@ -34,15 +34,15 @@ interface IUseRegisterCommandsOutput< * 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. + * @param {IUseEnrollCommandsProps} props - The properties to configure the command generator. + * @returns {IUseEnrollCommandsOutput} - An object containing the generated commands and methods to update the selected OS and optional parameters. */ -export function useRegisterAgentCommands< +export function useEnrollAgentCommands< OS extends IOperationSystem, Params extends string, >( - props: IUseRegisterCommandsProps, -): IUseRegisterCommandsOutput { + props: IUseEnrollCommandsProps, +): IUseEnrollCommandsOutput { const { osDefinitions, optionalParamsDefinitions } = props; // command generator settings const wazuhVersion = version; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx index 146589950a..d6a02b2ee2 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx @@ -1 +1 @@ -export { RegisterAgent } from './containers/register-agent/register-agent'; +export { EnrollAgent } from './containers/enroll-agent/enroll-agent'; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts index 3ae633b6eb..853400d253 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts @@ -1,6 +1,6 @@ import { TOperatingSystem } from '../config/os-commands-definitions'; -interface RegisterAgentData { +interface EnrollAgentData { icon: string; title: TOperatingSystem['name']; hr: boolean; @@ -15,4 +15,4 @@ interface CheckboxGroupComponentProps { onChange: (id: string) => void; } -export type { RegisterAgentData, CheckboxGroupComponentProps }; +export type { EnrollAgentData, CheckboxGroupComponentProps }; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts similarity index 99% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts index dadfdd1c60..bfb3b59c1c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts @@ -11,7 +11,7 @@ import { getWindowsInstallCommand, getWindowsStartCommand, transformOptionalsParamatersMacOSCommand, -} from './register-agent-os-commands-services'; +} from './enroll-agent-os-commands-services'; let test: any; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx similarity index 98% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx index 789d37f09f..b21f29af2c 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx @@ -3,8 +3,8 @@ import { IOptionalParameters, TOSEntryInstallCommand, TOSEntryProps, -} from '../core/register-commands/types'; -import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; +} from '../core/enroll-commands/types'; +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; export const getAllOptionals = ( optionals: IOptionalParameters, diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx similarity index 86% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx index a89d470aef..8766bbacd9 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx @@ -3,7 +3,7 @@ import { TOperatingSystem, TOptionalParameters, } from '../core/config/os-commands-definitions'; -import { RegisterAgentData } from '../interfaces/types'; +import { EnrollAgentData } from '../interfaces/types'; export interface ServerAddressOptions { label: string; @@ -38,14 +38,14 @@ export const parseNodesInOptions = ( nodetype: item.type, })); -export const getRegisterAgentFormValues = (form: UseFormReturn) => +export const getEnrollAgentFormValues = (form: UseFormReturn) => // return the values form the formFields and the value property Object.keys(form.fields).map(key => ({ name: key, value: form.fields[key].value, })); -export interface IParseRegisterFormValues { +export interface IParseEnrollFormValues { operatingSystem: { name: TOperatingSystem['name'] | ''; architecture: TOperatingSystem['architecture'] | ''; @@ -54,10 +54,10 @@ export interface IParseRegisterFormValues { optionalParams: Record; } -export const parseRegisterAgentFormValues = ( +export const parseEnrollAgentFormValues = ( formValues: { name: keyof UseFormReturn['fields']; value: any }[], - OSOptionsDefined: RegisterAgentData[], - initialValues?: IParseRegisterFormValues, + OSOptionsDefined: EnrollAgentData[], + initialValues?: IParseEnrollFormValues, ) => { // return the values form the formFields and the value property const parsedForm = @@ -68,7 +68,7 @@ export const parseRegisterAgentFormValues = ( name: '', }, optionalParams: {}, - } as IParseRegisterFormValues); + } as IParseEnrollFormValues); for (const field of formValues) { if (field.name === 'operatingSystemSelection') { diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx similarity index 97% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx index b77189ba57..3aa4d57aea 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/register-agent-steps-status-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx @@ -2,7 +2,7 @@ import { EuiStepStatus } from '@elastic/eui'; import { UseFormReturn } from '../components/form/types'; import { FormStepsDependencies, - RegisterAgentFormStatusManager, + EnrollAgentFormStatusManager, } from './form-status-manager'; const fieldsHaveErrors = ( @@ -233,7 +233,7 @@ export const getIncompleteSteps = ( username: ['username'], password: ['password'], }; - const statusManager = new RegisterAgentFormStatusManager(formFields, steps); + const statusManager = new EnrollAgentFormStatusManager(formFields, steps); // replace fields array using label names return statusManager @@ -257,7 +257,7 @@ export enum FORM_FIELDS_LABEL { export const getInvalidFields = ( formFields: UseFormReturn['fields'], ): FORM_FIELDS_LABEL[] => { - const statusManager = new RegisterAgentFormStatusManager(formFields); + const statusManager = new EnrollAgentFormStatusManager(formFields); return statusManager .getInvalidFields() diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx index 64ade460af..49fd32c466 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx @@ -4,7 +4,7 @@ import { } from '../../../components/common/form/types'; import { FormStepsDependencies, - RegisterAgentFormStatusManager, + EnrollAgentFormStatusManager, } from './form-status-manager'; const defaultFormFieldData: EnhancedFieldConfiguration = { @@ -40,20 +40,20 @@ const formFieldsDefault: UseFormReturn['fields'] = { }, }; -describe('RegisterAgentFormStatusManager', () => { +describe('EnrollAgentFormStatusManager', () => { it('should create a instance', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - expect(registerAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager).toBeDefined(); }); it('should return the form status', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - const formStatus = registerAgentFormStatusManager.getFormStatus(); + const formStatus = enrollAgentFormStatusManager.getFormStatus(); expect(formStatus).toEqual({ field1: 'empty', @@ -63,21 +63,21 @@ describe('RegisterAgentFormStatusManager', () => { }); it('should return the field status', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); - const fieldStatus = registerAgentFormStatusManager.getFieldStatus('field1'); + const fieldStatus = enrollAgentFormStatusManager.getFieldStatus('field1'); expect(fieldStatus).toEqual('empty'); }); it('should return error if fieldname not found', () => { - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, ); expect(() => - registerAgentFormStatusManager.getFieldStatus('field4'), + enrollAgentFormStatusManager.getFieldStatus('field4'), ).toThrowError('Fieldname not found'); }); @@ -86,13 +86,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1', 'field2'], step2: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step1')).toEqual( 'invalid', ); }); @@ -102,13 +102,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1', 'field2'], step2: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step2')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step2')).toEqual( 'complete', ); }); @@ -118,13 +118,13 @@ describe('RegisterAgentFormStatusManager', () => { step1: ['field1'], step2: ['field2', 'field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step1')).toEqual( 'empty', ); }); @@ -135,13 +135,13 @@ describe('RegisterAgentFormStatusManager', () => { step2: ['field2', 'field3'], step3: ['field3'], }; - const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( formFieldsDefault, formSteps, ); - expect(registerAgentFormStatusManager).toBeDefined(); - expect(registerAgentFormStatusManager.getFormStepsStatus()).toEqual({ + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getFormStepsStatus()).toEqual({ step1: 'empty', step2: 'invalid', step3: 'complete', diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx index 75bed9dc2e..dc190b04fa 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx @@ -17,7 +17,7 @@ interface FormFieldsStatusManager { getFormStepsStatus: () => FormStepsStatus; } -export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { +export class EnrollAgentFormStatusManager implements FormFieldsStatusManager { constructor( private readonly formFields: FormFields, private readonly formSteps?: FormStepsDependencies, diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts index 9ba9d4100d..fb9bc9d904 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts @@ -1,4 +1,4 @@ -import { TOperatingSystem } from '../hooks/use-register-agent-commands.test'; +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; export const scapeSpecialCharsForLinux = (password: string) => { const passwordScaped = password; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx similarity index 92% rename from plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx rename to plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx index 0030188a91..4ec57f0e49 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/register-agent-data.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx @@ -1,4 +1,4 @@ -import { RegisterAgentData } from '../interfaces/types'; +import { EnrollAgentData } from '../interfaces/types'; import LinuxDarkIcon from '../assets/images/themes/dark/linux-icon.svg'; import LinuxLightIcon from '../assets/images/themes/light/linux-icon.svg'; import WindowsDarkIcon from '../assets/images/themes/dark/windows-icon.svg'; @@ -9,7 +9,7 @@ import { getCore } from '../../../../plugin-services'; const darkMode = getCore()?.uiSettings?.get('theme:darkMode'); -export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ +export const OPERATING_SYSTEMS_OPTIONS: EnrollAgentData[] = [ { icon: darkMode ? LinuxDarkIcon : LinuxLightIcon, title: 'LINUX', From 4d4018265deb886c1d94ba84161d128e7c88bf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 14 Feb 2025 16:57:11 +0100 Subject: [PATCH 13/22] fix(enrollment): background color of copy overlay in dark mode --- .../components/command-output/command-output.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx index dc0e31600b..0bb1544f91 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx @@ -11,6 +11,15 @@ import React, { Fragment, useEffect, useState } from 'react'; import { TOperatingSystem } from '../../core/config/os-commands-definitions'; import { obfuscatePasswordInCommand } from '../../services/wazuh-password-service'; import './command-output.scss'; +import { getCore } from '../../../../../plugin-services'; + +const IS_DARK_THEME = getCore().uiSettings.get('theme:darkMode'); + +/* tslint-disable no-undef */ +if (IS_DARK_THEME) { + // eslint-disable-next-line unicorn/prefer-top-level-await + import('./command-output.dark.scss').then(); +} interface ICommandSectionProps { commandText: string; From cb4ef303d5142fdc6f874caface37af2b1fe4a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 17 Feb 2025 11:38:21 +0100 Subject: [PATCH 14/22] fix: remove a unused comment and enhance types --- .../public/application/application.tsx | 4 ++- .../enroll-agent/components/form/types.ts | 9 ++--- .../containers/enroll-agent/enroll-agent.tsx | 33 +++---------------- .../enroll-agent/containers/steps/steps.tsx | 11 +++---- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/application.tsx b/plugins/wazuh-fleet/public/application/application.tsx index 9a5dfe2764..63cb78b575 100644 --- a/plugins/wazuh-fleet/public/application/application.tsx +++ b/plugins/wazuh-fleet/public/application/application.tsx @@ -7,7 +7,9 @@ export function Application({ history }: { history: History }) { return ( - + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts index 06ee400f84..ceeabc245a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts @@ -23,13 +23,14 @@ export interface IInputForm { // / use form hook types export type SettingTypes = - | 'text' - | 'textarea' + | 'editor' + | 'filepicker' | 'number' + | 'password' | 'select' | 'switch' - | 'editor' - | 'filepicker'; + | 'text' + | 'textarea'; interface FieldConfiguration { initialValue: any; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx index 121b5edbe6..e579017f98 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx @@ -9,10 +9,8 @@ import { EuiSpacer, EuiProgress, } from '@elastic/eui'; -import { compose } from 'redux'; import './enroll-agent.scss'; import { Steps } from '../steps/steps'; -import { InputForm } from '../../components/form'; import { useForm } from '../../components/form/hooks'; import { FormConfiguration } from '../../components/form/types'; import { OsCard } from '../../components/os-selector/os-card/os-card'; @@ -27,25 +25,9 @@ import { } from '../../../../../plugin-services'; import { version } from '../../../../../../package.json'; -export const EnrollAgent = compose( - // TODO: add HOCs - // withErrorBoundary, - // withRouteResolvers({ enableMenu, ip, nestedResolve, savedSearch }), - // withGlobalBreadcrumb([ - // { - // text: endpointSummary.breadcrumbLabel, - // href: `#${endpointSummary.redirectTo()}`, - // }, - // { text: 'Enroll new agent' }, - // ]), - // withUserAuthorizationPrompt([ - // [{ action: 'agent:create', resource: '*:*:*' }], - // ]), - // eslint-disable-next-line react/display-name - WrappedComponent => props => , -)(() => { +export const EnrollAgent = () => { const configuration = {}; // TODO: Use a live state (reacts to changes through some hook that provides the configuration); - const [wazuhVersion, setWazuhVersion] = useState(''); + const [_wazuhVersion, setWazuhVersion] = useState(''); const [loading, setLoading] = useState(false); const initialFields: FormConfiguration = { operatingSystemSelection: { @@ -113,11 +95,10 @@ export const EnrollAgent = compose( try { const wazuhVersion = await getWazuhVersion(); - // get wazuh password configuration setWazuhVersion(wazuhVersion); setLoading(false); } catch { - setWazuhVersion(wazuhVersion); + setWazuhVersion(version); setLoading(false); // TODO: manage error @@ -127,10 +108,6 @@ export const EnrollAgent = compose( fetchData(); }, []); - const osCard = ( - - ); - return (
@@ -155,7 +132,7 @@ export const EnrollAgent = compose( ) : ( - + )} @@ -165,4 +142,4 @@ export const EnrollAgent = compose(
); -}); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx index ffa0acd811..079ac3937a 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx @@ -34,18 +34,15 @@ import { getServerCredentialsStepStatus, } from '../../services/enroll-agent-steps-status-services'; import OsCommandWarning from '../../components/command-output/os-warning'; +import { InputForm } from '../../components/form'; interface IStepsProps { form: UseFormReturn; - osCard: React.ReactElement; - connection: { - isUDP: boolean; - }; } const FORM_MESSAGE_CONJUNTION = ' and '; -export const Steps = ({ form, osCard }: IStepsProps) => { +export const Steps = ({ form }: IStepsProps) => { const initialParsedFormValues = { operatingSystem: { name: '', @@ -127,7 +124,9 @@ export const Steps = ({ form, osCard }: IStepsProps) => { const enrollAgentFormSteps = [ { title: 'Select the package to download and install on your system:', - children: osCard, + children: ( + + ), status: getOSSelectorStepStatus(form.fields), }, { From 58f454924499145e62ec68ed0f78b45e09f365a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 17 Feb 2025 11:43:40 +0100 Subject: [PATCH 15/22] fix(enrollment): remove unused data from fetching Wazuh server API version and loading effect --- .../containers/enroll-agent/enroll-agent.tsx | 56 ++----------------- 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx index e579017f98..bd6ae22c04 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -7,7 +7,6 @@ import { EuiPage, EuiPageBody, EuiSpacer, - EuiProgress, } from '@elastic/eui'; import './enroll-agent.scss'; import { Steps } from '../steps/steps'; @@ -19,16 +18,10 @@ import { validateEnrollmentKey, validateServerAddress, } from '../../utils/validations'; -import { - getEnrollAgentManagement, - getWazuhCore, -} from '../../../../../plugin-services'; -import { version } from '../../../../../../package.json'; +import { getEnrollAgentManagement } from '../../../../../plugin-services'; export const EnrollAgent = () => { const configuration = {}; // TODO: Use a live state (reacts to changes through some hook that provides the configuration); - const [_wazuhVersion, setWazuhVersion] = useState(''); - const [loading, setLoading] = useState(false); const initialFields: FormConfiguration = { operatingSystemSelection: { type: 'custom', @@ -78,36 +71,6 @@ export const EnrollAgent = () => { }; const form = useForm(initialFields); - const getWazuhVersion = async () => { - try { - const result = await getWazuhCore().http.server.request('GET', '/', {}); - - return result?.data?.data?.api_version; - } catch { - // TODO: manage error - - return version; - } - }; - - useEffect(() => { - const fetchData = async () => { - try { - const wazuhVersion = await getWazuhVersion(); - - setWazuhVersion(wazuhVersion); - setLoading(false); - } catch { - setWazuhVersion(version); - setLoading(false); - - // TODO: manage error - } - }; - - fetchData(); - }, []); - return (
@@ -123,18 +86,9 @@ export const EnrollAgent = () => { - {loading ? ( - <> - - - - - - ) : ( - - - - )} + + + From d5851d2192a0265eda572176c95b9a81c225c1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 17 Feb 2025 14:47:52 +0100 Subject: [PATCH 16/22] fet(enrollment): add test toe nsure the step status --- ...nroll-agent-steps-status-services.test.tsx | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.test.tsx diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.test.tsx new file mode 100644 index 0000000000..fce144c0fe --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.test.tsx @@ -0,0 +1,149 @@ +import { + getAgentCommandsStepStatus, + getOSSelectorStepStatus, + getServerAddressStepStatus, + getServerCredentialsStepStatus, + getOptionalParameterStepStatus, + showCommandsSections, + getIncompleteSteps, + getInvalidFields, +} from './enroll-agent-steps-status-services'; + +describe('status steps', () => { + it.each` + title | form | resultStatus + ${'Operating system step status current'} | ${{ operatingSystemSelection: { value: '' } }} | ${'current'} + ${'Operating system step status complete'} | ${{ operatingSystemSelection: { value: 'test' } }} | ${'complete'} + `('$title', ({ form, resultStatus }) => { + expect(getOSSelectorStepStatus(form)).toBe(resultStatus); + }); + + it.each` + title | form | resultStatus + ${'Server address step status disabled: no operating system defined'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: '', error: null } }} | ${'disabled'} + ${'Server address step status disabled: operating system has error'} | ${{ operatingSystemSelection: { value: 'test', error: 'error' }, serverAddress: { value: '', error: null } }} | ${'disabled'} + ${'Server address step status current: server address is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null } }} | ${'current'} + ${'Server address step status current: server address has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: 'error' } }} | ${'current'} + ${'Server address step status complete'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null } }} | ${'complete'} + `('$title', ({ form, resultStatus }) => { + expect(getServerAddressStepStatus(form)).toBe(resultStatus); + }); + + it.each` + title | form | resultStatus + ${'Credentials step status disabled: no operating system defined'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null } }} | ${'disabled'} + ${'Credentials step status disabled: operating system has error'} | ${{ operatingSystemSelection: { value: '', error: 'error' }, serverAddress: { value: 'https://localhost:55000', error: null } }} | ${'disabled'} + ${'Credentials step status disabled: server addres is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null } }} | ${'disabled'} + ${'Credentials step status disabled: server addres has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' } }} | ${'disabled'} + ${'Credentials step status current: username is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: '', error: null }, password: { value: '', error: null } }} | ${'current'} + ${'Credentials step status current: username has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: '', error: 'error' }, password: { value: '', error: null } }} | ${'current'} + ${'Credentials step status current: password is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: '', error: null } }} | ${'current'} + ${'Credentials step status current: password has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: '', error: 'error' } }} | ${'current'} + ${'Credentials step status complete'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${'complete'} + `('$title', ({ form, resultStatus }) => { + expect(getServerCredentialsStepStatus(form)).toBe(resultStatus); + }); + + it.each` + title | form | installCommandWasCopied | resultStatus + ${'Optionals step status disabled: no operating system defined'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: operating system has error'} | ${{ operatingSystemSelection: { value: '', error: 'error' }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: server address is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: server address has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: username is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: '', error: null }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: username has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: password is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: '', error: null } }} | ${false} | ${'disabled'} + ${'Optionals step status disabled: password has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: 'error' } }} | ${false} | ${'disabled'} + ${'Optionals step status complete: install command was copied'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${true} | ${'complete'} + ${'Optionals step status current: agent name is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Optionals step status current: agent name has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: 'error' }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Optionals step status current: verificationMode is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Optionals step status current: verificationMode has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: 'error' }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Optionals step status current: enrollmentKey is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${false} | ${'current'} + ${'Optionals step status current: enrollmentKey has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: 'error' } }} | ${false} | ${'current'} + ${'Optionals step status complete: '} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'complete'} + `('$title', ({ form, installCommandWasCopied, resultStatus }) => { + expect(getOptionalParameterStepStatus(form, installCommandWasCopied)).toBe( + resultStatus, + ); + }); + + it.each` + title | form | resultStatus + ${'Show install/start commands: no operating system defined'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: operating system has error'} | ${{ operatingSystemSelection: { value: '', error: 'error' }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: server address is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: server address has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: username is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: '', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: username has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: password is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: '', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: password has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: 'error' }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: agent name is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${true} + ${'Show install/start commands: agent name has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: 'error' }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: verification mode is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${true} + ${'Show install/start commands: verification mode has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: 'error' }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} + ${'Show install/start commands: enrollment key is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${true} + ${'Show install/start commands: enrollment key has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '0000', error: 'error' } }} | ${false} + ${'Show install/start commands: agent name, verification mode and enrollment key are empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${true} + `('$title', ({ form, resultStatus }) => { + expect(showCommandsSections(form)).toBe(resultStatus); + }); + + it.each` + title | form | installCommandWasCopied | resultStatus + ${'Agent commands step status: no operating system defined'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: operating system has error'} | ${{ operatingSystemSelection: { value: '', error: 'error' }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: server address is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: server address has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: username is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: '', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: username has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: password is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: '', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: password has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: 'error' }, agentName: { value: 'agent', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: agent name is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Agent commands step status: agent name is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${true} | ${'complete'} + ${'Agent commands step status: agent name has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: 'error' }, verificationMode: { value: 'none', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: verification mode is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'current'} + ${'Agent commands step status: verification mode is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${true} | ${'complete'} + ${'Agent commands step status: verification mode has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: 'error' }, enrollmentKey: { value: '00000000000000000000000000000000', error: null } }} | ${false} | ${'disabled'} + ${'Agent commands step status: enrollment key is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${false} | ${'current'} + ${'Agent commands step status: enrollment key is empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${true} | ${'complete'} + ${'Agent commands step status: enrollment key has error'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: 'agent', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '0000', error: 'error' } }} | ${false} | ${'disabled'} + ${'Agent commands step status: agent name, verification mode and enrollment key are empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${false} | ${'current'} + ${'Agent commands step status: agent name, verification mode and enrollment key are empty'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null }, agentName: { value: '', error: null }, verificationMode: { value: '', error: null }, enrollmentKey: { value: '', error: null } }} | ${true} | ${'complete'} + `( + '$title: $resultStatus', + ({ form, installCommandWasCopied, resultStatus }) => { + console.log({ installCommandWasCopied }); + expect(getAgentCommandsStepStatus(form, installCommandWasCopied)).toBe( + resultStatus, + ); + }, + ); + + it.each` + title | form | resultStatus + ${'Get incomplete steps: missing operating system'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${['operating system']} + ${'Get incomplete steps: missing server address'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: '', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${['server address']} + ${'Get incomplete steps: missing username'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: '', error: null }, password: { value: 'password', error: null } }} | ${['username']} + ${'Get incomplete steps: missing password'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: '', error: null } }} | ${['password']} + ${'Get incomplete steps: missing operating system, server address, username, password'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: '', error: null }, username: { value: '', error: null }, password: { value: '', error: null } }} | ${['operating system', 'server address', 'username', 'password']} + ${'Get incomplete steps: missing operating system, server address, username'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: '', error: null }, username: { value: '', error: null }, password: { value: 'password', error: null } }} | ${['operating system', 'server address', 'username']} + ${'Get incomplete steps: missing operating system, server address'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: '', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${['operating system', 'server address']} + ${'Get incomplete steps: missing operating system, username'} | ${{ operatingSystemSelection: { value: '', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: '', error: null }, password: { value: 'password', error: null } }} | ${['operating system', 'username']} + `('$title', ({ form, resultStatus }) => { + expect(getIncompleteSteps(form)).toEqual(resultStatus); + }); + + it.each` + title | form | resultStatus + ${'Get invalid steps: invalid server address'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${['server address']} + ${'Get invalid steps: invalid username'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null } }} | ${['username']} + ${'Get invalid steps: invalid password'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: null }, username: { value: 'user', error: null }, password: { value: 'password', error: 'error' } }} | ${['password']} + ${'Get invalid steps: invalid server address, username, password'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: '', error: 'error' }, password: { value: '', error: 'error' } }} | ${['server address', 'username', 'password']} + ${'Get invalid steps: invalid server address, username'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null } }} | ${['server address', 'username']} + ${'Get invalid steps: invalid server address'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: null }, password: { value: 'password', error: null } }} | ${['server address']} + ${'Get invalid steps: invalid server address, username'} | ${{ operatingSystemSelection: { value: 'test', error: null }, serverAddress: { value: 'https://localhost:55000', error: 'error' }, username: { value: 'user', error: 'error' }, password: { value: 'password', error: null } }} | ${['server address', 'username']} + `('$title', ({ form, resultStatus }) => { + expect(getInvalidFields(form)).toEqual(resultStatus); + }); +}); From 2f252d0353ea4df0d47c6913d1320b6dc3a6dbb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Mon, 17 Feb 2025 17:23:17 +0100 Subject: [PATCH 17/22] feat(enrollment): replace --register-agent flag to --enroll-agent --- .../enroll-agent-os-commands-services.test.ts | 26 +++++++++---------- .../enroll-agent-os-commands-services.tsx | 12 ++++----- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts index bfb3b59c1c..f86534727f 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts @@ -71,14 +71,14 @@ describe('getDEBAMD64InstallCommand', () => { const result = getDEBAMD64InstallCommand(props); expect(result).toBe( - "sudo dpkg -i ./wazuh-agent_5.0.0-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent --url 'localhost' --username 'user' --password 'pass' --verification-mode 'none' --name 'agent1' --key '00000000000000000000000000000000'", + "sudo dpkg -i ./wazuh-agent_5.0.0-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent --url 'localhost' --username 'user' --password 'pass' --verification-mode 'none' --name 'agent1' --key '00000000000000000000000000000000'", ); }); }); describe('getDEBAMD64InstallCommand', () => { it('should return the correct command', () => { - let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -96,7 +96,7 @@ describe('getDEBAMD64InstallCommand', () => { delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -117,7 +117,7 @@ describe('getDEBAMD64InstallCommand', () => { describe('getDEBARM64InstallCommand', () => { it('should return the correct command', () => { - let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + let expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -135,7 +135,7 @@ describe('getDEBARM64InstallCommand', () => { delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -156,7 +156,7 @@ describe('getDEBARM64InstallCommand', () => { describe('getRPMAMD64InstallCommand', () => { it('should return the correct command', () => { - let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -174,7 +174,7 @@ describe('getRPMAMD64InstallCommand', () => { delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -195,7 +195,7 @@ describe('getRPMAMD64InstallCommand', () => { describe('getRPMARM64InstallCommand', () => { it('should return the correct command', () => { - let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + let expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -213,7 +213,7 @@ describe('getRPMARM64InstallCommand', () => { delete test.optionals.enrollmentKey; delete test.optionals.agentName; - expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${[ + expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -246,7 +246,7 @@ describe('getLinuxStartCommand', () => { describe('getWindowsInstallCommand', () => { it('should return the correct install command', () => { - let expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ + let expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -264,7 +264,7 @@ describe('getWindowsInstallCommand', () => { delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${[ + expected = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -334,7 +334,7 @@ describe('transformOptionalsParamatersMacOSCommand', () => { describe('getMacOsInstallCommand', () => { it('should return the correct macOS installation script', () => { - let expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${[ + let expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', @@ -351,7 +351,7 @@ describe('getMacOsInstallCommand', () => { delete test.optionals.wazuhPassword; delete test.optionals.agentName; - expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${[ + expected = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-agent ${[ 'serverAddress', 'username', 'password', diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx index b21f29af2c..8a4ecbc1f6 100644 --- a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx @@ -67,7 +67,7 @@ export const getDEBAMD64InstallCommand = ( return [ // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted `sudo dpkg -i ./${packageName}`, - `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, ].join(' && '); }; @@ -80,7 +80,7 @@ export const getDEBARM64InstallCommand = ( return [ // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted `sudo dpkg -i ./${packageName}`, - `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, ].join(' && '); }; @@ -95,7 +95,7 @@ export const getRPMAMD64InstallCommand = ( return [ // `curl -o ${packageName} ${urlPackage}`, // TODO: enable when the packages are publically hosted `sudo rpm -ihv ${packageName}`, - `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, ].join(' && '); }; @@ -108,7 +108,7 @@ export const getRPMARM64InstallCommand = ( return [ // `curl -o ${packageName} ${urlPackage}`, // TODO: enable when the packages are publically hosted `sudo rpm -ihv ${packageName}`, - `sudo /usr/share/wazuh-agent/bin/wazuh-agent --register-agent ${optionals && getAllOptionals(optionals)}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, ].join(' && '); }; @@ -130,7 +130,7 @@ export const getWindowsInstallCommand = ( return [ // `Invoke-WebRequest -Uri ${urlPackage} -OutFile \$env:tmp\\wazuh-agent;`, // TODO: enable when the packages are publically hosted // https://stackoverflow.com/questions/1673967/how-to-run-an-exe-file-in-powershell-with-parameters-with-spaces-and-quotes - `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --register-agent ${ + `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --enroll-agent ${ optionals && getAllOptionals(optionals, name) }`, ].join(' '); @@ -180,7 +180,7 @@ export const getMacOsInstallCommand = ( const macOSInstallationScript = [ // `curl -so wazuh-agent.pkg ${urlPackage}`, 'sudo installer -pkg ./wazuh-agent.pkg -target /', - `/Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --register-agent ${macOSInstallationOptions}`, + `/Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-agent ${macOSInstallationOptions}`, ].join(' && '); return macOSInstallationScript; From 4522188744a2fabeda68278ec88deb10441f321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 18 Feb 2025 11:35:59 +0100 Subject: [PATCH 18/22] ci: add wazuh-fleet plugin --- .github/workflows/dev-environment.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/dev-environment.yml b/.github/workflows/dev-environment.yml index 306789cdaa..2b185a8e2b 100644 --- a/.github/workflows/dev-environment.yml +++ b/.github/workflows/dev-environment.yml @@ -59,6 +59,11 @@ jobs: path: 'wazuh/plugins/wazuh-core', container_path: 'wazuh-core', }, + { + name: 'Wazuh Fleet Management', + path: 'wazuh/plugins/wazuh-fleet', + container_path: 'wazuh-fleet', + }, ] steps: From 283fe601fc71b037beb8b8fa7ba641d087c87bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 19 Feb 2025 09:55:45 +0100 Subject: [PATCH 19/22] chore: add entry to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d90e670b91..f5d2c85b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Added pinned agent data validation when rendering the Inventory data, Stats and Configuration tabs in Agent preview of Endpoints Summary [#6800](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6800) - Added wz-link component to make redirections [#6848](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6848) - Added embedded and customized `dom-to-image-more` dependency [#6902](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6902) +- Added `wazuh-fleet` plugin [#7289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7289) +- Added enrollment agent assistant to `wazuh-fleet` plugin [#7289](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7289) ### Changed From d8436c7e6affa1d85a228ba41f77d6e5730b078a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 19 Feb 2025 14:06:53 +0100 Subject: [PATCH 20/22] docs: add fleet-management module --- docs/ref/modules/fleet-management/api-reference.md | 1 + docs/ref/modules/fleet-management/architecture.md | 1 + docs/ref/modules/fleet-management/description.md | 1 + 3 files changed, 3 insertions(+) create mode 100644 docs/ref/modules/fleet-management/api-reference.md create mode 100644 docs/ref/modules/fleet-management/architecture.md create mode 100644 docs/ref/modules/fleet-management/description.md diff --git a/docs/ref/modules/fleet-management/api-reference.md b/docs/ref/modules/fleet-management/api-reference.md new file mode 100644 index 0000000000..71c531a3b8 --- /dev/null +++ b/docs/ref/modules/fleet-management/api-reference.md @@ -0,0 +1 @@ +# API reference diff --git a/docs/ref/modules/fleet-management/architecture.md b/docs/ref/modules/fleet-management/architecture.md new file mode 100644 index 0000000000..c79bec1ac6 --- /dev/null +++ b/docs/ref/modules/fleet-management/architecture.md @@ -0,0 +1 @@ +# Architecture diff --git a/docs/ref/modules/fleet-management/description.md b/docs/ref/modules/fleet-management/description.md new file mode 100644 index 0000000000..f719117cf3 --- /dev/null +++ b/docs/ref/modules/fleet-management/description.md @@ -0,0 +1 @@ +# Description From 240c301ce31029711afe55654b20024873f43706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 19 Feb 2025 14:18:21 +0100 Subject: [PATCH 21/22] docs(enrollment): add docs related to enrollment agent assistant --- .../modules/fleet-management/description.md | 10 +++++- .../enroll-agent-assistant.png | Bin 0 -> 193621 bytes .../enrollment-agent-assistant.md | 33 ++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/ref/modules/fleet-management/enroll-agent-assistant.png create mode 100644 docs/ref/modules/fleet-management/enrollment-agent-assistant.md diff --git a/docs/ref/modules/fleet-management/description.md b/docs/ref/modules/fleet-management/description.md index f719117cf3..3adcec7cb3 100644 --- a/docs/ref/modules/fleet-management/description.md +++ b/docs/ref/modules/fleet-management/description.md @@ -1 +1,9 @@ -# Description +# Fleet management + +## Description + +This module allows to manage the fleet of agents. + +## Contents + +- [Enrollment agent assistant](./enrollment-agent-assistant.md) diff --git a/docs/ref/modules/fleet-management/enroll-agent-assistant.png b/docs/ref/modules/fleet-management/enroll-agent-assistant.png new file mode 100644 index 0000000000000000000000000000000000000000..e633e1a52cb7fd52f04d7230bfac703d4f4f9a31 GIT binary patch literal 193621 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

%9I=_mJg_NQyg3NU>y(`sR(dbUPnAv6i6#< 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 HCB8^6!$rAoSA zuP>%xhd7(^4>Q|w767zd*N+EBYDbvNp5AXL1mBAmzvr<4-r2^h_j+JM z43$uZe;m%#baW$Y1H=a{yw6BYCF%_5pam) zrKp45M2(Efot|rwYi;`*Z3Op{mR-D7n(VjMy!SFK1A~LcEe-1r{}?qzatj8lQFvfx%jVS zSQ#L4kL&z4d`k5lm^BX|qui-GBmPG%F2X=k$>W*V>wgdIKN+>}|6}_9)1ywh<2&px zo#emBa)1cm`7(bhp-9$EJ@&mk(cb%qHSw$o{U(Ez{Kbw_k7RZN6Y8Q8=;HM#q)uve z`BaJ#WC^&^e(vA^(*^9Hq5IQ2zt8%2a=hJN8{coVuT$pAEX@h)z>$kJ$;(Z`-C?WzSFG{?2 zC-1jZ=Y6UxKpNTi34>K#ZHSEgZzjmT1#+hCG$}xZlpRaIp8!JtX@2Neg;rX>WrsrP zZUO1yjnCXbg=8LnzxCDgI*=Ppye9Rl!lb_6vbmWQzcT2x*bDz(>k5vCV!4qhUhmd~o>T`q)Mi^-UmY@yl>`7X zsXUXM9DTbtI!82X0CqLuuFJk^)HKsA-k;TXHy*P4x+Kv}re}X}zdYD{8GMaO_e0he zq|~!^LGQ@QF6s~+`C|GyfsuDSvp+F$X+DFkC&~X#p?2nUsSIed3j(*OR-q80WrhgY z?A>K=5HResZj!rd`=IU4(VjtVu7aY{F*~7Sbmp7anBPchZ?>y65WW7Dt#xY(^t;)Z zklsGnHK~Z{E!~R<+K?RQkTq{<2=tJ$%zz%j_ts9fgO6{WtOh1~=GujHM&rNoy?3Ff zjK9r)^ekM=+eOE8(MdB!(lqGo5cdcC!F-#i%SrbVKYN9xJ_ntvk3XqC=8S+TY}xxo zS>C2oXEHMvXDQlFU~}afOw0k@(yCJM$^JMY(IN$!<=Hx;KEaM2vJ^C0({d?k$BkYf z!y_cgaPBubHo!(%#6{Y(L%CPa!D{$GwL2i{OI8>}5bN?N1KD=4MR3Q(Je%}@&U+MG zwJa#?i<`k#$0r$U+C7OfoW#lW1O4}1-!Eb*#U(on%8QrZn}SXb9?~Ai>@d312w^oJ zqSiXk4l!N9Ggz5bHWOE7fGis7(a5&XdFR*-a~QL0+sF@3D(Jvj~I zfPLOOo+USVxbxRghW@z1-S6s%HTAwF@x8*ue$~mniO0%XsaX@!!;?0h_+p`U&)uih z5h8ylaOhG8dluYmasu&c#Be)K;%NDKdE*Y11~!2MlqS~O1-)}i!_#j^46&xxH)yYM z`{WLZz1Sp&^G&{MmIrJZR&yya-)c~deJCXt(`^Qu@xIx=lSLW9a(VSTVc}*lyDlv2KsBf*%s*M1WHEh4}= z>)dv}qd&Ad(V$)FQ}tf7T13)aV+yxYx1GgE(57Wrg!=0qW7b3gxk27Yz#^PZQ>*H? zhU)U~Vef}yPqGEyC$v5D`4GJ@+rFU_k*oRUt+wN<=#S{$p5=>y`uyWQW@YE1!I@8# z9?p6Ii|M$*v<+3n_p@=)C7Ee7$7|5(z+L6ozf8-`0`cB1jk)kRMi8HLc?xG_HUqn% zq&i`l+tI+T=je-l{z~}!BK9JpUO)dGEo>56gti#^C?wLIC~4P_8|CEy_CDSzmGiXP zt)qhyn1gC^gb2uU zGt&p|YhBW|!|l{P!OmZA`!0C#F+4RPv!0gJP$X!uihl%QYx_QEPjfqmPPJ>`t7`VF zeVn`Dp8WbN@{@#ouGSI=X(cCJ>6%l=t#Iew`ZS6I@BAU&b~14ZF{wwUNS3vS=G!s1 z5P2>-t`F*6Y9=EGzw+rb8i4Ck^cyS&y*JbQc%(7-C{t0oLFX*TpiCll=kAiNX@9EA zC=tCGm|t9NPO{jPZ;$&hr$eRW?bPQV4Sv0{^POIr*#F_--k7Y)l>54#Q|hzA>Pynk zG{4Qsj||lH4ong!LQuY}oqxV=;GDYoQDwBU zV8K1oYolEojjNYgbm|`1$SoBRJvSnuh9(aOyqB-g@+aq@XuWQ@W+q1XYJWAJ)%Zp& zO$dx^@PHnCyJ(vA2~XR0H{fq$KS7-_j~nXJt1f6Xtud@Ah(2;x?WI;8H{VW_q4We( z&TGfWfgST$uR!dByh!!AJ}#BZ`iTxt8(74Rci#A^w>Nv2t(eDb`)j;Q!#hsad&Vam z2;A{3%uYASudlcpI4EtTo#t&}=oXw*l%65f8`@oBFg2<=c_F3X>ku;m92nyiw(we| zt#-Q97j)p2eluuYl@^x~L;B8!^5@a4(3UH@(6n5?Fz7fVVpKR9L+@}{$F!uE8;wAT zWq|9eY=+@L)imQp4?rN%BuJGO7^$+Al*UCzm7%!ke`n-thw zvylMk!=x4d_&_mLVsL};anm)Y#kV@9!Vo?INnJ{LBZm=fo_1}cxm$`wsNp9OKAySz z=e})2sQ&H5LDXat zYGQ_oA8Li=r z{m?qB{A1|OFvl&rxL5qx!N0=jS#KyhzY^;+;v4kFO&{B2J(rd2*02YL1Bok%Vq+8v z&bY_8Y=PjS%wmyIG@W`78x>wSxe}+X!%6$vBL62DS;@SM-hPX;q10VdcXyQ?uyksN z2uVhEK=N^*YD>zTP1Ws(55~n{R?0=u=FB;t80?biRhlzU2K%dew8=G^x<^I)avtWY z`qr@gK5NY&Xq4di!`a~XeM(5*RL6}axrb9drIMN9p02^!UMp?JC-z>>9bl%?QTG-% zZbD--uX0pcVS0s$p{G@Rbg-}KWGan|)nOL0+R3nMGYK@8IJZC%Dk-c^y)vHCYVmvzI+rRY+};Jq84)_AFtR zyFsDnNHjXl#wl^>)usvSqY}4ezZ~mGsph$f2Bm&Le%d*UylIc}at+Y-yKC6T{g;gx zs!h}Pk(t6$cBl=51hzsMm}>k~(}xnK`200(*1sqYAhCKD;M=s|qO5mM2wtLK=9ENa z{)t(#lzg=^sePlOwfqJuT;;^N9ME~qSR00oRC*$+kA8;8peN@fCv?V9L9Z*^b_$Wz zg5P|)o!w=v;k{Nlbo*w8TWYb<3-S58(EJwRQh2D4oP=ud-xEmJrg^8Iby>KBNpu>2 zU6N47-vDszbxee0I>4A;>YT6-QI=w5z~MbVYXMTZcfEIa6ryUD?E<0Jw)7f|*f zPk6RqD>rOFak+n@PL2#e09)>o?++4DU@vN1Yj$>+)qxRNvePSaQ+9AF7+P$YmpWlyqa4?z`apYI;qb*`p^VadwM8=at)x zYPl6NGBdkS>ugim9Ta_Kko9|xZ9)Df<^0}(A7w;dLmW!%6c}C3>5bOaMyl|j@%J@w z(X|S?OZHq6K8F`c_05IT-LY}S4LWss4VZPM7s$1ESUnLfmFH^N*J|n&eP8I;q2Bm4 zaO7D#zFC6bHOe8C7alw{OtgN{zo>qwc(NmUw0Mnbj7f?Dy85{JNYG*&Zj{j0UTyL= zmtsw*_gmM6$_9;nbHHQmxu@lNay{=)0^$$B{0BvJUIBe7Se`Ys+%ZRjqgB;(6O zV%O_aH$A$I^(;TPZ_6L3R`e{~wXZBm=U|TH;ok{WcJswo#UCMy~1o;t~c4T6%%t*W)M*-@!vk z{Ef`Ci9@W^D$U@KB{RJ_?uaq?t}by$MdD5izObaWj=z#6E0m%T1S@LOj4rB5kHVHc zO;B%W=;Sc!1Z6(o;V^QVxTtA%33FfkFX8u)HrG*lm2G3)kw*wfUb^S>%lI3Xu zC2VkyX;uY{3lh^HP6+gTrwlfD6jMlhm4A~IUe3eQW<&)Ltxnd149O<86*iA|{9*$- zIL=SusOJ38eBJ~&(jS3r7_2(5Xw!E!!W~ipq}da33oX(Fla=$J;Rlpid;dzSs>HDc zpZ8JINUY7llRWzYua31u)3ytROCNOnPXKIu3TQ^Im4gtF=&8juADqjF)+%9we{yBI<^M{49Pa*~7Q$FnSd#H@Kx03A|nz zbqY*4WIYz{`T4~*Y*Z7B*I!5*tSn$zyWMgJF&SEC2ptocGI63a-r6ZenrATn1A%Gm<|i zE)#r_O%nI!l{90?**SOND~7qw3)4&dQqP~szrPW`nuewgZZql*XOP%Eb|9^?oA<%` z&);-qJCF%CeSJjV*^^A_b>Q-p>$+lqRVP|%^o1-`@J%CSKsKI72Ls8;OzE=pIMkgt z`WV~JfSZMO?c3o^>D?=XU(HgcAUk(`wF`5#3*~bh-AC@CCOIKAav7{bDW)vu>|M*@ zR{fDDsa(zMeGQ(;3%z_|Uw8E~cqa2$(mO}|q|BNv7YepKtHt{817J5* z5OZE%Qw5)EK)&rcG-Sh{5YoZ|?uu#&}7jn>P_8=KA4_iSH^JI-RFoX4ha-T|2;@+`>-gRD?5rWx0DqV~7x zQ3Fa@pW#I8)IOv?^vTGK^NHvKU)hZjUiQ^@MVy+Ev7oY*S8nCB0}%1Qg5)c-G^~dw zmOuwQ{_M3_E!N;Fz0fX;7K&|=X@ficS(Ny_@T$Rp-_`>}ZPmrHZJ2>DoEuE1}C?N4r$Ex|@tT4g3)hqV3zCiB$enjVr7hzy{xuf#1bqko=LLYkG+bxX zuz1NT&`TaY+qp!W3VnS}zQXb{WsHEYYQ8l?qSQTqs6bgG-c)5_Sr&09rX~-B!FOO_ znH~Q1Gh-W2N^EsyQ7(0>#zX1wy9kWaeOw^hFmRvRD&1AO3{IpySQdW4(LoG*g)yM= zs{(HDriRsPa{nz|aveDk+^aG-e(K=mg=NuD_oblT1LAIhG8FQg0y(ZxD z_erSq3~i|wAG=qVMzD1;sIdA%*+^uklX{n`uDC&x~&E zgUXlnJ;a~gVC0xMVuQ41V^KQZ)@%5E*kP}NYRE64PmH3d4l)?L1Ei%mK+?Br zL}`j;&Uq2f%^i!I8D&^ePDtv%dw)gZ`d7R)fglU53{t~u&pV7~*b-ID?aHq3@cqog zxy}iVd#$9rKvuJ%XIA?^-JJ2$(w~%-*G%PZEpU6~vsvvK7_+U$ z71O^G?|usmp-!;e(Sb=htz z$+bb1H7ztj<)usIt4;Y1NC>Q=);Tdz)DE`}P#4IT4W)^Wfn@lfVnH3XgX=LyoBKoP zfrt~ws3`X_A;0K5QY(ZBm%~ekZFb}7SWTtly~_b6x*m;&l!F@h z!7nWC_zm_e)PoeMQ`Pj^Fw*%89yO9B-tv<$c)+CCo4)cuJNva&;8l0L!YOeL3b^^% zKJDWj>GJAxtYq7pFZilYEQ5^XrEv8?ngaVtE!8P7@9tO>oXI4IxCRDNOy_M5?N<^p zGG3T3(5bsRqGppB92hcrXakmofxF1J2Fu+j8_@43rC&v5#COzmky5_OjA7pm-(t!A z?c_~`#NCe?X-6im)bS0-<})I*1wV$)u@4>Qs;_C{W2O(wC+CW(hPYXZF%(35&mE0- zH#5xgM+4L7o3dRiUVYsd7DN?b2EMJYq|;%?;g+9M76(jdmhnKm?VfmUk89-6+dXul z8+V0&dn?69LhNn$PA5g3A+~+fCQ%JV*#~wYNRt=r-=>FoozX{(W$5u4%cgtoLKcQ3PHnMYA z5SjM;MRAh3TV?9zG@!oVKgXaeDQ_GlUAUwv(LaIeKaC^-4R zs~FBS)B-FmN~w-2)JOe$ZAM#cl;=j1b2T%+ zZy4gZ*6~J9YLy!QR}Lj!T3g27Tm6Z62NTn#;Xrds<)_v*{J6BBq2g;;(gS=lYOLtt z-Xw!z9!;+MMdfg*MbE)+)D3xdhiSu*ZXc^uMg3-U^pTMU>WnjMU*gBc4>gi4>kmqJ z!L5$(<;lzZq7^!=6d9@!kK%EmR2N#AhJZ^eY&ADZVk|qn zSh}H6q2S9%n~FLJ>or?W3gR_zji5T*sAWe)p!Y8d0qNA#7Cqy%3Y4=mmiv$}kc>tl zQpzCT*ccUm2EndPR|^@FqC#EDD5XZArdksYit_~KQW}`x(vy|LF@@vrz7K|x!UQMS zj`L{KV6mCCtxST;aW|#D!`9eyj@r#vJW{h=o+~1?+|u*mH7_K4|1$bs4jgilIjw3=2H*$gz^?UL zZf?a>zm&e&aZ9cm$x0^mRl#V~P#zX^r8Fi~e!$xi^=8d>B0P5XW7BB_^A3HQw=J4b*)}l+F@geb4FGoHMYAeOaVHopHzb$lt_P7*Oi0vP&>H zD5|05tnr9UFr-Z3A#@)I5jfc~p@9?zK~hSsI7~0!31x)FYhx>(1OILHz}7B&rkr2? zat5oX&jH;YOX($6o11W^X2u-PMZzs*1u@f#Nk+u=*44@ZSY`cA)nzNCfs2^r5>I<3 zYYpIH>IP}EsVtmnf;bLQFnw&>EdL}WYzU#Rbfb|?JN4G&!L<}|qXUbt-`ClRab^a* z?bu|ajy_gSe9MbZz^9(v{csgzMe$*WbCbb%d3Akw0|@*&XU?WmXFuXD#4+|&bVgCS zG#Eb^9_uN=W7%xq#FGByn=%;1ui$ZSVBRx+#{!ztpHHAyCYHJTX1A@HW`3Qzyn*?u zzlxq~yUUnWEK-EOA8bY+R_>N3uy{Zt;qd)DE!(B#mU6th5Z1V_d^52xo>juENhRic zr)-Y?!DN5WGBlo2@Ui;-$EpUim4M z+@JCe`??VDs-!nTU?-tDc|TQj)RBgeSa%VM%2_LToW){}barj800-_>>n8%FW1Dz) z$EcevG93>OA#dvUs_nR!3BzE{B;=5rm=dqGer0D|zX?r|`yJsmkL}E%A9c?% ziT(wBJFZJ;;OvLc3ADK)!F%ieoaO5Gck(G)uy=+>_5=S!BH)X?ZHL`q%A@!RDYMHt zE2Wp#hBEu`;{4mlyt(!uwt0G|6me2oPb8zeHo%*=L5b1Ze-M{5*1p?6y_PypH8i0j za@KgK%8t^+yKS_J68DAVT_#6{3vUwq0w3t}qgZBRqzY?Wz^O&Nz9?;fqiYPOo>$Gy zyS;aow7lFRTJ0BoUVe2$rsjej#E>w;mELX`h>dkoQ}z$IUYbQTmpEL-A~ z-5Z$$WaSJ1nzwU&vErm7)3oo#QC437{c699P5oJ$mVGphgfFmsOxB6ZGKoz~N&kHC z_tri;#4naZ{0V&XOcBCLd#`bwm>u*xeQ@D-DFvvQ>_a9tcLs2D&|$;KL)XaO#^Oj9 zq8AI5JD-xOUc%B-{0)!J_QnV65!F0qX8LT^momn)w%|xM+!S`(en3eLI4qA=t5s1B z^Gmlh%b)~I76Rbw!T^0a;1;+U5_|h;zMmW#veZ&MSVLFTGwv}-H@cq`lr`@M(`B&F zw}+hV;RmeUY}GX<&v}!rD_;LTQ@j_>K%^ zdEt96XlQx2mg$Irh-yr#Ah_TnzLSF6@#~i=76WJYp#Wv>4jp%=?C3e*3dOLc?I#yl zm>IzS>_aW7EQ@ybsai`@kP%1`jj4eXT9HLo*kXM99%^O){MrS^&*l40th5yIq^0Qd z4NZD%OIKH(02d4)7Q>-uTGjCUoJjv-Oa|&FM+2-C( zfR))oS*-__oa&3i*O$qXV(<%(pXo2~9o?_4uY!b5FOv-olp9slw=Eyz`_)pTZs&Qq znIqo&?#(Ah$%l*dUnh#WbbB<=6!+XVj`wX-Geav;d4J{AYLK&Ym}SV4#of~lQgpOp zWkg3|(SnzYN%IU7vWw}KD;cTf6fJ`lWXioF(`G~snD}{M>Dw#P zcmZ0Y?{(`ezH9C-du{;TalA5`wILjLy^%#6UQ^vIDcxADKhZTew;?wIobWxET%-vF zoAtNNf-W<5$#e_|Qvj%d*D$ z=_$IKTn_r2t3Hb49G3v-%Ndg2$VULoLGnyz)1bNx5U5MpGinA3sUs2izZ(2%f~01y z!f%UA1m@{nR`gE2P{qtT%>_VPLMo^wHf(Y$(#00SLZr_FD3+|NtTI%;U@lKp<>cM| z`A`?x6Ys54dyZZb&x#{hPh`Lx4*W~#wD4tj^S(Kc2@y9xv8$QPz%E@^`BX;I4Pfl~c`UKm zj8D{jy4Qp*w)ETk@a%WM&h;NF|HO|c{{tBM|26iBJs&IcCz*7f#ugHc2&(WlwzuRQ zf7(PlA8d$WMVq5`aRn*I0Jjcu=v`j>&@xpv5BuFhbNuX@>S-Y6{mkeyWC`k_zeCo5 zI{}0N*w{*T(%rv*1DXATsF4f8{wxm$*3+o6sQcUBFl4{*m1j)|rz;J)UjwWaT%`5% z_0zaLpsoH>5Lkf!>~#acSYB)X^574^=OwEGia#1!c?i7Z2}itn@#wT^*BkYyeuLHh zqC}5`0YFdiRR*fxcoM&iydQ#~`x6riFleDQyxeqGPMa2TMuYu7?CO7hbGQcN;=Obq zv)wuk&IJZm_P@E-|FZdi2KIc}{|xN^u^t=JN)Gk)MI8GQihz3-vZDT6z>$Sprv3~MXo1kSHaFPvEHjBX4z5oNu-)QtjTipXVhUj73CPb^ zD9`N{_=ilwVAE`W;>HiI+%?wAosJbFQPaJ}X!C6TFk`cV!~e3e5)cqD^qVS)h!Dv) z)(LT~suBVGYIIW zuBic^;7D?);cH2dHC(i~1=+jHnQU3`;nY~1AzwWY@bU|p%8Oyl=+O-jdjQ591sCAz`!$7XT15P(){qP6Y`xrTPGzn>`t z>=G_n9SVO$bkMI09z<`j{#nbfAY;Z`N`Km **_NOTE:_** The user to enroll the agent must have permissions to execute this operation. + +The server address option is set with the `enrollment.dns` setting if this is defined that allows to the user avoids filling the input when accesing to this page. + +Once the required and optionals parameters are provided without errors, the commands to enroll the agent will be displayed, allowing to the user to copy them and execute them in the host. + +## Remember parameters + +Ther server address can be saved to be remember the next time the user access to the page through set the `enrollment.dns` setting using the save icon near to the server address input or through Dashboard Management > Advanced settings. + +> **_NOTE:_** Save the `enrollment.dns` setting requires write permissions in the tenant. + +The rest of parameters should be specified each time the user access to the enrollment agent assistant. From 5a44932a9e9fe7915a2656f9121fa4b3e67ebc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 19 Feb 2025 15:52:20 +0100 Subject: [PATCH 22/22] docs: add fleet management --- docs/SUMMARY.md | 5 +++++ docs/ref/modules/README.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b2510f2754..c854da31da 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -24,6 +24,11 @@ - [Installation](ref/getting-started/installation.md) - [Configuration](ref/configuration.md) - [Modules](ref/modules/README.md) + - [Fleet management](ref/modules/fleet-management/description.md) + - [Description](ref/modules/fleet-management/description.md) + - [Architecture](ref/modules/fleet-management/architecture.md) + - [API reference](ref/modules/fleet-management/api-reference.md) + - [Enrollment agent assistant](ref/modules/fleet-management/enrollment-agent-assistant.md) - [Upgrade](ref/upgrade.md) - [Uninstall](ref/uninstall.md) - [Back Up and Restore](ref/backup-restore.md) diff --git a/docs/ref/modules/README.md b/docs/ref/modules/README.md index a55ecc055e..a197b05b2e 100644 --- a/docs/ref/modules/README.md +++ b/docs/ref/modules/README.md @@ -1 +1,3 @@ # Modules + +- [Fleet management](./fleet-management/description.md)