From 03bdb9976ce7b7918413e3e7fde20d34cf9ada60 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Wed, 17 May 2023 10:47:19 -0300 Subject: [PATCH 01/24] parent component --- public/controllers/agent/index.js | 4 +- .../container/os-card-container.scss | 7 + .../container/os-card-container.tsx | 8 + public/controllers/register-agent/index.tsx | 2 + public/services/routes.js | 169 ++++++++---------- public/templates/agents-prev/agents-prev.html | 42 ++++- public/templates/visualize/dashboards.html | 33 +++- 7 files changed, 154 insertions(+), 111 deletions(-) create mode 100644 public/controllers/register-agent/container/os-card-container.scss create mode 100644 public/controllers/register-agent/container/os-card-container.tsx create mode 100644 public/controllers/register-agent/index.tsx diff --git a/public/controllers/agent/index.js b/public/controllers/agent/index.js index c0fefdc072..b4cfe8a759 100644 --- a/public/controllers/agent/index.js +++ b/public/controllers/agent/index.js @@ -11,10 +11,10 @@ */ import { AgentsPreviewController } from './agents-preview'; import { AgentsController } from './agents'; -import { RegisterAgent } from './components/register-agent'; +import RegisterAgent from '../register-agent/container/os-card-container'; import { ExportConfiguration } from './components/export-configuration'; import { AgentsWelcome } from '../../components/common/welcome/agents-welcome'; -import { Mitre } from '../../components/overview' +import { Mitre } from '../../components/overview'; import { AgentsPreview } from './components/agents-preview'; import { AgentsTable } from './components/agents-table'; import { MainModule } from '../../components/common/modules/main'; diff --git a/public/controllers/register-agent/container/os-card-container.scss b/public/controllers/register-agent/container/os-card-container.scss new file mode 100644 index 0000000000..5ab4759aa5 --- /dev/null +++ b/public/controllers/register-agent/container/os-card-container.scss @@ -0,0 +1,7 @@ +.container { + box-sizing: border-box; + height: 1271px; + margin-top: 44px; + background: #ffffff; + border: 1px solid rgba(52, 55, 65, 0.2); +} diff --git a/public/controllers/register-agent/container/os-card-container.tsx b/public/controllers/register-agent/container/os-card-container.tsx new file mode 100644 index 0000000000..7fd47c2088 --- /dev/null +++ b/public/controllers/register-agent/container/os-card-container.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import './os-card-container.scss'; + +const Container: React.FC = () => { + return
; +}; + +export default Container; diff --git a/public/controllers/register-agent/index.tsx b/public/controllers/register-agent/index.tsx new file mode 100644 index 0000000000..fd6b56af64 --- /dev/null +++ b/public/controllers/register-agent/index.tsx @@ -0,0 +1,2 @@ +export Container from './container/os-card-container'; + diff --git a/public/services/routes.js b/public/services/routes.js index 6b24043526..1f352ff20e 100644 --- a/public/services/routes.js +++ b/public/services/routes.js @@ -15,12 +15,7 @@ import 'angular-route'; // Functions to be executed before loading certain routes -import { - settingsWizard, - getSavedSearch, - getIp, - getWzConfig, -} from './resolves'; +import { settingsWizard, getSavedSearch, getIp, getWzConfig } from './resolves'; // HTML templates import healthCheckTemplate from '../templates/health-check/health-check.html'; @@ -52,12 +47,7 @@ const assignPreviousLocation = ($rootScope, $location) => { function ip($q, $rootScope, $window, $location) { const wzMisc = new WzMisc(); assignPreviousLocation($rootScope, $location); - return getIp( - $q, - $window, - $location, - wzMisc - ); + return getIp($q, $window, $location, wzMisc); } function nestedResolve($q, errorHandler, $rootScope, $location, $window) { @@ -77,25 +67,16 @@ function nestedResolve($q, errorHandler, $rootScope, $location, $window) { GenericRequest, errorHandler, wzMisc, - location && location.includes('/health-check') - ) + location && location.includes('/health-check'), + ), ); } -function savedSearch( - $location, - $window, - $rootScope, - $route -) { +function savedSearch($location, $window, $rootScope, $route) { const healthCheckStatus = $window.sessionStorage.getItem('healthCheck'); if (!healthCheckStatus) return; assignPreviousLocation($rootScope, $location); - return getSavedSearch( - $location, - $window, - $route - ); + return getSavedSearch($location, $window, $route); } function wzConfig($q, $rootScope, $location) { @@ -112,7 +93,7 @@ function clearRuleId(commonData) { function enableWzMenu($rootScope, $location) { const location = $location.path(); $rootScope.hideWzMenu = location.includes('/health-check'); - if(!$rootScope.hideWzMenu){ + if (!$rootScope.hideWzMenu) { AppState.setWzMenu(); } } @@ -120,73 +101,73 @@ function enableWzMenu($rootScope, $location) { //Routes const app = getAngularModule(); -app.config(($routeProvider) => { +app.config($routeProvider => { $routeProvider - .when('/health-check', { - template: healthCheckTemplate, - resolve: { wzConfig, ip }, - outerAngularWrapperRoute: true - }) - .when('/agents/:agent?/:tab?/:tabView?', { - template: agentsTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - reloadOnSearch: false, - outerAngularWrapperRoute: true - }) - .when('/agents-preview/', { - template: agentsPrevTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - reloadOnSearch: false, - outerAngularWrapperRoute: true - }) - .when('/manager/', { - template: managementTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, - reloadOnSearch: false, - outerAngularWrapperRoute: true - }) - .when('/manager/:tab?', { - template: managementTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, - outerAngularWrapperRoute: true - }) - .when('/overview/', { - template: overviewTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - reloadOnSearch: false, - outerAngularWrapperRoute: true - }) - .when('/settings', { - template: settingsTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - reloadOnSearch: false, - outerAngularWrapperRoute: true - }) - .when('/security', { - template: securityTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - outerAngularWrapperRoute: true - }) - .when('/wazuh-dev', { - template: toolsTemplate, - resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, - outerAngularWrapperRoute: true - }) - .when('/blank-screen', { - template: blankScreenTemplate, - resolve: { enableWzMenu }, - outerAngularWrapperRoute: true - }) - .when('/', { - redirectTo: '/overview/', - outerAngularWrapperRoute: true - }) - .when('', { - redirectTo: '/overview/', - outerAngularWrapperRoute: true - }) - .otherwise({ - redirectTo: '/overview', - outerAngularWrapperRoute: true - }); + .when('/health-check', { + template: healthCheckTemplate, + resolve: { wzConfig, ip }, + outerAngularWrapperRoute: true, + }) + .when('/agents/:agent?/:tab?/:tabView?', { + template: agentsTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + reloadOnSearch: false, + outerAngularWrapperRoute: true, + }) + .when('/agents-preview/', { + template: agentsPrevTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + reloadOnSearch: false, + outerAngularWrapperRoute: true, + }) + .when('/manager/', { + template: managementTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, + reloadOnSearch: false, + outerAngularWrapperRoute: true, + }) + .when('/manager/:tab?', { + template: managementTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch, clearRuleId }, + outerAngularWrapperRoute: true, + }) + .when('/overview/', { + template: overviewTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + reloadOnSearch: false, + outerAngularWrapperRoute: true, + }) + .when('/settings', { + template: settingsTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + reloadOnSearch: false, + outerAngularWrapperRoute: true, + }) + .when('/security', { + template: securityTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + outerAngularWrapperRoute: true, + }) + .when('/wazuh-dev', { + template: toolsTemplate, + resolve: { enableWzMenu, nestedResolve, ip, savedSearch }, + outerAngularWrapperRoute: true, + }) + .when('/blank-screen', { + template: blankScreenTemplate, + resolve: { enableWzMenu }, + outerAngularWrapperRoute: true, + }) + .when('/', { + redirectTo: '/overview/', + outerAngularWrapperRoute: true, + }) + .when('', { + redirectTo: '/overview/', + outerAngularWrapperRoute: true, + }) + .otherwise({ + redirectTo: '/overview', + outerAngularWrapperRoute: true, + }); }); diff --git a/public/templates/agents-prev/agents-prev.html b/public/templates/agents-prev/agents-prev.html index cec8cb3637..573720fea4 100644 --- a/public/templates/agents-prev/agents-prev.html +++ b/public/templates/agents-prev/agents-prev.html @@ -1,6 +1,14 @@ -
+
- +
-
+
- Error fetching - agents + + Error fetching agents

{{ ctrl.errorInit || 'Internal error' }}

-
@@ -37,7 +60,10 @@ layout-align="start space-around" >
- +
diff --git a/public/templates/visualize/dashboards.html b/public/templates/visualize/dashboards.html index f03852120a..da9a9c35e5 100644 --- a/public/templates/visualize/dashboards.html +++ b/public/templates/visualize/dashboards.html @@ -1,6 +1,9 @@
- +
@@ -34,7 +37,9 @@ ng-if="reportBusy && reportStatus && showModuleDashboard" class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--justifyContentSpaceAround euiFlexGroup--directionRow euiFlexGroup--responsive" > -
+
@@ -82,7 +87,8 @@ d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 0 0-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 0 1 0 8.373zM8 15A6.956 6.956 0 0 1 3.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 0 0 2.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 0 1 8 15zm-5.601-2.813a6.963 6.963 0 0 1 0-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 0 0 3 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 1 1 8 4a4 4 0 0 1 0 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 0 0 8 3a4.979 4.979 0 0 0-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 0 1 8 1zm0-1a8.001 8.001 0 1 0 .003 16.002A8.001 8.001 0 0 0 8 0z" > - + + No agents were added to this manager: @@ -90,8 +96,14 @@
-
-
+
+
-
+
- +
From 75cce24a327bd28794aa08efd44e9654300737c3 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Fri, 19 May 2023 14:30:21 -0300 Subject: [PATCH 02/24] Added a title to the container and updated filenames --- public/controllers/agent/index.js | 2 +- .../container/os-card-container.scss | 7 ------- .../container/os-card-container.tsx | 8 ------- .../container/register-agent.scss | 21 +++++++++++++++++++ .../container/register-agent.tsx | 10 +++++++++ public/controllers/register-agent/index.tsx | 3 +-- 6 files changed, 33 insertions(+), 18 deletions(-) delete mode 100644 public/controllers/register-agent/container/os-card-container.scss delete mode 100644 public/controllers/register-agent/container/os-card-container.tsx create mode 100644 public/controllers/register-agent/container/register-agent.scss create mode 100644 public/controllers/register-agent/container/register-agent.tsx diff --git a/public/controllers/agent/index.js b/public/controllers/agent/index.js index b4cfe8a759..c9e06604aa 100644 --- a/public/controllers/agent/index.js +++ b/public/controllers/agent/index.js @@ -11,7 +11,7 @@ */ import { AgentsPreviewController } from './agents-preview'; import { AgentsController } from './agents'; -import RegisterAgent from '../register-agent/container/os-card-container'; +import { RegisterAgent } from '../register-agent/container/register-agent'; import { ExportConfiguration } from './components/export-configuration'; import { AgentsWelcome } from '../../components/common/welcome/agents-welcome'; import { Mitre } from '../../components/overview'; diff --git a/public/controllers/register-agent/container/os-card-container.scss b/public/controllers/register-agent/container/os-card-container.scss deleted file mode 100644 index 5ab4759aa5..0000000000 --- a/public/controllers/register-agent/container/os-card-container.scss +++ /dev/null @@ -1,7 +0,0 @@ -.container { - box-sizing: border-box; - height: 1271px; - margin-top: 44px; - background: #ffffff; - border: 1px solid rgba(52, 55, 65, 0.2); -} diff --git a/public/controllers/register-agent/container/os-card-container.tsx b/public/controllers/register-agent/container/os-card-container.tsx deleted file mode 100644 index 7fd47c2088..0000000000 --- a/public/controllers/register-agent/container/os-card-container.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import './os-card-container.scss'; - -const Container: React.FC = () => { - return
; -}; - -export default Container; diff --git a/public/controllers/register-agent/container/register-agent.scss b/public/controllers/register-agent/container/register-agent.scss new file mode 100644 index 0000000000..317685dde5 --- /dev/null +++ b/public/controllers/register-agent/container/register-agent.scss @@ -0,0 +1,21 @@ +.container { + box-sizing: border-box; + height: 1271px; + margin-top: 44px; + background: #ffffff; + border: 1px solid rgba(52, 55, 65, 0.2); + max-width: 1030px; +} + +.title { + margin-top: 51px; + margin-bottom: 51px; + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-size: 30px; + line-height: 36px; + color: #343741; + display: flex; + justify-content: center; +} diff --git a/public/controllers/register-agent/container/register-agent.tsx b/public/controllers/register-agent/container/register-agent.tsx new file mode 100644 index 0000000000..87a780b0e7 --- /dev/null +++ b/public/controllers/register-agent/container/register-agent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import './register-agent.scss'; + +export const RegisterAgent: React.FC = () => { + return ( +
+
Deploy new agent
+
+ ); +}; diff --git a/public/controllers/register-agent/index.tsx b/public/controllers/register-agent/index.tsx index fd6b56af64..d516e34a58 100644 --- a/public/controllers/register-agent/index.tsx +++ b/public/controllers/register-agent/index.tsx @@ -1,2 +1 @@ -export Container from './container/os-card-container'; - +export { RegisterAgent } from './container/register-agent'; From edd6ff48a81c3f751f6a76dd36094bf11767ea06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Tue, 23 May 2023 12:29:03 -0300 Subject: [PATCH 03/24] Update register-agent.scss --- public/controllers/register-agent/container/register-agent.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/controllers/register-agent/container/register-agent.scss b/public/controllers/register-agent/container/register-agent.scss index 317685dde5..6f5dc8ed6c 100644 --- a/public/controllers/register-agent/container/register-agent.scss +++ b/public/controllers/register-agent/container/register-agent.scss @@ -5,6 +5,8 @@ background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); max-width: 1030px; + padding-left: 72px; + padding-right: 63px; } .title { From 8b646f51d2d4b3afa49ef399ad83eff35fdffd20 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Wed, 24 May 2023 11:13:34 -0300 Subject: [PATCH 04/24] [Redesign add agent] Register agent reuse common/form component (Settings > Configuration) (#5446) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * Remove react use inside hook test file * Fix review requested changes --- public/components/common/form/hooks.test.tsx | 357 ++++++++++--------- public/components/common/form/hooks.tsx | 179 ++++++---- public/components/common/form/index.tsx | 22 +- public/components/common/form/types.ts | 93 ++++- 4 files changed, 397 insertions(+), 254 deletions(-) diff --git a/public/components/common/form/hooks.test.tsx b/public/components/common/form/hooks.test.tsx index 38d3f19a27..c96ebe60b8 100644 --- a/public/components/common/form/hooks.test.tsx +++ b/public/components/common/form/hooks.test.tsx @@ -1,178 +1,189 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useForm } from './hooks'; +import { FormConfiguration, IInputForm } from './types'; describe('[hook] useForm', () => { - - it(`[hook] useForm. Verify the initial state`, async () => { - - const initialFields = { - 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 = { - 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 = { - 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 = { - text1: { - type: fieldType, - initialValue: initialFieldValue, - validate: (value: string): string | undefined => value.length ? undefined : `Validation error: string can be empty.` - } - }; - - const { result } = renderHook(() => useForm(initialFields)); - - // assert initial state - expect(result.current.fields.text1.changed).toBe(false); - expect(result.current.fields.text1.error).toBeUndefined(); - expect(result.current.fields.text1.type).toBe(fieldType); - expect(result.current.fields.text1.value).toBe(initialFieldValue); - expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); - expect(result.current.fields.text1.onChange).toBeDefined(); - - // change the input - const changedValue = ''; - act(() => { - result.current.fields.text1.onChange({ - target: { - value: changedValue - } - }); - }); - - // assert changed state - expect(result.current.fields.text1.changed).toBe(true); - expect(result.current.fields.text1.error).toBeTruthy(); - expect(result.current.fields.text1.type).toBe(fieldType); - expect(result.current.fields.text1.value).toBe(changedValue); - expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); - }); + it(`[hook] useForm. Verify the initial state`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + }); + + it(`[hook] useForm. Verify the initial state. Multiple fields.`, async () => { + const initialFields: FormConfiguration = { + text1: { + type: 'text', + initialValue: '', + }, + number1: { + type: 'number', + initialValue: 1, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe('text'); + expect(result.current.fields.text1.value).toBe(''); + expect(result.current.fields.text1.initialValue).toBe(''); + expect(result.current.fields.text1.onChange).toBeDefined(); + + expect(result.current.fields.number1.changed).toBe(false); + expect(result.current.fields.number1.error).toBeUndefined(); + expect(result.current.fields.number1.type).toBe('number'); + expect(result.current.fields.number1.value).toBe(1); + expect(result.current.fields.number1.initialValue).toBe(1); + expect(result.current.fields.number1.onChange).toBeDefined(); + }); + + it(`[hook] useForm lifecycle. Set the initial value. Change the field value. Undo changes. Change the field. Do changes.`, async () => { + const initialFieldValue = ''; + const fieldType = 'text'; + + const initialFields: FormConfiguration = { + text1: { + type: fieldType, + initialValue: initialFieldValue, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + expect(result.current.fields.text1.onChange).toBeDefined(); + + // change the input + const changedValue = 't'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // undone changes + act(() => { + result.current.undoChanges(); + }); + + // assert undo changes state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // change the input + const changedValue2 = 'e'; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue2, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue2); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + + // done changes + act(() => { + result.current.doChanges(); + }); + + // assert do changes state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue2); + expect(result.current.fields.text1.initialValue).toBe(changedValue2); + }); + + it(`[hook] useForm lifecycle. Set the initial value. Change the field value to invalid value`, async () => { + const initialFieldValue = 'test'; + const fieldType = 'text'; + + const initialFields: FormConfiguration = { + text1: { + type: fieldType, + initialValue: initialFieldValue, + validate: (value: string): string | undefined => + value.length ? undefined : `Validation error: string can be empty.`, + }, + }; + + const { result } = renderHook(() => useForm(initialFields)); + + // assert initial state + expect(result.current.fields.text1.changed).toBe(false); + expect(result.current.fields.text1.error).toBeUndefined(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(initialFieldValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + expect(result.current.fields.text1.onChange).toBeDefined(); + + // change the input + const changedValue = ''; + act(() => { + result.current.fields.text1.onChange({ + target: { + value: changedValue, + }, + }); + }); + + // assert changed state + expect(result.current.fields.text1.changed).toBe(true); + expect(result.current.fields.text1.error).toBeTruthy(); + expect(result.current.fields.text1.type).toBe(fieldType); + expect(result.current.fields.text1.value).toBe(changedValue); + expect(result.current.fields.text1.initialValue).toBe(initialFieldValue); + }); + + it('[hook] useForm. Verify the hook behavior when receives a custom field type', async () => { + const formFields: FormConfiguration = { + customField: { + type: 'custom', + initialValue: 'default value', + component: (props:IInputForm) => (<>any component), + }, + }; + + const { result } = renderHook(() => useForm(formFields)); + expect(result.current.fields.customField.component).toBeInstanceOf(Function); + expect(result.current.fields.customField.type).toBe('custom'); + }); }); diff --git a/public/components/common/form/hooks.tsx b/public/components/common/form/hooks.tsx index f3837b4b32..1d2f09e020 100644 --- a/public/components/common/form/hooks.tsx +++ b/public/components/common/form/hooks.tsx @@ -1,91 +1,142 @@ import { useState, useRef } from 'react'; import { isEqual } from 'lodash'; import { EpluginSettingType } from '../../../../common/constants'; +import { + CustomSettingType, + EnhancedFields, + FormConfiguration, + SettingTypes, + UseFormReturn, +} from './types'; -function getValueFromEvent(event, type){ - return (getValueFromEventType[type] || getValueFromEventType.default)(event); -}; -const getValueFromEventType = { - [EpluginSettingType.switch] : (event: any) => event.target.checked, +interface IgetValueFromEventType { + [key: string]: (event: any) => any; +} + +const getValueFromEventType: IgetValueFromEventType = { + [EpluginSettingType.switch]: (event: any) => event.target.checked, [EpluginSettingType.editor]: (value: any) => value, - [EpluginSettingType.filepicker]: (value: any) => value, + custom: (event:any) => event.target, default: (event: any) => event.target.value, }; -export const useForm = (fields) => { - const [formFields, setFormFields] = useState(Object.entries(fields).reduce((accum, [fieldKey, fieldConfiguration]) => ({ - ...accum, - [fieldKey]: { - currentValue: fieldConfiguration.initialValue, - initialValue: fieldConfiguration.initialValue, - } - }), {})); +/** + * 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 | string, +): any { + + return getValueFromEventType.hasOwnProperty(type) ? getValueFromEventType[type](event) : getValueFromEventType.default(event) +} + + + +export const useForm = (fields: FormConfiguration): UseFormReturn => { + const [formFields, setFormFields] = useState<{ + [key: string]: { currentValue: any; initialValue: any }; + }>( + Object.entries(fields).reduce( + (accum, [fieldKey, fieldConfiguration]) => ({ + ...accum, + [fieldKey]: { + currentValue: fieldConfiguration.initialValue, + initialValue: fieldConfiguration.initialValue, + }, + }), + {}, + ), + ); - const fieldRefs = useRef({}); + const fieldRefs = useRef<{ [key: string]: any }>({}); - const enhanceFields = Object.entries(formFields).reduce((accum, [fieldKey, {currentValue: value, ...restFieldState}]) => ({ - ...accum, - [fieldKey]: { - ...fields[fieldKey], - ...restFieldState, - type: fields[fieldKey].type, - value, - changed: !isEqual(restFieldState.initialValue, value), - error: fields[fieldKey]?.validate?.(value), - setInputRef: (reference) => {fieldRefs.current[fieldKey] = reference}, - inputRef: fieldRefs.current[fieldKey], - onChange: (event) => { - const inputValue = getValueFromEvent(event, fields[fieldKey].type); - const currentValue = fields[fieldKey]?.transformChangedInputValue?.(inputValue) ?? inputValue; - setFormFields(state => ({ - ...state, - [fieldKey]: { - ...state[fieldKey], - currentValue, - } - })) + const enhanceFields = Object.entries(formFields).reduce( + (accum, [fieldKey, { currentValue: value, ...restFieldState }]) => ({ + ...accum, + [fieldKey]: { + ...fields[fieldKey], + ...restFieldState, + type: fields[fieldKey].type, + value, + changed: !isEqual(restFieldState.initialValue, value), + error: fields[fieldKey]?.validate?.(value), + setInputRef: (reference: any) => { + fieldRefs.current[fieldKey] = reference; + }, + inputRef: fieldRefs.current[fieldKey], + onChange: (event: any) => { + const inputValue = getValueFromEvent(event, fields[fieldKey].type); + const currentValue = + fields[fieldKey]?.transformChangedInputValue?.(inputValue) ?? + inputValue; + setFormFields(state => ({ + ...state, + [fieldKey]: { + ...state[fieldKey], + currentValue, + }, + })); + }, }, - } - }), {}); + }), + {}, + ); const changed = Object.fromEntries( - Object.entries(enhanceFields).filter(([, {changed}]) => changed).map(([fieldKey, {value}]) => ([fieldKey, fields[fieldKey]?.transformChangedOutputValue?.(value) ?? value])) + Object.entries(enhanceFields as EnhancedFields) + .filter(([, { changed }]) => changed) + .map(([fieldKey, { value }]) => [ + fieldKey, + fields[fieldKey]?.transformChangedOutputValue?.(value) ?? value, + ]), ); const errors = Object.fromEntries( - Object.entries(enhanceFields).filter(([, {error}]) => error).map(([fieldKey, {error}]) => ([fieldKey, error])) - ); + Object.entries(enhanceFields as EnhancedFields) + .filter(([, { error }]) => error) + .map(([fieldKey, { error }]) => [fieldKey, error]), + ) as { [key: string]: string }; - function undoChanges(){ - setFormFields(state => Object.fromEntries( - Object.entries(state).map(([fieldKey, fieldConfiguration]) => ([ - fieldKey, - { - ...fieldConfiguration, - currentValue: fieldConfiguration.initialValue - } - ])) - )); - }; + function undoChanges() { + setFormFields(state => + Object.fromEntries( + Object.entries(state).map(([fieldKey, fieldConfiguration]) => [ + fieldKey, + { + ...fieldConfiguration, + currentValue: fieldConfiguration.initialValue, + }, + ]), + ), + ); + } - function doChanges(){ - setFormFields(state => Object.fromEntries( - Object.entries(state).map(([fieldKey, fieldConfiguration]) => ([ - fieldKey, - { - ...fieldConfiguration, - initialValue: fieldConfiguration.currentValue - } - ])) - )); - }; + function doChanges() { + setFormFields(state => + Object.fromEntries( + Object.entries(state).map(([fieldKey, fieldConfiguration]) => [ + fieldKey, + { + ...fieldConfiguration, + initialValue: fieldConfiguration.currentValue, + }, + ]), + ), + ); + } return { fields: enhanceFields, changed, errors, undoChanges, - doChanges + doChanges, }; }; diff --git a/public/components/common/form/index.tsx b/public/components/common/form/index.tsx index 2a6da4610e..0c3fa8a681 100644 --- a/public/components/common/form/index.tsx +++ b/public/components/common/form/index.tsx @@ -7,6 +7,23 @@ import { InputFormSwitch } from './input_switch'; import { InputFormFilePicker } from './input_filepicker'; import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { SettingTypes } from './types'; + +interface InputFormProps { + type: SettingTypes; + value: any; + onChange: (event: React.ChangeEvent) => void; + error?: string; + label?: string; + 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, @@ -18,9 +35,10 @@ export const InputForm = ({ footer, preInput, postInput, -...rest}) => { + ...rest +}: InputFormComponentProps) => { - const ComponentInput = Input[type]; + const ComponentInput = Input[type as keyof typeof Input] as React.ComponentType; if(!ComponentInput){ return null; diff --git a/public/components/common/form/types.ts b/public/components/common/form/types.ts index e064804790..737c0c8ab9 100644 --- a/public/components/common/form/types.ts +++ b/public/components/common/form/types.ts @@ -1,19 +1,82 @@ -import { TPluginSettingWithKey } from "../../../../common/constants"; +import { TPluginSettingWithKey } from '../../../../common/constants'; export interface IInputFormType { - field: TPluginSettingWithKey - value: any - onChange: (event: any) => void - isInvalid?: boolean - options: any - setInputRef: (reference: any) => void -}; + field: TPluginSettingWithKey; + value: any; + onChange: (event: any) => void; + isInvalid?: boolean; + options: any; + setInputRef: (reference: any) => void; +} 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) -}; + field: TPluginSettingWithKey; + initialValue: any; + onChange: (event: any) => void; + label?: string; + preInput?: (options: { value: any; error: string | null }) => JSX.Element; + postInput?: (options: { value: any; error: string | null }) => JSX.Element; +} + +/// use form hook types +export type SettingTypes = + | 'text' + | 'textarea' + | 'number' + | 'select' + | 'switch' + | 'editor' + | 'filepicker'; + +interface FieldConfiguration { + initialValue: any; + validate?: (value: any) => string | undefined; + transformChangedInputValue?: (value: any) => any; + transformChangedOutputValue?: (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; +} + +export interface FormConfiguration { + [key: string]: DefaultFieldConfiguration | CustomFieldConfiguration; +} + +interface EnhancedField { + currentValue: any; + initialValue: any; + value: any; + changed: boolean; + error: string | null; + 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 interface EnhancedFields { + [key: string]: EnhancedDefaultField | EnhancedCustomField; +} + +export interface UseFormReturn { + fields: EnhancedFields; + changed: { [key: string]: any }; + errors: { [key: string]: string }; + undoChanges: () => void; + doChanges: () => void; +} From 106cda1ac0f00694fc472929c8466f7cc4d1b541 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:36:24 -0300 Subject: [PATCH 05/24] [Redesign add agent] Add register agent command generator (#5469) --- .../config/os-commands-definitions.ts | 148 +++++++ .../core/register-commands/README.md | 340 +++++++++++++++ .../command-generator.test.ts | 398 ++++++++++++++++++ .../command-generator/command-generator.ts | 179 ++++++++ .../register-commands/exceptions/index.ts | 85 ++++ .../optional-parameters-manager.test.ts | 229 ++++++++++ .../optional-parameters-manager.ts | 49 +++ .../get-install-command.service.test.ts | 118 ++++++ .../services/get-install-command.service.ts | 38 ++ .../search-os-definitions.service.test.ts | 199 +++++++++ .../services/search-os-definitions.service.ts | 86 ++++ .../core/register-commands/types.ts | 98 +++++ .../register-agent/hooks/README.md | 175 ++++++++ .../hooks/use-register-agent-commands.test.ts | 237 +++++++++++ .../hooks/use-register-agent-commands.ts | 109 +++++ 15 files changed, 2488 insertions(+) create mode 100644 public/controllers/register-agent/config/os-commands-definitions.ts create mode 100644 public/controllers/register-agent/core/register-commands/README.md create mode 100644 public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts create mode 100644 public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts create mode 100644 public/controllers/register-agent/core/register-commands/exceptions/index.ts create mode 100644 public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts create mode 100644 public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts create mode 100644 public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts create mode 100644 public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts create mode 100644 public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts create mode 100644 public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts create mode 100644 public/controllers/register-agent/core/register-commands/types.ts create mode 100644 public/controllers/register-agent/hooks/README.md create mode 100644 public/controllers/register-agent/hooks/use-register-agent-commands.test.ts create mode 100644 public/controllers/register-agent/hooks/use-register-agent-commands.ts diff --git a/public/controllers/register-agent/config/os-commands-definitions.ts b/public/controllers/register-agent/config/os-commands-definitions.ts new file mode 100644 index 0000000000..dc69156571 --- /dev/null +++ b/public/controllers/register-agent/config/os-commands-definitions.ts @@ -0,0 +1,148 @@ +import { IOSDefinition, tOptionalParams } from '../core/register-commands/types'; + +// Defined OS combinations +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => + `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'rpm', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => + `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +const windowsDefinition: IOSDefinition = { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x86', + urlPackage: props => + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, + installCommand: props => + `Invoke-WebRequest -Uri ${props.urlPackage} -OutFile \${env.tmp}\\wazuh-agent.${props.extension}; msiexec.exe /i \${env.tmp}\\wazuh-agent.${props.extension} /q`, + startCommand: props => `Start-Service -Name wazuh-agent`, + }, + ], +}; + +const macDefinition: IOSDefinition = { + name: 'mac', + options: [ + { + extension: 'pkg', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, + installCommand: props => + `mac -so wazuh-agent.pkg ${props.urlPackage} && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target /`, + startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + }, + ], +}; + +export const osCommandsDefinitions = [ + linuxDefinition, + windowsDefinition, + macDefinition, +]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + agent_name: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + } + } +} diff --git a/public/controllers/register-agent/core/register-commands/README.md b/public/controllers/register-agent/core/register-commands/README.md new file mode 100644 index 0000000000..d53288e940 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/README.md @@ -0,0 +1,340 @@ +# Documentation + +- [Register Agent](#register-agent) + - [Solution details](#solution-details) + - [Configuration details](#configuration-details) + - [OS Definitions](#os-definitions) + - [Configuration example](#configuration-example) + - [Validations](#validations) + - [Optional Parameters Configuration](#optional-parameters-configuration) + - [Configuration example](#configuration-example-1) + - [Validations](#validations-1) + - [Command Generator](#command-generator) + - [Get install command](#get-install-command) + - [Get start command](#get-start-command) + - [Get url package](#get-url-package) + - [Get all commands](#get-all-commands) + +# Register Agent + +The register agent is a process that will allow the user to register an agent in the Wazuh Manager. The plugin will provide a form where the user will be able to select the OS and the package that he wants to install. The plugin will generate the registration commands and will show them to the user. + +# Solution details + +To optimize and make more easier the process to generate the registration commands we have created a class called `Command Generator` that given a set of parameters it will generate the registration commands. + +## Configuration + +To make the command generator works we need to configure the following parameters and pass them to the class: + +## OS Definitions + +The OS definitions are a set of parameters that will be used to generate the registration commands. The parameters are the following: + +```ts + + +// global types + +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +/// .... + +interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +export interface IOSCommandsDefinition { + extension: OS['extension']; + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + startCommand: (props: tOSEntryProps) => string; +} + +``` + +This configuration will define the different OS that we want to support and the different packages that we want to support for each OS. The `urlPackage` function will be used to generate the URL to download the package, the `installCommand` function will be used to generate the command to install the package and the `startCommand` function will be used to generate the command to start the agent. + +### Configuration example + +```ts + +const osDefinitions: IOSDefinition[] = [{ + name: 'linux', + options: [ + { + extension: 'rpm', + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + { + extension: 'deb', + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + } + ], +}, +{ + name: 'windows', + options: [ + { + extension: 'msi', + architecture: '32/64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + ], + } +}; +``` + +## Validations + +The `Command Generator` will validate the OS Definitions received and will throw an error if the configuration is not valid. The validations are the following: + +- The OS Definitions must not have duplicated OS names defined. +- The OS Definitions must not have duplicated options defined. +- The OS names would be defined in the `tOS` type. +- The Package Extensions would be defined in the `tPackageExtensions` type. + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Optional Parameters Configuration + +The optional parameters are a set of parameters that will be added to the registration commands. The parameters are the following: + +```ts + +export type tOptionalParamsName = + | 'server_address' + | 'agent_name' + | 'protocol' + | 'agent_group' + | 'wazuh_password'; + +export type tOptionalParams = { + [key in tOptionalParamsName]: { + property: string; + getParamCommand: (props) => string; + }; +} + +``` + +This configuration will define the different optional parameters that we want to support and the way how to we will process and show in the commands.The `getParamCommand` is the function that will process the props received and show the final command format. + +### Configuration example + +```ts + +export const optionalParameters: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => 'returns the optional param command' + } + }, + any_other: { + property: 'PARAM NAME IN THE COMMAND', + getParamCommand: props => 'returns the optional param command' + }, +} + +``` + +## Validations + +The `Command Generator` will validate the Optional Parameters received and will throw an error if the configuration is not valid. The validations are the following: + +- The Optional Parameters must not have duplicated names defined. +- The Optional Parameters name would be defined in the `tOptionalParamsName` type. + + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Command Generator + +To use the command generator we need to import the class and create a new instance of the class. The class will receive the OS Definitions and the Optional Parameters as parameters. + +```ts +import { CommandGenerator } from 'path/command-generator'; + +// Commange Generator interface/contract + +export interface ICommandGenerator extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams(props: IOptionalParameters): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +``` + +When the class is created the definitions provided will be validated and if the configuration is not valid an error will be thrown. The errors were mentioned in the configurations `Validations` section. + +### Get install command + +To generate the install command we need to call the `getInstallComand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// get install command +const installCommand = commandGenerator.getInstallCommand(); + +``` + +The `Command Generator` will search the OS provided and search in the OS Definitions and will process the command using the `installCommand` function defined in the OS Definition. If the OS is not found an error will be thrown. +If the `getInstallCommand` but the OS is not selected an error will be thrown. + +## Get start command + +To generate the install command we need to call the `getStartCommand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// get start command +const installCommand = commandGenerator.getStartCommand(); + +``` + +## Get url package + +To generate the install command we need to call the `getUrlPackage` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +const urlPackage = commandGenerator.getUrlPackage(); + +``` + +## Get all commands + +To generate the install command we need to call the `getAllCommands` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested commands. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', + extension: 'rpm' +}); + +// specify to the command generator the optional parameters that we want to use +commandGenerator.addOptionalParams({ + server_address: 'server-ip', + agent_name: 'agent-name', + any_parameter: 'any-value' +}); + +// get all commands +const installCommand = commandGenerator.getAllCommands(); + +``` + +If we specify the optional parameters the `Command Generator` will process the commands and will add the optional parameters to the commands. The optional parameters processing will be only applied to the commands that have the optional parameters defined in the Optional Parameters Definitions. If the OS Definition does not have the optional parameters defined the `Command Generator` will ignore the optional parameters. + +### getAllComands output + +```ts + +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + extension: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} + +``` \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts new file mode 100644 index 0000000000..71fd77ebd3 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -0,0 +1,398 @@ +import { CommandGenerator } from './command-generator'; +import { + IOSDefinition, + IOperationSystem, + IOptionalParameters, + tOptionalParams, +} from '../types'; +import { DuplicatedOSException, DuplicatedOSOptionException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; + +const mockedCommandValue = 'mocked command'; +const mockedCommandsResponse = jest.fn().mockReturnValue(mockedCommandValue); + +// Defined OS combinations +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +// Defined Optional Parameters + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + extension: 'msi', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, +]; + +const optionalParams: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_name: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => `${props.property}=${props.value}`, + }, + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => `${props.property}=${props.value}`, + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => `${props.property}=${props.value}`, + }, +}; + +const optionalValues: IOptionalParameters = { + server_address: '', + agent_name: '', + protocol: '', + agent_group: '', + wazuh_password: '', +}; + +describe('Command Generator', () => { + it('should create an valid instance', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + expect(commandGenerator).toBeDefined(); + }); + + it('should return the install command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getInstallCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return the start command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getStartCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return all the commands for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + commandGenerator.addOptionalParams(optionalValues); + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: 'linux', + architecture: 'x64', + extension: 'deb', + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: {}, + }); + }); + + it('should return commands with the filled optional params', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: '10.10.10.121', + agent_name: 'agent1', + protocol: 'tcp', + agent_group: '', + wazuh_password: '123456', + }; + commandGenerator.addOptionalParams(optionalValues); + + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: selectedOs.name, + architecture: selectedOs.architecture, + extension: selectedOs.extension, + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + agent_name: optionalParams.agent_name.getParamCommand({ + property: optionalParams.agent_name.property, + value: optionalValues.agent_name, + name: 'agent_name', + }), + protocol: optionalParams.protocol.getParamCommand({ + property: optionalParams.protocol.property, + value: optionalValues.protocol, + name: 'protocol', + }), + wazuh_password: optionalParams.wazuh_password.getParamCommand({ + property: optionalParams.wazuh_password.property, + value: optionalValues.wazuh_password, + name: 'wazuh_password', + }), + }, + }); + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSOptionException); + } + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + extension: 'deb', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSException); + } + }); + + it('should return an ERROR when we want to get commands and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getAllCommands(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the install command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getInstallCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the start command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getStartCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when receive an empty version', () => { + try { + new CommandGenerator(osDefinitions, optionalParams, ''); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + + it('should receives the solved optional params when the install command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getInstallCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); + + it('should receives the solved optional params when the start command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getStartCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); +}); \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts new file mode 100644 index 0000000000..8dd6b69889 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts @@ -0,0 +1,179 @@ +import { + ICommandsResponse, + IOSCommandsDefinition, + IOSDefinition, + IOperationSystem, + IOptionalParameters, + IOptionalParametersManager, + tOptionalParams, +} from '../types'; +import { ICommandGenerator } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from '../services/search-os-definitions.service'; +import { OptionalParametersManager } from '../optional-parameters-manager/optional-parameters-manager'; +import { NoArchitectureSelectedException, NoExtensionSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; +import { version } from '../../../../../../package.json'; + +export class CommandGenerator implements ICommandGenerator { + os: OS['name'] | null = null; + osDefinitionSelected: IOSCommandsDefinition | null = null; + optionalsManager: IOptionalParametersManager; + protected optionals: IOptionalParameters | object = {}; + constructor( + public osDefinitions: IOSDefinition[], + protected optionalParams: tOptionalParams, + public wazuhVersion: string = version, + ) { + // validate os definitions received + validateOSDefinitionsDuplicated(this.osDefinitions); + validateOSDefinitionHasDuplicatedOptions(this.osDefinitions); + if(wazuhVersion == ''){ + throw new WazuhVersionUndefinedException(); + } + this.optionalsManager = new OptionalParametersManager(optionalParams); + } + + /** + * This method selects the operating system to use based on the given parameters + * @param params - The operating system parameters to select + */ + selectOS(params: OS) { + try { + // Check if the selected operating system is valid + this.osDefinitionSelected = this.checkIfOSisValid(params); + // Set the selected operating system + this.os = params.name; + } catch (error) { + // If the selected operating system is not valid, reset the selected OS and OS definition + this.osDefinitionSelected = null; + this.os = null; + throw error; + } + } + + /** + * This method adds the optional parameters to use based on the given parameters + * @param props - The optional parameters to select + * @returns The selected optional parameters + */ + addOptionalParams(props: IOptionalParameters): void { + // Get all the optional parameters based on the given parameters + this.optionals = this.optionalsManager.getAllOptionalParams(props); + } + + /** + * This method checks if the selected operating system is valid + * @param params - The operating system parameters to check + * @returns The selected operating system definition + * @throws An error if the operating system is not valid + */ + private checkIfOSisValid(params: OS): IOSCommandsDefinition { + const { name, architecture, extension } = params; + if (!name) { + throw new NoOSSelectedException(); + } + if (!architecture) { + throw new NoArchitectureSelectedException(); + } + if (!extension) { + throw new NoExtensionSelectedException(); + } + + const option = searchOSDefinitions(this.osDefinitions, { + name, + architecture, + extension, + }); + return option; + } + + /** + * This method gets the URL package for the selected operating system + * @returns The URL package for the selected operating system + * @throws An error if the operating system is not selected + */ + getUrlPackage(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + return this.osDefinitionSelected.urlPackage({ + wazuhVersion: this.wazuhVersion, + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + name: this.os as OS['name'], + }); + } + + /** + * This method gets the install command for the selected operating system + * @returns The install command for the selected operating system + * @throws An error if the operating system is not selected + */ + getInstallCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.installCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + urlPackage: this.getUrlPackage(), + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets the start command for the selected operating system + * @returns The start command for the selected operating system + * @throws An error if the operating system is not selected + */ + getStartCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.startCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets all the commands for the selected operating system + * @returns An object containing all the commands for the selected operating system + * @throws An error if the operating system is not selected + */ + getAllCommands(): ICommandsResponse { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return { + wazuhVersion: this.wazuhVersion, + os: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + extension: this.osDefinitionSelected.extension as OS['extension'], + url_package: this.getUrlPackage(), + install_command: this.getInstallCommand(), + start_command: this.getStartCommand(), + optionals: this.optionals, + }; + } + + /** + * Returns the optional paramaters processed + * @returns optionals + */ + getOptionalParamsCommands(): IOptionalParameters | object { + return this.optionals; + } + +} diff --git a/public/controllers/register-agent/core/register-commands/exceptions/index.ts b/public/controllers/register-agent/core/register-commands/exceptions/index.ts new file mode 100644 index 0000000000..c84f513157 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/exceptions/index.ts @@ -0,0 +1,85 @@ +export class NoOptionFoundException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No OS option found for "${osName}" "${architecture}" "${extension}".Please check the OS definitions."`, + ); + } +} + +export class NoOSOptionFoundException extends Error { + constructor(osName: string) { + super( + `No OS option found for "${osName}".Please check the OS definitions."`, + ); + } +} + +export class NoStartCommandDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No start command definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoInstallCommandDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No install command definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoPackageURLDefinitionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `No package URL definition found for "${osName}" "${architecture}" "${extension}". Please check the OS definitions.`, + ); + } +} + +export class NoOptionalParamFoundException extends Error { + constructor(paramName: string) { + super( + `Optional parameter "${paramName}" not found. Please check the optional parameters definitions.`, + ); + } +} + +export class DuplicatedOSException extends Error { + constructor(osName: string) { + super(`Duplicate OS name found: ${osName}`); + } +} + +export class DuplicatedOSOptionException extends Error { + constructor(osName: string, architecture: string, extension: string) { + super( + `Duplicate OS option found for "${osName}" "${architecture}" "${extension}"`, + ); + } +} + +export class WazuhVersionUndefinedException extends Error { + constructor() { + super(`Wazuh version not defined`); + } +} + +export class NoOSSelectedException extends Error { + constructor() { + super(`OS not selected. Please select`); + } +} + +export class NoArchitectureSelectedException extends Error { + constructor() { + super(`Architecture not selected. Please select`); + } +} + +export class NoExtensionSelectedException extends Error { + constructor() { + super(`Extension not selected. Please select`); + } +} diff --git a/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts new file mode 100644 index 0000000000..af6bef5b15 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts @@ -0,0 +1,229 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { + IOptionalParameters, + tOptionalParams, + tOptionalParamsCommandProps, +} from '../types'; +import { OptionalParametersManager } from './optional-parameters-manager'; + +type tOptionalParamsFieldname = + | 'server_address' + | 'protocol' + | 'agent_group' + | 'wazuh_password' + | 'another_valid_fieldname'; + +const returnOptionalParam = ( + props: tOptionalParamsCommandProps, +) => { + const { property, value } = props; + return `${property}=${value}`; +}; +const optionalParametersDefinition: tOptionalParams = + { + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: returnOptionalParam, + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: returnOptionalParam, + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: returnOptionalParam, + }, + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: returnOptionalParam, + }, + another_valid_fieldname: { + property: 'WAZUH_ANOTHER_PROPERTY', + getParamCommand: returnOptionalParam, + }, + }; + +describe('Optional Parameters Manager', () => { + it('should create an instance successfully', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + expect(optParamManager).toBeDefined(); + }); + + it.each([ + ['server_address', '10.10.10.27'], + ['protocol', 'TCP'], + ['agent_group', 'group1'], + ['wazuh_password', '123456'], + ['another_valid_fieldname', 'another_valid_value'] + ])( + `should return the corresponding command for "%s" param with "%s" value`, + (name, value) => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const commandParam = optParamManager.getOptionalParam({ + name: name as tOptionalParamsFieldname, + value, + }); + const defs = + optionalParametersDefinition[ + name as keyof typeof optionalParametersDefinition + ]; + expect(commandParam).toBe( + defs.getParamCommand({ + property: defs.property, + value, + name: name as tOptionalParamsFieldname, + }), + ); + }, + ); + + it('should return ERROR when the param received is not defined in the params definition', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const invalidParam = 'invalid_optional_param'; + try { + // @ts-ignore + optParamManager.getOptionalParam({ name: invalidParam, value: 'value' }); + } catch (error) { + expect(error).toBeInstanceOf(NoOptionalParamFoundException); + } + }); + + it('should return the corresponding command for all the params', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues: IOptionalParameters = { + protocol: 'TCP', + agent_group: 'group1', + wazuh_password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + expect(resolvedParams).toEqual({ + agent_group: optionalParametersDefinition.agent_group.getParamCommand({ + name: 'agent_group', + property: optionalParametersDefinition.agent_group.property, + value: paramsValues.agent_group, + }), + protocol: optionalParametersDefinition.protocol.getParamCommand({ + name: 'protocol', + property: optionalParametersDefinition.protocol.property, + value: paramsValues.protocol, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + wazuh_password: + optionalParametersDefinition.wazuh_password.getParamCommand({ + name: 'wazuh_password', + property: optionalParametersDefinition.wazuh_password.property, + value: paramsValues.wazuh_password, + }), + another_valid_fieldname: + optionalParametersDefinition.another_valid_fieldname.getParamCommand({ + name: 'another_valid_fieldname', + property: + optionalParametersDefinition.another_valid_fieldname.property, + value: paramsValues.another_valid_fieldname, + }), + } as IOptionalParameters); + }); + + it('should return the corresponse command for all the params with NOT empty values', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues: IOptionalParameters = { + protocol: 'TCP', + agent_group: 'group1', + wazuh_password: '123456', + server_address: 'server', + another_valid_fieldname: 'another_valid_value', + }; + + const resolvedParams = optParamManager.getAllOptionalParams(paramsValues); + expect(resolvedParams).toEqual({ + agent_group: optionalParametersDefinition.agent_group.getParamCommand({ + name: 'agent_group', + property: optionalParametersDefinition.agent_group.property, + value: paramsValues.agent_group, + }), + protocol: optionalParametersDefinition.protocol.getParamCommand({ + name: 'protocol', + property: optionalParametersDefinition.protocol.property, + value: paramsValues.protocol, + }), + server_address: + optionalParametersDefinition.server_address.getParamCommand({ + name: 'server_address', + property: optionalParametersDefinition.server_address.property, + value: paramsValues.server_address, + }), + wazuh_password: + optionalParametersDefinition.wazuh_password.getParamCommand({ + name: 'wazuh_password', + property: optionalParametersDefinition.wazuh_password.property, + value: paramsValues.wazuh_password, + }), + another_valid_fieldname: + optionalParametersDefinition.another_valid_fieldname.getParamCommand({ + name: 'another_valid_fieldname', + property: + optionalParametersDefinition.another_valid_fieldname.property, + value: paramsValues.another_valid_fieldname, + }), + } as IOptionalParameters); + }); + + it('should return ERROR when the param received is not defined in the params definition', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = { + serverAddress: 'invalid server address property value', + }; + + try { + // @ts-ignore + optParamManager.getAllOptionalParams(paramsValues); + } catch (error) { + expect(error).toBeInstanceOf(NoOptionalParamFoundException); + } + }); + + it('should return empty object response when receive an empty params object', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = {}; + // @ts-ignore + const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); + }); + + it('should return empty object response when receive all the params values with empty string ("")', () => { + const optParamManager = new OptionalParametersManager( + optionalParametersDefinition, + ); + const paramsValues = { + server_address: '', + agent_name: '', + protocol: '', + agent_group: '', + wazuh_password: '', + }; + // @ts-ignore + const optionals = optParamManager.getAllOptionalParams(paramsValues); + expect(optionals).toEqual({}); + }); +}); diff --git a/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts new file mode 100644 index 0000000000..34b943f797 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts @@ -0,0 +1,49 @@ +import { NoOptionalParamFoundException } from '../exceptions'; +import { IOptionalParamInput, IOptionalParameters, IOptionalParametersManager, tOptionalParams } from '../types'; + +export class OptionalParametersManager implements IOptionalParametersManager { + constructor(private optionalParamsConfig: tOptionalParams) {} + + /** + * Returns the command string for a given optional parameter. + * @param props - An object containing the optional parameter name and value. + * @returns The command string for the given optional parameter. + * @throws NoOptionalParamFoundException if the given optional parameter name is not found in the configuration. + */ + getOptionalParam(props: IOptionalParamInput) { + const { value, name } = props; + if (!this.optionalParamsConfig[name]) { + throw new NoOptionalParamFoundException(name); + } + return this.optionalParamsConfig[name].getParamCommand({ + value, + property: this.optionalParamsConfig[name].property, + name + }); + } + + /** + * Returns an object containing the command strings for all optional parameters with non-empty values. + * @param paramsValues - An object containing the optional parameter names and values. + * @returns An object containing the command strings for all optional parameters with non-empty values. + * @throws NoOptionalParamFoundException if any of the given optional parameter names is not found in the configuration. + */ + getAllOptionalParams(paramsValues: IOptionalParameters){ + // get keys for only the optional params with values !== '' + const optionalParams = Object.keys(paramsValues).filter(key => paramsValues[key as keyof typeof paramsValues] !== '') as Array; + const resolvedOptionalParams: any = {}; + for(const param of optionalParams){ + if(!this.optionalParamsConfig[param]){ + throw new NoOptionalParamFoundException(param as string); + } + + const paramDef = this.optionalParamsConfig[param]; + resolvedOptionalParams[param as string] = paramDef.getParamCommand({ + name: param as Params, + value: paramsValues[param] as string, + property: paramDef.property + }) as string; + } + return resolvedOptionalParams; + } +} diff --git a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts new file mode 100644 index 0000000000..733737e8c5 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts @@ -0,0 +1,118 @@ +import { getInstallCommandByOS } from './get-install-command.service'; +import { IOSCommandsDefinition, IOSDefinition, IOptionalParameters } from '../types'; +import { + NoInstallCommandDefinitionException, + NoPackageURLDefinitionException, + WazuhVersionUndefinedException, +} from '../exceptions'; + + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password' | 'another_optional_parameter'; + +const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + installCommand: props => 'install command mocked', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', +}; +describe('getInstallCommandByOS', () => { + it('should return the correct install command for each OS', () => { + const installCommand = getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + expect(installCommand).toBe('install command mocked'); + }); + + it('should return ERROR when the version is not received', () => { + try { + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + it('should return ERROR when the OS has no install command', () => { + // @ts-ignore + const osDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + try { + getInstallCommandByOS( + osDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(NoInstallCommandDefinitionException); + } + }); + it('should return ERROR when the OS has no package url', () => { + try { + getInstallCommandByOS(validOsDefinition, '', '4.4', 'linux'); + } catch (error) { + expect(error).toBeInstanceOf(NoPackageURLDefinitionException); + } + }); + + it('should return install command with optional parameters', () => { + const mockedInstall = jest.fn(); + const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + extension: 'deb', + installCommand: mockedInstall, + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + + const optionalParams: IOptionalParameters = { + agent_group: 'WAZUH_GROUP=agent_group', + agent_name: 'WAZUH_NAME=agent_name', + protocol: 'WAZUH_PROTOCOL=UDP', + server_address: 'WAZUH_MANAGER=server_address', + wazuh_password: 'WAZUH_PASSWORD=1231323', + another_optional_parameter: 'params value' + }; + + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + optionalParams + ); + expect(mockedInstall).toBeCalledTimes(1); + expect(mockedInstall).toBeCalledWith(expect.objectContaining({ optionals: optionalParams })); + }) +}); diff --git a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts new file mode 100644 index 0000000000..d66e75c8c0 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts @@ -0,0 +1,38 @@ +import { NoInstallCommandDefinitionException, NoPackageURLDefinitionException, WazuhVersionUndefinedException } from "../exceptions"; +import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from "../types"; + +/** + * Returns the installation command for a given operating system. + * @param {IOSCommandsDefinition} osDefinition - The definition of the operating system. + * @param {string} packageUrl - The URL of the package to install. + * @param {string} version - The version of Wazuh to install. + * @param {string} osName - The name of the operating system. + * @param {IOptionalParameters} [optionals] - Optional parameters to include in the command. + * @returns {string} The installation command for the given operating system. + * @throws {NoInstallCommandDefinitionException} If the installation command is not defined for the given operating system. + * @throws {NoPackageURLDefinitionException} If the package URL is not defined. + * @throws {WazuhVersionUndefinedException} If the Wazuh version is not defined. + */ +export function getInstallCommandByOS(osDefinition: IOSCommandsDefinition, packageUrl: string, version: string, osName: string, optionals?: IOptionalParameters) { + + if (!osDefinition.installCommand) { + throw new NoInstallCommandDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + } + + if(!packageUrl || packageUrl === ''){ + throw new NoPackageURLDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + } + + if(!version || version === ''){ + throw new WazuhVersionUndefinedException(); + } + + return osDefinition.installCommand({ + urlPackage: packageUrl, + wazuhVersion: version, + name: osName as OS['name'], + architecture: osDefinition.architecture, + extension: osDefinition.extension, + optionals, + }); +} \ No newline at end of file diff --git a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts new file mode 100644 index 0000000000..f08b1f016d --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts @@ -0,0 +1,199 @@ +import { + NoOSOptionFoundException, + NoOptionFoundException, +} from '../exceptions'; +import { IOSDefinition } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from './search-os-definitions.service'; + +const mockedInstallCommand = (props: any) => 'install command mocked'; +const mockedStartCommand = (props: any) => 'start command mocked'; +const mockedUrlPackage = (props: any) => 'https://package-url.com'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const validOSDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, +]; + +describe('search OS definitions services', () => { + describe('searchOSDefinitions', () => { + it('should return the OS definition if the OS name is found', () => { + const result = searchOSDefinitions(validOSDefinitions, { + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + expect(result).toMatchObject(validOSDefinitions[0].options[0]); + }); + + it('should throw an error if the OS name is not found', () => { + expect(() => + searchOSDefinitions(validOSDefinitions, { + // @ts-ignore + name: 'invalid-os', + architecture: 'x64', + extension: 'deb', + }), + ).toThrow(NoOSOptionFoundException); + }); + + it('should throw an error if the OS name is found but the architecture is not found', () => { + expect(() => + searchOSDefinitions(validOSDefinitions, { + name: 'linux', + architecture: 'invalid-architecture', + extension: 'deb', + }), + ).toThrow(NoOptionFoundException); + }); + }); + + describe('validateOSDefinitionsDuplicated', () => { + it('should not throw an error if there are no duplicated OS definitions', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + extension: 'msi', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionsDuplicated(osDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions', () => { + const osDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + // @ts-ignore + packageManager: 'aix', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }; + const osDefinitions: IOSDefinition[] = [osDefinition, osDefinition]; + + expect(() => validateOSDefinitionsDuplicated(osDefinitions)).toThrow(); + }); + }); + + describe('validateOSDefinitionHasDuplicatedOptions', () => { + it('should not throw an error if there are no duplicated OS definitions with different options', () => { + expect(() => + validateOSDefinitionHasDuplicatedOptions(validOSDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions with different options', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + { + extension: 'deb', + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionHasDuplicatedOptions(osDefinitions), + ).toThrow(); + }); + }); +}); diff --git a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts new file mode 100644 index 0000000000..fcd5509681 --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -0,0 +1,86 @@ +import { + DuplicatedOSException, + DuplicatedOSOptionException, + NoOSOptionFoundException, + NoOptionFoundException, +} from '../exceptions'; +import { IOSDefinition, IOperationSystem } from '../types'; + +/** + * Searches for the OS definition option that matches the given operation system parameters. + * Throws an exception if no matching option is found. + * + * @param osDefinitions - The list of OS definitions to search through. + * @param params - The operation system parameters to match against. + * @returns The matching OS definition option. + * @throws NoOSOptionFoundException - If no matching OS definition is found. + * @throws NoOptionFoundException - If no matching OS definition option is found. + */ +export function searchOSDefinitions( + osDefinitions: IOSDefinition[], + params: IOperationSystem, +){ + const { name, architecture, extension } = params; + + const osDefinition = osDefinitions.find(os => os.name === name); + if (!osDefinition) { + throw new NoOSOptionFoundException(name); + } + + const osDefinitionOption = osDefinition.options.find( + option => + option.architecture === architecture && option.extension === extension, + ); + + if (!osDefinitionOption) { + throw new NoOptionFoundException(name, architecture, extension); + } + + return osDefinitionOption; +}; + +/** + * Validates that there are no duplicated OS definitions in the given list. + * Throws an exception if a duplicated OS definition is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSException - If a duplicated OS definition is found. + */ +export function validateOSDefinitionsDuplicated( + osDefinitions: IOSDefinition[], +){ + const osNames = new Set(); + + for (const osDefinition of osDefinitions) { + if (osNames.has(osDefinition.name)) { + throw new DuplicatedOSException(osDefinition.name); + } + osNames.add(osDefinition.name); + } +}; + +/** + * Validates that there are no duplicated OS definition options in the given list. + * Throws an exception if a duplicated OS definition option is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSOptionException - If a duplicated OS definition option is found. + */ +export function validateOSDefinitionHasDuplicatedOptions( + osDefinitions: IOSDefinition[], +){ + for (const osDefinition of osDefinitions) { + const options = new Set(); + for (const option of osDefinition.options) { + let ext_arch_manager = `${option.extension}_${option.architecture}`; + if (options.has(ext_arch_manager)) { + throw new DuplicatedOSOptionException( + osDefinition.name, + option.extension, + option.architecture, + ); + } + options.add(ext_arch_manager); + } + } +}; diff --git a/public/controllers/register-agent/core/register-commands/types.ts b/public/controllers/register-agent/core/register-commands/types.ts new file mode 100644 index 0000000000..efd36433ff --- /dev/null +++ b/public/controllers/register-agent/core/register-commands/types.ts @@ -0,0 +1,98 @@ +///////////////////////////////////////////////////////// +/// Domain +///////////////////////////////////////////////////////// +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +export type IOptionalParameters = { + [key in Params]: string; +}; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +interface IOptionalParamsWithValues { + optionals?: IOptionalParameters +} + + +type tOSEntryProps = IOSProps & IOptionalParamsWithValues; + +export interface IOSCommandsDefinition { + extension: OS['extension']; + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + startCommand: (props: tOSEntryProps) => string; +} + +export interface IOSProps extends IOperationSystem { + wazuhVersion: string; +} + +/////////////////////////////////////////////////////////////////// +//// Commands optional parameters +/////////////////////////////////////////////////////////////////// +interface IOptionalParamProps { + property: string; + value: string; +} + +export type tOptionalParamsCommandProps = IOptionalParamProps & { + name: T; +}; +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOptionalParamInput { + value: string; + name: T; +} +export interface IOptionalParametersManager { + getOptionalParam(props: IOptionalParamInput): string; + getAllOptionalParams(paramsValues: IOptionalParameters): object; +} + +/////////////////////////////////////////////////////////////////// +/// Command creator class +/////////////////////////////////////////////////////////////////// + +export type IOSInputs = IOperationSystem & IOptionalParameters; +export interface ICommandGenerator extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams(props: IOptionalParameters): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + extension: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} diff --git a/public/controllers/register-agent/hooks/README.md b/public/controllers/register-agent/hooks/README.md new file mode 100644 index 0000000000..0b06f1b93e --- /dev/null +++ b/public/controllers/register-agent/hooks/README.md @@ -0,0 +1,175 @@ +# Documentation + +- [useRegisterAgentCommand hook](#useregisteragentcommand-hook) +- [Advantages](#advantages) +- [Usage](#usage) +- [Types](#types) + - [Hook props](#hook-props) + - [Hook output](#hook-output) +- [Hook with Generic types](#hook-with-generic-types) + - [Operating systems types example](#operating-systems-types-example) + +## useRegisterAgentCommand hook + +This hook makes use of the `Command Generator class` to generate the commands to register agents in the manager and allows to use it in React components. + +## Advantages + +- Ease of use of the Command Generator class. +- The hook returns the methods envolved to create the register commands by the operating system and optionas specified. +- The commands generate are stored in the state of the hook and can be used in the component. + + +## Usage + +```ts + +import { useRegisterAgentCommands } from 'path/to/use-register-agent-commands'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + +/* + the props recived by the hook must implement types: + - OS: IOSDefinition[] + - optional parameters: tOptionalParams +*/ + +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(); + +// select OS depending on the specified OS defined in the hook configuration +selectOS({ + name: 'name-OS', + architecture: 'architecture-OS', + extension: 'extension-OS', +}) + +// add optionals params depending on the specified optional parameters in the hook configuration +setOptionalParams({ + field_1: 'value_1', + field_2: 'value_2', + ... +}) + +/** the commands and the optional params will be processed and stored in the hook state **/ + +// install command +console.log('install command for the selected OS with optionals params', installCommand); +// start command +console.log('start command for the selected OS with optionals params', startCommand); +// optionals params processed +console.log('optionals params processed', optionalParamsParsed); + +``` + +## Types + +### Hook props + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} +``` + +### Hook output + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +interface IUseRegisterCommandsOutput { + selectOS: (params: OS) => void; + setOptionalParams: (params: IOptionalParameters) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | {}; +} +``` + +## Hook with Generic types + +We can pass the types with the OS posibilities options and the optionals params defined. +And the hook will validate and show warning in compilation and development time. + +#### Operating systems types example + +```ts +// global types + +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOperationSystem { + name: string; + architecture: string; + extension: string; +} + +/// .... + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + + +// pass it to the hook and it will use the types when we are selecting the OS +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(OSdefintions, paramsDefinitions); + +// when the options are not valid depending on the types defined, the IDE will show a warning +selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'rpm', +}) + +```` + diff --git a/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts new file mode 100644 index 0000000000..ab0f6727df --- /dev/null +++ b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts @@ -0,0 +1,237 @@ +import React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { useRegisterAgentCommands } from './use-register-agent-commands'; +import { + IOSDefinition, + tOptionalParams, +} from '../core/register-commands/types'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; + extension: 'rpm' | 'deb'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; + extension: 'msi'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; + extension: 'pkg'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + extension: 'deb', + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + installCommand: props => `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + extension: 'deb', + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +export const osCommandsDefinitions = [linuxDefinition]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = + { + optional1: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + optional2: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + }; + +describe('useRegisterAgentCommands hook', () => { + it('should return installCommand and startCommand null when the hook is initialized', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return ERROR when get installCommand and the OS received is NOT valid', () => { + const { + result: { + current: { selectOS }, + }, + } = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + try { + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + }); + } catch (error) { + if (error instanceof Error) + expect(error.message).toContain('No OS option found for'); + } + }); + + it('should change the commands when the OS is selected successfully', async () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS } = hook.result.current; + const { result } = hook; + + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64' && item.extension === 'deb', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + }); + expect(result.current.installCommand).not.toBe(''); + expect(result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(1); + expect(spyStart).toBeCalledTimes(1); + }); + + it('should return commands empty when set optional params and OS is NOT selected', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return optional params empty when optional params are not added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { optionalParamsParsed } = hook.result.current; + expect(optionalParamsParsed).toEqual({}); + }); + + it('should return optional params when optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + const spy1 = jest.spyOn( + optionalParamsDefinitions.optional1, + 'getParamCommand', + ); + const spy2 = jest.spyOn( + optionalParamsDefinitions.optional2, + 'getParamCommand', + ); + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(spy1).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(1); + }); + + it('should update the commands when the OS is selected and optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS, setOptionalParams } = hook.result.current; + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64' && item.extension === 'deb', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + extension: 'deb', + }); + + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).not.toBe(''); + expect(hook.result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(2); + expect(spyStart).toBeCalledTimes(2); + }); +}); diff --git a/public/controllers/register-agent/hooks/use-register-agent-commands.ts b/public/controllers/register-agent/hooks/use-register-agent-commands.ts new file mode 100644 index 0000000000..800c198039 --- /dev/null +++ b/public/controllers/register-agent/hooks/use-register-agent-commands.ts @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { CommandGenerator } from '../core/register-commands/command-generator/command-generator'; +import { + IOSDefinition, + IOperationSystem, + IOptionalParameters, + tOptionalParams, +} from '../core/register-commands/types'; +import { version } from '../../../../package.json'; + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} + +interface IUseRegisterCommandsOutput { + selectOS: (params: OS) => void; + setOptionalParams: (params: IOptionalParameters) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | {}; +} + + +/** + * Custom hook that generates install and start commands based on the selected OS and optional parameters. + * + * @template T - The type of the selected OS. + * @param {IUseRegisterCommandsProps} props - The properties to configure the command generator. + * @returns {IUseRegisterCommandsOutput} - An object containing the generated commands and methods to update the selected OS and optional parameters. + */ +export function useRegisterAgentCommands(props: IUseRegisterCommandsProps): IUseRegisterCommandsOutput { + const { osDefinitions, optionalParamsDefinitions } = props; + // command generator settings + const wazuhVersion = version; + const osCommands: IOSDefinition[] = osDefinitions as IOSDefinition[]; + const optionalParams: tOptionalParams = optionalParamsDefinitions as tOptionalParams; + const commandGenerator = new CommandGenerator( + osCommands, + optionalParams, + wazuhVersion, + ); + + const [osSelected, setOsSelected] = useState(null); + const [optionalParamsValues, setOptionalParamsValues] = useState< + IOptionalParameters| {} + >({}); + const [optionalParamsParsed, setOptionalParamsParsed] = useState | {}>({}); + const [installCommand, setInstallCommand] = useState(''); + const [startCommand, setStartCommand] = useState(''); + + + /** + * Generates the install and start commands based on the selected OS and optional parameters. + * If no OS is selected, the method returns early without generating any commands. + * The generated commands are then set as state variables for later use. + */ + const generateCommands = () => { + if (!osSelected) return; + if (osSelected) { + commandGenerator.selectOS(osSelected); + } + if (optionalParamsValues) { + commandGenerator.addOptionalParams( + optionalParamsValues as IOptionalParameters, + ); + } + const installCommand = commandGenerator.getInstallCommand(); + const startCommand = commandGenerator.getStartCommand(); + setInstallCommand(installCommand); + setStartCommand(startCommand); + } + + useEffect(() => { + generateCommands(); + }, [osSelected, optionalParamsValues]); + + + /** + * Sets the selected OS for the command generator and updates the state variables accordingly. + * + * @param {T} params - The selected OS to be set. + * @returns {void} + */ + const selectOS = (params: OS) => { + commandGenerator.selectOS(params); + setOsSelected(params); + }; + + /** + * Sets the optional parameters for the command generator and updates the state variables accordingly. + * + * @param {IOptionalParameters} params - The optional parameters to be set. + * @returns {void} + */ + const setOptionalParams = (params: IOptionalParameters): void => { + commandGenerator.addOptionalParams(params); + setOptionalParamsValues(params); + setOptionalParamsParsed(commandGenerator.getOptionalParamsCommands()); + }; + + return { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } +}; From 9ada6baa2b1184bcb914c391211a0f5fe662f9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:23:21 -0300 Subject: [PATCH 06/24] Create reusable card for operating systems (#5462) * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * Update os-card.scss * macos card update * undoing merging as it was causing checkboxes not to work * test * file ds_store * file ds_store * file ds_store * remove files DS_store * remove files DS_store --------- Co-authored-by: Maximiliano Ibarra Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 5 +- public/assets/images/icons/linux-icon.svg | 3 + public/assets/images/icons/mac-icon.svg | 4 ++ public/assets/images/icons/windows-icon.svg | 13 ++++ public/components/common/form/hooks.test.tsx | 46 ++++++++++--- public/components/common/form/hooks.tsx | 31 +++++---- public/components/common/form/index.tsx | 65 +++++++++++------- .../checkbox-group/checkbox-group.scss | 51 ++++++++++++++ .../checkbox-group/checkbox-group.test.tsx | 55 +++++++++++++++ .../checkbox-group/checkbox-group.tsx | 62 +++++++++++++++++ .../components/os-card/os-card.scss | 50 ++++++++++++++ .../components/os-card/os-card.test.tsx | 17 +++++ .../components/os-card/os-card.tsx | 56 +++++++++++++++ .../container/register-agent.scss | 1 - .../container/register-agent.test.tsx | 18 +++++ .../container/register-agent.tsx | 15 +++- .../utils/register-agent-data.tsx | 25 +++++++ 18 files changed, 467 insertions(+), 50 deletions(-) delete mode 100644 .DS_Store create mode 100644 public/assets/images/icons/linux-icon.svg create mode 100644 public/assets/images/icons/mac-icon.svg create mode 100644 public/assets/images/icons/windows-icon.svg create mode 100644 public/controllers/register-agent/components/checkbox-group/checkbox-group.scss create mode 100644 public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx create mode 100644 public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx create mode 100644 public/controllers/register-agent/components/os-card/os-card.scss create mode 100644 public/controllers/register-agent/components/os-card/os-card.test.tsx create mode 100644 public/controllers/register-agent/components/os-card/os-card.tsx create mode 100644 public/controllers/register-agent/container/register-agent.test.tsx create mode 100644 public/controllers/register-agent/utils/register-agent-data.tsx diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index bd241bd50263ce7ea237ae2bf16dba59846221ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHM-EPw`82!AJHdVK27l0-tq)6P3G1!4mRVhzPLiLY}m zq#1qYHc&uMv`bCeqZh!H0^6K04j2cF1I7X4fN|h|Z~*UYPS%9?zPf5k?dZY7d;jFljjPTRu97qQJeZJMJsLH|i|j+>XM58}d`z%7P;G0PcbI`;Lw5geMht+A_D1`GKeDBDa@@U8^O0v2c9!=d&J;R_G(j z;ws=3^DVVv=Fp-eSAj!)v^q)0t0%21LpMZ^0V3{Z$1PV+np>rPIJreT@TrL&3hP*~ zqiIDAWA7r8o{shikqq+w2=+Cts%Vvuu@%_T%{05lGmANXiP5#_fZkj;GQPvaLdvf) z`oR4{XIy6ECU}(L&Z7rzcMtvSqMsHVxITW|Zy9A7eV z@aEYQxQaDFyiWU*=9-<#C>Q9H98E~~W$ZE9h@lRE{p{$ZU!PNcRj;&)kLOKJ!LFqh zm{gOD15@e1tcrT625e6L{y&x8n4paV)8c?A?X-6qi1zXaY(8+_Yum^lkU24LtWZi& m$n7{#ZpVRF|1d<|29-5+jbnv4gY@q|1eo`~G~N_b?Z6+4d}LSv diff --git a/.gitignore b/.gitignore index 5ca1c192ff..2cc137cef8 100644 --- a/.gitignore +++ b/.gitignore @@ -81,4 +81,7 @@ cypress/report/ cypress/cookies.json # Customization plugin assets -public/assets/custom/* \ No newline at end of file +public/assets/custom/* + +# Mac files +.DS_Store \ No newline at end of file diff --git a/public/assets/images/icons/linux-icon.svg b/public/assets/images/icons/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/public/assets/images/icons/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/icons/mac-icon.svg b/public/assets/images/icons/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/public/assets/images/icons/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/icons/windows-icon.svg b/public/assets/images/icons/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/public/assets/images/icons/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/components/common/form/hooks.test.tsx b/public/components/common/form/hooks.test.tsx index c96ebe60b8..283c4809bf 100644 --- a/public/components/common/form/hooks.test.tsx +++ b/public/components/common/form/hooks.test.tsx @@ -1,4 +1,7 @@ +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 { useForm } from './hooks'; import { FormConfiguration, IInputForm } from './types'; @@ -174,16 +177,43 @@ describe('[hook] useForm', () => { }); it('[hook] useForm. Verify the hook behavior when receives a custom field type', async () => { - const formFields: FormConfiguration = { - customField: { - type: 'custom', - initialValue: 'default value', - component: (props:IInputForm) => (<>any component), - }, + const CustomComponent = (props: any) => { + const { onChange, field, initialValue } = props; + const [value, setValue] = useState(initialValue || ''); + + const handleOnChange = (e: any) => { + setValue(e.target.value); + onChange(e); }; + return ( + <> + {field} + + + ); + }; + + const formFields: FormConfiguration = { + customField: { + type: 'custom', + initialValue: 'default value', + component: props => CustomComponent(props), + }, + }; + const { result } = renderHook(() => useForm(formFields)); - expect(result.current.fields.customField.component).toBeInstanceOf(Function); - expect(result.current.fields.customField.type).toBe('custom'); + 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/public/components/common/form/hooks.tsx b/public/components/common/form/hooks.tsx index 1d2f09e020..be7e1a8fa8 100644 --- a/public/components/common/form/hooks.tsx +++ b/public/components/common/form/hooks.tsx @@ -9,35 +9,36 @@ import { UseFormReturn, } from './types'; - interface IgetValueFromEventType { [key: string]: (event: any) => any; } -const getValueFromEventType: IgetValueFromEventType = { - [EpluginSettingType.switch]: (event: any) => event.target.checked, - [EpluginSettingType.editor]: (value: any) => value, - custom: (event:any) => event.target, - 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 + * + * @param event + * @param type * @returns event value */ function getValueFromEvent( event: any, - type: SettingTypes | CustomSettingType | string, + type: SettingTypes | CustomSettingType, ): any { - - return getValueFromEventType.hasOwnProperty(type) ? getValueFromEventType[type](event) : getValueFromEventType.default(event) + return (getValueFromEventType[type] || getValueFromEventType.default)(event); } - +const getValueFromEventType: IgetValueFromEventType = { + [EpluginSettingType.switch]: (event: any) => event.target.checked, + [EpluginSettingType.editor]: (value: any) => value, + [EpluginSettingType.filepicker]: (value: any) => value, + [EpluginSettingType.select]: (event: any) => event.target.value, + [EpluginSettingType.text]: (event: any) => event.target.value, + [EpluginSettingType.textarea]: (event: any) => event.target.value, + [EpluginSettingType.number]: (event: any) => event.target.value, + custom: (event: any) => event.target.value, + default: (event: any) => event.target.value, +}; export const useForm = (fields: FormConfiguration): UseFormReturn => { const [formFields, setFormFields] = useState<{ diff --git a/public/components/common/form/index.tsx b/public/components/common/form/index.tsx index 0c3fa8a681..0f267589c1 100644 --- a/public/components/common/form/index.tsx +++ b/public/components/common/form/index.tsx @@ -7,6 +7,7 @@ import { InputFormSwitch } from './input_switch'; import { InputFormFilePicker } from './input_filepicker'; import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { OsCard } from '../../../controllers/register-agent/components/os-card/os-card'; import { SettingTypes } from './types'; interface InputFormProps { @@ -15,10 +16,18 @@ interface InputFormProps { onChange: (event: React.ChangeEvent) => void; error?: string; label?: string; - 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); + 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 { @@ -37,12 +46,13 @@ export const InputForm = ({ postInput, ...rest }: InputFormComponentProps) => { + const ComponentInput = Input[ + type as keyof typeof Input + ] as React.ComponentType; - const ComponentInput = Input[type as keyof typeof Input] as React.ComponentType; - - if(!ComponentInput){ + if (!ComponentInput) { return null; - }; + } const isInvalid = Boolean(error); @@ -55,23 +65,29 @@ export const InputForm = ({ /> ); - 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; + if (type === 'custom') { + return ; + } + return label ? ( + + <> + {typeof header === 'function' ? header({ value, error }) : header} + + {typeof preInput === 'function' + ? preInput({ value, error }) + : preInput} + {input} + {typeof postInput === 'function' + ? postInput({ value, error }) + : postInput} + + {typeof footer === 'function' ? footer({ value, error }) : footer} + + + ) : ( + input + ); }; const Input = { @@ -82,4 +98,5 @@ const Input = { select: InputFormSelect, text: InputFormText, textarea: InputFormTextArea, + custom: OsCard, }; diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss b/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss new file mode 100644 index 0000000000..89e0e58513 --- /dev/null +++ b/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss @@ -0,0 +1,51 @@ +.checkbox-group-container { + display: flex; + flex-wrap: wrap; + margin-top: 26px; + margin-bottom: 11px; +} + +.checkbox-item { + width: 50%; + display: flex; + flex-direction: row-reverse; + justify-content: center; +} + +.checkbox-group-container.single-architecture { + margin-top: 44px; + display: flex; + justify-content: center; +} + +.checkbox-group-container.double-architecture { + margin-top: 24px; + display: flex; + flex-direction: column; + .checkbox-item:first-child { + margin-bottom: 13px; + } + .checkbox-item { + display: flex; + flex-direction: row-reverse; + justify-content: start; + } +} + +.architecture-label { + margin-left: 8px; + font-style: normal; + font-weight: 400; + font-size: 14px; + color: #343741; +} + +.first-card-four-items { + .checkbox-item:nth-child(n + 3) { + padding-top: 16px; + } +} + +.first-of-row { + padding-right: 17px; +} diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx b/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx new file mode 100644 index 0000000000..c2fa3c3fb0 --- /dev/null +++ b/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { CheckboxGroupComponent } from './checkbox-group'; + +describe('CheckboxGroupComponent', () => { + const data = ['Option 1', 'Option 2', 'Option 3']; + const cardIndex = 0; + const selectedOption = 'option-0-0'; + const onOptionChange = jest.fn(); + + test('renders checkbox items with correct labels', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + expect(checkboxItems).toHaveLength(data.length); + + expect(checkboxItems[0]).toHaveAttribute('id', 'option-0-0'); + expect(checkboxItems[1]).toHaveAttribute('id', 'option-0-1'); + expect(checkboxItems[2]).toHaveAttribute('id', 'option-0-2'); + + expect(checkboxItems[0]).toBeChecked(); + expect(checkboxItems[1]).not.toBeChecked(); + expect(checkboxItems[2]).not.toBeChecked(); + + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.getByText('Option 2')).toBeInTheDocument(); + expect(screen.getByText('Option 3')).toBeInTheDocument(); + }); + + test('calls onOptionChange when a checkbox is selected', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + + fireEvent.click(checkboxItems[1]); + + expect(onOptionChange).toHaveBeenCalledTimes(1); + expect(onOptionChange).toHaveBeenCalledWith('option-0-1'); + }); +}); diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx b/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx new file mode 100644 index 0000000000..160bcfd66a --- /dev/null +++ b/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { EuiRadioGroup } from '@elastic/eui'; +import './checkbox-group.scss'; + +interface RegisterAgentData { + icon: string; + title: string; + hr: boolean; + architecture: string[]; +} + +interface Props { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +const CheckboxGroupComponent: React.FC = ({ + data, + cardIndex, + selectedOption, + onOptionChange, +}) => { + const handleOptionChange = (optionId: string) => { + onOptionChange(optionId); + }; + + const isSingleArchitecture = data.length === 1; + const isDoubleArchitecture = data.length === 2; + const isFirstCardWithFourItems = cardIndex === 0 && data.length === 4; + + return ( +
+ {data.map((arch, idx) => ( +
+ {arch} + handleOptionChange(id)} + /> +
+ ))} +
+ ); +}; + +export { CheckboxGroupComponent }; +export type { RegisterAgentData }; diff --git a/public/controllers/register-agent/components/os-card/os-card.scss b/public/controllers/register-agent/components/os-card/os-card.scss new file mode 100644 index 0000000000..d4d3b41649 --- /dev/null +++ b/public/controllers/register-agent/components/os-card/os-card.scss @@ -0,0 +1,50 @@ +.card { + height: 183px; +} + +.cardTitle { + display: flex; + align-items: center; + margin-top: 28px; +} + +.cardIcon { + margin-right: 10px; +} + +.euiCard__content .euiCard__titleButton { + text-decoration: none !important; +} + +.cardText { + font-style: normal; + font-weight: 700; + font-size: 18px; + display: flex; + align-items: center; + text-align: center; + letter-spacing: 0.6px; + color: #343741; +} + +.hr { + border: 1px solid #d3dae6; +} + +.cardContent { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.checkboxGroupContainer { + flex-basis: 50%; +} + +.architectureItem { + margin-bottom: 8px; +} + +.last-card { + margin-right: 63px; +} diff --git a/public/controllers/register-agent/components/os-card/os-card.test.tsx b/public/controllers/register-agent/components/os-card/os-card.test.tsx new file mode 100644 index 0000000000..da1a61c120 --- /dev/null +++ b/public/controllers/register-agent/components/os-card/os-card.test.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { OsCard } from './os-card'; + +describe('OsCard', () => { + test('renders three cards with different titles', () => { + render(); + + const cardTitles = screen.getAllByTestId('card-title'); + expect(cardTitles).toHaveLength(3); + + expect(cardTitles[0]).toHaveTextContent('LINUX'); + expect(cardTitles[1]).toHaveTextContent('WINDOWS'); + expect(cardTitles[2]).toHaveTextContent('macOS'); + }); +}); diff --git a/public/controllers/register-agent/components/os-card/os-card.tsx b/public/controllers/register-agent/components/os-card/os-card.tsx new file mode 100644 index 0000000000..d9c0f2f00b --- /dev/null +++ b/public/controllers/register-agent/components/os-card/os-card.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiCheckbox, +} from '@elastic/eui'; +import { REGISTER_AGENT_DATA } from '../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; +import './os-card.scss'; + +export const OsCard = () => { + const [selectedOption, setSelectedOption] = useState( + undefined, + ); + + const handleOptionChange = (optionId: string) => { + setSelectedOption(optionId); + }; + + return ( +
+ + {REGISTER_AGENT_DATA.map((data, index) => ( + + + Icon + {data.title} +
+ } + display='plain' + hasBorder + onClick={() => {}} + > + {data.hr &&
} + {/* */} + + + + + ))} + +
+ ); +}; diff --git a/public/controllers/register-agent/container/register-agent.scss b/public/controllers/register-agent/container/register-agent.scss index 6f5dc8ed6c..c127bd0e9c 100644 --- a/public/controllers/register-agent/container/register-agent.scss +++ b/public/controllers/register-agent/container/register-agent.scss @@ -12,7 +12,6 @@ .title { margin-top: 51px; margin-bottom: 51px; - font-family: 'Inter'; font-style: normal; font-weight: 400; font-size: 30px; diff --git a/public/controllers/register-agent/container/register-agent.test.tsx b/public/controllers/register-agent/container/register-agent.test.tsx new file mode 100644 index 0000000000..061960686a --- /dev/null +++ b/public/controllers/register-agent/container/register-agent.test.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { RegisterAgent } from './register-agent'; + +describe('RegisterAgent', () => { + test('renders the component', () => { + render(); + + // Verifica que el título esté presente + const titleElement = screen.getByText('Deploy new agent'); + expect(titleElement).toBeInTheDocument(); + + // Verifica que el componente InputForm esté presente + const component = screen.getByTestId('os-card'); + expect(component).toBeInTheDocument(); + }); +}); diff --git a/public/controllers/register-agent/container/register-agent.tsx b/public/controllers/register-agent/container/register-agent.tsx index 87a780b0e7..5617bf3606 100644 --- a/public/controllers/register-agent/container/register-agent.tsx +++ b/public/controllers/register-agent/container/register-agent.tsx @@ -1,10 +1,23 @@ -import React from 'react'; +import React, { ChangeEvent } from 'react'; +import { InputForm } from '../../../components/common/form'; import './register-agent.scss'; export const RegisterAgent: React.FC = () => { + const handleChange = (event: ChangeEvent) => { + // ver + }; + return (
Deploy new agent
+
); }; diff --git a/public/controllers/register-agent/utils/register-agent-data.tsx b/public/controllers/register-agent/utils/register-agent-data.tsx new file mode 100644 index 0000000000..a32b9464b0 --- /dev/null +++ b/public/controllers/register-agent/utils/register-agent-data.tsx @@ -0,0 +1,25 @@ +import { RegisterAgentData } from '../components/checkbox-group/checkbox-group'; +import LinuxIcon from '../../../../public/assets/images/icons/linux-icon.svg'; +import WindowsIcon from '../../../../public/assets/images/icons/windows-icon.svg'; +import MacIcon from '../../../../public/assets/images/icons/mac-icon.svg'; + +export const REGISTER_AGENT_DATA: RegisterAgentData[] = [ + { + icon: LinuxIcon, + title: 'LINUX', + hr: true, + architecture: ['RPM amd64', 'RPM aarch64', 'DEB amd64', 'DEB aarch64'], + }, + { + icon: WindowsIcon, + title: 'WINDOWS', + hr: true, + architecture: ['MSI 32/64'], + }, + { + icon: MacIcon, + title: 'macOS', + hr: true, + architecture: ['Intel', 'Apple Silicon'], + }, +]; From 10a9d9dbe045ccc18078b64fed10c1914039fde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:53:25 -0300 Subject: [PATCH 07/24] 5518 inputs logic server address name password and group (#5554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * step component * Passing interfaces to a separate file, updating styles, and component logic * Update interfaces and clean up code * update of folder structure and step logic * tcp, udp, protocols, password, groups, logics * input logic server address name password groups and styles * group input logic * oscards input logic * oscards input logic * styles * regex * styles and settings * styles * various adjustments * cleaning up code and changing some styles * cleaning up code * cleaning code * update password * gitignore * gitignore * correcting validation text in input agent name * correcting validation text in input agent name * corrección de validación de input de nombre del agente * cleaning code * cleaning code * regex that differentiates between FQDN and IP * Use of PLUGIN_VERSION_SHORT * Use of PLUGIN_VERSION_SHORT * link * Revert "Merge branch '4205-redesign-add-agent-page' into 5518-inputs-logic-server-address-name-password-and-group" This reverts commit a4c6fb5d24a482e80f9595a879d141ff2d7fa5bb, reversing changes made to 5a0d2cb0e71972eb8f68b16f035ebc977220379f. * link and revert * characteres valid * correction of styles when bringing changes from parent branch * change tooltip to popover * moving validations to a separate file with their tests * corrections and cleaning of comments * camel case * change in function * type * remove type * fullWidth * type * change * conditional * change label a to Euilink * change label a to Euilink * conditional * delete usePrevious * delete usePrevious * deleted files ds store * test correction and placeholder * show architecture instead of id * removing console css warnings * fixed regex fqdn * fixed regex fqdn * data * changelog * changelog --------- Co-authored-by: Maximiliano Ibarra Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> --- CHANGELOG.md | 10 + public/components/common/form/hooks.tsx | 2 +- public/components/common/form/index.tsx | 9 +- .../components/common/form/input_select.tsx | 30 ++- public/components/common/form/input_text.tsx | 27 +- public/components/common/form/types.ts | 3 +- public/controllers/agent/index.js | 2 +- .../steps/wz-manager-address.tsx | 6 +- .../components/os-card/os-card.tsx | 56 ----- .../checkbox-group/checkbox-group.scss | 19 +- .../checkbox-group/checkbox-group.test.tsx | 8 +- .../checkbox-group/checkbox-group.tsx | 19 +- .../{ => step-one}/os-card/os-card.scss | 4 + .../{ => step-one}/os-card/os-card.test.tsx | 2 +- .../components/step-one/os-card/os-card.tsx | 73 ++++++ .../components/steps-three/group-input.scss | 8 + .../components/steps-three/group-input.tsx | 96 +++++++ .../container/register-agent.tsx | 23 -- .../register-agent}/register-agent.scss | 6 +- .../register-agent}/register-agent.test.tsx | 4 +- .../register-agent/register-agent.tsx | 238 ++++++++++++++++++ .../containers/steps/steps.scss | 56 +++++ .../register-agent/containers/steps/steps.tsx | 234 +++++++++++++++++ public/controllers/register-agent/index.tsx | 2 +- .../register-agent/interfaces/types.ts | 16 ++ .../services/register-agent-services.tsx | 238 ++++++++++++++++++ .../utils/register-agent-data.tsx | 20 +- .../register-agent/utils/validations.test.tsx | 68 +++++ .../register-agent/utils/validations.tsx | 50 ++++ 29 files changed, 1185 insertions(+), 144 deletions(-) delete mode 100644 public/controllers/register-agent/components/os-card/os-card.tsx rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.scss (77%) rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.test.tsx (87%) rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.tsx (77%) rename public/controllers/register-agent/components/{ => step-one}/os-card/os-card.scss (94%) rename public/controllers/register-agent/components/{ => step-one}/os-card/os-card.test.tsx (90%) create mode 100644 public/controllers/register-agent/components/step-one/os-card/os-card.tsx create mode 100644 public/controllers/register-agent/components/steps-three/group-input.scss create mode 100644 public/controllers/register-agent/components/steps-three/group-input.tsx delete mode 100644 public/controllers/register-agent/container/register-agent.tsx rename public/controllers/register-agent/{container => containers/register-agent}/register-agent.scss (84%) rename public/controllers/register-agent/{container => containers/register-agent}/register-agent.test.tsx (85%) create mode 100644 public/controllers/register-agent/containers/register-agent/register-agent.tsx create mode 100644 public/controllers/register-agent/containers/steps/steps.scss create mode 100644 public/controllers/register-agent/containers/steps/steps.tsx create mode 100644 public/controllers/register-agent/interfaces/types.ts create mode 100644 public/controllers/register-agent/services/register-agent-services.tsx create mode 100644 public/controllers/register-agent/utils/validations.test.tsx create mode 100644 public/controllers/register-agent/utils/validations.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index bb65a829e1..c0d9e0b9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the Wazuh app project will be documented in this file. +## Wazuh v4.6.0 - OpenSearch Dashboards 2.6.0 - Revision 4500 + +### Added + +### Changed + +- Changed the deploy a new agent page from step one to step three. [#5554](https://github.com/wazuh/wazuh-kibana-app/pull/5554) [5462](https://github.com/wazuh/wazuh-kibana-app/pull/5462) + +### Fixed + ## Wazuh v4.5.0 - OpenSearch Dashboards 2.6.0 - Revision 4500 ### Added diff --git a/public/components/common/form/hooks.tsx b/public/components/common/form/hooks.tsx index be7e1a8fa8..63ff8fdb72 100644 --- a/public/components/common/form/hooks.tsx +++ b/public/components/common/form/hooks.tsx @@ -103,7 +103,7 @@ export const useForm = (fields: FormConfiguration): UseFormReturn => { Object.entries(enhanceFields as EnhancedFields) .filter(([, { error }]) => error) .map(([fieldKey, { error }]) => [fieldKey, error]), - ) as { [key: string]: string }; + ); function undoChanges() { setFormFields(state => diff --git a/public/components/common/form/index.tsx b/public/components/common/form/index.tsx index 0f267589c1..2b7cb7ec0f 100644 --- a/public/components/common/form/index.tsx +++ b/public/components/common/form/index.tsx @@ -7,10 +7,9 @@ import { InputFormSwitch } from './input_switch'; import { InputFormFilePicker } from './input_filepicker'; import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { OsCard } from '../../../controllers/register-agent/components/os-card/os-card'; import { SettingTypes } from './types'; -interface InputFormProps { +export interface InputFormProps { type: SettingTypes; value: any; onChange: (event: React.ChangeEvent) => void; @@ -65,10 +64,6 @@ export const InputForm = ({ /> ); - if (type === 'custom') { - return ; - } - return label ? ( <> @@ -98,5 +93,5 @@ const Input = { select: InputFormSelect, text: InputFormText, textarea: InputFormTextArea, - custom: OsCard, + custom: ({ component, ...rest }) => component(rest), }; diff --git a/public/components/common/form/input_select.tsx b/public/components/common/form/input_select.tsx index a8f02e99d7..b212f3f068 100644 --- a/public/components/common/form/input_select.tsx +++ b/public/components/common/form/input_select.tsx @@ -2,12 +2,26 @@ import React from 'react'; import { EuiSelect } from '@elastic/eui'; import { IInputFormType } from './types'; -export const InputFormSelect = ({ options, value, onChange }: IInputFormType) => { - return ( - - ) +export const InputFormSelect = ({ + options, + value, + onChange, + placeholder, + selectedOptions, + isDisabled, + isClearable, + dataTestSubj, +}: IInputFormType) => { + return ( + + ); }; diff --git a/public/components/common/form/input_text.tsx b/public/components/common/form/input_text.tsx index feb0d218ee..c8e3d730d4 100644 --- a/public/components/common/form/input_text.tsx +++ b/public/components/common/form/input_text.tsx @@ -1,14 +1,21 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; -import { IInputFormType } from "./types"; +import { IInputFormType } from './types'; -export const InputFormText = ({ value, isInvalid, onChange }: IInputFormType) => { - return ( - - ); +export const InputFormText = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, +}: IInputFormType) => { + return ( + + ); }; diff --git a/public/components/common/form/types.ts b/public/components/common/form/types.ts index 737c0c8ab9..301914479c 100644 --- a/public/components/common/form/types.ts +++ b/public/components/common/form/types.ts @@ -19,6 +19,7 @@ export interface IInputForm { } /// use form hook types + export type SettingTypes = | 'text' | 'textarea' @@ -54,7 +55,7 @@ interface EnhancedField { initialValue: any; value: any; changed: boolean; - error: string | null; + error: string | null | undefined; setInputRef: (reference: any) => void; inputRef: any; onChange: (event: any) => void; diff --git a/public/controllers/agent/index.js b/public/controllers/agent/index.js index c9e06604aa..51a445bb00 100644 --- a/public/controllers/agent/index.js +++ b/public/controllers/agent/index.js @@ -11,7 +11,7 @@ */ import { AgentsPreviewController } from './agents-preview'; import { AgentsController } from './agents'; -import { RegisterAgent } from '../register-agent/container/register-agent'; +import { RegisterAgent } from '../../controllers/register-agent/containers/register-agent/register-agent'; import { ExportConfiguration } from './components/export-configuration'; import { AgentsWelcome } from '../../components/common/welcome/agents-welcome'; import { Mitre } from '../../components/overview'; diff --git a/public/controllers/agent/register-agent/steps/wz-manager-address.tsx b/public/controllers/agent/register-agent/steps/wz-manager-address.tsx index 0c46c70676..8bfd679e2f 100644 --- a/public/controllers/agent/register-agent/steps/wz-manager-address.tsx +++ b/public/controllers/agent/register-agent/steps/wz-manager-address.tsx @@ -11,14 +11,14 @@ const WzManagerAddressInput = (props: Props) => { const [value, setValue] = useState(''); useEffect(() => { - if(defaultValue){ + if (defaultValue) { setValue(defaultValue); onChange(defaultValue); - }else{ + } else { setValue(''); onChange(''); } - },[]) + }, []); /** * Handles the change of the selected node IP * @param value diff --git a/public/controllers/register-agent/components/os-card/os-card.tsx b/public/controllers/register-agent/components/os-card/os-card.tsx deleted file mode 100644 index d9c0f2f00b..0000000000 --- a/public/controllers/register-agent/components/os-card/os-card.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiCheckbox, -} from '@elastic/eui'; -import { REGISTER_AGENT_DATA } from '../../utils/register-agent-data'; -import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; -import './os-card.scss'; - -export const OsCard = () => { - const [selectedOption, setSelectedOption] = useState( - undefined, - ); - - const handleOptionChange = (optionId: string) => { - setSelectedOption(optionId); - }; - - return ( -
- - {REGISTER_AGENT_DATA.map((data, index) => ( - - - Icon - {data.title} -
- } - display='plain' - hasBorder - onClick={() => {}} - > - {data.hr &&
} - {/* */} - - - - - ))} - -
- ); -}; diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss similarity index 77% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.scss rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss index 89e0e58513..ce3cc09745 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss @@ -1,14 +1,14 @@ .checkbox-group-container { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr 1fr; margin-top: 26px; - margin-bottom: 11px; + justify-content: center; } .checkbox-item { - width: 50%; display: flex; flex-direction: row-reverse; + align-items: center; justify-content: center; } @@ -28,7 +28,8 @@ .checkbox-item { display: flex; flex-direction: row-reverse; - justify-content: start; + // justify-content: start; + align-self: baseline; } } @@ -36,16 +37,12 @@ margin-left: 8px; font-style: normal; font-weight: 400; - font-size: 14px; + font-size: 12px; color: #343741; } - .first-card-four-items { .checkbox-item:nth-child(n + 3) { padding-top: 16px; + justify-content: center; } } - -.first-of-row { - padding-right: 17px; -} diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx similarity index 87% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx index c2fa3c3fb0..e2dec80399 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { CheckboxGroupComponent } from './checkbox-group'; +import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; describe('CheckboxGroupComponent', () => { const data = ['Option 1', 'Option 2', 'Option 3']; @@ -50,6 +50,10 @@ describe('CheckboxGroupComponent', () => { fireEvent.click(checkboxItems[1]); expect(onOptionChange).toHaveBeenCalledTimes(1); - expect(onOptionChange).toHaveBeenCalledWith('option-0-1'); + expect(onOptionChange).toHaveBeenCalledWith( + expect.objectContaining({ + target: { value: `option-${cardIndex}-1` }, + }), + ); }); }); diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx similarity index 77% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx index 160bcfd66a..09043dea50 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx @@ -2,13 +2,6 @@ import React from 'react'; import { EuiRadioGroup } from '@elastic/eui'; import './checkbox-group.scss'; -interface RegisterAgentData { - icon: string; - title: string; - hr: boolean; - architecture: string[]; -} - interface Props { data: string[]; cardIndex: number; @@ -23,14 +16,9 @@ const CheckboxGroupComponent: React.FC = ({ selectedOption, onOptionChange, }) => { - const handleOptionChange = (optionId: string) => { - onOptionChange(optionId); - }; - const isSingleArchitecture = data.length === 1; const isDoubleArchitecture = data.length === 2; const isFirstCardWithFourItems = cardIndex === 0 && data.length === 4; - return (
= ({ > {arch} handleOptionChange(id)} + onChange={(id: string) => { + onOptionChange({ target: { value: id } }); + }} />
))} @@ -59,4 +49,3 @@ const CheckboxGroupComponent: React.FC = ({ }; export { CheckboxGroupComponent }; -export type { RegisterAgentData }; diff --git a/public/controllers/register-agent/components/os-card/os-card.scss b/public/controllers/register-agent/components/step-one/os-card/os-card.scss similarity index 94% rename from public/controllers/register-agent/components/os-card/os-card.scss rename to public/controllers/register-agent/components/step-one/os-card/os-card.scss index d4d3b41649..364734dc6b 100644 --- a/public/controllers/register-agent/components/os-card/os-card.scss +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.scss @@ -48,3 +48,7 @@ .last-card { margin-right: 63px; } + +.cardsCallOut { + margin-top: 16px; +} diff --git a/public/controllers/register-agent/components/os-card/os-card.test.tsx b/public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx similarity index 90% rename from public/controllers/register-agent/components/os-card/os-card.test.tsx rename to public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx index da1a61c120..ebb927853d 100644 --- a/public/controllers/register-agent/components/os-card/os-card.test.tsx +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { OsCard } from './os-card'; +import { OsCard } from '../../step-one/os-card/os-card'; describe('OsCard', () => { test('renders three cards with different titles', () => { diff --git a/public/controllers/register-agent/components/step-one/os-card/os-card.tsx b/public/controllers/register-agent/components/step-one/os-card/os-card.tsx new file mode 100644 index 0000000000..c1db7610a3 --- /dev/null +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLink, + EuiCheckbox, +} from '@elastic/eui'; +import { REGISTER_AGENT_DATA_STEP_ONE } from '../../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; +import './os-card.scss'; +import { webDocumentationLink } from '../../../../../../common/services/web_documentation'; + +interface Props { + setStatusCheck: string; + onChange: React.ChangeEventHandler; +} + +export const OsCard = ({ onChange, value }: Props) => { + return ( +
+ + {REGISTER_AGENT_DATA_STEP_ONE.map((data, index) => ( + + + Icon + {data.title} +
+ } + display='plain' + hasBorder + onClick={() => {}} + className='card' + > + {data.hr &&
} + + + + ))} + + + For additional systems and architectures, please check our{' '} + + steps + + . + + } + > +
+ ); +}; diff --git a/public/controllers/register-agent/components/steps-three/group-input.scss b/public/controllers/register-agent/components/steps-three/group-input.scss new file mode 100644 index 0000000000..e69348c07b --- /dev/null +++ b/public/controllers/register-agent/components/steps-three/group-input.scss @@ -0,0 +1,8 @@ +.groupTitle { + margin-top: '32px'; + flex-direction: 'row'; + font-style: normal; + font-weight: 700; + font-size: 12px; + line-height: 20px; +} diff --git a/public/controllers/register-agent/components/steps-three/group-input.tsx b/public/controllers/register-agent/components/steps-three/group-input.tsx new file mode 100644 index 0000000000..2878738617 --- /dev/null +++ b/public/controllers/register-agent/components/steps-three/group-input.tsx @@ -0,0 +1,96 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiComboBox, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; + +const popoverAgentGroup = ( + + Learn about{' '} + + Select a group. + + +); + +const GroupInput = ({ value, options, onChange }) => { + const [isPopoverAgentGroup, setIsPopoverAgentGroup] = useState(false); + + const onButtonAgentGroup = () => + setIsPopoverAgentGroup(isPopoverAgentGroup => !isPopoverAgentGroup); + const closeAgentGroup = () => setIsPopoverAgentGroup(false); + return ( + <> + + + + Select one or more existing groups + + } + isOpen={isPopoverAgentGroup} + closePopover={closeAgentGroup} + anchorPosition='rightCenter' + > + {popoverAgentGroup} + + + + { + onChange({ + target: { value: group }, + }); + }} + isDisabled={!options?.groups.length} + isClearable={true} + data-test-subj='demoComboBox' + data-testid='group-input-combobox' + /> + {!options?.groups.length && ( + <> + + + )} + + ); +}; + +export default GroupInput; diff --git a/public/controllers/register-agent/container/register-agent.tsx b/public/controllers/register-agent/container/register-agent.tsx deleted file mode 100644 index 5617bf3606..0000000000 --- a/public/controllers/register-agent/container/register-agent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { ChangeEvent } from 'react'; -import { InputForm } from '../../../components/common/form'; -import './register-agent.scss'; - -export const RegisterAgent: React.FC = () => { - const handleChange = (event: ChangeEvent) => { - // ver - }; - - return ( -
-
Deploy new agent
- -
- ); -}; diff --git a/public/controllers/register-agent/container/register-agent.scss b/public/controllers/register-agent/containers/register-agent/register-agent.scss similarity index 84% rename from public/controllers/register-agent/container/register-agent.scss rename to public/controllers/register-agent/containers/register-agent/register-agent.scss index c127bd0e9c..a0d9bd03d7 100644 --- a/public/controllers/register-agent/container/register-agent.scss +++ b/public/controllers/register-agent/containers/register-agent/register-agent.scss @@ -1,6 +1,6 @@ .container { box-sizing: border-box; - height: 1271px; + max-height: 1271px; margin-top: 44px; background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); @@ -20,3 +20,7 @@ display: flex; justify-content: center; } +.close { + display: flex; + margin-top: 17px; +} diff --git a/public/controllers/register-agent/container/register-agent.test.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx similarity index 85% rename from public/controllers/register-agent/container/register-agent.test.tsx rename to public/controllers/register-agent/containers/register-agent/register-agent.test.tsx index 061960686a..5f4dd452be 100644 --- a/public/controllers/register-agent/container/register-agent.test.tsx +++ b/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx @@ -5,7 +5,9 @@ import { RegisterAgent } from './register-agent'; describe('RegisterAgent', () => { test('renders the component', () => { - render(); + const mockHasAgents = jest.fn(); + + render(); // Verifica que el título esté presente const titleElement = screen.getByText('Deploy new agent'); diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.tsx new file mode 100644 index 0000000000..0b89f4f450 --- /dev/null +++ b/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -0,0 +1,238 @@ +import React, { useState, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiButtonEmpty, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiProgress, +} from '@elastic/eui'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { ErrorHandler } from '../../../../react-services/error-management'; +import { getMasterRemoteConfiguration } from '../../../agent/components/register-agent-service'; +import './register-agent.scss'; +import { Steps } from '../steps/steps'; +import { InputForm } from '../../../../components/common/form'; +import { getGroups } from '../../services/register-agent-services'; +import { useForm } from '../../../../components/common/form/hooks'; +import { FormConfiguration } from '../../../../components/common/form/types'; +import { useSelector } from 'react-redux'; +import { withReduxProvider } from '../../../../components/common/hocs'; +import GroupInput from '../../components/steps-three/group-input'; +import { OsCard } from '../../components/step-one/os-card/os-card'; +import { + validateServerAddress, + validateAgentName, +} from '../../utils/validations'; + +export const RegisterAgent = withReduxProvider( + ({ getWazuhVersion, hasAgents, addNewAgent, reload }) => { + const configuration = useSelector( + (state: { appConfig: { data: any } }) => state.appConfig.data, + ); + + const [wazuhVersion, setWazuhVersion] = useState(''); + const [udpProtocol, setUdpProtocol] = useState(false); + const [connectionSecure, setConnectionSecure] = useState( + true, + ); + const [haveUdpProtocol, setHaveUdpProtocol] = useState( + false, + ); + const [haveConnectionSecure, setHaveConnectionSecure] = useState< + boolean | null + >(false); + const [loading, setLoading] = useState(false); + const [wazuhPassword, setWazuhPassword] = useState(''); + const [groups, setGroups] = useState([]); + const [needsPassword, setNeedsPassword] = useState(false); + const [hideTextPassword, setHideTextPassword] = useState( + false, + ); + + const initialFields: FormConfiguration = { + operatingSystemSelection: { + type: 'custom', + initialValue: '', + component: props => { + return ; + }, + options: { + groups, + }, + }, + + //IP: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 + // O ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + + // FQDN: Maximum of 63 characters per label. + // Can only contain numbers, letters and hyphens (-) + // Labels cannot begin or end with a hyphen + // Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. + // Minimum 3 labels + + serverAddress: { + type: 'text', + initialValue: configuration['enrollment.dns'] || '', + validate: validateServerAddress, + }, + agentName: { + type: 'text', + initialValue: '', + validate: validateAgentName, + }, + + agentGroups: { + type: 'custom', + initialValue: [], + component: props => { + return ; + }, + options: { + groups, + }, + }, + }; + + const form = useForm(initialFields); + + const getRemoteConfig = async () => { + const remoteConfig = await getMasterRemoteConfiguration(); + if (remoteConfig) { + setHaveUdpProtocol(remoteConfig.isUdp); + setHaveConnectionSecure(remoteConfig.haveSecureConnection); + setUdpProtocol(remoteConfig.isUdp); + setConnectionSecure(remoteConfig.haveSecureConnection); + } + }; + + const getAuthInfo = async () => { + try { + const result = await WzRequest.apiReq( + 'GET', + '/agents/000/config/auth/auth', + {}, + ); + return (result.data || {}).data || {}; + } catch (error) { + ErrorHandler.handleError(error); + } + }; + + useEffect(() => { + const fetchData = async () => { + try { + const wazuhVersion = await getWazuhVersion(); + let wazuhPassword = ''; + let hideTextPassword = false; + await getRemoteConfig(); + const authInfo = await getAuthInfo(); + const needsPassword = (authInfo.auth || {}).use_password === 'yes'; + if (needsPassword) { + wazuhPassword = + configuration['enrollment.password'] || + authInfo['authd.pass'] || + ''; + if (wazuhPassword) { + hideTextPassword = true; + } + } + const groups = await getGroups(); + + setNeedsPassword(needsPassword); + setHideTextPassword(hideTextPassword); + setWazuhPassword(wazuhPassword); + setWazuhVersion(wazuhVersion); + setGroups(groups); + setLoading(false); + } catch (error) { + setWazuhVersion(wazuhVersion); + setLoading(false); + const options = { + context: 'RegisterAgent', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + display: true, + store: false, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + ErrorHandler.handleError(error, options); + } + }; + + fetchData(); + }, []); + + const agentGroup = ; + const osCard = ( + + ); + + return ( +
+ + + + + +
+ {hasAgents() ? ( + addNewAgent(false)} + iconType='cross' + > + ) : ( + reload()} + iconType='refresh' + > + Refresh + + )} +
+ + + +

Deploy new agent

+
+
+
+ + {loading ? ( + <> + + + + + + ) : ( + + + + )} +
+
+
+
+
+
+ ); + }, +); diff --git a/public/controllers/register-agent/containers/steps/steps.scss b/public/controllers/register-agent/containers/steps/steps.scss new file mode 100644 index 0000000000..d18ea5c07e --- /dev/null +++ b/public/controllers/register-agent/containers/steps/steps.scss @@ -0,0 +1,56 @@ +.stepTitle { + font-style: normal; + font-weight: 700; + font-size: 16px; + letter-spacing: 0.6px; + color: #343741; + flex-direction: row; +} + +.stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + color: #343741; + margin-bottom: 9px; +} + +.stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + color: #343741; + margin-bottom: 20px; +} + +.titleAndIcon { + display: flex; + flex-direction: row; +} + +.warningForAgentName { + margin-top: 10px; +} + +.euiToolTipAnchor { + margin-left: 7px; +} + +.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/public/controllers/register-agent/containers/steps/steps.tsx b/public/controllers/register-agent/containers/steps/steps.tsx new file mode 100644 index 0000000000..bf456eff0e --- /dev/null +++ b/public/controllers/register-agent/containers/steps/steps.tsx @@ -0,0 +1,234 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiSteps, + EuiText, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { InputForm } from '../../../../components/common/form'; +import './steps.scss'; +import { + REGISTER_AGENT_DATA_STEP_THREE, + REGISTER_AGENT_DATA_STEP_TWO, +} from '../../utils/register-agent-data'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; + +const popoverServerAddress = ( + + Learn about{' '} + + Server address. + + +); + +const popoverAgentName = ( + + Learn about{' '} + + Assigning an agent name. + + +); + +const warningForAgentName = + 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; + +export const Steps = ({ + needsPassword, + hideTextPassword, + agentGroup, + form, + osCard, +}) => { + const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); + const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + + const onButtonServerAddress = () => + setIsPopoverServerAddress( + isPopoverServerAddress => !isPopoverServerAddress, + ); + const closeServerAddress = () => setIsPopoverServerAddress(false); + + const onButtonAgentName = () => + setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); + const closeAgentName = () => setIsPopoverAgentName(false); + + const firstSetOfSteps = [ + { + title: ( + +

Select the package to download and install on your system:

+
+ ), + children: osCard, + status: form.fields.operatingSystemSelection.value + ? 'complete' + : 'current', + }, + { + title: ( + + + + Server address + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + + ), + children: ( + + + {REGISTER_AGENT_DATA_STEP_TWO.map((data, index) => ( + + + {data.subtitle} + + + ))} + + } + fullWidth={false} + placeholder='Server address' + /> + + ), + status: !form.fields.operatingSystemSelection.value + ? 'disabled' + : !form.fields.serverAddress.value && + form.fields.operatingSystemSelection.value + ? 'current' + : form.fields.operatingSystemSelection.value && + form.fields.serverAddress.value + ? 'complete' + : '', + }, + ...(!(!needsPassword || hideTextPassword) + ? [ + { + title: ( + +

Wazuh password

+
+ ), + children: ( + + { + 'No ha establecido una contraseña. Se le asigno una por defecto' + } + + ), + }, + ] + : []), + { + title: ( + +

Optional settings

+
+ ), + children: ( + + + {REGISTER_AGENT_DATA_STEP_THREE.map((data, index) => ( + + {data.subtitle} + + ))} + + + + + + Assign an agent name + + } + isOpen={isPopoverAgentName} + closePopover={closeAgentName} + anchorPosition='rightCenter' + > + {popoverAgentName} + + + + + } + placeholder='Agent name' + /> + + {agentGroup} + + ), + status: + !form.fields.operatingSystemSelection.value || + !form.fields.serverAddress.value + ? 'disabled' + : form.fields.serverAddress.value !== '' + ? 'current' + : form.fields.agentGroups.value.length > 0 + ? 'complete' + : '', + }, + ]; + + return ; +}; diff --git a/public/controllers/register-agent/index.tsx b/public/controllers/register-agent/index.tsx index d516e34a58..146589950a 100644 --- a/public/controllers/register-agent/index.tsx +++ b/public/controllers/register-agent/index.tsx @@ -1 +1 @@ -export { RegisterAgent } from './container/register-agent'; +export { RegisterAgent } from './containers/register-agent/register-agent'; diff --git a/public/controllers/register-agent/interfaces/types.ts b/public/controllers/register-agent/interfaces/types.ts new file mode 100644 index 0000000000..06de5db134 --- /dev/null +++ b/public/controllers/register-agent/interfaces/types.ts @@ -0,0 +1,16 @@ +interface RegisterAgentData { + icon: string; + title: string; + hr: boolean; + architecture: string[]; +} + +interface CheckboxGroupComponentProps { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +export type { RegisterAgentData, CheckboxGroupComponentProps }; diff --git a/public/controllers/register-agent/services/register-agent-services.tsx b/public/controllers/register-agent/services/register-agent-services.tsx new file mode 100644 index 0000000000..262afedd64 --- /dev/null +++ b/public/controllers/register-agent/services/register-agent-services.tsx @@ -0,0 +1,238 @@ +import { WzRequest } from '../../../react-services/wz-request'; +import { ServerAddressOptions } from '../register-agent/steps'; + +type Protocol = 'TCP' | 'UDP'; + +type RemoteItem = { + connection: 'syslog' | 'secure'; + ipv6: 'yes' | 'no'; + protocol: Protocol[]; + allowed_ips?: string[]; + queue_size?: string; +}; + +type RemoteConfig = { + name: string; + isUdp: boolean | null; + haveSecureConnection: boolean | null; +}; + +/** + * Get the cluster status + */ +export const clusterStatusResponse = async (): Promise => { + const clusterStatus = await WzRequest.apiReq('GET', '/cluster/status', {}); + if ( + clusterStatus.data.data.enabled === 'yes' && + clusterStatus.data.data.running === 'yes' + ) { + // Cluster mode + return true; + } else { + // Manager mode + return false; + } +}; + +/** + * Get the remote configuration from api + */ +async function getRemoteConfiguration(nodeName: string): Promise { + let config: RemoteConfig = { + name: nodeName, + isUdp: false, + haveSecureConnection: false, + }; + + try { + const clusterStatus = await clusterStatusResponse(); + let result; + if (clusterStatus) { + result = await WzRequest.apiReq( + 'GET', + `/cluster/${nodeName}/configuration/request/remote`, + {}, + ); + } else { + result = await WzRequest.apiReq( + 'GET', + '/manager/configuration/request/remote', + {}, + ); + } + const items = ((result.data || {}).data || {}).affected_items || []; + const remote = items[0]?.remote; + if (remote) { + const remoteFiltered = remote.filter((item: RemoteItem) => { + return item.connection === 'secure'; + }); + + remoteFiltered.length > 0 + ? (config.haveSecureConnection = true) + : (config.haveSecureConnection = false); + + let protocolsAvailable: Protocol[] = []; + remote.forEach((item: RemoteItem) => { + // get all protocols available + item.protocol.forEach(protocol => { + protocolsAvailable = protocolsAvailable.concat(protocol); + }); + }); + + config.isUdp = + getRemoteProtocol(protocolsAvailable) === 'UDP' ? true : false; + } + return config; + } catch (error) { + return config; + } +} + +/** + * Get the remote protocol available from list of protocols + * @param protocols + */ +function getRemoteProtocol(protocols: Protocol[]) { + if (protocols.length === 1) { + return protocols[0]; + } else { + return !protocols.includes('TCP') ? 'UDP' : 'TCP'; + } +} + +/** + * Get the remote configuration from nodes registered in the cluster and decide the protocol to setting up in deploy agent param + * @param nodeSelected + * @param defaultServerAddress + */ +async function getConnectionConfig( + nodeSelected: ServerAddressOptions, + defaultServerAddress?: string, +) { + const nodeName = nodeSelected?.label; + const nodeIp = nodeSelected?.value; + if (!defaultServerAddress) { + if (nodeSelected.nodetype !== 'custom') { + const remoteConfig = await getRemoteConfiguration(nodeName); + return { + serverAddress: nodeIp, + udpProtocol: remoteConfig.isUdp, + connectionSecure: remoteConfig.haveSecureConnection, + }; + } else { + return { + serverAddress: nodeName, + udpProtocol: false, + connectionSecure: true, + }; + } + } else { + return { + serverAddress: defaultServerAddress, + udpProtocol: false, + connectionSecure: true, + }; + } +} + +type NodeItem = { + name: string; + ip: string; + type: string; +}; + +type NodeResponse = { + data: { + data: { + affected_items: NodeItem[]; + }; + }; +}; + +/** + * Get the list of the cluster nodes and parse it into a list of options + */ +export const getNodeIPs = async (): Promise => { + return await WzRequest.apiReq('GET', '/cluster/nodes', {}); +}; + +/** + * Get the list of the manager and parse it into a list of options + */ +export const getManagerNode = async (): Promise => { + const managerNode = await WzRequest.apiReq('GET', '/manager/api/config', {}); + return ( + managerNode?.data?.data?.affected_items?.map(item => ({ + label: item.node_name, + value: item.node_api_config.host, + nodetype: 'master', + })) || [] + ); +}; + +/** + * Parse the nodes list from the API response to a format that can be used by the EuiComboBox + * @param nodes + */ +export const parseNodesInOptions = ( + nodes: NodeResponse, +): ServerAddressOptions[] => { + return nodes.data.data.affected_items.map((item: NodeItem) => ({ + label: item.name, + value: item.ip, + nodetype: item.type, + })); +}; + +/** + * Get the list of the cluster nodes from API and parse it into a list of options + */ +export const fetchClusterNodesOptions = async (): Promise< + ServerAddressOptions[] +> => { + const clusterStatus = await clusterStatusResponse(); + if (clusterStatus) { + // Cluster mode + // Get the cluster nodes + const nodes = await getNodeIPs(); + return parseNodesInOptions(nodes); + } else { + // Manager mode + // Get the manager node + return await getManagerNode(); + } +}; + +/** + * Get the master node data from the list of cluster nodes + * @param nodeIps + */ +export const getMasterNode = ( + nodeIps: ServerAddressOptions[], +): ServerAddressOptions[] => { + return nodeIps.filter(nodeIp => nodeIp.nodetype === 'master'); +}; + +/** + * Get the remote configuration from manager + * This function get the config from manager mode or cluster mode + */ +export const getMasterRemoteConfiguration = async () => { + const nodes = await fetchClusterNodesOptions(); + const masterNode = getMasterNode(nodes); + return await getRemoteConfiguration(masterNode[0].label); +}; + +export { getConnectionConfig, getRemoteConfiguration }; + +export const getGroups = async () => { + try { + const result = await WzRequest.apiReq('GET', '/groups', {}); + return result.data.data.affected_items.map(item => ({ + label: item.name, + id: item.name, + })); + } catch (error) { + throw new Error(error); + } +}; diff --git a/public/controllers/register-agent/utils/register-agent-data.tsx b/public/controllers/register-agent/utils/register-agent-data.tsx index a32b9464b0..c0e4e79a94 100644 --- a/public/controllers/register-agent/utils/register-agent-data.tsx +++ b/public/controllers/register-agent/utils/register-agent-data.tsx @@ -1,9 +1,9 @@ -import { RegisterAgentData } from '../components/checkbox-group/checkbox-group'; +import { RegisterAgentData } from '../interfaces/types'; import LinuxIcon from '../../../../public/assets/images/icons/linux-icon.svg'; import WindowsIcon from '../../../../public/assets/images/icons/windows-icon.svg'; import MacIcon from '../../../../public/assets/images/icons/mac-icon.svg'; -export const REGISTER_AGENT_DATA: RegisterAgentData[] = [ +export const REGISTER_AGENT_DATA_STEP_ONE: RegisterAgentData[] = [ { icon: LinuxIcon, title: 'LINUX', @@ -23,3 +23,19 @@ export const REGISTER_AGENT_DATA: RegisterAgentData[] = [ architecture: ['Intel', 'Apple Silicon'], }, ]; + +export const REGISTER_AGENT_DATA_STEP_TWO = [ + { + title: 'Server address', + subtitle: + 'This is the address the agent uses to communicate with the Wazuh server. Enter an IP address or a fully qualified domain name (FDQN).', + }, +]; + +export const REGISTER_AGENT_DATA_STEP_THREE = [ + { + title: 'Optional settings', + subtitle: + 'The deployment sets the endpoint hostname as the agent name by default. Optionally, you can set your own name in the field below.', + }, +]; diff --git a/public/controllers/register-agent/utils/validations.test.tsx b/public/controllers/register-agent/utils/validations.test.tsx new file mode 100644 index 0000000000..e51dd972fd --- /dev/null +++ b/public/controllers/register-agent/utils/validations.test.tsx @@ -0,0 +1,68 @@ +import { validateServerAddress, validateAgentName } from './validations'; + +describe('Validations', () => { + it('should return undefined for an empty value', () => { + const result = validateServerAddress(''); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid FQDN', () => { + const validFQDN = 'example.fqdn.valid'; + const result = validateServerAddress(validFQDN); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid IP', () => { + const validIP = '192.168.1.1'; + const result = validateServerAddress(validIP); + expect(result).toBeUndefined(); + }); + + it('should return an error message for an invalid FQDN', () => { + const invalidFQDN = 'example.fqdn'; + const result = validateServerAddress(invalidFQDN); + expect(result).toBe( + 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.', + ); + }); + + test('should return an error message for an invalid IP', () => { + const invalidIP = '999.999.999.999.999'; + const result = validateServerAddress(invalidIP); + expect(result).toBe('Not a valid IP'); + }); + + 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, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid format', () => { + const invalidAgentName = 'agent$name'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid length', () => { + const invalidAgentName = 'a'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe('The minimum length is 2 characters.'); + }); + + test('should return an empty string for a valid agent name', () => { + const validAgentName = 'agent_name'; + const result = validateAgentName(validAgentName); + expect(result).toBe(''); + }); +}); diff --git a/public/controllers/register-agent/utils/validations.tsx b/public/controllers/register-agent/utils/validations.tsx new file mode 100644 index 0000000000..6b5a17978f --- /dev/null +++ b/public/controllers/register-agent/utils/validations.tsx @@ -0,0 +1,50 @@ +export const validateServerAddress = value => { + const isFQDN = + /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; + + const isIP = + /^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$/; + const numbersAndPoints = /^[0-9.]+$/; + const areLettersNumbersAndColons = /^[a-zA-Z0-9:]+$/; + const letters = /[a-zA-Z]/; + const isFQDNFormatValid = isFQDN.test(value); + const isIPFormatValid = isIP.test(value); + const areNumbersAndPoints = numbersAndPoints.test(value); + const hasLetters = letters.test(value); + const hasPoints = value.includes('.'); + + let validation = undefined; + if (value.length === 0) { + return validation; + } else if (isFQDNFormatValid && value !== '') { + return validation; // FQDN valid + } else if (isIPFormatValid && value !== '') { + return validation; // IP valid + } else if (hasPoints && hasLetters && !isFQDNFormatValid) { + return (validation = + 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.'); // FQDN invalid + } else if ( + (areNumbersAndPoints || areLettersNumbersAndColons) && + !isIPFormatValid + ) { + return (validation = 'Not a valid IP'); // IP invalid + } +}; + +export const validateAgentName = value => { + if (value.length === 0) { + return undefined; + } + const regex = /^[A-Za-z.\-_,]+$/; + + const isLengthValid = value.length >= 2 && value.length <= 63; + const isFormatValid = regex.test(value); + if (!isFormatValid && !isLengthValid) { + return 'The minimum length is 2 characters. The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"'; + } else if (!isLengthValid) { + return 'The minimum length is 2 characters.'; + } else if (!isFormatValid) { + return 'The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"'; + } + return ''; +}; From 11a462d890466bb2d0dcd1943fe0331db4e655c9 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:45:02 -0300 Subject: [PATCH 08/24] [Redesign add agent] Integration commands generator with UI (#5593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * step component * Passing interfaces to a separate file, updating styles, and component logic * Update interfaces and clean up code * update of folder structure and step logic * tcp, udp, protocols, password, groups, logics * input logic server address name password groups and styles * group input logic * oscards input logic * oscards input logic * styles * regex * styles and settings * styles * various adjustments * cleaning up code and changing some styles * cleaning up code * cleaning code * update password * gitignore * gitignore * correcting validation text in input agent name * correcting validation text in input agent name * corrección de validación de input de nombre del agente * cleaning code * cleaning code * regex that differentiates between FQDN and IP * Use of PLUGIN_VERSION_SHORT * Use of PLUGIN_VERSION_SHORT * link * Revert "Merge branch '4205-redesign-add-agent-page' into 5518-inputs-logic-server-address-name-password-and-group" This reverts commit a4c6fb5d24a482e80f9595a879d141ff2d7fa5bb, reversing changes made to 5a0d2cb0e71972eb8f68b16f035ebc977220379f. * link and revert * characteres valid * correction of styles when bringing changes from parent branch * change tooltip to popover * moving validations to a separate file with their tests * corrections and cleaning of comments * camel case * change in function * type * remove type * fullWidth * type * change * conditional * change label a to Euilink * change label a to Euilink * conditional * delete usePrevious * delete usePrevious * deleted files ds store * test correction and placeholder * show architecture instead of id * Add register agent form values parser * Remove extension on operating system type * Add command sections with form values * Create new components for steps inputs * Fix some types * Renamed some options * Move commands config inside core folder * Fix server address error message display * Create methods to get form steps status * Allow select more than group * Hide agent group param when is empty * Fix steps form statuses * Remove break lines in commands * Add white space in error messages * Fix steps form status * Added new command component white custom copy and language * Fixed step form status --------- Co-authored-by: chantal.kelm Co-authored-by: Chantal Belén kelm <99441266+chantal-kelm@users.noreply.github.com> --- public/components/common/form/types.ts | 3 +- .../command-output/command-output.tsx | 65 ++++ .../group-input.scss | 0 .../group-input.tsx | 0 .../optionals-inputs/optionals-inputs.tsx | 102 ++++++ .../checkbox-group/checkbox-group.scss | 0 .../checkbox-group/checkbox-group.test.tsx | 0 .../checkbox-group/checkbox-group.tsx | 0 .../os-card/os-card.scss | 0 .../os-card/os-card.test.tsx | 0 .../os-card/os-card.tsx | 7 +- .../server-address/server-address-input.tsx | 35 ++ .../server-address/server-address-title.tsx | 53 +++ .../config/os-commands-definitions.ts | 148 --------- .../register-agent/register-agent.scss | 4 +- .../register-agent/register-agent.tsx | 47 ++- .../register-agent/containers/steps/steps.tsx | 307 ++++++++---------- .../core/config/os-commands-definitions.ts | 194 +++++++++++ .../core/register-commands/README.md | 13 - .../command-generator.test.ts | 20 +- .../command-generator/command-generator.ts | 12 +- .../register-commands/exceptions/index.ts | 27 +- .../get-install-command.service.test.ts | 6 - .../services/get-install-command.service.ts | 5 +- .../search-os-definitions.service.test.ts | 25 +- .../services/search-os-definitions.service.ts | 10 +- .../core/register-commands/types.ts | 5 +- .../register-agent/hooks/README.md | 8 - .../hooks/use-register-agent-commands.test.ts | 16 +- .../register-agent/interfaces/types.ts | 6 +- .../services/register-agent-services.tsx | 78 ++++- .../register-agent-steps-status-services.tsx | 145 +++++++++ .../utils/register-agent-data.tsx | 6 +- .../register-agent/utils/validations.tsx | 12 +- 34 files changed, 862 insertions(+), 497 deletions(-) create mode 100644 public/controllers/register-agent/components/command-output/command-output.tsx rename public/controllers/register-agent/components/{steps-three => group-input}/group-input.scss (100%) rename public/controllers/register-agent/components/{steps-three => group-input}/group-input.tsx (100%) create mode 100644 public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx rename public/controllers/register-agent/components/{step-one => os-selector}/checkbox-group/checkbox-group.scss (100%) rename public/controllers/register-agent/components/{step-one => os-selector}/checkbox-group/checkbox-group.test.tsx (100%) rename public/controllers/register-agent/components/{step-one => os-selector}/checkbox-group/checkbox-group.tsx (100%) rename public/controllers/register-agent/components/{step-one => os-selector}/os-card/os-card.scss (100%) rename public/controllers/register-agent/components/{step-one => os-selector}/os-card/os-card.test.tsx (100%) rename public/controllers/register-agent/components/{step-one => os-selector}/os-card/os-card.tsx (89%) create mode 100644 public/controllers/register-agent/components/server-address/server-address-input.tsx create mode 100644 public/controllers/register-agent/components/server-address/server-address-title.tsx delete mode 100644 public/controllers/register-agent/config/os-commands-definitions.ts create mode 100644 public/controllers/register-agent/core/config/os-commands-definitions.ts create mode 100644 public/controllers/register-agent/services/register-agent-steps-status-services.tsx diff --git a/public/components/common/form/types.ts b/public/components/common/form/types.ts index 301914479c..762f73962f 100644 --- a/public/components/common/form/types.ts +++ b/public/components/common/form/types.ts @@ -70,8 +70,9 @@ interface EnhancedCustomField extends EnhancedField { component: (props: any) => JSX.Element; } +export type EnhancedFieldConfiguration = EnhancedDefaultField | EnhancedCustomField; export interface EnhancedFields { - [key: string]: EnhancedDefaultField | EnhancedCustomField; + [key: string]: EnhancedFieldConfiguration; } export interface UseFormReturn { diff --git a/public/controllers/register-agent/components/command-output/command-output.tsx b/public/controllers/register-agent/components/command-output/command-output.tsx new file mode 100644 index 0000000000..88830a66c7 --- /dev/null +++ b/public/controllers/register-agent/components/command-output/command-output.tsx @@ -0,0 +1,65 @@ +import { + EuiCodeBlock, + EuiCopy, + EuiIcon, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; + +interface ICommandSectionProps { + commandText: string; + showCommand: boolean; + onCopy: () => void; + os?: 'WINDOWS' | string; +} + +export default function CommandOutput(props: ICommandSectionProps) { + const { commandText, showCommand, onCopy, os } = props; + const getHighlightCodeLanguage = (os: 'WINDOWS' | string) => { + if (os.toLowerCase() === 'windows') { + return 'powershell'; + } else { + return 'bash'; + } + }; + + const [language, setLanguage] = useState(getHighlightCodeLanguage(os || '')); + + const onHandleCopy = (command: any) => { + onCopy && onCopy(); + return command + }; + + const [commandToCopy, setCommandToCopy] = useState(commandText); + + return ( + + + +
+ + {showCommand ? commandText : ''} + + {showCommand && ( + + {commandToCopy => ( +
onHandleCopy(commandToCopy)}> +

+ Copy command +

+
+ )} +
+ )} +
+ +
+
+ ); +} diff --git a/public/controllers/register-agent/components/steps-three/group-input.scss b/public/controllers/register-agent/components/group-input/group-input.scss similarity index 100% rename from public/controllers/register-agent/components/steps-three/group-input.scss rename to public/controllers/register-agent/components/group-input/group-input.scss diff --git a/public/controllers/register-agent/components/steps-three/group-input.tsx b/public/controllers/register-agent/components/group-input/group-input.tsx similarity index 100% rename from public/controllers/register-agent/components/steps-three/group-input.tsx rename to public/controllers/register-agent/components/group-input/group-input.tsx diff --git a/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx b/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx new file mode 100644 index 0000000000..0a7560c3a8 --- /dev/null +++ b/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -0,0 +1,102 @@ +import React, { Fragment, useState } from 'react'; +import { UseFormReturn } from '../../../../components/common/form/types'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiCallOut, + EuiLink, +} from '@elastic/eui'; +import { InputForm } from '../../../../components/common/form'; +import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/register-agent-data'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; +interface OptionalsInputsProps { + formFields: UseFormReturn['fields']; +} + +const OptionalsInputs = (props: OptionalsInputsProps) => { + const { formFields } = props; + const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + const onButtonAgentName = () => + setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); + const closeAgentName = () => setIsPopoverAgentName(false); + + const popoverAgentName = ( + + Learn about{' '} + + Assigning an agent name. + + + ); + + const warningForAgentName = + 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; + return ( + + + {OPTIONAL_PARAMETERS_TEXT.map((data, index) => ( + + {data.subtitle} + + ))} + + + + + + Assign an agent name + + } + isOpen={isPopoverAgentName} + closePopover={closeAgentName} + anchorPosition='rightCenter' + > + {popoverAgentName} + + + + + } + placeholder='Agent name' + /> + + + + ); +}; + +export default OptionalsInputs; diff --git a/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss similarity index 100% rename from public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss rename to public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss diff --git a/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx similarity index 100% rename from public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx rename to public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx diff --git a/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx similarity index 100% rename from public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx rename to public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx diff --git a/public/controllers/register-agent/components/step-one/os-card/os-card.scss b/public/controllers/register-agent/components/os-selector/os-card/os-card.scss similarity index 100% rename from public/controllers/register-agent/components/step-one/os-card/os-card.scss rename to public/controllers/register-agent/components/os-selector/os-card/os-card.scss diff --git a/public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx b/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx similarity index 100% rename from public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx rename to public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx diff --git a/public/controllers/register-agent/components/step-one/os-card/os-card.tsx b/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx similarity index 89% rename from public/controllers/register-agent/components/step-one/os-card/os-card.tsx rename to public/controllers/register-agent/components/os-selector/os-card/os-card.tsx index c1db7610a3..18b08a2e7e 100644 --- a/public/controllers/register-agent/components/step-one/os-card/os-card.tsx +++ b/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx @@ -7,21 +7,22 @@ import { EuiLink, EuiCheckbox, } from '@elastic/eui'; -import { REGISTER_AGENT_DATA_STEP_ONE } from '../../../utils/register-agent-data'; -import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; import './os-card.scss'; import { webDocumentationLink } from '../../../../../../common/services/web_documentation'; interface Props { setStatusCheck: string; onChange: React.ChangeEventHandler; + value: any; } export const OsCard = ({ onChange, value }: Props) => { return (
- {REGISTER_AGENT_DATA_STEP_ONE.map((data, index) => ( + {OPERATING_SYSTEMS_OPTIONS.map((data, index) => ( { + const { formField } = props; + + return ( + + + {SERVER_ADDRESS_TEXTS.map((data, index) => ( + + + {data.subtitle} + + + ))} + + } + fullWidth={false} + placeholder='Server address' + /> + + ); +}; + +export default ServerAddressInput; diff --git a/public/controllers/register-agent/components/server-address/server-address-title.tsx b/public/controllers/register-agent/components/server-address/server-address-title.tsx new file mode 100644 index 0000000000..76475c5a6d --- /dev/null +++ b/public/controllers/register-agent/components/server-address/server-address-title.tsx @@ -0,0 +1,53 @@ +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink, EuiPopover } from "@elastic/eui" +import React, { useState } from "react"; +import { webDocumentationLink } from "../../../../../common/services/web_documentation"; +import { PLUGIN_VERSION_SHORT } from "../../../../../common/constants"; + +const ServerAddressTitle = () => { + const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); + const closeServerAddress = () => setIsPopoverServerAddress(false); + const onButtonServerAddress = () => + setIsPopoverServerAddress( + isPopoverServerAddress => !isPopoverServerAddress, + ); + + const popoverServerAddress = ( + + Learn about{' '} + + Server address. + + + ); + + return ( + + + Server address + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + ) +} + +export default ServerAddressTitle; \ No newline at end of file diff --git a/public/controllers/register-agent/config/os-commands-definitions.ts b/public/controllers/register-agent/config/os-commands-definitions.ts deleted file mode 100644 index dc69156571..0000000000 --- a/public/controllers/register-agent/config/os-commands-definitions.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { IOSDefinition, tOptionalParams } from '../core/register-commands/types'; - -// Defined OS combinations -export interface ILinuxOSTypes { - name: 'linux'; - architecture: 'x64' | 'x86'; - extension: 'rpm' | 'deb'; -} -export interface IWindowsOSTypes { - name: 'windows'; - architecture: 'x86'; - extension: 'msi'; -} - -export interface IMacOSTypes { - name: 'mac'; - architecture: '32/64'; - extension: 'pkg'; -} - -export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; - - -export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; - -/////////////////////////////////////////////////////////////////// -/// Operating system commands definitions -/////////////////////////////////////////////////////////////////// - -const linuxDefinition: IOSDefinition = { - name: 'linux', - options: [ - { - extension: 'deb', - architecture: '32/64', - urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, - installCommand: props => - `sudo yum install -y ${props.urlPackage}`, - startCommand: props => `sudo systemctl start wazuh-agent`, - }, - { - extension: 'deb', - architecture: 'x64', - urlPackage: props => - `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, - installCommand: props => - `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, - startCommand: props => `sudo systemctl start wazuh-agent`, - }, - { - extension: 'rpm', - architecture: '32/64', - urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, - installCommand: props => - `sudo yum install -y ${props.urlPackage}`, - startCommand: props => `sudo systemctl start wazuh-agent`, - }, - { - extension: 'deb', - architecture: 'x64', - urlPackage: props => - `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.${props.extension}`, - installCommand: props => - `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, - startCommand: props => `sudo systemctl start wazuh-agent`, - }, - ], -}; - -const windowsDefinition: IOSDefinition = { - name: 'windows', - options: [ - { - extension: 'msi', - architecture: 'x86', - urlPackage: props => - `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, - installCommand: props => - `Invoke-WebRequest -Uri ${props.urlPackage} -OutFile \${env.tmp}\\wazuh-agent.${props.extension}; msiexec.exe /i \${env.tmp}\\wazuh-agent.${props.extension} /q`, - startCommand: props => `Start-Service -Name wazuh-agent`, - }, - ], -}; - -const macDefinition: IOSDefinition = { - name: 'mac', - options: [ - { - extension: 'pkg', - architecture: '32/64', - urlPackage: props => - `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.${props.extension}`, - installCommand: props => - `mac -so wazuh-agent.pkg ${props.urlPackage} && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target /`, - startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, - }, - ], -}; - -export const osCommandsDefinitions = [ - linuxDefinition, - windowsDefinition, - macDefinition, -]; - -/////////////////////////////////////////////////////////////////// -/// Optional parameters definitions -/////////////////////////////////////////////////////////////////// - -export const optionalParamsDefinitions: tOptionalParams = { - server_address: { - property: 'WAZUH_MANAGER', - getParamCommand: props => { - const { property, value } = props; - return `${property}=${value}`; - } - }, - agent_name: { - property: 'WAZUH_AGENT_NAME', - getParamCommand: props => { - const { property, value } = props; - return `${property}=${value}`; - } - }, - protocol: { - property: 'WAZUH_MANAGER_PROTOCOL', - getParamCommand: props => { - const { property, value } = props; - return `${property}=${value}`; - } - }, - agent_group: { - property: 'WAZUH_AGENT_GROUP', - getParamCommand: props => { - const { property, value } = props; - return `${property}=${value}`; - } - }, - wazuh_password: { - property: 'WAZUH_PASSWORD', - getParamCommand: props => { - const { property, value } = props; - return `${property}=${value}`; - } - } -} diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.scss b/public/controllers/register-agent/containers/register-agent/register-agent.scss index a0d9bd03d7..6c32c5a91a 100644 --- a/public/controllers/register-agent/containers/register-agent/register-agent.scss +++ b/public/controllers/register-agent/containers/register-agent/register-agent.scss @@ -1,6 +1,6 @@ .container { box-sizing: border-box; - max-height: 1271px; + min-height: 1271px; margin-top: 44px; background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); @@ -23,4 +23,4 @@ .close { display: flex; margin-top: 17px; -} +} \ No newline at end of file diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.tsx index 0b89f4f450..9bdb61396a 100644 --- a/public/controllers/register-agent/containers/register-agent/register-agent.tsx +++ b/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -9,6 +9,7 @@ import { EuiPageBody, EuiSpacer, EuiProgress, + EuiButton, } from '@elastic/eui'; import { WzRequest } from '../../../../react-services/wz-request'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; @@ -23,24 +24,27 @@ import { useForm } from '../../../../components/common/form/hooks'; import { FormConfiguration } from '../../../../components/common/form/types'; import { useSelector } from 'react-redux'; import { withReduxProvider } from '../../../../components/common/hocs'; -import GroupInput from '../../components/steps-three/group-input'; -import { OsCard } from '../../components/step-one/os-card/os-card'; +import GroupInput from '../../components/group-input/group-input'; +import { OsCard } from '../../components/os-selector/os-card/os-card'; import { validateServerAddress, validateAgentName, } from '../../utils/validations'; +interface IRegisterAgentProps { + getWazuhVersion: () => Promise; + hasAgents: () => Promise; + addNewAgent: (agent: any) => Promise; + reload: () => void; +} + export const RegisterAgent = withReduxProvider( - ({ getWazuhVersion, hasAgents, addNewAgent, reload }) => { + ({ getWazuhVersion, hasAgents, addNewAgent, reload }: IRegisterAgentProps) => { const configuration = useSelector( (state: { appConfig: { data: any } }) => state.appConfig.data, ); const [wazuhVersion, setWazuhVersion] = useState(''); - const [udpProtocol, setUdpProtocol] = useState(false); - const [connectionSecure, setConnectionSecure] = useState( - true, - ); const [haveUdpProtocol, setHaveUdpProtocol] = useState( false, ); @@ -50,8 +54,8 @@ export const RegisterAgent = withReduxProvider( const [loading, setLoading] = useState(false); const [wazuhPassword, setWazuhPassword] = useState(''); const [groups, setGroups] = useState([]); - const [needsPassword, setNeedsPassword] = useState(false); - const [hideTextPassword, setHideTextPassword] = useState( + const [needsPassword, setNeedsPassword] = useState(false); + const [hideTextPassword, setHideTextPassword] = useState( false, ); @@ -66,16 +70,6 @@ export const RegisterAgent = withReduxProvider( groups, }, }, - - //IP: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 - // O ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - - // FQDN: Maximum of 63 characters per label. - // Can only contain numbers, letters and hyphens (-) - // Labels cannot begin or end with a hyphen - // Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. - // Minimum 3 labels - serverAddress: { type: 'text', initialValue: configuration['enrollment.dns'] || '', @@ -106,8 +100,6 @@ export const RegisterAgent = withReduxProvider( if (remoteConfig) { setHaveUdpProtocol(remoteConfig.isUdp); setHaveConnectionSecure(remoteConfig.haveSecureConnection); - setUdpProtocol(remoteConfig.isUdp); - setConnectionSecure(remoteConfig.haveSecureConnection); } }; @@ -143,7 +135,6 @@ export const RegisterAgent = withReduxProvider( } } const groups = await getGroups(); - setNeedsPassword(needsPassword); setHideTextPassword(hideTextPassword); setWazuhPassword(wazuhPassword); @@ -172,7 +163,6 @@ export const RegisterAgent = withReduxProvider( fetchData(); }, []); - const agentGroup = ; const osCard = ( ); @@ -222,11 +212,20 @@ export const RegisterAgent = withReduxProvider( form={form} needsPassword={needsPassword} hideTextPassword={hideTextPassword} - agentGroup={agentGroup} osCard={osCard} + connection={{ + isSecure: haveConnectionSecure ? true : false, + isUDP: haveUdpProtocol ? true : false, + }} + wazuhPassword={wazuhPassword} /> )} + + + reload()}>Close + + diff --git a/public/controllers/register-agent/containers/steps/steps.tsx b/public/controllers/register-agent/containers/steps/steps.tsx index bf456eff0e..95e14d2144 100644 --- a/public/controllers/register-agent/containers/steps/steps.tsx +++ b/public/controllers/register-agent/containers/steps/steps.tsx @@ -1,80 +1,105 @@ -import React, { Fragment, useState } from 'react'; -import { - EuiSteps, - EuiText, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiCallOut, - EuiPopover, - EuiButtonEmpty, - EuiLink, -} from '@elastic/eui'; -import { InputForm } from '../../../../components/common/form'; +import React, { Fragment, useEffect, useState } from 'react'; +import { EuiSteps, EuiTitle } from '@elastic/eui'; import './steps.scss'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; import { - REGISTER_AGENT_DATA_STEP_THREE, - REGISTER_AGENT_DATA_STEP_TWO, -} from '../../utils/register-agent-data'; -import { webDocumentationLink } from '../../../../../common/services/web_documentation'; -import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; - -const popoverServerAddress = ( - - Learn about{' '} - - Server address. - - -); + IParseRegisterFormValues, + getRegisterAgentFormValues, + parseRegisterAgentFormValues, +} from '../../services/register-agent-services'; -const popoverAgentName = ( - - Learn about{' '} - - Assigning an agent name. - - -); +import { useRegisterAgentCommands } from '../../hooks/use-register-agent-commands'; +import { + osCommandsDefinitions, + optionalParamsDefinitions, + tOperatingSystem, + tOptionalParameters, +} from '../../core/config/os-commands-definitions'; +import { UseFormReturn } from '../../../../components/common/form/types'; +import CommandOutput from '../../components/command-output/command-output'; +import ServerAddressTitle from '../../components/server-address/server-address-title'; +import ServerAddressInput from '../../components/server-address/server-address-input'; +import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; +import { getAgentCommandsStepStatus, tFormStepsStatus, getOSSelectorStepStatus, getServerAddressStepStatus, getOptionalParameterStepStatus, showCommandsSections } from '../../services/register-agent-steps-status-services'; -const warningForAgentName = - 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; +interface IStepsProps { + needsPassword: boolean; + hideTextPassword: boolean; + form: UseFormReturn; + osCard: React.ReactElement; + connection: { + isUDP: boolean; + isSecure: boolean; + }; + wazuhPassword: string; +} export const Steps = ({ needsPassword, hideTextPassword, - agentGroup, form, osCard, -}) => { - const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); - const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + connection, + wazuhPassword, +}: IStepsProps) => { + const [registerAgentFormValues, setRegisterAgentFormValues] = + useState({ + operatingSystem: { + name: '', + architecture: '', + }, + optionalParams: { + agentGroups: '', + agentName: '', + serverAddress: '', + wazuhPassword, + }, + }); - const onButtonServerAddress = () => - setIsPopoverServerAddress( - isPopoverServerAddress => !isPopoverServerAddress, + useEffect(() => { + // get form values and parse them divided in OS and optional params + const registerAgentFormValuesParsed = parseRegisterAgentFormValues( + getRegisterAgentFormValues(form), + OPERATING_SYSTEMS_OPTIONS, ); - const closeServerAddress = () => setIsPopoverServerAddress(false); + setRegisterAgentFormValues(registerAgentFormValuesParsed); + setInstallCommandStepStatus(getAgentCommandsStepStatus(form.fields, installCommandWasCopied)) + setStartCommandStepStatus(getAgentCommandsStepStatus(form.fields, startCommandWasCopied)) + }, [form.fields]); + + const { installCommand, startCommand, selectOS, setOptionalParams } = + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }); + + // install - start commands step state + const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); + const [installCommandStepStatus, setInstallCommandStepStatus] = useState(getAgentCommandsStepStatus(form.fields, false)) + const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); + const [startCommandStepStatus, setStartCommandStepStatus] = useState(getAgentCommandsStepStatus(form.fields, false)) + + useEffect(() => { + if ( + registerAgentFormValues.operatingSystem.name !== '' && + registerAgentFormValues.operatingSystem.architecture !== '' + ) { + selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); + } + setOptionalParams(registerAgentFormValues.optionalParams); + setInstallCommandWasCopied(false); + setStartCommandWasCopied(false); + }, [registerAgentFormValues]); + + useEffect(() => { + setInstallCommandStepStatus(getAgentCommandsStepStatus(form.fields, installCommandWasCopied)) + }, [installCommandWasCopied]) - const onButtonAgentName = () => - setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); - const closeAgentName = () => setIsPopoverAgentName(false); + useEffect(() => { + setStartCommandStepStatus(getAgentCommandsStepStatus(form.fields, startCommandWasCopied)) + }, [startCommandWasCopied]) - const firstSetOfSteps = [ + const registerAgentFormSteps = [ { title: ( @@ -82,62 +107,12 @@ export const Steps = ({ ), children: osCard, - status: form.fields.operatingSystemSelection.value - ? 'complete' - : 'current', + status: getOSSelectorStepStatus(form.fields), }, { - title: ( - - - - Server address - - } - isOpen={isPopoverServerAddress} - closePopover={closeServerAddress} - anchorPosition='rightCenter' - > - {popoverServerAddress} - - - - ), - children: ( - - - {REGISTER_AGENT_DATA_STEP_TWO.map((data, index) => ( - - - {data.subtitle} - - - ))} - - } - fullWidth={false} - placeholder='Server address' - /> - - ), - status: !form.fields.operatingSystemSelection.value - ? 'disabled' - : !form.fields.serverAddress.value && - form.fields.operatingSystemSelection.value - ? 'current' - : form.fields.operatingSystemSelection.value && - form.fields.serverAddress.value - ? 'complete' - : '', + title: , + children: , + status: getServerAddressStepStatus(form.fields), }, ...(!(!needsPassword || hideTextPassword) ? [ @@ -163,72 +138,44 @@ export const Steps = ({

Optional settings

), + children: , + status: getOptionalParameterStepStatus(form.fields, installCommandWasCopied, startCommandWasCopied) + }, + { + title: ( + +

+ Run the following commands to download and install the Wazuh agent: +

+
+ ), + children: ( + setInstallCommandWasCopied(true)} + /> + ), + status: installCommandStepStatus, + }, + { + title: ( + +

Start the Wazuh agent:

+
+ ), children: ( - - - {REGISTER_AGENT_DATA_STEP_THREE.map((data, index) => ( - - {data.subtitle} - - ))} - - - - - - Assign an agent name - - } - isOpen={isPopoverAgentName} - closePopover={closeAgentName} - anchorPosition='rightCenter' - > - {popoverAgentName} - - - - - } - placeholder='Agent name' - /> - - {agentGroup} - + setStartCommandWasCopied(true)} + /> ), - status: - !form.fields.operatingSystemSelection.value || - !form.fields.serverAddress.value - ? 'disabled' - : form.fields.serverAddress.value !== '' - ? 'current' - : form.fields.agentGroups.value.length > 0 - ? 'complete' - : '', + status: startCommandStepStatus, }, ]; - return ; + return ; }; diff --git a/public/controllers/register-agent/core/config/os-commands-definitions.ts b/public/controllers/register-agent/core/config/os-commands-definitions.ts new file mode 100644 index 0000000000..73a370f917 --- /dev/null +++ b/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -0,0 +1,194 @@ +import { IOSDefinition, tOptionalParams } from '../register-commands/types'; + +// Defined OS combinations + +/** Linux options **/ +export interface ILinuxAMDRPM { + name: 'LINUX'; + architecture: 'RPM amd64'; +} + +export interface ILinuxAARCHRPM { + name: 'LINUX'; + architecture: 'RPM aarch64'; +} + +export interface ILinuxAMDDEB { + name: 'LINUX'; + architecture: 'DEB amd64'; +} + +export interface ILinuxAARCHDEB { + name: 'LINUX'; + architecture: 'DEB aarch64'; +} + +type ILinuxOSTypes = + | ILinuxAMDRPM + | ILinuxAARCHRPM + | ILinuxAMDDEB + | ILinuxAARCHDEB; + +/** Windows options **/ +export interface IWindowsOSTypes { + name: 'WINDOWS'; + architecture: 'MSI 32/64'; +} + +/** MacOS options **/ +export interface IMacOSIntel { + name: 'macOS'; + architecture: 'Intel'; +} + +export interface IMacOSApple { + name: 'macOS'; + architecture: 'Apple Silicon'; +} + +type IMacOSTypes = IMacOSApple | IMacOSIntel; + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +export type tOptionalParameters = + | 'serverAddress' + | 'agentName' + | 'agentGroups' + | 'wazuhPassword'; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +const linuxDefinition: IOSDefinition = { + name: 'LINUX', + options: [ + { + architecture: 'DEB amd64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, + installCommand: props => + `sudo yum install -y ${props.urlPackage} ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + architecture: 'DEB aarch64', + 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 ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + architecture: 'RPM amd64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, + installCommand: props => + `sudo yum install -y ${props.urlPackage} ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + architecture: 'RPM aarch64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +const windowsDefinition: IOSDefinition = { + name: 'WINDOWS', + options: [ + { + architecture: 'MSI 32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1`, + installCommand: props => + `Invoke-WebRequest -Uri ${ + props.urlPackage + } -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `Start-Service -Name wazuh-agent`, + }, + ], +}; + +const macDefinition: IOSDefinition = { + name: 'macOS', + options: [ + { + architecture: 'Intel', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1`, + installCommand: props => + `mac -so wazuh-agent.pkg ${ + props.urlPackage + } && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target / + ${props.optionals?.serverAddress || ''} + ${props.optionals?.agentName || ''} + ${props.optionals?.agentGroups || ''} + ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + }, + { + architecture: 'Apple Silicon', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1`, + installCommand: props => + `mac -so wazuh-agent.pkg ${ + props.urlPackage + } && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target + ${props.optionals?.serverAddress || ''} + ${props.optionals?.agentName || ''} + ${props.optionals?.agentGroups || ''} + ${props.optionals?.wazuhPassword || ''}`, + startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + }, + ], +}; + +export const osCommandsDefinitions = [ + linuxDefinition, + windowsDefinition, + macDefinition, +]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = { + serverAddress: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + agentName: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + agentGroups: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => { + const { property, value } = props; + let parsedValue = value; + if (Array.isArray(value)) { + parsedValue = value.length > 0 ? value.join(',') : ''; + } + return parsedValue ? `${property}='${parsedValue}'` : ''; + }, + }, + wazuhPassword: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, +}; diff --git a/public/controllers/register-agent/core/register-commands/README.md b/public/controllers/register-agent/core/register-commands/README.md index d53288e940..02e7600561 100644 --- a/public/controllers/register-agent/core/register-commands/README.md +++ b/public/controllers/register-agent/core/register-commands/README.md @@ -48,7 +48,6 @@ export type tOptionalParams = { export interface IOperationSystem { name: string; architecture: string; - extension: string; } /// .... @@ -56,18 +55,15 @@ export interface IOperationSystem { interface ILinuxOSTypes { name: 'linux'; architecture: 'x64' | 'x86'; - extension: 'rpm' | 'deb'; } interface IWindowsOSTypes { name: 'windows'; architecture: 'x86'; - extension: 'msi'; } interface IMacOSTypes { name: 'mac'; architecture: '32/64'; - extension: 'pkg'; } type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options @@ -80,7 +76,6 @@ export interface IOSDefinition { - extension: OS['extension']; architecture: OS['architecture']; urlPackage: (props: tOSEntryProps) => string; installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; @@ -99,14 +94,12 @@ const osDefinitions: IOSDefinition[] = [{ name: 'linux', options: [ { - extension: 'rpm', architecture: 'amd64', urlPackage: props => 'add url package', installCommand: props => 'add install command', startCommand: props => `add start command`, }, { - extension: 'deb', architecture: 'amd64', urlPackage: props => 'add url package', installCommand: props => 'add install command', @@ -118,7 +111,6 @@ const osDefinitions: IOSDefinition[] = [{ name: 'windows', options: [ { - extension: 'msi', architecture: '32/64', urlPackage: props => 'add url package', installCommand: props => 'add install command', @@ -237,7 +229,6 @@ const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters) commandGenerator.selectOS({ name: 'linux', architecture: 'amd64', - extension: 'rpm' }); // get install command @@ -262,7 +253,6 @@ const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters) commandGenerator.selectOS({ name: 'linux', architecture: 'amd64', - extension: 'rpm' }); // get start command @@ -284,7 +274,6 @@ const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters) commandGenerator.selectOS({ name: 'linux', architecture: 'amd64', - extension: 'rpm' }); const urlPackage = commandGenerator.getUrlPackage(); @@ -305,7 +294,6 @@ const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters) commandGenerator.selectOS({ name: 'linux', architecture: 'amd64', - extension: 'rpm' }); // specify to the command generator the optional parameters that we want to use @@ -330,7 +318,6 @@ export interface ICommandsResponse { wazuhVersion: string; os: string; architecture: string; - extension: string; url_package: string; install_command: string; start_command: string; diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts index 71fd77ebd3..ae9af6d24e 100644 --- a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -1,7 +1,6 @@ import { CommandGenerator } from './command-generator'; import { IOSDefinition, - IOperationSystem, IOptionalParameters, tOptionalParams, } from '../types'; @@ -14,18 +13,15 @@ const mockedCommandsResponse = jest.fn().mockReturnValue(mockedCommandValue); export interface ILinuxOSTypes { name: 'linux'; architecture: 'x64' | 'x86'; - extension: 'rpm' | 'deb'; } export interface IWindowsOSTypes { name: 'windows'; architecture: 'x86'; - extension: 'msi'; } export interface IMacOSTypes { name: 'mac'; architecture: '32/64'; - extension: 'pkg'; } export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; @@ -41,14 +37,12 @@ const osDefinitions: IOSDefinition[] = [ options: [ { architecture: 'x64', - extension: 'deb', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, }, { - architecture: 'x64', - extension: 'msi', + architecture: 'x86', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, @@ -107,7 +101,6 @@ describe('Command Generator', () => { commandGenerator.selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); commandGenerator.addOptionalParams(optionalValues); const command = commandGenerator.getInstallCommand(); @@ -123,7 +116,6 @@ describe('Command Generator', () => { commandGenerator.selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); commandGenerator.addOptionalParams(optionalValues); const command = commandGenerator.getStartCommand(); @@ -139,14 +131,12 @@ describe('Command Generator', () => { commandGenerator.selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); commandGenerator.addOptionalParams(optionalValues); const commands = commandGenerator.getAllCommands(); expect(commands).toEqual({ os: 'linux', architecture: 'x64', - extension: 'deb', wazuhVersion: '4.4', install_command: mockedCommandValue, start_command: mockedCommandValue, @@ -165,7 +155,6 @@ describe('Command Generator', () => { const selectedOs: tOperatingSystem = { name: 'linux', architecture: 'x64', - extension: 'deb', }; commandGenerator.selectOS(selectedOs); @@ -182,7 +171,6 @@ describe('Command Generator', () => { expect(commands).toEqual({ os: selectedOs.name, architecture: selectedOs.architecture, - extension: selectedOs.extension, wazuhVersion: '4.4', install_command: mockedCommandValue, start_command: mockedCommandValue, @@ -219,14 +207,12 @@ describe('Command Generator', () => { options: [ { architecture: 'x64', - extension: 'deb', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, }, { architecture: 'x64', - extension: 'deb', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, @@ -250,7 +236,6 @@ describe('Command Generator', () => { options: [ { architecture: 'x64', - extension: 'deb', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, @@ -262,7 +247,6 @@ describe('Command Generator', () => { options: [ { architecture: 'x64', - extension: 'deb', installCommand: mockedCommandsResponse, startCommand: mockedCommandsResponse, urlPackage: mockedCommandsResponse, @@ -340,7 +324,6 @@ describe('Command Generator', () => { const selectedOs: tOperatingSystem = { name: 'linux', architecture: 'x64', - extension: 'deb', }; commandGenerator.selectOS(selectedOs); @@ -373,7 +356,6 @@ describe('Command Generator', () => { const selectedOs: tOperatingSystem = { name: 'linux', architecture: 'x64', - extension: 'deb', }; commandGenerator.selectOS(selectedOs); diff --git a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts index 8dd6b69889..6974478eac 100644 --- a/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts +++ b/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts @@ -14,7 +14,7 @@ import { validateOSDefinitionsDuplicated, } from '../services/search-os-definitions.service'; import { OptionalParametersManager } from '../optional-parameters-manager/optional-parameters-manager'; -import { NoArchitectureSelectedException, NoExtensionSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; +import { NoArchitectureSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; import { version } from '../../../../../../package.json'; export class CommandGenerator implements ICommandGenerator { @@ -71,21 +71,17 @@ export class CommandGenerator { - const { name, architecture, extension } = params; + const { name, architecture } = params; if (!name) { throw new NoOSSelectedException(); } if (!architecture) { throw new NoArchitectureSelectedException(); } - if (!extension) { - throw new NoExtensionSelectedException(); - } const option = searchOSDefinitions(this.osDefinitions, { name, architecture, - extension, }); return option; } @@ -102,7 +98,6 @@ export class CommandGenerator, @@ -140,7 +134,6 @@ export class CommandGenerator, }); @@ -160,7 +153,6 @@ export class CommandGenerator = { architecture: 'x64', - extension: 'deb', installCommand: props => 'install command mocked', startCommand: props => 'start command mocked', urlPackage: props => 'https://package-url.com', @@ -63,7 +59,6 @@ describe('getInstallCommandByOS', () => { // @ts-ignore const osDefinition: IOSCommandsDefinition = { architecture: 'x64', - extension: 'deb', startCommand: props => 'start command mocked', urlPackage: props => 'https://package-url.com', }; @@ -90,7 +85,6 @@ describe('getInstallCommandByOS', () => { const mockedInstall = jest.fn(); const validOsDefinition: IOSCommandsDefinition = { architecture: 'x64', - extension: 'deb', installCommand: mockedInstall, startCommand: props => 'start command mocked', urlPackage: props => 'https://package-url.com', diff --git a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts index d66e75c8c0..c8fabc3ebf 100644 --- a/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts +++ b/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts @@ -16,11 +16,11 @@ import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from ".. export function getInstallCommandByOS(osDefinition: IOSCommandsDefinition, packageUrl: string, version: string, osName: string, optionals?: IOptionalParameters) { if (!osDefinition.installCommand) { - throw new NoInstallCommandDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + throw new NoInstallCommandDefinitionException(osName, osDefinition.architecture); } if(!packageUrl || packageUrl === ''){ - throw new NoPackageURLDefinitionException(osName, osDefinition.architecture, osDefinition.extension); + throw new NoPackageURLDefinitionException(osName, osDefinition.architecture); } if(!version || version === ''){ @@ -32,7 +32,6 @@ export function getInstallCommandByOS[] name: 'linux', options: [ { - extension: 'deb', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -51,7 +46,6 @@ const validOSDefinitions: IOSDefinition[] name: 'windows', options: [ { - extension: 'msi', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -67,7 +61,6 @@ describe('search OS definitions services', () => { const result = searchOSDefinitions(validOSDefinitions, { name: 'linux', architecture: 'x64', - extension: 'deb', }); expect(result).toMatchObject(validOSDefinitions[0].options[0]); }); @@ -78,20 +71,10 @@ describe('search OS definitions services', () => { // @ts-ignore name: 'invalid-os', architecture: 'x64', - extension: 'deb', }), ).toThrow(NoOSOptionFoundException); }); - - it('should throw an error if the OS name is found but the architecture is not found', () => { - expect(() => - searchOSDefinitions(validOSDefinitions, { - name: 'linux', - architecture: 'invalid-architecture', - extension: 'deb', - }), - ).toThrow(NoOptionFoundException); - }); + }); describe('validateOSDefinitionsDuplicated', () => { @@ -101,7 +84,6 @@ describe('search OS definitions services', () => { name: 'linux', options: [ { - extension: 'deb', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -113,7 +95,6 @@ describe('search OS definitions services', () => { name: 'windows', options: [ { - extension: 'msi', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -133,7 +114,6 @@ describe('search OS definitions services', () => { name: 'linux', options: [ { - extension: 'deb', architecture: 'x64', // @ts-ignore packageManager: 'aix', @@ -162,7 +142,6 @@ describe('search OS definitions services', () => { name: 'linux', options: [ { - extension: 'deb', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, @@ -174,14 +153,12 @@ describe('search OS definitions services', () => { name: 'linux', options: [ { - extension: 'deb', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, urlPackage: mockedUrlPackage, }, { - extension: 'deb', architecture: 'x64', installCommand: mockedInstallCommand, startCommand: mockedStartCommand, diff --git a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts index fcd5509681..0ceefada65 100644 --- a/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts +++ b/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -14,13 +14,12 @@ import { IOSDefinition, IOperationSystem } from '../types'; * @param params - The operation system parameters to match against. * @returns The matching OS definition option. * @throws NoOSOptionFoundException - If no matching OS definition is found. - * @throws NoOptionFoundException - If no matching OS definition option is found. */ export function searchOSDefinitions( osDefinitions: IOSDefinition[], params: IOperationSystem, ){ - const { name, architecture, extension } = params; + const { name, architecture } = params; const osDefinition = osDefinitions.find(os => os.name === name); if (!osDefinition) { @@ -29,11 +28,11 @@ export function searchOSDefinitions - option.architecture === architecture && option.extension === extension, + option.architecture === architecture, ); if (!osDefinitionOption) { - throw new NoOptionFoundException(name, architecture, extension); + throw new NoOptionFoundException(name, architecture); } return osDefinitionOption; @@ -72,11 +71,10 @@ export function validateOSDefinitionHasDuplicatedOptions(); for (const option of osDefinition.options) { - let ext_arch_manager = `${option.extension}_${option.architecture}`; + let ext_arch_manager = `${option.architecture}`; if (options.has(ext_arch_manager)) { throw new DuplicatedOSOptionException( osDefinition.name, - option.extension, option.architecture, ); } diff --git a/public/controllers/register-agent/core/register-commands/types.ts b/public/controllers/register-agent/core/register-commands/types.ts index efd36433ff..ebce4b3dfd 100644 --- a/public/controllers/register-agent/core/register-commands/types.ts +++ b/public/controllers/register-agent/core/register-commands/types.ts @@ -4,7 +4,6 @@ export interface IOperationSystem { name: string; architecture: string; - extension: string; } export type IOptionalParameters = { @@ -28,7 +27,6 @@ interface IOptionalParamsWithValues { type tOSEntryProps = IOSProps & IOptionalParamsWithValues; export interface IOSCommandsDefinition { - extension: OS['extension']; architecture: OS['architecture']; urlPackage: (props: tOSEntryProps) => string; installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; @@ -60,7 +58,7 @@ export type tOptionalParams = { }; export interface IOptionalParamInput { - value: string; + value: any; name: T; } export interface IOptionalParametersManager { @@ -90,7 +88,6 @@ export interface ICommandsResponse { wazuhVersion: string; os: string; architecture: string; - extension: string; url_package: string; install_command: string; start_command: string; diff --git a/public/controllers/register-agent/hooks/README.md b/public/controllers/register-agent/hooks/README.md index 0b06f1b93e..d3ec96adc1 100644 --- a/public/controllers/register-agent/hooks/README.md +++ b/public/controllers/register-agent/hooks/README.md @@ -46,7 +46,6 @@ const { selectOS({ name: 'name-OS', architecture: 'architecture-OS', - extension: 'extension-OS', }) // add optionals params depending on the specified optional parameters in the hook configuration @@ -76,7 +75,6 @@ console.log('optionals params processed', optionalParamsParsed); export interface IOperationSystem { name: string; architecture: string; - extension: string; } interface IUseRegisterCommandsProps { @@ -92,7 +90,6 @@ interface IUseRegisterCommandsProps { @@ -126,7 +123,6 @@ export type tOptionalParams = { export interface IOperationSystem { name: string; architecture: string; - extension: string; } /// .... @@ -134,18 +130,15 @@ export interface IOperationSystem { export interface ILinuxOSTypes { name: 'linux'; architecture: 'x64' | 'x86'; - extension: 'rpm' | 'deb'; } export interface IWindowsOSTypes { name: 'windows'; architecture: 'x86'; - extension: 'msi'; } export interface IMacOSTypes { name: 'mac'; architecture: '32/64'; - extension: 'pkg'; } export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; @@ -168,7 +161,6 @@ const { selectOS({ name: 'linux', architecture: 'x64', - extension: 'rpm', }) ```` diff --git a/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts index ab0f6727df..20a4de7b32 100644 --- a/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts +++ b/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts @@ -11,18 +11,15 @@ type tOptionalParamsNames = 'optional1' | 'optional2'; export interface ILinuxOSTypes { name: 'linux'; architecture: 'x64' | 'x86'; - extension: 'rpm' | 'deb'; } export interface IWindowsOSTypes { name: 'windows'; architecture: 'x86'; - extension: 'msi'; } export interface IMacOSTypes { name: 'mac'; architecture: '32/64'; - extension: 'pkg'; } export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; @@ -31,18 +28,16 @@ const linuxDefinition: IOSDefinition = { name: 'linux', options: [ { - extension: 'deb', architecture: '32/64', urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.${props.extension}`, + `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`, }, { - extension: 'deb', architecture: 'x64', urlPackage: props => - `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}.${props.extension}`, + `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`, @@ -102,7 +97,6 @@ describe('useRegisterAgentCommands hook', () => { selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); }); } catch (error) { @@ -124,7 +118,7 @@ describe('useRegisterAgentCommands hook', () => { const optionSelected = osCommandsDefinitions .find(os => os.name === 'linux') ?.options.find( - item => item.architecture === 'x64' && item.extension === 'deb', + item => item.architecture === 'x64', ); const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); const spyStart = jest.spyOn(optionSelected!, 'startCommand'); @@ -133,7 +127,6 @@ describe('useRegisterAgentCommands hook', () => { selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); }); expect(result.current.installCommand).not.toBe(''); @@ -211,7 +204,7 @@ describe('useRegisterAgentCommands hook', () => { const optionSelected = osCommandsDefinitions .find(os => os.name === 'linux') ?.options.find( - item => item.architecture === 'x64' && item.extension === 'deb', + item => item.architecture === 'x64', ); const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); const spyStart = jest.spyOn(optionSelected!, 'startCommand'); @@ -220,7 +213,6 @@ describe('useRegisterAgentCommands hook', () => { selectOS({ name: 'linux', architecture: 'x64', - extension: 'deb', }); setOptionalParams({ diff --git a/public/controllers/register-agent/interfaces/types.ts b/public/controllers/register-agent/interfaces/types.ts index 06de5db134..f9fe6c02fc 100644 --- a/public/controllers/register-agent/interfaces/types.ts +++ b/public/controllers/register-agent/interfaces/types.ts @@ -1,8 +1,10 @@ +import { tOperatingSystem } from '../config/os-commands-definitions'; + interface RegisterAgentData { icon: string; - title: string; + title: tOperatingSystem['name']; hr: boolean; - architecture: string[]; + architecture: tOperatingSystem['architecture'][] } interface CheckboxGroupComponentProps { diff --git a/public/controllers/register-agent/services/register-agent-services.tsx b/public/controllers/register-agent/services/register-agent-services.tsx index 262afedd64..ed32979bab 100644 --- a/public/controllers/register-agent/services/register-agent-services.tsx +++ b/public/controllers/register-agent/services/register-agent-services.tsx @@ -1,5 +1,10 @@ +import { UseFormReturn } from '../../../components/common/form/types'; import { WzRequest } from '../../../react-services/wz-request'; -import { ServerAddressOptions } from '../register-agent/steps'; +import { + tOperatingSystem, + tOptionalParameters, +} from '../core/config/os-commands-definitions'; +import { RegisterAgentData } from '../interfaces/types'; type Protocol = 'TCP' | 'UDP'; @@ -106,7 +111,7 @@ function getRemoteProtocol(protocols: Protocol[]) { * @param defaultServerAddress */ async function getConnectionConfig( - nodeSelected: ServerAddressOptions, + nodeSelected: any, defaultServerAddress?: string, ) { const nodeName = nodeSelected?.label; @@ -174,9 +179,7 @@ export const getManagerNode = async (): Promise => { * 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[] => { +export const parseNodesInOptions = (nodes: NodeResponse): any[] => { return nodes.data.data.affected_items.map((item: NodeItem) => ({ label: item.name, value: item.ip, @@ -187,9 +190,7 @@ export const parseNodesInOptions = ( /** * Get the list of the cluster nodes from API and parse it into a list of options */ -export const fetchClusterNodesOptions = async (): Promise< - ServerAddressOptions[] -> => { +export const fetchClusterNodesOptions = async (): Promise => { const clusterStatus = await clusterStatusResponse(); if (clusterStatus) { // Cluster mode @@ -207,9 +208,7 @@ export const fetchClusterNodesOptions = async (): Promise< * Get the master node data from the list of cluster nodes * @param nodeIps */ -export const getMasterNode = ( - nodeIps: ServerAddressOptions[], -): ServerAddressOptions[] => { +export const getMasterNode = (nodeIps: any[]): any[] => { return nodeIps.filter(nodeIp => nodeIp.nodetype === 'master'); }; @@ -236,3 +235,60 @@ export const getGroups = async () => { throw new Error(error); } }; + +export const getRegisterAgentFormValues = (form: UseFormReturn) => { + // return the values form the formFields and the value property + return Object.keys(form.fields).map(key => { + return { + name: key, + value: form.fields[key].value, + }; + }); +}; + +export interface IParseRegisterFormValues { + operatingSystem: { + name: tOperatingSystem['name'] | ''; + architecture: tOperatingSystem['architecture'] | ''; + }; + // optionalParams is an object that their key is defined in tOptionalParameters and value must be string + optionalParams: { + [FIELD in tOptionalParameters]: any; + }; +} + +export const parseRegisterAgentFormValues = ( + formValues: { name: keyof UseFormReturn['fields']; value: any }[], + OSOptionsDefined: RegisterAgentData[], +) => { + // return the values form the formFields and the value property + const parsedForm = { + operatingSystem: { + architecture: '', + name: '', + }, + optionalParams: {}, + } as IParseRegisterFormValues; + formValues.forEach(field => { + if (field.name === 'operatingSystemSelection') { + // search the architecture defined in architecture array and get the os name defined in title array in the same index + const operatingSystem = OSOptionsDefined.find(os => + os.architecture.includes(field.value), + ); + if (operatingSystem) { + parsedForm.operatingSystem = { + name: operatingSystem.title, + architecture: field.value, + }; + } + } else { + if (field.name === 'agentGroups') { + parsedForm.optionalParams[field.name as any] = field.value.map(item => item.id) + } else { + parsedForm.optionalParams[field.name as any] = field.value; + } + } + }); + + return parsedForm; +}; \ No newline at end of file diff --git a/public/controllers/register-agent/services/register-agent-steps-status-services.tsx b/public/controllers/register-agent/services/register-agent-steps-status-services.tsx new file mode 100644 index 0000000000..7065d38290 --- /dev/null +++ b/public/controllers/register-agent/services/register-agent-steps-status-services.tsx @@ -0,0 +1,145 @@ +import { EuiStepStatus } from '@elastic/eui'; +import { UseFormReturn } from '../../../components/common/form/types'; + +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 Error('fields to check are not defined in formFields'); + } + + const haveError = fieldsToCheck.some(key => { + return formFields[key]?.error; + }); + return haveError; +}; + +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 Error('fields to check are not defined in formFields'); + } + + if (fieldsHaveErrors(fieldsToCheck, formFields)) { + return false; + } + + if (fieldsAreEmpty(fieldsToCheck, formFields)) { + return false; + } + + return true; +}; + +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 Error('fields to check are not defined in formFields'); + } + + const notEmpty = fieldsToCheck.some(key => { + return formFields[key]?.value?.length > 0; + }); + return !notEmpty; +}; + +export const showCommandsSections = ( + formFields: UseFormReturn['fields'], +): boolean => { + if ( + !formFields.operatingSystemSelection.value || + formFields.serverAddress.value === '' || + formFields.serverAddress.error + ) { + return false; + } else if ( + formFields.serverAddress.value === '' && + formFields.agentName.value === '' + ) { + return true; + } else if (!fieldsHaveErrors(['agentGroups', 'agentName'], formFields)) { + return true; + } else { + return false; + } +}; + +/******** Form Steps status getters ********/ + +export type tFormStepsStatus = EuiStepStatus | 'current' | 'disabled' | ''; + +export const getOSSelectorStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + return formFields.operatingSystemSelection.value ? 'complete' : 'current'; +}; + +export const getAgentCommandsStepStatus = ( + formFields: UseFormReturn['fields'], + wasCopied: boolean, +): tFormStepsStatus | 'disabled' => { + if (!showCommandsSections(formFields)) { + return 'disabled'; + } else if (showCommandsSections(formFields) && wasCopied) { + return 'complete'; + } else { + return 'current'; + } +}; + +export const getServerAddressStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error + ) { + return 'disabled'; + } else if ( + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'current'; + } else { + return 'complete'; + } +}; + +export const getOptionalParameterStepStatus = ( + formFields: UseFormReturn['fields'], + installCommandWasCopied: boolean, +): tFormStepsStatus => { + // when previous step are not complete + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error || + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'disabled'; + } else if ( + installCommandWasCopied || + anyFieldIsComplete(['agentName', 'agentGroups'], formFields) + ) { + return 'complete'; + } else { + return 'current'; + } +}; diff --git a/public/controllers/register-agent/utils/register-agent-data.tsx b/public/controllers/register-agent/utils/register-agent-data.tsx index c0e4e79a94..44a7f52619 100644 --- a/public/controllers/register-agent/utils/register-agent-data.tsx +++ b/public/controllers/register-agent/utils/register-agent-data.tsx @@ -3,7 +3,7 @@ import LinuxIcon from '../../../../public/assets/images/icons/linux-icon.svg'; import WindowsIcon from '../../../../public/assets/images/icons/windows-icon.svg'; import MacIcon from '../../../../public/assets/images/icons/mac-icon.svg'; -export const REGISTER_AGENT_DATA_STEP_ONE: RegisterAgentData[] = [ +export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ { icon: LinuxIcon, title: 'LINUX', @@ -24,7 +24,7 @@ export const REGISTER_AGENT_DATA_STEP_ONE: RegisterAgentData[] = [ }, ]; -export const REGISTER_AGENT_DATA_STEP_TWO = [ +export const SERVER_ADDRESS_TEXTS = [ { title: 'Server address', subtitle: @@ -32,7 +32,7 @@ export const REGISTER_AGENT_DATA_STEP_TWO = [ }, ]; -export const REGISTER_AGENT_DATA_STEP_THREE = [ +export const OPTIONAL_PARAMETERS_TEXT = [ { title: 'Optional settings', subtitle: diff --git a/public/controllers/register-agent/utils/validations.tsx b/public/controllers/register-agent/utils/validations.tsx index 6b5a17978f..c3eab3de31 100644 --- a/public/controllers/register-agent/utils/validations.tsx +++ b/public/controllers/register-agent/utils/validations.tsx @@ -1,4 +1,12 @@ -export const validateServerAddress = value => { +//IP: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 +// O ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + +// FQDN: Maximum of 63 characters per label. +// Can only contain numbers, letters and hyphens (-) +// Labels cannot begin or end with a hyphen +// Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. +// Minimum 3 labels +export const validateServerAddress = (value: any) => { const isFQDN = /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; @@ -31,7 +39,7 @@ export const validateServerAddress = value => { } }; -export const validateAgentName = value => { +export const validateAgentName = (value: any) => { if (value.length === 0) { return undefined; } From 1c31e45cc4d36df101f28920b7d85a5608b29038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:46:45 -0300 Subject: [PATCH 09/24] [Redesign add agent] Dark mode (#5620) * remove custom color styles to make the elastic dark mode work by default on the agent registration page * add development for images to have dark mode in the section deploy a new agent * changelog about dark mode * Cleaning console.log from assets file * Adding suggested style modifications in the agent registration section * add a style hint so that text cannot be selected on cards * add suggested changes to the styles in the register an agent section * correction added to the word wizard * added coding enhancements in the agent registration section * adding an enhancement to eliminate the console error * adding an enhancement to eliminate the console error --- CHANGELOG.md | 3 + .../assets/images/themes/dark/linux-icon.svg | 3 + public/assets/images/themes/dark/mac-icon.svg | 4 + .../images/themes/dark/windows-icon.svg | 13 ++ .../assets/images/themes/light/linux-icon.svg | 3 + .../assets/images/themes/light/mac-icon.svg | 4 + .../images/themes/light/windows-icon.svg | 13 ++ .../components/group-input/group-input.tsx | 1 - .../optionals-inputs/optionals-inputs.tsx | 1 - .../checkbox-group/checkbox-group.scss | 6 +- .../checkbox-group/checkbox-group.tsx | 4 +- .../os-selector/os-card/os-card.scss | 7 +- .../os-selector/os-card/os-card.tsx | 1 - .../register-agent/register-agent.scss | 11 +- .../register-agent/register-agent.tsx | 37 +++- .../containers/steps/steps.scss | 3 - .../register-agent/containers/steps/steps.tsx | 75 ++++---- .../utils/register-agent-data.tsx | 18 +- public/styles/theme/dark/index.dark.scss | 174 ++++++++++-------- public/utils/assets.ts | 3 +- 20 files changed, 236 insertions(+), 148 deletions(-) create mode 100644 public/assets/images/themes/dark/linux-icon.svg create mode 100644 public/assets/images/themes/dark/mac-icon.svg create mode 100644 public/assets/images/themes/dark/windows-icon.svg create mode 100644 public/assets/images/themes/light/linux-icon.svg create mode 100644 public/assets/images/themes/light/mac-icon.svg create mode 100644 public/assets/images/themes/light/windows-icon.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d9e0b9fc..c45b8f981a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added +- Added development so that the images of the new agent deployment page also have dark mode. [5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) + ### Changed - Changed the deploy a new agent page from step one to step three. [#5554](https://github.com/wazuh/wazuh-kibana-app/pull/5554) [5462](https://github.com/wazuh/wazuh-kibana-app/pull/5462) +- Removed the custom colors that did not allow to activate the default dark mode of elastic. [5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) ### Fixed diff --git a/public/assets/images/themes/dark/linux-icon.svg b/public/assets/images/themes/dark/linux-icon.svg new file mode 100644 index 0000000000..c76c7d6328 --- /dev/null +++ b/public/assets/images/themes/dark/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/themes/dark/mac-icon.svg b/public/assets/images/themes/dark/mac-icon.svg new file mode 100644 index 0000000000..2eae996a06 --- /dev/null +++ b/public/assets/images/themes/dark/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/themes/dark/windows-icon.svg b/public/assets/images/themes/dark/windows-icon.svg new file mode 100644 index 0000000000..74d5b551f8 --- /dev/null +++ b/public/assets/images/themes/dark/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/assets/images/themes/light/linux-icon.svg b/public/assets/images/themes/light/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/public/assets/images/themes/light/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/images/themes/light/mac-icon.svg b/public/assets/images/themes/light/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/public/assets/images/themes/light/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/images/themes/light/windows-icon.svg b/public/assets/images/themes/light/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/public/assets/images/themes/light/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/controllers/register-agent/components/group-input/group-input.tsx b/public/controllers/register-agent/components/group-input/group-input.tsx index 2878738617..68f436252c 100644 --- a/public/controllers/register-agent/components/group-input/group-input.tsx +++ b/public/controllers/register-agent/components/group-input/group-input.tsx @@ -50,7 +50,6 @@ const GroupInput = ({ value, options, onChange }) => { fontWeight: 700, fontSize: '12px', lineHeight: '20px', - color: '#343741', }} > Select one or more existing groups diff --git a/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx b/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx index 0a7560c3a8..28c91f69af 100644 --- a/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx +++ b/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -70,7 +70,6 @@ const OptionalsInputs = (props: OptionalsInputsProps) => { fontWeight: 700, fontSize: '12px', lineHeight: '20px', - color: '#343741', }} > Assign an agent name diff --git a/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss index ce3cc09745..b8b985d165 100644 --- a/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss +++ b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss @@ -9,7 +9,7 @@ display: flex; flex-direction: row-reverse; align-items: center; - justify-content: center; + justify-content: left; } .checkbox-group-container.single-architecture { @@ -28,7 +28,7 @@ .checkbox-item { display: flex; flex-direction: row-reverse; - // justify-content: start; + justify-content: left; align-self: baseline; } } @@ -38,11 +38,9 @@ font-style: normal; font-weight: 400; font-size: 12px; - color: #343741; } .first-card-four-items { .checkbox-item:nth-child(n + 3) { padding-top: 16px; - justify-content: center; } } diff --git a/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx index 09043dea50..461e6e0943 100644 --- a/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx +++ b/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx @@ -34,7 +34,9 @@ const CheckboxGroupComponent: React.FC = ({ idx === 0 || idx === 2 ? ' first-of-row' : '' }`} > - {arch} + { } display='plain' hasBorder - onClick={() => {}} className='card' > {data.hr &&
} diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.scss b/public/controllers/register-agent/containers/register-agent/register-agent.scss index 6c32c5a91a..f51d4384e0 100644 --- a/public/controllers/register-agent/containers/register-agent/register-agent.scss +++ b/public/controllers/register-agent/containers/register-agent/register-agent.scss @@ -1,4 +1,4 @@ -.container { +.register-agent-wizard-container { box-sizing: border-box; min-height: 1271px; margin-top: 44px; @@ -9,18 +9,19 @@ padding-right: 63px; } -.title { +.register-agent-wizard-title { margin-top: 51px; margin-bottom: 51px; font-style: normal; font-weight: 400; font-size: 30px; line-height: 36px; - color: #343741; display: flex; justify-content: center; } -.close { + +.register-agent-wizard-close { display: flex; margin-top: 17px; -} \ No newline at end of file + float: right; +} diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.tsx index 9bdb61396a..f9d1fa499e 100644 --- a/public/controllers/register-agent/containers/register-agent/register-agent.tsx +++ b/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -39,7 +39,12 @@ interface IRegisterAgentProps { } export const RegisterAgent = withReduxProvider( - ({ getWazuhVersion, hasAgents, addNewAgent, reload }: IRegisterAgentProps) => { + ({ + getWazuhVersion, + hasAgents, + addNewAgent, + reload, + }: IRegisterAgentProps) => { const configuration = useSelector( (state: { appConfig: { data: any } }) => state.appConfig.data, ); @@ -55,9 +60,7 @@ export const RegisterAgent = withReduxProvider( const [wazuhPassword, setWazuhPassword] = useState(''); const [groups, setGroups] = useState([]); const [needsPassword, setNeedsPassword] = useState(false); - const [hideTextPassword, setHideTextPassword] = useState( - false, - ); + const [hideTextPassword, setHideTextPassword] = useState(false); const initialFields: FormConfiguration = { operatingSystemSelection: { @@ -173,14 +176,16 @@ export const RegisterAgent = withReduxProvider( - -
+ +
{hasAgents() ? ( addNewAgent(false)} iconType='cross' - > + > + Close + ) : ( -

Deploy new agent

+

+ Deploy new agent +

@@ -221,9 +228,19 @@ export const RegisterAgent = withReduxProvider( /> )} - + - reload()}>Close + reload()} + > + Close + diff --git a/public/controllers/register-agent/containers/steps/steps.scss b/public/controllers/register-agent/containers/steps/steps.scss index d18ea5c07e..b6ef8d579a 100644 --- a/public/controllers/register-agent/containers/steps/steps.scss +++ b/public/controllers/register-agent/containers/steps/steps.scss @@ -3,7 +3,6 @@ font-weight: 700; font-size: 16px; letter-spacing: 0.6px; - color: #343741; flex-direction: row; } @@ -12,7 +11,6 @@ font-weight: 400; font-size: 14px; line-height: 24px; - color: #343741; margin-bottom: 9px; } @@ -21,7 +19,6 @@ font-weight: 400; font-size: 14px; line-height: 24px; - color: #343741; margin-bottom: 20px; } diff --git a/public/controllers/register-agent/containers/steps/steps.tsx b/public/controllers/register-agent/containers/steps/steps.tsx index 95e14d2144..5e8b06f120 100644 --- a/public/controllers/register-agent/containers/steps/steps.tsx +++ b/public/controllers/register-agent/containers/steps/steps.tsx @@ -1,5 +1,5 @@ import React, { Fragment, useEffect, useState } from 'react'; -import { EuiSteps, EuiTitle } from '@elastic/eui'; +import { EuiSteps } from '@elastic/eui'; import './steps.scss'; import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; import { @@ -20,7 +20,14 @@ import CommandOutput from '../../components/command-output/command-output'; import ServerAddressTitle from '../../components/server-address/server-address-title'; import ServerAddressInput from '../../components/server-address/server-address-input'; import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; -import { getAgentCommandsStepStatus, tFormStepsStatus, getOSSelectorStepStatus, getServerAddressStepStatus, getOptionalParameterStepStatus, showCommandsSections } from '../../services/register-agent-steps-status-services'; +import { + getAgentCommandsStepStatus, + tFormStepsStatus, + getOSSelectorStepStatus, + getServerAddressStepStatus, + getOptionalParameterStepStatus, + showCommandsSections, +} from '../../services/register-agent-steps-status-services'; interface IStepsProps { needsPassword: boolean; @@ -63,8 +70,12 @@ export const Steps = ({ OPERATING_SYSTEMS_OPTIONS, ); setRegisterAgentFormValues(registerAgentFormValuesParsed); - setInstallCommandStepStatus(getAgentCommandsStepStatus(form.fields, installCommandWasCopied)) - setStartCommandStepStatus(getAgentCommandsStepStatus(form.fields, startCommandWasCopied)) + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); }, [form.fields]); const { installCommand, startCommand, selectOS, setOptionalParams } = @@ -75,9 +86,11 @@ export const Steps = ({ // install - start commands step state const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); - const [installCommandStepStatus, setInstallCommandStepStatus] = useState(getAgentCommandsStepStatus(form.fields, false)) + const [installCommandStepStatus, setInstallCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); - const [startCommandStepStatus, setStartCommandStepStatus] = useState(getAgentCommandsStepStatus(form.fields, false)) + const [startCommandStepStatus, setStartCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); useEffect(() => { if ( @@ -92,19 +105,23 @@ export const Steps = ({ }, [registerAgentFormValues]); useEffect(() => { - setInstallCommandStepStatus(getAgentCommandsStepStatus(form.fields, installCommandWasCopied)) - }, [installCommandWasCopied]) + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + }, [installCommandWasCopied]); useEffect(() => { - setStartCommandStepStatus(getAgentCommandsStepStatus(form.fields, startCommandWasCopied)) - }, [startCommandWasCopied]) + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + }, [startCommandWasCopied]); const registerAgentFormSteps = [ { title: ( - -

Select the package to download and install on your system:

-
+ + Select the package to download and install on your system: + ), children: osCard, status: getOSSelectorStepStatus(form.fields), @@ -117,11 +134,7 @@ export const Steps = ({ ...(!(!needsPassword || hideTextPassword) ? [ { - title: ( - -

Wazuh password

-
- ), + title: Wazuh password, children: ( { @@ -133,21 +146,19 @@ export const Steps = ({ ] : []), { - title: ( - -

Optional settings

-
- ), + title: Optional settings, children: , - status: getOptionalParameterStepStatus(form.fields, installCommandWasCopied, startCommandWasCopied) + status: getOptionalParameterStepStatus( + form.fields, + installCommandWasCopied, + startCommandWasCopied, + ), }, { title: ( - -

- Run the following commands to download and install the Wazuh agent: -

-
+ + Run the following commands to download and install the Wazuh agent: + ), children: ( -

Start the Wazuh agent:

- - ), + title: Start the Wazuh agent:, children: ( tr { color: #dfe5ef; } -#wz-search-filter-bar-input{ +#wz-search-filter-bar-input { box-shadow: none; } -.kuiLocalSearchInput, .kuiLocalSearchInput:focus { +.kuiLocalSearchInput, +.kuiLocalSearchInput:focus { border: 1px solid #343741 !important; background: #16171c; color: #dfe5ef; } .wzMultipleSelector .panel-primary { - border: 1px solid #343741!important; + border: 1px solid #343741 !important; -webkit-box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1) !important; box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1) !important; border-radius: 2px; @@ -239,11 +243,12 @@ table thead > tr { border-left: 1px dashed #343741; } -md-dialog.md-default-theme.md-content-overflow .md-actions, -md-dialog.md-content-overflow .md-actions, -md-dialog.md-default-theme.md-content-overflow md-dialog-actions, -md-dialog.md-content-overflow md-dialog-actions, -md-divider.md-default-theme, md-divider { +md-dialog.md-default-theme.md-content-overflow .md-actions, +md-dialog.md-content-overflow .md-actions, +md-dialog.md-default-theme.md-content-overflow md-dialog-actions, +md-dialog.md-content-overflow md-dialog-actions, +md-divider.md-default-theme, +md-divider { border-top-color: rgb(52, 55, 65); } @@ -272,18 +277,18 @@ md-divider.md-default-theme, md-divider { background-color: #0b4462; } -.CodeMirror-hints{ +.CodeMirror-hints { background-color: #16171c !important; border-color: #000; - color: #dfe5ef!important; + color: #dfe5ef !important; } -.CodeMirror-hint{ - color: #dfe5ef!important; +.CodeMirror-hint { + color: #dfe5ef !important; } -.CodeMirror-hint:hover{ - background-color: #25262E; +.CodeMirror-hint:hover { + background-color: #25262e; } .wz-input-text { @@ -293,7 +298,7 @@ md-divider.md-default-theme, md-divider { } .wz-menu { - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3)!important; + box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3) !important; } .wz-menu-select { @@ -309,29 +314,36 @@ md-divider.md-default-theme, md-divider { } .extraHeader { - border-bottom: 1px solid #2e2f34!important; + border-bottom: 1px solid #2e2f34 !important; } -.wzMultipleSelectorAdding{ - background-color: #037200!important; +.wzMultipleSelectorAdding { + background-color: #037200 !important; } -.wzMultipleSelectorRemoving{ - background-color: #990000!important; +.wzMultipleSelectorRemoving { + background-color: #990000 !important; } -.wzMultipleSelectorSelect{ +.wzMultipleSelectorSelect { background-color: #16171c; border: 1px solid rgb(52, 55, 65); } -.wz-button, .wz-button-groups, .refresh-agents-btn { - background-color: #1BA9F5 !important; - border-color: #1BA9F5 !important; +.wz-button, +.wz-button-groups, +.refresh-agents-btn { + background-color: #1ba9f5 !important; + border-color: #1ba9f5 !important; color: #000 !important; } -.wz-button-groups.active, .wz-button-groups:not([disabled]):hover, .wz-button.active, .wz-button:not([disabled]):hover, .wz-button-flat:not([disabled]):hover, .refresh-agents-btn:hover { +.wz-button-groups.active, +.wz-button-groups:not([disabled]):hover, +.wz-button.active, +.wz-button:not([disabled]):hover, +.wz-button-flat:not([disabled]):hover, +.refresh-agents-btn:hover { background-color: #0a9dec !important; border-color: #0a9dec !important; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 2px 2px -1px rgba(0, 0, 0, 0.3) !important; @@ -339,77 +351,78 @@ md-divider.md-default-theme, md-divider { } .kuiButton--hollow:hover { - color: #006E8A !important; + color: #006e8a !important; text-decoration: underline !important; } - .wz-menu-select { - filter: invert(0) !important; + filter: invert(0) !important; } -.logtest{ - border-left: 1px solid #343741!important; - box-shadow: -2px 0px 2px -1px rgba(0, 0, 0, 0.3)!important; +.logtest { + border-left: 1px solid #343741 !important; + box-shadow: -2px 0px 2px -1px rgba(0, 0, 0, 0.3) !important; background: #1a1b20; z-index: 10; } .wz-menu-left-side { - border-right: 1px solid #343741!important; - background: #1d1e24!important; + border-right: 1px solid #343741 !important; + background: #1d1e24 !important; } .wz-menu-sections { background: #1a1b20; } - -.wz-module-header-agent, .wz-module-header-nav { - border-bottom: 1px solid #343741!important; - background: #1d1e24!important; +.wz-module-header-agent, +.wz-module-header-nav { + border-bottom: 1px solid #343741 !important; + background: #1d1e24 !important; } .wz-welcome-page-agent-info { - box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), 0 1px 5px -2px rgba(0, 0, 0, 0.3)!important; - background: #1d1e24!important; + box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.3), + 0 1px 5px -2px rgba(0, 0, 0, 0.3) !important; + background: #1d1e24 !important; } -.wz-welcome-page-agent-info .wz-welcome-page-agent-info-details{ - background: #1a1b20!important; - border-bottom: 1px solid #343741!important; +.wz-welcome-page-agent-info .wz-welcome-page-agent-info-details { + background: #1a1b20 !important; + border-bottom: 1px solid #343741 !important; } .details-row { - background: #16171c!important; - border-top: 1px solid #343741!important; + background: #16171c !important; + border-top: 1px solid #343741 !important; } -.wz-inventory{ +.wz-inventory { .detail-tooltip { background-color: #16171c; } } .flyout-body .euiAccordion { - border-bottom: 1px solid #343741!important; + border-bottom: 1px solid #343741 !important; } .module-discover-table .euiTableRow.euiTableRow-isExpandedRow .euiTableRowCell { - background: #1d1e24!important; + background: #1d1e24 !important; } .module-discover-table .euiTableRow-isExpandedRow .euiTableCellContent { - background: #1d1e24!important; + background: #1d1e24 !important; } -.wz-search-bar > div > div > div.euiComboBox__inputWrap{ - background: #16171c!important; - box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.2), 0 3px 2px -2px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.1)!important; +.wz-search-bar > div > div > div.euiComboBox__inputWrap { + background: #16171c !important; + box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.2), + 0 3px 2px -2px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.1) !important; } .euiComboBoxPlaceholder { - color: #DFE5EF!important; + color: #dfe5ef !important; } svg .legend text { @@ -418,7 +431,7 @@ svg .legend text { /* welcome-agent */ -.wz-welcome-page-agent-tabs{ +.wz-welcome-page-agent-tabs { padding: 12px 16px 1px 10px; min-height: 54px; border-bottom: 1px solid #343741; @@ -436,15 +449,16 @@ svg .legend text { .wz-menu-agent-info { background-color: #1a1b20; - border-bottom: 1px solid #343741!important; + border-bottom: 1px solid #343741 !important; } .flyout-row { border: none; } -.application .euiAccordion, .flyout-body .euiAccordion { - border-bottom: 1px solid #343741!important; +.application .euiAccordion, +.flyout-body .euiAccordion { + border-bottom: 1px solid #343741 !important; } .sidepanel-infoBtnStyle { diff --git a/public/utils/assets.ts b/public/utils/assets.ts index 12d02c6029..771139a85a 100644 --- a/public/utils/assets.ts +++ b/public/utils/assets.ts @@ -1,7 +1,8 @@ import { ASSETS_BASE_URL_PREFIX } from '../../common/constants'; import { getUiSettings } from '../kibana-services'; -export const getAssetURL = (assetURL: string) => `${ASSETS_BASE_URL_PREFIX}${assetURL}`; +export const getAssetURL = (assetURL: string) => + `${ASSETS_BASE_URL_PREFIX}${assetURL}`; export const getThemeAssetURL = (asset: string, theme?: string) => { theme = theme || (getUiSettings()?.get('theme:darkMode') ? 'dark' : 'light'); From 2fc1dc5203fcc958a664b8b5ebe85ae9b012b394 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Fri, 30 Jun 2023 14:21:27 -0300 Subject: [PATCH 10/24] [Redesign add agent] Add and validate register agent commands (#5622) * Add show/hide password in command component * Add protocol and password types * Add more step status methods * Add os commands service * Resolve strings replacements in command component * Change macos packages name by arch * Add \n to the macos params * Fixed parsed macos params inside echo * Add -e in mac os install command * Remove sudo from macos install command with echo * Add sudo to linux before optional params * Fix PR review comments --- .../command-output/command-output.tsx | 55 ++++++-- .../register-agent/register-agent.tsx | 18 +-- .../register-agent/containers/steps/steps.tsx | 63 +++++---- .../core/config/os-commands-definitions.ts | 76 +++++------ .../core/register-commands/types.ts | 5 +- .../register-agent-os-commands-services.tsx | 129 ++++++++++++++++++ .../services/register-agent-services.tsx | 3 +- .../register-agent-steps-status-services.tsx | 16 +++ 8 files changed, 271 insertions(+), 94 deletions(-) create mode 100644 public/controllers/register-agent/services/register-agent-os-commands-services.tsx diff --git a/public/controllers/register-agent/components/command-output/command-output.tsx b/public/controllers/register-agent/components/command-output/command-output.tsx index 88830a66c7..dafb26d4b5 100644 --- a/public/controllers/register-agent/components/command-output/command-output.tsx +++ b/public/controllers/register-agent/components/command-output/command-output.tsx @@ -3,19 +3,22 @@ import { EuiCopy, EuiIcon, EuiSpacer, + EuiSwitch, + EuiSwitchEvent, EuiText, } from '@elastic/eui'; -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; interface ICommandSectionProps { commandText: string; showCommand: boolean; onCopy: () => void; os?: 'WINDOWS' | string; + password?: string; } export default function CommandOutput(props: ICommandSectionProps) { - const { commandText, showCommand, onCopy, os } = props; + const { commandText, showCommand, onCopy, os, password } = props; const getHighlightCodeLanguage = (os: 'WINDOWS' | string) => { if (os.toLowerCase() === 'windows') { return 'powershell'; @@ -23,15 +26,45 @@ export default function CommandOutput(props: ICommandSectionProps) { return 'bash'; } }; - - const [language, setLanguage] = useState(getHighlightCodeLanguage(os || '')); + const [havePassword, setHavePassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); const onHandleCopy = (command: any) => { onCopy && onCopy(); - return command + return command; // the return is needed to avoid a bug in EuiCopy }; - const [commandToCopy, setCommandToCopy] = useState(commandText); + const [commandToShow, setCommandToShow] = useState(commandText); + + useEffect(() => { + if (password) { + setHavePassword(true); + osdfucatePassword(password); + } else { + setHavePassword(false); + setCommandToShow(commandText); + } + }, [password, commandText, showPassword]) + + const osdfucatePassword = (password: string) => { + if(!password) return; + if(!commandText) return; + + if(showPassword){ + setCommandToShow(commandText); + }else{ + // search password in commandText and replace with * for every character + const findPassword = commandText.search(password); + if (findPassword > -1) { + let command = commandText; + setCommandToShow(command.replace(/WAZUH_REGISTRATION_PASSWORD='([^']+)'/,`WAZUH_REGISTRATION_PASSWORD='${'*'.repeat(password.length)}'`)); + } + } + } + + const onChangeShowPassword = (event: EuiSwitchEvent) => { + setShowPassword(event.target.checked); + } return ( @@ -44,12 +77,15 @@ export default function CommandOutput(props: ICommandSectionProps) { }} language={getHighlightCodeLanguage(os || '')} > - {showCommand ? commandText : ''} + {showCommand ? commandToShow : ''} {showCommand && ( - {commandToCopy => ( -
onHandleCopy(commandToCopy)}> + {copy => ( +
onHandleCopy(copy())} + >

Copy command

@@ -59,6 +95,7 @@ export default function CommandOutput(props: ICommandSectionProps) { )}
+ {showCommand && havePassword ? : null} ); diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.tsx index f9d1fa499e..8ae23213cd 100644 --- a/public/controllers/register-agent/containers/register-agent/register-agent.tsx +++ b/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -48,19 +48,14 @@ export const RegisterAgent = withReduxProvider( const configuration = useSelector( (state: { appConfig: { data: any } }) => state.appConfig.data, ); - const [wazuhVersion, setWazuhVersion] = useState(''); const [haveUdpProtocol, setHaveUdpProtocol] = useState( false, ); - const [haveConnectionSecure, setHaveConnectionSecure] = useState< - boolean | null - >(false); const [loading, setLoading] = useState(false); const [wazuhPassword, setWazuhPassword] = useState(''); const [groups, setGroups] = useState([]); const [needsPassword, setNeedsPassword] = useState(false); - const [hideTextPassword, setHideTextPassword] = useState(false); const initialFields: FormConfiguration = { operatingSystemSelection: { @@ -102,7 +97,6 @@ export const RegisterAgent = withReduxProvider( const remoteConfig = await getMasterRemoteConfiguration(); if (remoteConfig) { setHaveUdpProtocol(remoteConfig.isUdp); - setHaveConnectionSecure(remoteConfig.haveSecureConnection); } }; @@ -123,23 +117,19 @@ export const RegisterAgent = withReduxProvider( const fetchData = async () => { try { const wazuhVersion = await getWazuhVersion(); - let wazuhPassword = ''; - let hideTextPassword = false; await getRemoteConfig(); const authInfo = await getAuthInfo(); + // get wazuh password configuration + let wazuhPassword = ''; const needsPassword = (authInfo.auth || {}).use_password === 'yes'; if (needsPassword) { wazuhPassword = configuration['enrollment.password'] || authInfo['authd.pass'] || ''; - if (wazuhPassword) { - hideTextPassword = true; - } } const groups = await getGroups(); setNeedsPassword(needsPassword); - setHideTextPassword(hideTextPassword); setWazuhPassword(wazuhPassword); setWazuhVersion(wazuhVersion); setGroups(groups); @@ -218,13 +208,11 @@ export const RegisterAgent = withReduxProvider( )} diff --git a/public/controllers/register-agent/containers/steps/steps.tsx b/public/controllers/register-agent/containers/steps/steps.tsx index 5e8b06f120..da865cf9de 100644 --- a/public/controllers/register-agent/containers/steps/steps.tsx +++ b/public/controllers/register-agent/containers/steps/steps.tsx @@ -1,5 +1,5 @@ import React, { Fragment, useEffect, useState } from 'react'; -import { EuiSteps } from '@elastic/eui'; +import { EuiCallOut, EuiLink, EuiSteps, EuiTitle } from '@elastic/eui'; import './steps.scss'; import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; import { @@ -27,47 +27,49 @@ import { getServerAddressStepStatus, getOptionalParameterStepStatus, showCommandsSections, + getPasswordStepStatus, } from '../../services/register-agent-steps-status-services'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; interface IStepsProps { needsPassword: boolean; - hideTextPassword: boolean; form: UseFormReturn; osCard: React.ReactElement; connection: { isUDP: boolean; - isSecure: boolean; }; wazuhPassword: string; } export const Steps = ({ needsPassword, - hideTextPassword, form, osCard, connection, wazuhPassword, }: IStepsProps) => { + const initialParsedFormValues = { + operatingSystem: { + name: '', + architecture: '', + }, + optionalParams: { + agentGroups: '', + agentName: '', + serverAddress: '', + wazuhPassword, + protocol: connection.isUDP ? 'UDP' : '', + }, + } as IParseRegisterFormValues; const [registerAgentFormValues, setRegisterAgentFormValues] = - useState({ - operatingSystem: { - name: '', - architecture: '', - }, - optionalParams: { - agentGroups: '', - agentName: '', - serverAddress: '', - wazuhPassword, - }, - }); + useState(initialParsedFormValues); useEffect(() => { // get form values and parse them divided in OS and optional params const registerAgentFormValuesParsed = parseRegisterAgentFormValues( getRegisterAgentFormValues(form), OPERATING_SYSTEMS_OPTIONS, + initialParsedFormValues, ); setRegisterAgentFormValues(registerAgentFormValuesParsed); setInstallCommandStepStatus( @@ -99,7 +101,7 @@ export const Steps = ({ ) { selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); } - setOptionalParams(registerAgentFormValues.optionalParams); + setOptionalParams({ ...registerAgentFormValues.optionalParams }); setInstallCommandWasCopied(false); setStartCommandWasCopied(false); }, [registerAgentFormValues]); @@ -131,17 +133,32 @@ export const Steps = ({ children: , status: getServerAddressStepStatus(form.fields), }, - ...(!(!needsPassword || hideTextPassword) + ...(needsPassword && !wazuhPassword ? [ { title: Wazuh password, children: ( - - { - 'No ha establecido una contraseña. Se le asigno una por defecto' + + The Wazuh password is required but wasn't defined. Please check our{' '} + + steps + + } - + iconType='iInCircle' + className='warningForAgentName' + /> ), + status: getPasswordStepStatus(form.fields), }, ] : []), @@ -151,7 +168,6 @@ export const Steps = ({ status: getOptionalParameterStepStatus( form.fields, installCommandWasCopied, - startCommandWasCopied, ), }, { @@ -166,6 +182,7 @@ export const Steps = ({ showCommand={showCommandsSections(form.fields)} os={registerAgentFormValues.operatingSystem.name} onCopy={() => setInstallCommandWasCopied(true)} + password={registerAgentFormValues.optionalParams.wazuhPassword} /> ), status: installCommandStepStatus, diff --git a/public/controllers/register-agent/core/config/os-commands-definitions.ts b/public/controllers/register-agent/core/config/os-commands-definitions.ts index 73a370f917..562b487e1a 100644 --- a/public/controllers/register-agent/core/config/os-commands-definitions.ts +++ b/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -1,3 +1,4 @@ +import { getLinuxDEBInstallCommand, getLinuxRPMInstallCommand, getLinuxStartCommand, getMacOsInstallCommand, getMacosStartCommand, getWindowsInstallCommand, getWindowsStartCommand } from '../../services/register-agent-os-commands-services'; import { IOSDefinition, tOptionalParams } from '../register-commands/types'; // Defined OS combinations @@ -54,7 +55,8 @@ export type tOptionalParameters = | 'serverAddress' | 'agentName' | 'agentGroups' - | 'wazuhPassword'; + | 'wazuhPassword' + | 'protocol'; /////////////////////////////////////////////////////////////////// /// Operating system commands definitions @@ -66,34 +68,30 @@ const linuxDefinition: IOSDefinition = { { architecture: 'DEB amd64', urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, - installCommand: props => - `sudo yum install -y ${props.urlPackage} ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo systemctl start wazuh-agent`, + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.deb`, + installCommand: props => getLinuxDEBInstallCommand(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_${props.architecture}`, - installCommand: props => - `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo systemctl start wazuh-agent`, + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_aarch64.deb`, + installCommand: props => getLinuxDEBInstallCommand(props), + startCommand: props => getLinuxStartCommand(props), }, { architecture: 'RPM amd64', urlPackage: props => - `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, - installCommand: props => - `sudo yum install -y ${props.urlPackage} ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo systemctl start wazuh-agent`, + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, + installCommand: props => getLinuxRPMInstallCommand(props), + startCommand: props => getLinuxStartCommand(props), }, { architecture: 'RPM aarch64', urlPackage: props => - `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64`, - installCommand: props => - `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo systemctl start wazuh-agent`, + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.aarch64.rpm`, + installCommand: props => getLinuxRPMInstallCommand(props), + startCommand: props => getLinuxStartCommand(props), }, ], }; @@ -104,12 +102,9 @@ const windowsDefinition: IOSDefinition = { { architecture: 'MSI 32/64', urlPackage: props => - `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1`, - installCommand: props => - `Invoke-WebRequest -Uri ${ - props.urlPackage - } -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${props.optionals?.serverAddress || ''} ${props.optionals?.agentName || ''} ${props.optionals?.agentGroups || ''} ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `Start-Service -Name wazuh-agent`, + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.msi`, + installCommand: props => getWindowsInstallCommand(props), + startCommand: props => getWindowsStartCommand(props), }, ], }; @@ -120,30 +115,16 @@ const macDefinition: IOSDefinition = { { architecture: 'Intel', urlPackage: props => - `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1`, - installCommand: props => - `mac -so wazuh-agent.pkg ${ - props.urlPackage - } && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target / - ${props.optionals?.serverAddress || ''} - ${props.optionals?.agentName || ''} - ${props.optionals?.agentGroups || ''} - ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + `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`, - installCommand: props => - `mac -so wazuh-agent.pkg ${ - props.urlPackage - } && sudo launchctl setenv && sudo installer -pkg ./wazuh-agent.pkg -target - ${props.optionals?.serverAddress || ''} - ${props.optionals?.agentName || ''} - ${props.optionals?.agentGroups || ''} - ${props.optionals?.wazuhPassword || ''}`, - startCommand: props => `sudo /Library/Ossec/bin/wazuh-control start`, + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.arm64.pkg`, + installCommand: props => getMacOsInstallCommand(props), + startCommand: props => getMacosStartCommand(props), }, ], }; @@ -184,8 +165,15 @@ export const optionalParamsDefinitions: tOptionalParams = { return parsedValue ? `${property}='${parsedValue}'` : ''; }, }, + protocol: { + property: 'WAZUH_PROTOCOL', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, wazuhPassword: { - property: 'WAZUH_PASSWORD', + property: 'WAZUH_REGISTRATION_PASSWORD', getParamCommand: props => { const { property, value } = props; return value !== '' ? `${property}='${value}'` : ''; diff --git a/public/controllers/register-agent/core/register-commands/types.ts b/public/controllers/register-agent/core/register-commands/types.ts index ebce4b3dfd..243477a999 100644 --- a/public/controllers/register-agent/core/register-commands/types.ts +++ b/public/controllers/register-agent/core/register-commands/types.ts @@ -24,12 +24,13 @@ interface IOptionalParamsWithValues { } -type tOSEntryProps = IOSProps & IOptionalParamsWithValues; +export type tOSEntryProps = IOSProps & IOptionalParamsWithValues; +export type tOSEntryInstallCommand = tOSEntryProps & { urlPackage: string }; export interface IOSCommandsDefinition { architecture: OS['architecture']; urlPackage: (props: tOSEntryProps) => string; - installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + installCommand: (props: tOSEntryInstallCommand) => string; startCommand: (props: tOSEntryProps) => string; } diff --git a/public/controllers/register-agent/services/register-agent-os-commands-services.tsx b/public/controllers/register-agent/services/register-agent-os-commands-services.tsx new file mode 100644 index 0000000000..818a91945a --- /dev/null +++ b/public/controllers/register-agent/services/register-agent-os-commands-services.tsx @@ -0,0 +1,129 @@ +import { tOptionalParameters } from '../core/config/os-commands-definitions'; +import { + IOptionalParameters, + tOSEntryInstallCommand, + tOSEntryProps, +} from '../core/register-commands/types'; +import { tOperatingSystem } from '../hooks/use-register-agent-commands.test'; + +const getAllOptionals = ( + optionals: IOptionalParameters, + osName?: tOperatingSystem['name'], +) => { + // create paramNameOrderList, which is an array of the keys of optionals add interface + const paramNameOrderList: (keyof IOptionalParameters)[] = + ['serverAddress', 'wazuhPassword', 'agentGroups', 'agentName', 'protocol']; + + if (!optionals) return ''; + let paramsText = Object.entries(paramNameOrderList).reduce( + (acc, [key, value]) => { + if (optionals[value]) { + acc += `${optionals[value]} `; + } + return acc; + }, + '', + ); + + if (osName && osName.toLowerCase() === 'windows' && optionals.serverAddress) { + // when os is windows we must to add wazuh registration server with server address + paramsText = + paramsText + `WAZUH_REGISTRATION_SERVER=${optionals.serverAddress.replace('WAZUH_MANAGER=','')} `; + } + + return paramsText; +}; + +const getAllOptionalsMacos = ( + optionals: IOptionalParameters +) => { + // create paramNameOrderList, which is an array of the keys of optionals add interface + const paramNameOrderList: (keyof IOptionalParameters)[] = + ['serverAddress', 'wazuhPassword', 'agentGroups', 'agentName', 'protocol']; + + if (!optionals) return ''; + return Object.entries(paramNameOrderList).reduce( + (acc, [key, value]) => { + if (optionals[value]) { + acc += `${optionals[value]}\\n`; + } + return acc; + }, + '', + ); +}; + +/******* Linux *******/ + + +// Install command +export const getLinuxRPMInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage } = props; + return `sudo ${ + optionals && getAllOptionals(optionals) + }yum install -y ${urlPackage}`; +}; + +export const getLinuxDEBInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage } = props; + return `curl -so wazuh-agent.deb ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }dpkg -i ./wazuh-agent.deb`; +}; + +// Start command +export const getLinuxStartCommand = ( + _props: tOSEntryProps, +) => { + return `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; +}; + +/******** Windows ********/ + +export const getWindowsInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, name } = props; + return `Invoke-WebRequest -Uri ${urlPackage} -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${ + optionals && getAllOptionals(optionals, name) + }`; +}; + +export const getWindowsStartCommand = ( + _props: tOSEntryProps, +) => { + return `NET START WazuhSvc`; +}; + +/******** MacOS ********/ + +export const getMacOsInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage } = props; + // Set macOS installation script with environment variables + const optionalsText = optionals && getAllOptionalsMacos(optionals); + const macOSInstallationOptions = `${optionalsText}` + .replace(/\' ([a-zA-Z])/g, "' && $1") // Separate environment variables with && + .replace(/\"/g, '\\"') // Escape double quotes + .trim(); + + // If no variables are set, the echo will be empty + const macOSInstallationSetEnvVariablesScript = macOSInstallationOptions + ? `echo -e "${macOSInstallationOptions}" > /tmp/wazuh_envs && ` + : ``; + + // Merge environment variables with installation script + const macOSInstallationScript = `curl -so wazuh-agent.pkg ${urlPackage} && ${macOSInstallationSetEnvVariablesScript}sudo installer -pkg ./wazuh-agent.pkg -target /`; + return macOSInstallationScript; +}; + +export const getMacosStartCommand = ( + _props: tOSEntryProps, +) => { + return `sudo /Library/Ossec/bin/wazuh-control start`; +}; diff --git a/public/controllers/register-agent/services/register-agent-services.tsx b/public/controllers/register-agent/services/register-agent-services.tsx index ed32979bab..8200224bb2 100644 --- a/public/controllers/register-agent/services/register-agent-services.tsx +++ b/public/controllers/register-agent/services/register-agent-services.tsx @@ -260,9 +260,10 @@ export interface IParseRegisterFormValues { export const parseRegisterAgentFormValues = ( formValues: { name: keyof UseFormReturn['fields']; value: any }[], OSOptionsDefined: RegisterAgentData[], + initialValues?: IParseRegisterFormValues ) => { // return the values form the formFields and the value property - const parsedForm = { + const parsedForm = initialValues || { operatingSystem: { architecture: '', name: '', diff --git a/public/controllers/register-agent/services/register-agent-steps-status-services.tsx b/public/controllers/register-agent/services/register-agent-steps-status-services.tsx index 7065d38290..90e7ca64ae 100644 --- a/public/controllers/register-agent/services/register-agent-steps-status-services.tsx +++ b/public/controllers/register-agent/services/register-agent-steps-status-services.tsx @@ -143,3 +143,19 @@ export const getOptionalParameterStepStatus = ( return 'current'; } }; + + +export const getPasswordStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error || + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'disabled'; + }else{ + return 'complete'; + } +} \ No newline at end of file From b92c8c5220620483b4321a76b024ae81f966fc31 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Mon, 3 Jul 2023 10:34:09 -0300 Subject: [PATCH 11/24] Fixed imports in tests --- .../checkbox-group/checkbox-group.test.tsx | 2 +- .../os-selector/os-card/os-card.test.tsx | 2 +- .../register-agent/register-agent.test.tsx | 20 ------------------- 3 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx index e2dec80399..7a9e9e79a2 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; describe('CheckboxGroupComponent', () => { const data = ['Option 1', 'Option 2', 'Option 3']; diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx index ebb927853d..f8f379e20c 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { OsCard } from '../../step-one/os-card/os-card'; +import { OsCard } from '../os-card/os-card'; describe('OsCard', () => { test('renders three cards with different titles', () => { diff --git a/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx deleted file mode 100644 index 5f4dd452be..0000000000 --- a/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; -import { RegisterAgent } from './register-agent'; - -describe('RegisterAgent', () => { - test('renders the component', () => { - const mockHasAgents = jest.fn(); - - render(); - - // Verifica que el título esté presente - const titleElement = screen.getByText('Deploy new agent'); - expect(titleElement).toBeInTheDocument(); - - // Verifica que el componente InputForm esté presente - const component = screen.getByTestId('os-card'); - expect(component).toBeInTheDocument(); - }); -}); From 537fb6870db3438b3f791d256dbe9fd2b1a68857 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Mon, 3 Jul 2023 10:57:59 -0300 Subject: [PATCH 12/24] Fix components unit tests --- .../checkbox-group/checkbox-group.test.tsx | 8 +++--- .../os-selector/os-card/os-card.test.tsx | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx index 7a9e9e79a2..d42289ff98 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx @@ -22,9 +22,9 @@ describe('CheckboxGroupComponent', () => { const checkboxItems = screen.getAllByRole('radio'); expect(checkboxItems).toHaveLength(data.length); - expect(checkboxItems[0]).toHaveAttribute('id', 'option-0-0'); - expect(checkboxItems[1]).toHaveAttribute('id', 'option-0-1'); - expect(checkboxItems[2]).toHaveAttribute('id', 'option-0-2'); + expect(checkboxItems[0]).toHaveAttribute('id', 'Option 1'); + expect(checkboxItems[1]).toHaveAttribute('id', 'Option 2'); + expect(checkboxItems[2]).toHaveAttribute('id', 'Option 3'); expect(checkboxItems[0]).toBeChecked(); expect(checkboxItems[1]).not.toBeChecked(); @@ -52,7 +52,7 @@ describe('CheckboxGroupComponent', () => { expect(onOptionChange).toHaveBeenCalledTimes(1); expect(onOptionChange).toHaveBeenCalledWith( expect.objectContaining({ - target: { value: `option-${cardIndex}-1` }, + target: { value: `Option ${cardIndex}` }, }), ); }); diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx index f8f379e20c..59b0657948 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx @@ -3,6 +3,31 @@ import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { OsCard } from '../os-card/os-card'; +jest.mock('../../../../../kibana-services', () => ({ + ...(jest.requireActual('../../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), +})); + describe('OsCard', () => { test('renders three cards with different titles', () => { render(); From 7617c96fadfd74506aad24d570701bdb03d974a4 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Mon, 3 Jul 2023 11:23:40 -0300 Subject: [PATCH 13/24] Fix unit test checkbox group component --- .../os-selector/checkbox-group/checkbox-group.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx index d42289ff98..186fc0d240 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx @@ -6,7 +6,7 @@ import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; describe('CheckboxGroupComponent', () => { const data = ['Option 1', 'Option 2', 'Option 3']; const cardIndex = 0; - const selectedOption = 'option-0-0'; + const selectedOption = 'Option 1'; const onOptionChange = jest.fn(); test('renders checkbox items with correct labels', () => { @@ -52,7 +52,7 @@ describe('CheckboxGroupComponent', () => { expect(onOptionChange).toHaveBeenCalledTimes(1); expect(onOptionChange).toHaveBeenCalledWith( expect.objectContaining({ - target: { value: `Option ${cardIndex}` }, + target: { value: `Option 2` }, }), ); }); From 3d187b170d54294d6e054425ae25228326fa4d14 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Mon, 3 Jul 2023 11:59:34 -0300 Subject: [PATCH 14/24] Fix os card unit test with mock uiSettings --- .../components/os-selector/os-card/os-card.test.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx index 59b0657948..d01a27c141 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx @@ -26,6 +26,11 @@ jest.mock('../../../../../kibana-services', () => ({ return; }, }), + getUiSettings: jest.fn().mockReturnValue({ + get: (name) => { + return true; + }, + }), })); describe('OsCard', () => { From bade293e280bf78657a636eac0ff02abf85e845d Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 4 Jul 2023 12:00:58 -0300 Subject: [PATCH 15/24] modify the fqdn regex because it interferes with an ipv4 instance --- .../public/controllers/register-agent/utils/validations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/controllers/register-agent/utils/validations.tsx b/plugins/main/public/controllers/register-agent/utils/validations.tsx index c3eab3de31..00fb93410f 100644 --- a/plugins/main/public/controllers/register-agent/utils/validations.tsx +++ b/plugins/main/public/controllers/register-agent/utils/validations.tsx @@ -8,7 +8,7 @@ // Minimum 3 labels export const validateServerAddress = (value: any) => { const isFQDN = - /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; + /^(?!-)(?!.*--)(?!.*\d$)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; const isIP = /^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$/; From d0b89f445532dcfa9477f658da6616be11cf4ac6 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> Date: Fri, 7 Jul 2023 07:00:39 -0300 Subject: [PATCH 16/24] [Redesign add page] Add form status callout message (#5634) * Add form status manager and unit tests * Add empty and invalid fields messages * Hide commands code block when exists warning messages * Fix fields names in warning messages * Updated CHANGELOG --- CHANGELOG.md | 2 + .../register-agent/containers/steps/steps.tsx | 85 ++++++++-- .../services/form-status-manager.test.tsx | 145 ++++++++++++++++++ .../services/form-status-manager.tsx | 116 ++++++++++++++ .../register-agent-steps-status-services.tsx | 77 +++++++--- 5 files changed, 392 insertions(+), 33 deletions(-) create mode 100644 plugins/main/public/controllers/register-agent/services/form-status-manager.test.tsx create mode 100644 plugins/main/public/controllers/register-agent/services/form-status-manager.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e32db3b0..276f72d427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All notable changes to the Wazuh app project will be documented in this file. - Added new CLI to generate API data from specification file [#5519](https://github.com/wazuh/wazuh-kibana-app/pull/5519) - Added specific RBAC permissions to Security section [#5551](https://github.com/wazuh/wazuh-kibana-app/pull/5551) - Added development so that the images of the new agent deployment page also have dark mode. [#5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) +- Added register agent form status callout message [#5634](https://github.com/wazuh/wazuh-kibana-app/pull/5634) + ### Changed diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx index da865cf9de..26140391e9 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx @@ -28,6 +28,10 @@ import { getOptionalParameterStepStatus, showCommandsSections, getPasswordStepStatus, + getIncompleteSteps, + getInvalidFields, + tFormFieldsLabel, + tFormStepsLabel, } from '../../services/register-agent-steps-status-services'; import { webDocumentationLink } from '../../../../../common/services/web_documentation'; @@ -61,9 +65,17 @@ export const Steps = ({ protocol: connection.isUDP ? 'UDP' : '', }, } as IParseRegisterFormValues; + const [missingStepsName, setMissingStepsName] = useState( + [], + ); + const [invalidFieldsName, setInvalidFieldsName] = useState< + tFormFieldsLabel[] + >([]); const [registerAgentFormValues, setRegisterAgentFormValues] = useState(initialParsedFormValues); + const FORM_MESSAGE_CONJUNTION = ' and '; + useEffect(() => { // get form values and parse them divided in OS and optional params const registerAgentFormValuesParsed = parseRegisterAgentFormValues( @@ -78,6 +90,8 @@ export const Steps = ({ setStartCommandStepStatus( getAgentCommandsStepStatus(form.fields, startCommandWasCopied), ); + setMissingStepsName(getIncompleteSteps(form.fields) || []); + setInvalidFieldsName(getInvalidFields(form.fields) || []); }, [form.fields]); const { installCommand, startCommand, selectOS, setOptionalParams } = @@ -142,7 +156,8 @@ export const Steps = ({ color='warning' title={ - The Wazuh password is required but wasn't defined. Please check our{' '} + The Wazuh password is required but wasn't defined. Please + check our{' '} ), children: ( - setInstallCommandWasCopied(true)} - password={registerAgentFormValues.optionalParams.wazuhPassword} - /> + <> + {missingStepsName?.length ? ( + + ) : null} + {invalidFieldsName?.length ? ( + + ) : null} + {!missingStepsName?.length && !invalidFieldsName?.length ? ( + setInstallCommandWasCopied(true)} + password={registerAgentFormValues.optionalParams.wazuhPassword} + /> + ) : null} + ), status: installCommandStepStatus, }, { title: Start the Wazuh agent:, children: ( - setStartCommandWasCopied(true)} - /> + <> + {missingStepsName?.length ? ( + + ) : null} + {invalidFieldsName?.length ? ( + + ) : null} + {!missingStepsName?.length && !invalidFieldsName?.length ? ( + setStartCommandWasCopied(true)} + /> + ) : null} + ), status: startCommandStepStatus, }, diff --git a/plugins/main/public/controllers/register-agent/services/form-status-manager.test.tsx b/plugins/main/public/controllers/register-agent/services/form-status-manager.test.tsx new file mode 100644 index 0000000000..db512362f5 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/services/form-status-manager.test.tsx @@ -0,0 +1,145 @@ +import { + EnhancedFieldConfiguration, + UseFormReturn, +} from '../../../components/common/form/types'; +import { + FormStepsDependencies, + RegisterAgentFormStatusManager, +} from './form-status-manager'; + +const defaultFormFieldData: EnhancedFieldConfiguration = { + changed: true, + value: 'value1', + error: '', + currentValue: '', + initialValue: '', + type: 'text', + onChange: () => { + console.log('onChange'); + }, + setInputRef: () => { + console.log('setInputRef'); + }, + inputRef: null, +}; + +const formFieldsDefault: UseFormReturn['fields'] = { + field1: { + ...defaultFormFieldData, + value: '', + error: null, + }, + field2: { + ...defaultFormFieldData, + value: '', + error: 'error message', + }, + field3: { + ...defaultFormFieldData, + value: 'value valid', + error: null, + }, +}; + +describe('RegisterAgentFormStatusManager', () => { + it('should create a instance', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + }); + + it('should return the form status', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + const formStatus = registerAgentFormStatusManager.getFormStatus(); + expect(formStatus).toEqual({ + field1: 'empty', + field2: 'invalid', + field3: 'complete', + }); + }); + + it('should return the field status', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + const fieldStatus = registerAgentFormStatusManager.getFieldStatus('field1'); + expect(fieldStatus).toEqual('empty'); + }); + + it('should return error if fieldname not found', () => { + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + ); + expect(() => + registerAgentFormStatusManager.getFieldStatus('field4'), + ).toThrowError('Fieldname not found'); + }); + + it('should return a INVALID when the step have an error', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1', 'field2'], + step2: ['field3'], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + 'invalid', + ); + }); + + it('should return COMPLETE when the step have no errors and is not empty', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1', 'field2'], + step2: ['field3'], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step2')).toEqual( + 'complete', + ); + }); + + it('should return EMPTY when the step all fields empty', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: [ 'field2', + 'field3' ], + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getStepStatus('step1')).toEqual( + 'empty', + ); + }); + + it('should return all the steps status', () => { + const formSteps: FormStepsDependencies = { + step1: ['field1'], + step2: [ 'field2', + 'field3' ], + step3: ['field3'] + }; + const registerAgentFormStatusManager = new RegisterAgentFormStatusManager( + formFieldsDefault, + formSteps, + ); + expect(registerAgentFormStatusManager).toBeDefined(); + expect(registerAgentFormStatusManager.getFormStepsStatus()).toEqual({ + step1: 'empty', + step2: 'invalid', + step3: 'complete' + }); + }); +}); diff --git a/plugins/main/public/controllers/register-agent/services/form-status-manager.tsx b/plugins/main/public/controllers/register-agent/services/form-status-manager.tsx new file mode 100644 index 0000000000..a250e2d413 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/services/form-status-manager.tsx @@ -0,0 +1,116 @@ +import { UseFormReturn } from '../../../components/common/form/types'; + +type FieldStatus = 'invalid' | 'empty' | 'complete'; +type FormStatus = { + [key: string]: FieldStatus; +}; + +type FormFields = UseFormReturn['fields']; +type FormFieldName = keyof FormFields; + +export type FormStepsDependencies = { + [key: string]: FormFieldName[]; +}; + +type FormStepsStatus = { + [key: string]: FieldStatus; +}; + +interface FormFieldsStatusManager { + getFieldStatus: (fieldname: FormFieldName) => FieldStatus; + getFormStatus: () => FormStatus; + getStepStatus: (stepName: string) => FieldStatus; + getFormStepsStatus: () => FormStepsStatus; +} + +export class RegisterAgentFormStatusManager implements FormFieldsStatusManager { + constructor( + private formFields: FormFields, + private formSteps?: FormStepsDependencies, + ) {} + + getFieldStatus = (fieldname: FormFieldName): FieldStatus => { + const field = this.formFields[fieldname]; + if (!field) { + throw Error('Fieldname not found'); + } + + if (field.error) { + return 'invalid'; + } + + if (field.value?.length === 0) { + return 'empty'; + } + + return 'complete'; + }; + + getFormStatus = (): FormStatus => { + const fieldNames = Object.keys(this.formFields); + const formStatus: FormStatus | object = {}; + + fieldNames.forEach((fieldName: string) => { + formStatus[fieldName] = this.getFieldStatus(fieldName); + }); + + return formStatus as FormStatus; + }; + + getStepStatus = (stepName: string): FieldStatus => { + if (!this.formSteps) { + throw Error('Form steps not defined'); + } + const stepFields = this.formSteps[stepName]; + if (!stepFields) { + throw Error('Step name not found'); + } + + const formStepStatus: FormStepsStatus | object = {}; + stepFields.forEach((fieldName: FormFieldName) => { + formStepStatus[fieldName] = this.getFieldStatus(fieldName); + }); + + const stepStatus = Object.values(formStepStatus); + + // if any is invalid + if (stepStatus.includes('invalid')) { + return 'invalid'; + } else if (stepStatus.includes('empty')) { + // if all are empty + return 'empty'; + } else { + // if all are complete + return 'complete'; + } + }; + + getFormStepsStatus = (): FormStepsStatus => { + if (!this.formSteps) { + throw Error('Form steps not defined'); + } + + const formStepsStatus: FormStepsStatus | object = {}; + Object.keys(this.formSteps).forEach((stepName: string) => { + formStepsStatus[stepName] = this.getStepStatus(stepName); + }); + + return formStepsStatus as FormStepsStatus; + }; + + getIncompleteSteps = (): string[] => { + const formStepsStatus = this.getFormStepsStatus(); + const notCompleteSteps = Object.entries(formStepsStatus).filter( + ([ _, status ]) => status === 'empty', + ); + return notCompleteSteps.map(( [ stepName, _]) => stepName); + }; + + getInvalidFields = (): string[] => { + const formStatus = this.getFormStatus(); + const invalidFields = Object.entries(formStatus).filter( + ([ _, status ]) => status === 'invalid', + ); + return invalidFields.map(([ fieldName, _ ]) => fieldName); + } +} diff --git a/plugins/main/public/controllers/register-agent/services/register-agent-steps-status-services.tsx b/plugins/main/public/controllers/register-agent/services/register-agent-steps-status-services.tsx index 90e7ca64ae..0136b0ec2b 100644 --- a/plugins/main/public/controllers/register-agent/services/register-agent-steps-status-services.tsx +++ b/plugins/main/public/controllers/register-agent/services/register-agent-steps-status-services.tsx @@ -1,5 +1,9 @@ import { EuiStepStatus } from '@elastic/eui'; import { UseFormReturn } from '../../../components/common/form/types'; +import { + FormStepsDependencies, + RegisterAgentFormStatusManager, +} from './form-status-manager'; const fieldsHaveErrors = ( fieldsToCheck: string[], @@ -19,7 +23,7 @@ const fieldsHaveErrors = ( return haveError; }; -const anyFieldIsComplete = ( +const fieldsAreEmpty = ( fieldsToCheck: string[], formFields: UseFormReturn['fields'], ) => { @@ -31,18 +35,13 @@ const anyFieldIsComplete = ( throw Error('fields to check are not defined in formFields'); } - if (fieldsHaveErrors(fieldsToCheck, formFields)) { - return false; - } - - if (fieldsAreEmpty(fieldsToCheck, formFields)) { - return false; - } - - return true; + const notEmpty = fieldsToCheck.some(key => { + return formFields[key]?.value?.length > 0; + }); + return !notEmpty; }; -const fieldsAreEmpty = ( +const anyFieldIsComplete = ( fieldsToCheck: string[], formFields: UseFormReturn['fields'], ) => { @@ -54,12 +53,18 @@ const fieldsAreEmpty = ( throw Error('fields to check are not defined in formFields'); } - const notEmpty = fieldsToCheck.some(key => { - return formFields[key]?.value?.length > 0; - }); - return !notEmpty; + if (fieldsHaveErrors(fieldsToCheck, formFields)) { + return false; + } + + if (fieldsAreEmpty(fieldsToCheck, formFields)) { + return false; + } + + return true; }; + export const showCommandsSections = ( formFields: UseFormReturn['fields'], ): boolean => { @@ -144,8 +149,7 @@ export const getOptionalParameterStepStatus = ( } }; - -export const getPasswordStepStatus = ( +export const getPasswordStepStatus = ( formFields: UseFormReturn['fields'], ): tFormStepsStatus => { if ( @@ -155,7 +159,42 @@ export const getPasswordStepStatus = ( formFields.serverAddress.error ) { return 'disabled'; - }else{ + } else { return 'complete'; } -} \ No newline at end of file +}; + +export enum tFormStepsLabel { + operatingSystemSelection = 'operating system', + serverAddress = 'server address', +} + +export const getIncompleteSteps = ( + formFields: UseFormReturn['fields'], +): tFormStepsLabel[] => { + const steps: FormStepsDependencies = { + operatingSystemSelection: ['operatingSystemSelection'], + serverAddress: ['serverAddress'], + }; + const statusManager = new RegisterAgentFormStatusManager(formFields, steps); + // replace fields array using label names + return statusManager.getIncompleteSteps().map(field => { + return tFormStepsLabel[field] || field; + }); +}; + +export enum tFormFieldsLabel { + agentName = 'agent name', + agentGroups = 'agent groups', + serverAddress = 'server address', +} + +export const getInvalidFields = ( + formFields: UseFormReturn['fields'], +): tFormFieldsLabel[] => { + const statusManager = new RegisterAgentFormStatusManager(formFields); + + return statusManager.getInvalidFields().map(field => { + return tFormFieldsLabel[field] || field; + }); +}; From be7b1cd42f635e4488159e83b9d90c4065d85226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:52:15 -0300 Subject: [PATCH 17/24] Step 2: the design triggers warnings (#5649) * changing design to remove console warnings * update changelog * Changes in the display of pop-up windows in the agents log section * semicolon is added --- CHANGELOG.md | 1 - .../public/components/common/form/index.tsx | 2 +- .../components/group-input/group-input.scss | 5 +- .../components/group-input/group-input.tsx | 23 ++-- .../optionals-inputs/optionals-inputs.tsx | 19 ++-- .../server-address/server-address-input.tsx | 35 ------ .../server-address/server-address-title.tsx | 53 --------- .../server-address/server-address.tsx | 103 ++++++++++++++++++ .../containers/steps/steps.scss | 14 ++- .../register-agent/containers/steps/steps.tsx | 34 +++--- 10 files changed, 155 insertions(+), 134 deletions(-) delete mode 100644 plugins/main/public/controllers/register-agent/components/server-address/server-address-input.tsx delete mode 100644 plugins/main/public/controllers/register-agent/components/server-address/server-address-title.tsx create mode 100644 plugins/main/public/controllers/register-agent/components/server-address/server-address.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 276f72d427..f1fc056e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Added development so that the images of the new agent deployment page also have dark mode. [#5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) - Added register agent form status callout message [#5634](https://github.com/wazuh/wazuh-kibana-app/pull/5634) - ### Changed - Changed of regular expression in RBAC. [#5201](https://github.com/wazuh/wazuh-kibana-app/pull/5201) diff --git a/plugins/main/public/components/common/form/index.tsx b/plugins/main/public/components/common/form/index.tsx index 2b7cb7ec0f..d10797ca0d 100644 --- a/plugins/main/public/components/common/form/index.tsx +++ b/plugins/main/public/components/common/form/index.tsx @@ -14,7 +14,7 @@ export interface InputFormProps { value: any; onChange: (event: React.ChangeEvent) => void; error?: string; - label?: string; + label?: string | React.ReactNode; header?: | React.ReactNode | ((props: { value: any; error?: string }) => React.ReactNode); diff --git a/plugins/main/public/controllers/register-agent/components/group-input/group-input.scss b/plugins/main/public/controllers/register-agent/components/group-input/group-input.scss index e69348c07b..575880c792 100644 --- a/plugins/main/public/controllers/register-agent/components/group-input/group-input.scss +++ b/plugins/main/public/controllers/register-agent/components/group-input/group-input.scss @@ -1,7 +1,4 @@ -.groupTitle { - margin-top: '32px'; - flex-direction: 'row'; - font-style: normal; +.registerAgentLabels { font-weight: 700; font-size: 12px; line-height: 20px; diff --git a/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx b/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx index 68f436252c..e12c301850 100644 --- a/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx +++ b/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx @@ -10,6 +10,7 @@ import { } from '@elastic/eui'; import { webDocumentationLink } from '../../../../../common/services/web_documentation'; import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; +import './group-input.scss'; const popoverAgentGroup = ( @@ -35,25 +36,31 @@ const GroupInput = ({ value, options, onChange }) => { const closeAgentGroup = () => setIsPopoverAgentGroup(false); return ( <> - + + +

+ Select one or more existing groups +

+
- Select one or more existing groups - + > } isOpen={isPopoverAgentGroup} closePopover={closeAgentGroup} diff --git a/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx index 28c91f69af..28806fb2fa 100644 --- a/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx +++ b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -13,6 +13,7 @@ import { InputForm } from '../../../../components/common/form'; import { OPTIONAL_PARAMETERS_TEXT } from '../../utils/register-agent-data'; import { webDocumentationLink } from '../../../../../common/services/web_documentation'; import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; +import '../group-input/group-input.scss'; interface OptionalsInputsProps { formFields: UseFormReturn['fields']; } @@ -56,24 +57,28 @@ const OptionalsInputs = (props: OptionalsInputsProps) => { fullWidth={false} label={ <> - + + +

Assign an agent name

+
- Assign an agent name - + > } isOpen={isPopoverAgentName} closePopover={closeAgentName} diff --git a/plugins/main/public/controllers/register-agent/components/server-address/server-address-input.tsx b/plugins/main/public/controllers/register-agent/components/server-address/server-address-input.tsx deleted file mode 100644 index 493735b88b..0000000000 --- a/plugins/main/public/controllers/register-agent/components/server-address/server-address-input.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import React, { Fragment } from 'react'; -import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; -import { EnhancedFieldConfiguration } from '../../../../components/common/form/types'; -import { InputForm } from '../../../../components/common/form'; - -interface ServerAddressInputProps { - formField: EnhancedFieldConfiguration; -} - -const ServerAddressInput = (props: ServerAddressInputProps) => { - const { formField } = props; - - return ( - - - {SERVER_ADDRESS_TEXTS.map((data, index) => ( - - - {data.subtitle} - - - ))} - - } - fullWidth={false} - placeholder='Server address' - /> - - ); -}; - -export default ServerAddressInput; diff --git a/plugins/main/public/controllers/register-agent/components/server-address/server-address-title.tsx b/plugins/main/public/controllers/register-agent/components/server-address/server-address-title.tsx deleted file mode 100644 index 76475c5a6d..0000000000 --- a/plugins/main/public/controllers/register-agent/components/server-address/server-address-title.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink, EuiPopover } from "@elastic/eui" -import React, { useState } from "react"; -import { webDocumentationLink } from "../../../../../common/services/web_documentation"; -import { PLUGIN_VERSION_SHORT } from "../../../../../common/constants"; - -const ServerAddressTitle = () => { - const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); - const closeServerAddress = () => setIsPopoverServerAddress(false); - const onButtonServerAddress = () => - setIsPopoverServerAddress( - isPopoverServerAddress => !isPopoverServerAddress, - ); - - const popoverServerAddress = ( - - Learn about{' '} - - Server address. - - - ); - - return ( - - - Server address - - } - isOpen={isPopoverServerAddress} - closePopover={closeServerAddress} - anchorPosition='rightCenter' - > - {popoverServerAddress} - - - ) -} - -export default ServerAddressTitle; \ No newline at end of file diff --git a/plugins/main/public/controllers/register-agent/components/server-address/server-address.tsx b/plugins/main/public/controllers/register-agent/components/server-address/server-address.tsx new file mode 100644 index 0000000000..8b9a981e6d --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/server-address/server-address.tsx @@ -0,0 +1,103 @@ +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { SERVER_ADDRESS_TEXTS } from '../../utils/register-agent-data'; +import { EnhancedFieldConfiguration } from '../../../../components/common/form/types'; +import { InputForm } from '../../../../components/common/form'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; +import '../group-input/group-input.scss'; + +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); + + return ( + + + {SERVER_ADDRESS_TEXTS.map((data, index) => ( + + + {data.subtitle} + + + ))} + + + + + + Assign a server address + + + + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + + + } + fullWidth={false} + placeholder='Server address' + /> + + ); +}; + +export default ServerAddressInput; diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss index b6ef8d579a..337cc41298 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss @@ -1,9 +1,11 @@ -.stepTitle { - font-style: normal; - font-weight: 700; - font-size: 16px; - letter-spacing: 0.6px; - flex-direction: row; +.register-agent-wizard-container { + .euiStep__title { + font-style: normal; + font-weight: 700; + font-size: 16px; + letter-spacing: 0.6px; + flex-direction: row; + } } .stepSubtitleServerAddress { diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx index 26140391e9..bfa904db8c 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx @@ -17,8 +17,7 @@ import { } from '../../core/config/os-commands-definitions'; import { UseFormReturn } from '../../../../components/common/form/types'; import CommandOutput from '../../components/command-output/command-output'; -import ServerAddressTitle from '../../components/server-address/server-address-title'; -import ServerAddressInput from '../../components/server-address/server-address-input'; +import ServerAddress from '../../components/server-address/server-address'; import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; import { getAgentCommandsStepStatus, @@ -134,23 +133,19 @@ export const Steps = ({ const registerAgentFormSteps = [ { - title: ( - - Select the package to download and install on your system: - - ), + title: 'Select the package to download and install on your system:', children: osCard, status: getOSSelectorStepStatus(form.fields), }, { - title: , - children: , + title: 'Server address', + children: , status: getServerAddressStepStatus(form.fields), }, ...(needsPassword && !wazuhPassword ? [ { - title: Wazuh password, + title: 'Wazuh password', children: ( Optional settings
, + title: 'Optional settings', children: , status: getOptionalParameterStepStatus( form.fields, @@ -186,17 +181,16 @@ export const Steps = ({ ), }, { - title: ( - - Run the following commands to download and install the Wazuh agent: - - ), + title: + 'Run the following commands to download and install the Wazuh agent:', children: ( <> {missingStepsName?.length ? ( ) : null} @@ -224,13 +218,15 @@ export const Steps = ({ status: installCommandStepStatus, }, { - title: Start the Wazuh agent:, + title: 'Start the Wazuh agent:', children: ( <> {missingStepsName?.length ? ( ) : null} From 0241a595acf81d3f1a7a0fa980b90d4502e2e9fb Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 11 Jul 2023 11:01:14 -0300 Subject: [PATCH 18/24] update changelog --- CHANGELOG.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1fc056e71..14c4d60527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,9 @@ All notable changes to the Wazuh app project will be documented in this file. - Added `ignore` and `restrict` options to Syslog configuration. [#5203](https://github.com/wazuh/wazuh-kibana-app/pull/5203) - Added the `extensions.github` and `extensions.office` settings to the default configuration file [#5376](https://github.com/wazuh/wazuh-kibana-app/pull/5376) - Added new global error treatment (client-side) [#4163](https://github.com/wazuh/wazuh-kibana-app/pull/4163) -- Added a description to step 3 of the deploy a new agent section. [#5419](https://github.com/wazuh/wazuh-kibana-app/pull/5419) -- Added a title to the agent name input of the deploy a new agent section. [#5429](https://github.com/wazuh/wazuh-kibana-app/pull/5429) -- Added callout below the agent name entry of the deploy a new agent section. [#5429](https://github.com/wazuh/wazuh-kibana-app/pull/5429) + - Added new CLI to generate API data from specification file [#5519](https://github.com/wazuh/wazuh-kibana-app/pull/5519) - Added specific RBAC permissions to Security section [#5551](https://github.com/wazuh/wazuh-kibana-app/pull/5551) -- Added development so that the images of the new agent deployment page also have dark mode. [#5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) -- Added register agent form status callout message [#5634](https://github.com/wazuh/wazuh-kibana-app/pull/5634) ### Changed @@ -28,7 +24,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the placeholder of the agent name input of the deploy a new agent section. [#5429](https://github.com/wazuh/wazuh-kibana-app/pull/5429) - Changed the query to search for an agent in `management/configuration`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) - Changed the search bar in management/log to the one used in the rest of the app. [#5476](https://github.com/wazuh/wazuh-kibana-app/pull/5476) -- Changed the deploy a new agent page from step one to step three. [#5554](https://github.com/wazuh/wazuh-kibana-app/pull/5554) [#5462](https://github.com/wazuh/wazuh-kibana-app/pull/5462) +- Changed the design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) ### Fixed @@ -56,7 +52,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Removed pretty parameter from cron job requests. [#5532](https://github.com/wazuh/wazuh-kibana-app/pull/5532) - Removed unnecessary requests in `Management/Status` section. [#5528](https://github.com/wazuh/wazuh-kibana-app/pull/5528) - Removed obsolete code that caused duplicate requests to the api in `Management`. [#5485](https://github.com/wazuh/wazuh-kibana-app/pull/5485) -- Removed the custom colors that did not allow to activate the default dark mode of elastic. [#5620](https://github.com/wazuh/wazuh-kibana-app/pull/5620) ## Wazuh v4.5.0 - OpenSearch Dashboards 2.6.0 - Revision 01 From bf647a70114601b5ed7a023b2f9ce0943782c0c4 Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 11 Jul 2023 11:14:25 -0300 Subject: [PATCH 19/24] update changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c4d60527..993df19be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ All notable changes to the Wazuh app project will be documented in this file. - Added `ignore` and `restrict` options to Syslog configuration. [#5203](https://github.com/wazuh/wazuh-kibana-app/pull/5203) - Added the `extensions.github` and `extensions.office` settings to the default configuration file [#5376](https://github.com/wazuh/wazuh-kibana-app/pull/5376) - Added new global error treatment (client-side) [#4163](https://github.com/wazuh/wazuh-kibana-app/pull/4163) - - Added new CLI to generate API data from specification file [#5519](https://github.com/wazuh/wazuh-kibana-app/pull/5519) - Added specific RBAC permissions to Security section [#5551](https://github.com/wazuh/wazuh-kibana-app/pull/5551) From 63187b4ef14fd576b593082da177feca71f2afd3 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Mon, 31 Jul 2023 13:35:57 -0300 Subject: [PATCH 20/24] Add requested fixs on texts --- .../main/public/controllers/agent/wazuh-config/index.ts | 2 +- .../components/os-selector/os-card/os-card.tsx | 2 +- .../controllers/register-agent/containers/steps/steps.tsx | 2 +- .../register-agent/core/config/os-commands-definitions.ts | 8 ++++---- .../register-agent/utils/register-agent-data.tsx | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/main/public/controllers/agent/wazuh-config/index.ts b/plugins/main/public/controllers/agent/wazuh-config/index.ts index f10c7994c1..c8bafbbe2a 100644 --- a/plugins/main/public/controllers/agent/wazuh-config/index.ts +++ b/plugins/main/public/controllers/agent/wazuh-config/index.ts @@ -108,7 +108,7 @@ const architectureButtonsMacos = [ }, { id: 'arm64', - label: 'Apple Silicon', + label: 'Apple silicon', }, ]; diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx index 3c8dfddbf8..1e88422e5b 100644 --- a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx @@ -62,7 +62,7 @@ export const OsCard = ({ onChange, value }: Props) => { )} rel='noopener noreferrer' > - steps + documentation
.
diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx index bfa904db8c..596e282e86 100644 --- a/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx @@ -160,7 +160,7 @@ export const Steps = ({ )} rel='noopener noreferrer' > - steps + documentation } diff --git a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts index 562b487e1a..8c0ee96452 100644 --- a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts +++ b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -33,7 +33,7 @@ type ILinuxOSTypes = /** Windows options **/ export interface IWindowsOSTypes { name: 'WINDOWS'; - architecture: 'MSI 32/64'; + architecture: 'MSI 32/64 bits'; } /** MacOS options **/ @@ -44,7 +44,7 @@ export interface IMacOSIntel { export interface IMacOSApple { name: 'macOS'; - architecture: 'Apple Silicon'; + architecture: 'Apple silicon'; } type IMacOSTypes = IMacOSApple | IMacOSIntel; @@ -100,7 +100,7 @@ const windowsDefinition: IOSDefinition = { name: 'WINDOWS', options: [ { - architecture: 'MSI 32/64', + architecture: 'MSI 32/64 bits', urlPackage: props => `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.msi`, installCommand: props => getWindowsInstallCommand(props), @@ -120,7 +120,7 @@ const macDefinition: IOSDefinition = { startCommand: props => getMacosStartCommand(props), }, { - architecture: 'Apple Silicon', + architecture: 'Apple silicon', urlPackage: props => `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.arm64.pkg`, installCommand: props => getMacOsInstallCommand(props), diff --git a/plugins/main/public/controllers/register-agent/utils/register-agent-data.tsx b/plugins/main/public/controllers/register-agent/utils/register-agent-data.tsx index 1ea3c47d8a..378bf61d33 100644 --- a/plugins/main/public/controllers/register-agent/utils/register-agent-data.tsx +++ b/plugins/main/public/controllers/register-agent/utils/register-agent-data.tsx @@ -20,13 +20,13 @@ export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ icon: darkMode ? WindowsDarkIcon : WindowsLightIcon, title: 'WINDOWS', hr: true, - architecture: ['MSI 32/64'], + architecture: ['MSI 32/64 bits'], }, { icon: darkMode ? MacDarkIcon : MacLightIcon, title: 'macOS', hr: true, - architecture: ['Intel', 'Apple Silicon'], + architecture: ['Intel', 'Apple silicon'], }, ]; From 8746a3a01ba8e9b09b6e9f24a79e21d1c7ec9831 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Tue, 1 Aug 2023 11:18:45 -0300 Subject: [PATCH 21/24] Add new rpm and deb install commands --- .../optionals-inputs/optionals-inputs.tsx | 16 ++++++---- .../core/config/os-commands-definitions.ts | 21 +++++++++----- .../register-agent-os-commands-services.tsx | 29 ++++++++++++------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx index 28806fb2fa..317e3b6c41 100644 --- a/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx +++ b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -24,15 +24,15 @@ const OptionalsInputs = (props: OptionalsInputsProps) => { const onButtonAgentName = () => setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); const closeAgentName = () => setIsPopoverAgentName(false); - + const agentNameDocLink = webDocumentationLink( + 'user-manual/reference/ossec-conf/client.html#enrollment-agent-name', + PLUGIN_VERSION_SHORT, + ) const popoverAgentName = ( Learn about{' '} @@ -94,7 +94,11 @@ const OptionalsInputs = (props: OptionalsInputsProps) => { /> {warningForAgentName}} iconType='iInCircle' className='warningForAgentName' /> diff --git a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts index 8c0ee96452..1391c825a6 100644 --- a/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts +++ b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -1,4 +1,11 @@ -import { getLinuxDEBInstallCommand, getLinuxRPMInstallCommand, getLinuxStartCommand, getMacOsInstallCommand, getMacosStartCommand, getWindowsInstallCommand, getWindowsStartCommand } from '../../services/register-agent-os-commands-services'; +import { + getDEBInstallCommand, + getRPMInstallCommand, + getLinuxStartCommand, + getMacOsInstallCommand, + getMacosStartCommand, + getWindowsInstallCommand, + getWindowsStartCommand } from '../../services/register-agent-os-commands-services'; import { IOSDefinition, tOptionalParams } from '../register-commands/types'; // Defined OS combinations @@ -69,28 +76,28 @@ const linuxDefinition: IOSDefinition = { 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 => getLinuxDEBInstallCommand(props), + installCommand: props => getDEBInstallCommand(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_aarch64.deb`, - installCommand: props => getLinuxDEBInstallCommand(props), + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.deb`, + installCommand: props => getDEBInstallCommand(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 => getLinuxRPMInstallCommand(props), + installCommand: props => getRPMInstallCommand(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 => getLinuxRPMInstallCommand(props), + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, + installCommand: props => getRPMInstallCommand(props), startCommand: props => getLinuxStartCommand(props), }, ], diff --git a/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx index 818a91945a..77adb03712 100644 --- a/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx +++ b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx @@ -53,28 +53,37 @@ const getAllOptionalsMacos = ( ); }; -/******* Linux *******/ +/******* RPM *******/ + +// curl -o wazuh-agent-4.4.5-1.x86_64.rpm https://packages.wazuh.com/4.x/yum/wazuh-agent-4.4.5-1.x86_64.rpm && sudo WAZUH_MANAGER='172.30.30.20' rpm -ihv wazuh-agent-4.4.5-1.x86_64.rpm -// Install command -export const getLinuxRPMInstallCommand = ( +export const getRPMInstallCommand = ( props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage } = props; - return `sudo ${ + const { optionals, urlPackage, wazuhVersion } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm` + return `curl -o ${packageName} ${urlPackage} && sudo ${ optionals && getAllOptionals(optionals) - }yum install -y ${urlPackage}`; + }rpm -ihv ${packageName}`; }; -export const getLinuxDEBInstallCommand = ( +/******* DEB *******/ + +// wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.4.5-1_amd64.deb && sudo WAZUH_MANAGER='172.30.30.20' dpkg -i ./wazuh-agent_4.4.5-1_amd64.deb + +export const getDEBInstallCommand = ( props: tOSEntryInstallCommand, ) => { - const { optionals, urlPackage } = props; - return `curl -so wazuh-agent.deb ${urlPackage} && sudo ${ + const { optionals, urlPackage, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb` + return `wget ${urlPackage} && sudo ${ optionals && getAllOptionals(optionals) - }dpkg -i ./wazuh-agent.deb`; + }dpkg -i ./${packageName}`; }; +/******* Linux *******/ + // Start command export const getLinuxStartCommand = ( _props: tOSEntryProps, From a56f9190d527d06ccfc6dbb8ccc63f7793133ffd Mon Sep 17 00:00:00 2001 From: "chantal.kelm" Date: Tue, 1 Aug 2023 11:24:21 -0300 Subject: [PATCH 22/24] modify fqdn regex --- .../public/controllers/register-agent/utils/validations.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/main/public/controllers/register-agent/utils/validations.tsx b/plugins/main/public/controllers/register-agent/utils/validations.tsx index 00fb93410f..52705b5e53 100644 --- a/plugins/main/public/controllers/register-agent/utils/validations.tsx +++ b/plugins/main/public/controllers/register-agent/utils/validations.tsx @@ -8,8 +8,7 @@ // Minimum 3 labels export const validateServerAddress = (value: any) => { const isFQDN = - /^(?!-)(?!.*--)(?!.*\d$)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; - + /^(?!-)(?!.*--)(?!.*\d$)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){1,}$/; const isIP = /^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$/; const numbersAndPoints = /^[0-9.]+$/; From 4cfb227b6af38c61b9d898466eee978e0caf0b88 Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Tue, 1 Aug 2023 11:32:56 -0300 Subject: [PATCH 23/24] Fix server address validation unit test --- .../controllers/register-agent/utils/validations.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/controllers/register-agent/utils/validations.test.tsx b/plugins/main/public/controllers/register-agent/utils/validations.test.tsx index e51dd972fd..edd7c4658d 100644 --- a/plugins/main/public/controllers/register-agent/utils/validations.test.tsx +++ b/plugins/main/public/controllers/register-agent/utils/validations.test.tsx @@ -19,7 +19,7 @@ describe('Validations', () => { }); it('should return an error message for an invalid FQDN', () => { - const invalidFQDN = 'example.fqdn'; + const invalidFQDN = 'example.'; const result = validateServerAddress(invalidFQDN); expect(result).toBe( 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.', From cd5739c37a6d2f79b74d6184883b2c6095c9fb5a Mon Sep 17 00:00:00 2001 From: Maximiliano Ibarra Date: Tue, 1 Aug 2023 12:06:09 -0300 Subject: [PATCH 24/24] Add type in command output types --- .../components/command-output/command-output.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx index dafb26d4b5..ce42d7aaa0 100644 --- a/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx +++ b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx @@ -8,12 +8,13 @@ import { EuiText, } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; +import { tOperatingSystem } from '../../core/config/os-commands-definitions'; interface ICommandSectionProps { commandText: string; showCommand: boolean; onCopy: () => void; - os?: 'WINDOWS' | string; + os?: tOperatingSystem['name']; password?: string; }