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: 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 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) 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..3adcec7cb3 --- /dev/null +++ b/docs/ref/modules/fleet-management/description.md @@ -0,0 +1,9 @@ +# 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 0000000000..e633e1a52c Binary files /dev/null and b/docs/ref/modules/fleet-management/enroll-agent-assistant.png differ diff --git a/docs/ref/modules/fleet-management/enrollment-agent-assistant.md b/docs/ref/modules/fleet-management/enrollment-agent-assistant.md new file mode 100644 index 0000000000..33d8b40d13 --- /dev/null +++ b/docs/ref/modules/fleet-management/enrollment-agent-assistant.md @@ -0,0 +1,33 @@ +# Enrollment agent assistant + +The enrollment agent assistant provides a guide to download, install, enroll and start the agent in a new host. + +![Enroll agent assistant](enroll-agent-assistant.png) + +## Options: + +The user can specify the required and optional parameters to enroll the agent through a form: + +| Option | Type | Description | Default value | Allowed values | +| --------------------- | -------- | --------------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------- | +| Operating system | Required | Define the operating system of the host | - | Any provided in the form | +| Server address | Required | Define the URL of the Wazuh server | - | Any valid URL string (protocol://address:port) | +| Username | Required | Define the username of Wazuh server | - | Any string | +| Password | Required | Define the passowrd of the Wazuh server user | - | Any string | +| Agent name | Optional | Define the agent name. If not defined, it will be used the hostname | - | Any string with 2 or more charecters. Allowed characters: A-Za-z0-9.-\_ | +| SSL verification mode | Optional | Define the verification mode of certificates against the Wazuh server | none | none, full | +| Enrollment key | Optional | Define the enrollment key | - | Any alphanumeric string of 32 characters | + +> **_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. diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index af0f9feb0b..5966f177c8 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -594,20 +594,11 @@ export const PLUGIN_SETTINGS: Record = { category: SettingCategory.GENERAL, type: EpluginSettingType.text, defaultValue: '', - validate: SettingsValidator.compose( - SettingsValidator.isString, - 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), + // 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, + // ), }, hideManagerAlerts: { title: 'Hide manager alerts', @@ -1020,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.'} 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/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/application.tsx b/plugins/wazuh-fleet/public/application/application.tsx new file mode 100644 index 0000000000..63cb78b575 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/application.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Router, Route, Switch, Redirect } from 'react-router-dom'; +import { History } from 'history'; +import { EnrollAgent } from './pages/enroll-agent'; + +export function Application({ history }: { history: 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..91be0dc054 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/mount.ts @@ -0,0 +1,32 @@ +import { setEnrollAgentManagement } from '../plugin-services'; +import { AppSetup } from './types'; + +export function appSetup({ registerApp, enrollmentAgentManagement }: 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, + // ), + }); + + // 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/enroll-agent/assets/images/themes/dark/icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/icon.svg new file mode 100644 index 0000000000..966e74def8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/linux-icon.svg new file mode 100644 index 0000000000..c76c7d6328 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/logo.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/logo.svg new file mode 100644 index 0000000000..ea25e5d2f9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/logo.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/mac-icon.svg new file mode 100644 index 0000000000..2eae996a06 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/windows-icon.svg new file mode 100644 index 0000000000..74d5b551f8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/dark/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/icon.svg new file mode 100644 index 0000000000..cc9df8577f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/linux-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/logo.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/logo.svg new file mode 100644 index 0000000000..a931095bb7 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/mac-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/windows-icon.svg b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/assets/images/themes/light/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..6ac780c794 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.scss @@ -0,0 +1,47 @@ +.enroll-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/enroll-agent/components/command-output/command-output.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx new file mode 100644 index 0000000000..0bb1544f91 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/command-output.tsx @@ -0,0 +1,121 @@ +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 { 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; + 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) => { + if (onCopy) { + onCopy(); + } + + return command; // the return is needed to avoid a bug in EuiCopy + }; + + const [commandToShow, setCommandToShow] = useState(commandText); + + const obfuscatePassword = (password: string) => { + if (!password) { + return; + } + + if (!commandText) { + return; + } + + if (showPassword) { + setCommandToShow(commandText); + } else { + 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 ( + + + +
+ + {showCommand ? commandToShow : ''} + + {showCommand && ( + + {copy => ( +
onHandleCopy(copy())} + > +

+ Copy command +

+
+ )} +
+ )} +
+ {showCommand && havePassword ? ( + <> + + + + ) : ( + + )} +
+
+ ); +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/os-warning.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/command-output/os-warning.tsx new file mode 100644 index 0000000000..5c4da92c99 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/__snapshots__/index.test.tsx.snap b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..c5edbf210e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/hooks.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.test.tsx new file mode 100644 index 0000000000..bb88d34527 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.test.tsx @@ -0,0 +1,631 @@ +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'; + +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('[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 > 0 + ? 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 = (event: any) => { + setValue(event.target.value); + onChange(event); + }; + + 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/enroll-agent/components/form/hooks.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.tsx new file mode 100644 index 0000000000..75a7de5909 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/hooks.tsx @@ -0,0 +1,311 @@ +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 +} + +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 + * + * @param event + * @param type + * @returns event value + */ +function getValueFromEvent( + event: any, + type: SettingTypes | CustomSettingType, +): any { + return (getValueFromEventType[type] || getValueFromEventType.default)(event); +} + +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 = [], + }, +) { + // eslint-disable-next-line unicorn/no-array-reduce + 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, +) { + // 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); + + 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: [], + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ changed, error, value }, _, { pathFormState, fieldDefinition }) => { + if (changed) { + result.changed[pathFormState] = value; + } + + if (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/enroll-agent/components/form/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.test.tsx new file mode 100644 index 0000000000..1969795df7 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.test.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useForm } from './hooks'; +import { InputForm } from './index'; + +jest.mock('@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 > 0 ? undefined : 'Validation error: string can not be empty') }} + ${'text'} | ${'test spaces'} | ${undefined} | ${{ validate: value => (value.length > 0 ? 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/enroll-agent/components/form/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.tsx new file mode 100644 index 0000000000..fa5ab2a643 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/index.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +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; + 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 + ); +}; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-editor.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-editor.tsx new file mode 100644 index 0000000000..a8d49d4d1b --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-filepicker.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-filepicker.tsx new file mode 100644 index 0000000000..d76ea2d5a9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-number.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-number.tsx new file mode 100644 index 0000000000..c06e8f0cf8 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-password.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-password.tsx new file mode 100644 index 0000000000..bad109fe35 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-password.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { EuiFieldPassword } from '@elastic/eui'; +import { IInputFormType } from './types'; + +export const InputFormPassword = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, + options, +}: IInputFormType) => ( + +); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-select.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-select.tsx new file mode 100644 index 0000000000..1c7c3e8d86 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-switch.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-switch.tsx new file mode 100644 index 0000000000..faf3e7d642 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-text.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-text.tsx new file mode 100644 index 0000000000..9faf2ea577 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/input-textarea.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/input-textarea.tsx new file mode 100644 index 0000000000..0f28418341 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/form/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts new file mode 100644 index 0000000000..ceeabc245a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/form/types.ts @@ -0,0 +1,105 @@ +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 = + | 'editor' + | 'filepicker' + | 'number' + | 'password' + | 'select' + | 'switch' + | 'text' + | 'textarea'; + +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: Record; +} + +export type FormConfiguration = Record< + 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 type EnhancedFields = Record; + +export interface UseFormReturn { + fields: EnhancedFields; + changed: Record; + errors: Record; + undoChanges: () => void; + doChanges: () => void; + forEach: ( + value: any, + key: string, + form: { + formDefinition: any; + formState: any; + pathFieldFormDefinition: string[]; + pathFormState: string[]; + fieldDefinition: FormConfiguration; + }, + ) => Record; +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/inputs/styles.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/inputs/styles.scss new file mode 100644 index 0000000000..b77ac3057a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/inputs/styles.scss @@ -0,0 +1,5 @@ +.enrollment-agent-form-input-label { + font-weight: 700; + font-size: 12px; + line-height: 20px; +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-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 new file mode 100644 index 0000000000..5e618f6573 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/optionals-inputs/__snapshots__/enrollment-key-input.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Enrollment key input match the snapshopt 1`] = ` +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-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 new file mode 100644 index 0000000000..6d6e221ab0 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-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 new file mode 100644 index 0000000000..5a9fc37e83 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-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 new file mode 100644 index 0000000000..a71df1a16a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-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 new file mode 100644 index 0000000000..8cbaa9a08d --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/index.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.test.tsx new file mode 100644 index 0000000000..ad86f97fe9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/index.tsx new file mode 100644 index 0000000000..f30f8aa08f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/password-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.test.tsx new file mode 100644 index 0000000000..f66c8f43bb --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/password-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/password-input.tsx new file mode 100644 index 0000000000..383feaebae --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/username-input.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.test.tsx new file mode 100644 index 0000000000..e26a8fa7b5 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/security/username-input.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/security/username-input.tsx new file mode 100644 index 0000000000..43d80082d0 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/components/server-address/server-address.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx new file mode 100644 index 0000000000..3ef2b16a1a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/components/server-address/server-address.tsx @@ -0,0 +1,194 @@ +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiLink, + EuiToolTip, +} from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +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'; +import { PLUGIN_VERSION_SHORT } from '../../../../../../common/constants'; +import '../inputs/styles.scss'; +import { getEnrollAgentManagement } 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 [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 ( + <> + + {SERVER_ADDRESS_TEXTS.map((data, index) => ( + + + {data.subtitle} + + + ))} + + + + + + + + + + + + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + + + } + fullWidth={true} + placeholder='https://server-address:55000' + postInput={ + <> + + + ) : ( + + ) + } + > + + + + + + } + /> + + + + ); +}; + +export default ServerAddressInput; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss new file mode 100644 index 0000000000..2994f56ef4 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.scss @@ -0,0 +1,5 @@ +.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/enroll-agent/enroll-agent.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx new file mode 100644 index 0000000000..bd6ae22c04 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/enroll-agent/enroll-agent.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiPage, + EuiPageBody, + EuiSpacer, +} from '@elastic/eui'; +import './enroll-agent.scss'; +import { Steps } from '../steps/steps'; +import { useForm } from '../../components/form/hooks'; +import { FormConfiguration } from '../../components/form/types'; +import { OsCard } from '../../components/os-selector/os-card/os-card'; +import { + validateAgentName, + validateEnrollmentKey, + validateServerAddress, +} from '../../utils/validations'; +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 initialFields: FormConfiguration = { + operatingSystemSelection: { + type: 'custom', + initialValue: '', + component: props => , + }, + serverAddress: { + type: 'text', + initialValue: + configuration[getEnrollAgentManagement().serverAddresSettingName] || '', // TODO: use the setting value as default value + validate: validateServerAddress, + }, + username: { + type: 'text', + initialValue: '', + }, + password: { + type: 'password', + initialValue: '', + }, + verificationMode: { + type: 'select', + initialValue: 'none', + options: { + 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); + + return ( +
+ + + + + + + + +

Enroll new agent

+
+
+
+ + + + +
+
+
+
+
+
+ ); +}; 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 new file mode 100644 index 0000000000..f00657fc1c --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.scss @@ -0,0 +1,51 @@ +.enroll-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/enroll-agent/containers/steps/steps.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx new file mode 100644 index 0000000000..079ac3937a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/containers/steps/steps.tsx @@ -0,0 +1,246 @@ +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/enroll-agent-data'; +import { + IParseEnrollFormValues, + getEnrollAgentFormValues, + parseEnrollAgentFormValues, +} from '../../services/enroll-agent-services'; +import { useEnrollAgentCommands } from '../../hooks/use-enroll-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 { SecurityInputs } from '../../components/security'; +import { + getAgentCommandsStepStatus, + TFormStepsStatus, + getOSSelectorStepStatus, + getServerAddressStepStatus, + getOptionalParameterStepStatus, + showCommandsSections, + getIncompleteSteps, + getInvalidFields, + FORM_FIELDS_LABEL, + FORM_STEPS_LABELS, + 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; +} + +const FORM_MESSAGE_CONJUNTION = ' and '; + +export const Steps = ({ form }: IStepsProps) => { + const initialParsedFormValues = { + operatingSystem: { + name: '', + architecture: '', + }, + optionalParams: { + agentName: '', + serverAddress: '', + }, + } as IParseEnrollFormValues; + const [missingStepsName, setMissingStepsName] = useState( + [], + ); + const [invalidFieldsName, setInvalidFieldsName] = useState< + FORM_FIELDS_LABEL[] + >([]); + const [enrollAgentFormValues, setEnrollAgentFormValues] = + useState(initialParsedFormValues); + const { installCommand, startCommand, selectOS, setOptionalParams } = + useEnrollAgentCommands({ + 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 + const enrollAgentFormValuesParsed = parseEnrollAgentFormValues( + getEnrollAgentFormValues(form), + OPERATING_SYSTEMS_OPTIONS, + initialParsedFormValues, + ); + + setEnrollAgentFormValues(enrollAgentFormValuesParsed); + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + setMissingStepsName(getIncompleteSteps(form.fields) || []); + setInvalidFieldsName(getInvalidFields(form.fields) || []); + }, [form.fields]); + + useEffect(() => { + if ( + enrollAgentFormValues.operatingSystem.name !== '' && + enrollAgentFormValues.operatingSystem.architecture !== '' + ) { + selectOS(enrollAgentFormValues.operatingSystem as TOperatingSystem); + } + + setOptionalParams( + { ...enrollAgentFormValues.optionalParams }, + enrollAgentFormValues.operatingSystem as TOperatingSystem, + ); + setInstallCommandWasCopied(false); + setStartCommandWasCopied(false); + }, [enrollAgentFormValues]); + + useEffect(() => { + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + }, [installCommandWasCopied]); + + useEffect(() => { + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + }, [startCommandWasCopied]); + + const enrollAgentFormSteps = [ + { + title: 'Select the package to download and install on your system:', + children: ( + + ), + status: getOSSelectorStepStatus(form.fields), + }, + { + title: 'Server address:', + children: , + status: getServerAddressStepStatus(form.fields), + }, + { + title: ( + + ), + children: ( + + ), + status: getServerCredentialsStepStatus(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 ? ( + <> + {/* TODO: remove the warning and spacer when the packages are publically hosted */} + + + setInstallCommandWasCopied(true)} + password={enrollAgentFormValues.optionalParams.password} + /> + + + ) : 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/enroll-agent/core/config/os-commands-definitions.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts new file mode 100644 index 0000000000..2eaeade29e --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/config/os-commands-definitions.ts @@ -0,0 +1,227 @@ +import { + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getWindowsInstallCommand, + getWindowsStartCommand, + getDEBAMD64InstallCommand, + getRPMAMD64InstallCommand, + getRPMARM64InstallCommand, + getDEBARM64InstallCommand, +} from '../../services/enroll-agent-os-commands-services'; +import { + scapeSpecialCharsForLinux, + scapeSpecialCharsForMacOS, + scapeSpecialCharsForWindows, +} from '../../services/wazuh-password-service'; +import { IOSDefinition, TOptionalParams } from '../enroll-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' + | 'username' + | 'password' + | 'verificationMode' + | 'agentName' + | 'enrollmentKey'; + +// ///////////////////////////////////////////////////////////////// +// / 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: '--url', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, + username: { + property: '--user', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, + password: { + property: '--password', + getParamCommand: (props, selectedOS) => { + const { property, value } = props; + + 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 value === '' ? '' : `${property} '${value}'`; + }, + }, + verificationMode: { + property: '--verification-mode', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, + agentName: { + property: '--name', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, + enrollmentKey: { + property: '--key', + getParamCommand: props => { + const { property, value } = props; + + return value === '' ? '' : `${property} '${value}'`; + }, + }, +}; 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/enroll-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 new file mode 100644 index 0000000000..55aad0899b --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.test.ts @@ -0,0 +1,431 @@ +import { IOSDefinition, IOptionalParameters, TOptionalParams } from '../types'; +import { + DuplicatedOSException, + DuplicatedOSOptionException, + NoOSSelectedException, + WazuhVersionUndefinedException, +} from '../exceptions'; +import { CommandGenerator } from './command-generator'; + +const MOCKED_COMMAND_VALUE = 'mocked command'; +const mockedCommandsResponse = jest.fn().mockReturnValue(MOCKED_COMMAND_VALUE); + +// 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' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey'; + +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: '--url', + getParamCommand: props => `${props.property} '${props.value}'`, + }, + agent_name: { + property: '--name', + getParamCommand: props => `${props.property} '${props.value}'`, + }, + username: { + property: '--user', + getParamCommand: props => `${props.property} '${props.value}'`, + }, + password: { + property: '--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 = { + server_address: '', + agent_name: '', + username: '', + password: '', + verificationMode: '', + enrollmentKey: '', +}; + +describe('Command Generator', () => { + it('should create an valid instance', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + + expect(commandGenerator).toBeDefined(); + }); + + it('should return the install command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + + const command = commandGenerator.getInstallCommand(); + + expect(command).toBe(MOCKED_COMMAND_VALUE); + }); + + it('should return the start command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + + const command = commandGenerator.getStartCommand(); + + expect(command).toBe(MOCKED_COMMAND_VALUE); + }); + + it('should return all the commands for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + + const commands = commandGenerator.getAllCommands(); + + expect(commands).toEqual({ + os: 'linux', + architecture: 'x64', + wazuhVersion: '5.0', + install_command: MOCKED_COMMAND_VALUE, + start_command: MOCKED_COMMAND_VALUE, + url_package: MOCKED_COMMAND_VALUE, + optionals: {}, + }); + }); + + it('should return commands with the filled optional params', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + const selectedOs: TOperatingSystem = { + name: 'linux', + architecture: 'x64', + }; + + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: '10.10.10.121', + agent_name: 'agent1', + 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: '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, + 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', + }), + username: optionalParams.username.getParamCommand({ + property: optionalParams.username.property, + value: optionalValues.username, + name: 'username', + }), + 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< + TOperatingSystem, + TOptionalParameters + >[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '5.0'); + } 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< + TOperatingSystem, + TOptionalParameters + >[] = [ + { + 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, '5.0'); + } 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, + '5.0', + ); + + 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, + '5.0', + ); + + 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, + '5.0', + ); + + 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, + '5.0', + ); + 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 receive the solved optional params when the start command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '5.0', + ); + 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', + }), + }, + }), + ); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.ts new file mode 100644 index 0000000000..2c2b029516 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/command-generator/command-generator.ts @@ -0,0 +1,192 @@ +import { + ICommandsResponse, + IOSCommandsDefinition, + IOSDefinition, + IOperationSystem, + IOptionalParameters, + IOptionalParametersManager, + TOptionalParams, + 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/enroll-agent/core/enroll-commands/exceptions/index.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/exceptions/index.ts new file mode 100644 index 0000000000..5d7c4419f2 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/exceptions/index.ts @@ -0,0 +1,99 @@ +// 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."`, + ); + } +} + +// 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."`, + ); + } +} + +// 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.`, + ); + } +} + +// 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.`, + ); + } +} + +// 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.`, + ); + } +} + +// 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.`, + ); + } +} + +// 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}"`); + } +} + +// 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`); + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..28bd2769fb --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.test.ts @@ -0,0 +1,212 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { + IOptionalParameters, + TOptionalParams, + TOptionalParamsCommandProps, +} from '../types'; +import { OptionalParametersManager } from './optional-parameters-manager'; + +type TOptionalParamsFieldname = + | 'server_address' + | 'username' + | 'password' + | 'another_valid_fieldname'; + +const returnOptionalParam = ( + props: TOptionalParamsCommandProps, +) => { + const { property, value } = props; + + return `${property} '${value}'`; +}; + +const optionalParametersDefinition: TOptionalParams = + { + username: { + property: '--user', + getParamCommand: returnOptionalParam, + }, + password: { + property: '--password', + getParamCommand: returnOptionalParam, + }, + server_address: { + property: '--url', + getParamCommand: returnOptionalParam, + }, + another_valid_fieldname: { + property: '--another-field', + 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'], + ['username', 'user'], + ['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 { + 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 = { + username: 'user', + password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + + expect(resolvedParams).toEqual({ + username: optionalParametersDefinition.username.getParamCommand({ + name: 'username', + property: optionalParametersDefinition.username.property, + value: paramsValues.username, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + 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', + 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 = { + username: 'user', + password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + + expect(resolvedParams).toEqual({ + username: optionalParametersDefinition.username.getParamCommand({ + name: 'username', + property: optionalParametersDefinition.username.property, + value: paramsValues.username, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + 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', + 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 { + 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 = {}; + 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: '', + username: '', + password: '', + }; + const optionals = optParamManager.getAllOptionalParams(paramsValues); + + expect(optionals).toEqual({}); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..a2c01c80bd --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -0,0 +1,76 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { + IOperationSystem, + IOptionalParamInput, + IOptionalParameters, + IOptionalParametersManager, + TOptionalParams, +} from '../types'; + +export class OptionalParametersManager + implements IOptionalParametersManager +{ + constructor(private readonly 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 (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, + value: paramsValues[param] as string, + property: paramDef.property, + }, + selectedOS, + ) as string; + } + + return resolvedOptionalParams; + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..f505496b71 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.test.ts @@ -0,0 +1,134 @@ +import { + IOSCommandsDefinition, + IOSDefinition, + IOptionalParameters, +} from '../types'; +import { + NoInstallCommandDefinitionException, + NoPackageURLDefinitionException, + WazuhVersionUndefinedException, +} from '../exceptions'; +import { getInstallCommandByOS } from './get-install-command.service'; + +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' + | 'username' + | 'password' + | 'verificationMode' + | 'enrollmentKey' + | 'another_optional_parameter'; + +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', + '5.0', + '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', () => { + 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', + '5.0', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(NoInstallCommandDefinitionException); + } + }); + it('should return ERROR when the OS has no package url', () => { + try { + 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< + TOperatingSystem, + TOptionalParameters + > = { + architecture: 'x64', + installCommand: mockedInstall, + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + 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', + '5.0', + 'linux', + optionalParams, + ); + expect(mockedInstall).toBeCalledTimes(1); + expect(mockedInstall).toBeCalledWith( + expect.objectContaining({ optionals: optionalParams }), + ); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..d323668320 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/get-install-command.service.ts @@ -0,0 +1,59 @@ +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< + 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 (!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/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..4ccb0424c4 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.test.ts @@ -0,0 +1,187 @@ +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< + TOperatingSystem, + TOptionalParamsNames +>[] = [ + { + 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, { + 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< + TOperatingSystem, + TOptionalParamsNames + >[] = [ + { + 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< + TOperatingSystem, + TOptionalParamsNames + > = { + name: 'linux', + options: [ + { + architecture: 'x64', + packageManager: 'aix', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }; + const osDefinitions: IOSDefinition< + TOperatingSystem, + TOptionalParamsNames + >[] = [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< + TOperatingSystem, + TOptionalParamsNames + >[] = [ + { + 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/enroll-agent/core/enroll-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 new file mode 100644 index 0000000000..b468466075 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/services/search-os-definitions.service.ts @@ -0,0 +1,89 @@ +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< + 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, + ); + + 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< + 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. + * 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< + OS extends IOperationSystem, + Params extends string, +>(osDefinitions: IOSDefinition[]) { + for (const osDefinition of osDefinitions) { + const options = new Set(); + + for (const option of osDefinition.options) { + const managerArchitecture = `${option.architecture}`; + + if (options.has(managerArchitecture)) { + throw new DuplicatedOSOptionException( + osDefinition.name, + option.architecture, + ); + } + + options.add(managerArchitecture); + } + } +} diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/types.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/types.ts new file mode 100644 index 0000000000..33f067d5b7 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/enroll-commands/types.ts @@ -0,0 +1,115 @@ +// /////////////////////////////////////////////////////// +// / Domain +// /////////////////////////////////////////////////////// +export interface IOperationSystem { + name: string; + architecture: string; +} + +export type IOptionalParameters = Record; + +// ///////////////////////////////////////////////////////////////// +// / Operating system commands definitions +// ///////////////////////////////////////////////////////////////// + +export interface IOSProps extends IOperationSystem { + wazuhVersion: string; +} + +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 IOSDefinition< + OS extends IOperationSystem, + Params extends string, +> { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +// ///////////////////////////////////////////////////////////////// +// // 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 = Record< + 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 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; +export interface ICommandGenerator< + OS extends IOperationSystem, + Params extends string, +> extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} 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 new file mode 100644 index 0000000000..2e4617157a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/core/register-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,the package that he wants to install, credentials and some options. 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/hooks/README.md b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md new file mode 100644 index 0000000000..18c98cea42 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/README.md @@ -0,0 +1,176 @@ +# Documentation + +- [useEnrollAgentCommand hook](#useenrollagentcommand-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) + +## useEnrollAgentCommand hook + +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 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 { useEnrollAgentCommands } from 'path/to/use-enroll-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 + } = useEnrollAgentCommands(); + +// 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 IUseEnrollCommandsProps< + OS extends IOperationSystem, + Params extends string, +> { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: TOptionalParams; +} +``` + +### Hook output + +```ts +export interface IOperationSystem { + name: string; + architecture: string; +} + +interface IUseEnrollCommandsOutput< + OS extends IOperationSystem, + Params extends string, +> { + 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' + | '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 { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed, +} = useEnrollAgentCommands( + 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/enroll-agent/hooks/use-enroll-agent-commands.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts new file mode 100644 index 0000000000..51d6d6472a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.test.ts @@ -0,0 +1,228 @@ +import React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { IOSDefinition, TOptionalParams } from '../core/enroll-commands/types'; +import { useEnrollAgentCommands } from './use-enroll-agent-commands'; + +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: '--url', + getParamCommand: props => { + const { property, value } = props; + + return `${property} '${value}'`; + }, + }, + optional2: { + property: '--name', + getParamCommand: props => { + const { property, value } = props; + + return `${property} '${value}'`; + }, + }, + }; + +describe('useEnrollAgentCommands hook', () => { + it('should return installCommand and startCommand null when the hook is initialized', () => { + const hook = renderHook(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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(() => + useEnrollAgentCommands({ + 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/enroll-agent/hooks/use-enroll-agent-commands.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts new file mode 100644 index 0000000000..0348735545 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/hooks/use-enroll-agent-commands.ts @@ -0,0 +1,133 @@ +import { useEffect, useState } from 'react'; +import { CommandGenerator } from '../core/enroll-commands/command-generator/command-generator'; +import { + IOSDefinition, + IOperationSystem, + IOptionalParameters, + TOptionalParams, +} from '../core/enroll-commands/types'; +import { version } from '../../../../../package.json'; + +interface IUseEnrollCommandsProps< + OS extends IOperationSystem, + Params extends string, +> { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: TOptionalParams; +} + +interface IUseEnrollCommandsOutput< + OS extends IOperationSystem, + Params extends string, +> { + selectOS: (params: OS) => void; + setOptionalParams: ( + params: IOptionalParameters, + selectedOS?: OS, + ) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | object; +} + +/** + * 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 {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 useEnrollAgentCommands< + OS extends IOperationSystem, + Params extends string, +>( + props: IUseEnrollCommandsProps, +): IUseEnrollCommandsOutput { + 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 | object + >({}); + const [optionalParamsParsed, setOptionalParamsParsed] = useState< + IOptionalParameters | object + >({}); + 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/enroll-agent/index.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx new file mode 100644 index 0000000000..d6a02b2ee2 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/index.tsx @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..853400d253 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/interfaces/types.ts @@ -0,0 +1,18 @@ +import { TOperatingSystem } from '../config/os-commands-definitions'; + +interface EnrollAgentData { + 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 { EnrollAgentData, CheckboxGroupComponentProps }; 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 new file mode 100644 index 0000000000..f86534727f --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.test.ts @@ -0,0 +1,378 @@ +import { + getAllOptionals, + getAllOptionalsMacos, + getDEBAMD64InstallCommand, + getDEBARM64InstallCommand, + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getRPMAMD64InstallCommand, + getRPMARM64InstallCommand, + getWindowsInstallCommand, + getWindowsStartCommand, + transformOptionalsParamatersMacOSCommand, +} from './enroll-agent-os-commands-services'; + +let test: any; + +beforeEach(() => { + test = { + optionals: { + 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: '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: "--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( + "--url '1.1.1.1' --username 'user' --password 'pass' --verification-mode 'none' --name 'test' --key '00000000000000000000000000000000'", + ); + }); +}); + +describe('getDEBAMD64InstallCommand', () => { + it('should return the correct install command', () => { + const props = { + optionals: { + 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: '5.0.0', + }; + 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 --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 --enroll-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.enrollmentKey; + delete test.optionals.agentName; + + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_amd64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-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 = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-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.enrollmentKey; + delete test.optionals.agentName; + + expected = `sudo dpkg -i ./wazuh-agent_${test.wazuhVersion}-1_arm64.deb && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-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 = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.x86_64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-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.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 --enroll-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 = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-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.enrollmentKey; + delete test.optionals.agentName; + + expected = `sudo rpm -ihv wazuh-agent-${test.wazuhVersion}-1.aarch64.rpm && sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + + 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 = `Start-Process msiexec.exe "/i $env:tmp\\wazuh-agent /q" -Wait; & 'C:\\Program Files\\wazuh-agent\\wazuh-agent.exe' --enroll-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + const withAllOptionals = getWindowsInstallCommand(test); + + expect(withAllOptionals).toEqual(expected); + + 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' --enroll-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + + const withServerAddresAndAgentGroupsOptions = + getWindowsInstallCommand(test); + + expect(withServerAddresAndAgentGroupsOptions).toEqual(expected); + }); +}); + +describe('getWindowsStartCommand', () => { + it('should return the correct start command', () => { + const expectedCommand = "NET START 'Wazuh Agent'"; + 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 result = getAllOptionalsMacos(test.optionals); + + expect(result).toBe( + [ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' '), + ); + }); +}); + +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 = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-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 = `sudo installer -pkg ./wazuh-agent.pkg -target / && /Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-agent ${[ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ] + .map(key => test.optionals[key]) + .filter(Boolean) + .join(' ')}`; + + 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/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 new file mode 100644 index 0000000000..8a4ecbc1f6 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-os-commands-services.tsx @@ -0,0 +1,191 @@ +import { TOptionalParameters } from '../core/config/os-commands-definitions'; +import { + IOptionalParameters, + TOSEntryInstallCommand, + TOSEntryProps, +} from '../core/enroll-commands/types'; +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; + +export const getAllOptionals = ( + 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', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ]; + + if (!optionals) { + return ''; + } + + return Object.values(paramNameOrderList) + .map(key => optionals[key]) + .filter(Boolean) + .join(' '); +}; + +export const getAllOptionalsMacos = ( + optionals: IOptionalParameters, +) => { + // create paramNameOrderList, which is an array of the keys of optionals add interface + const paramNameOrderList: (keyof IOptionalParameters)[] = + [ + 'serverAddress', + 'username', + 'password', + 'verificationMode', + 'agentName', + 'enrollmentKey', + ]; + + if (!optionals) { + return ''; + } + + return Object.values(paramNameOrderList) + .map(key => optionals[key]) + .filter(Boolean) + .join(' '); +}; + +/** ***** DEB *******/ + +export const getDEBAMD64InstallCommand = ( + props: TOSEntryInstallCommand, +) => { + const { optionals, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb`; + + return [ + // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo dpkg -i ./${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); +}; + +export const getDEBARM64InstallCommand = ( + props: TOSEntryInstallCommand, +) => { + const { optionals, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_arm64.deb`; + + return [ + // `wget ${urlPackage}`, // TODO: enable when the packages are publically hosted + `sudo dpkg -i ./${packageName}`, + `sudo /usr/share/wazuh-agent/bin/wazuh-agent --enroll-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); +}; + +/** ***** RPM *******/ + +export const getRPMAMD64InstallCommand = ( + props: TOSEntryInstallCommand, +) => { + const { optionals, wazuhVersion } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm`; + + 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 --enroll-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); +}; + +export const getRPMARM64InstallCommand = ( + props: TOSEntryInstallCommand, +) => { + const { optionals, wazuhVersion } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.aarch64.rpm`; + + 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 --enroll-agent ${optionals && getAllOptionals(optionals)}`, + ].join(' && '); +}; + +/** ***** Linux *******/ + +// Start command +export const getLinuxStartCommand = ( + _props: TOSEntryProps, +) => + `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; + +/** ****** Windows ********/ + +export const getWindowsInstallCommand = ( + props: TOSEntryInstallCommand, +) => { + const { optionals, name } = props; + + 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' --enroll-agent ${ + optionals && getAllOptionals(optionals, name) + }`, + ].join(' '); +}; + +export const getWindowsStartCommand = ( + _props: TOSEntryProps, +) => `NET START 'Wazuh Agent'`; + +/** ****** MacOS ********/ + +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, +) => { + const { optionals } = props; + const optionalsForCommand = { ...optionals }; + + 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?.password, + ).length; + + // We need to remove the " added by JSON.stringify + optionalsForCommand.password = `${JSON.stringify( + optionalsForCommand?.password, + ).slice(1, scapedPasswordLength - 1)}`; + } + + // Set macOS installation script with environment variables + const optionalsText = + optionalsForCommand && getAllOptionals(optionalsForCommand); + const macOSInstallationOptions = transformOptionalsParamatersMacOSCommand( + optionalsText || '', + ); + // Merge environment variables with installation script + const macOSInstallationScript = [ + // `curl -so wazuh-agent.pkg ${urlPackage}`, + 'sudo installer -pkg ./wazuh-agent.pkg -target /', + `/Library/Application\\ Support/Wazuh\\ agent.app/bin/wazuh-agent --enroll-agent ${macOSInstallationOptions}`, + ].join(' && '); + + return macOSInstallationScript; +}; + +export const getMacosStartCommand = ( + _props: TOSEntryProps, +) => `sudo /Library/Ossec/bin/wazuh-control start`; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx new file mode 100644 index 0000000000..8766bbacd9 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-services.tsx @@ -0,0 +1,92 @@ +import { UseFormReturn } from '../components/form/types'; +import { + TOperatingSystem, + TOptionalParameters, +} from '../core/config/os-commands-definitions'; +import { EnrollAgentData } from '../interfaces/types'; + +export interface ServerAddressOptions { + label: string; + value: string; + nodetype: string; +} + +interface NodeItem { + name: string; + ip: string; + type: string; +} + +interface NodeResponse { + data: { + data: { + affected_items: NodeItem[]; + }; + }; +} + +/** + * 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, + })); + +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 IParseEnrollFormValues { + 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 parseEnrollAgentFormValues = ( + formValues: { name: keyof UseFormReturn['fields']; value: any }[], + OSOptionsDefined: EnrollAgentData[], + initialValues?: IParseEnrollFormValues, +) => { + // return the values form the formFields and the value property + const parsedForm = + initialValues || + ({ + operatingSystem: { + architecture: '', + name: '', + }, + optionalParams: {}, + } as IParseEnrollFormValues); + + 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 { + parsedForm.optionalParams[field.name as any] = field.value; + } + } + + return parsedForm; +}; 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); + }); +}); diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx new file mode 100644 index 0000000000..3aa4d57aea --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/enroll-agent-steps-status-services.tsx @@ -0,0 +1,265 @@ +import { EuiStepStatus } from '@elastic/eui'; +import { UseFormReturn } from '../components/form/types'; +import { + FormStepsDependencies, + EnrollAgentFormStatusManager, +} 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 someFieldisEmptyValue = fieldsToCheck.some( + key => formFields[key]?.value?.length === 0, + ); + + return someFieldisEmptyValue; +}; + +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 ( + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + [ + 'operatingSystemSelection', + 'serverAddress', + 'username', + 'password', + 'agentName', + 'verificationMode', + 'enrollmentKey', + ], + formFields, + ) + ) { + return false; + } + + 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 ( + fieldsAreEmpty( + // required fields + ['operatingSystemSelection'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection'], + formFields, + ) + ) { + return 'disabled'; + } else if ( + fieldsAreEmpty( + // required fields + ['serverAddress'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['serverAddress'], + formFields, + ) + ) { + return 'current'; + } else { + return 'complete'; + } +}; + +export const getServerCredentialsStepStatus = ( + formFields: UseFormReturn['fields'], +): TFormStepsStatus => { + if ( + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection', 'serverAddress'], + formFields, + ) + ) { + return 'disabled'; + } else if ( + fieldsAreEmpty( + // required fields + ['username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['username', 'password'], + formFields, + ) + ) { + return 'current'; + } else { + return 'complete'; + } +}; + +export const getOptionalParameterStepStatus = ( + formFields: UseFormReturn['fields'], + installCommandWasCopied: boolean, +): TFormStepsStatus => { + // when previous step are not complete + if ( + fieldsAreEmpty( + // required fields + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) || + fieldsHaveErrors( + // check for errors + ['operatingSystemSelection', 'serverAddress', 'username', 'password'], + formFields, + ) + ) { + return 'disabled'; + } else if ( + installCommandWasCopied || + anyFieldIsComplete( + ['agentName', 'verificationMode', 'enrollmentKey'], + formFields, + ) + ) { + return 'complete'; + } else { + return 'current'; + } +}; + +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'], +): FORM_STEPS_LABELS[] => { + const steps: FormStepsDependencies = { + operatingSystemSelection: ['operatingSystemSelection'], + serverAddress: ['serverAddress'], + username: ['username'], + password: ['password'], + }; + const statusManager = new EnrollAgentFormStatusManager(formFields, steps); + + // replace fields array using label names + return statusManager + .getIncompleteSteps() + .map(field => FORM_STEPS_LABELS[field] || field); +}; + +export enum FORM_FIELDS_LABEL { + // eslint-disable-next-line @typescript-eslint/naming-convention + agentName = 'agent name', + // 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'], +): FORM_FIELDS_LABEL[] => { + const statusManager = new EnrollAgentFormStatusManager(formFields); + + return statusManager + .getInvalidFields() + .map(field => FORM_FIELDS_LABEL[field] || field); +}; 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 new file mode 100644 index 0000000000..49fd32c466 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.test.tsx @@ -0,0 +1,150 @@ +import { + EnhancedFieldConfiguration, + UseFormReturn, +} from '../../../components/common/form/types'; +import { + FormStepsDependencies, + EnrollAgentFormStatusManager, +} 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('EnrollAgentFormStatusManager', () => { + it('should create a instance', () => { + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + ); + + expect(enrollAgentFormStatusManager).toBeDefined(); + }); + + it('should return the form status', () => { + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + ); + const formStatus = enrollAgentFormStatusManager.getFormStatus(); + + expect(formStatus).toEqual({ + field1: 'empty', + field2: 'invalid', + field3: 'complete', + }); + }); + + it('should return the field status', () => { + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + ); + const fieldStatus = enrollAgentFormStatusManager.getFieldStatus('field1'); + + expect(fieldStatus).toEqual('empty'); + }); + + it('should return error if fieldname not found', () => { + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + ); + + expect(() => + enrollAgentFormStatusManager.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 enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.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 enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step2')).toEqual( + 'complete', + ); + }); + + it('should return EMPTY when the step all fields empty', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: ['field2', 'field3'], + }; + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + + expect(enrollAgentFormStatusManager).toBeDefined(); + expect(enrollAgentFormStatusManager.getStepStatus('step1')).toEqual( + 'empty', + ); + }); + + it('should return all the steps status', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: ['field2', 'field3'], + step3: ['field3'], + }; + const enrollAgentFormStatusManager = new EnrollAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + + 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 new file mode 100644 index 0000000000..dc190b04fa --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/form-status-manager.tsx @@ -0,0 +1,120 @@ +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 EnrollAgentFormStatusManager 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 = {}; + + // 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 { + 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 = {}; + + // eslint-disable-next-line unicorn/no-array-for-each + 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 = {}; + + // 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[] { + 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/enroll-agent/services/wazuh-password-service.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.test.ts new file mode 100644 index 0000000000..20432f850a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.test.ts @@ -0,0 +1,98 @@ +/* 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'} + `( + ' 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/enroll-agent/services/wazuh-password-service.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts new file mode 100644 index 0000000000..fb9bc9d904 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/wazuh-password-service.ts @@ -0,0 +1,82 @@ +import { TOperatingSystem } from '../hooks/use-enroll-agent-commands.test'; + +export const scapeSpecialCharsForLinux = (password: string) => { + 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 + .replaceAll(regex, `'"$&"'`) + .replaceAll(/(? { + const passwordScaped = password; + + // The double quote is escaped first and then the backslash followed by a single quote + 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) => { + 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 + .replaceAll(regex, `'"$&"'`) + .replaceAll(/(? { + const command = commandText; + const osName = os?.toLocaleLowerCase(); + + 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( + `--password '${scapeSpecialCharsForWindows(password)}'`, + () => + `--password '${'*'.repeat(scapeSpecialCharsForWindows(password).length)}'`, + ); + + return replacedString; + } + + case 'linux': { + const replacedString = command.replace( + `--password $'${scapeSpecialCharsForLinux(password)}'`, + () => + `--password $'${'*'.repeat(scapeSpecialCharsForLinux(password).length)}'`, + ); + + return replacedString; + } + + default: { + return commandText; + } + } +}; diff --git a/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.test.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.test.ts new file mode 100644 index 0000000000..ef286990c0 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/services/web-documentation-link.ts b/plugins/wazuh-fleet/public/application/pages/enroll-agent/services/web-documentation-link.ts new file mode 100644 index 0000000000..cc4f825f01 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-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/enroll-agent/utils/enroll-agent-data.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx new file mode 100644 index 0000000000..4ec57f0e49 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/enroll-agent-data.tsx @@ -0,0 +1,47 @@ +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'; +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: EnrollAgentData[] = [ + { + 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 valid URL including the port, the address can be 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/enroll-agent/utils/validations.test.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.test.tsx new file mode 100644 index 0000000000..e4764d7039 --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.test.tsx @@ -0,0 +1,92 @@ +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, ".", "-", "_"', + ); + }); + + 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(''); + }); + + 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/enroll-agent/utils/validations.tsx b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.tsx new file mode 100644 index 0000000000..2558aaa72c --- /dev/null +++ b/plugins/wazuh-fleet/public/application/pages/enroll-agent/utils/validations.tsx @@ -0,0 +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; + } + + const invalidCharacters = validateCharacters(value); + + if (value.length < 2) { + return `The minimum length is 2 characters.${ + invalidCharacters && ` ${invalidCharacters}` + }`; + } + + return `${invalidCharacters}`; +}; + +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.'; + } +}; + +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/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..eb6c9fa28a --- /dev/null +++ b/plugins/wazuh-fleet/public/application/types.ts @@ -0,0 +1,8 @@ +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 978706134a..ea85d5b57b 100644 --- a/plugins/wazuh-fleet/public/plugin-services.ts +++ b/plugins/wazuh-fleet/public/plugin-services.ts @@ -2,9 +2,14 @@ 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'); 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 3253a55bff..3e854be393 100644 --- a/plugins/wazuh-fleet/public/plugin.ts +++ b/plugins/wazuh-fleet/public/plugin.ts @@ -5,12 +5,28 @@ 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 implements Plugin { public setup(core: CoreSetup): WazuhFleetPluginSetup { + 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'); + }, + 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 {}; } 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"