diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index bd241bd502..0000000000 Binary files a/.DS_Store and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6e4fa4e3..ef1f96f0f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Migrate the timeFilter, metaFields, maxBuckets health checks inside the pattern check. [#5384](https://github.com/wazuh/wazuh-kibana-app/pull/5384) - 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 design of the wizard to add agents. [#5457](https://github.com/wazuh/wazuh-kibana-app/pull/5457) ### Fixed diff --git a/plugins/main/public/assets/images/icons/linux-icon.svg b/plugins/main/public/assets/images/icons/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/plugins/main/public/assets/images/icons/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/main/public/assets/images/icons/mac-icon.svg b/plugins/main/public/assets/images/icons/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/plugins/main/public/assets/images/icons/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/main/public/assets/images/icons/windows-icon.svg b/plugins/main/public/assets/images/icons/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/plugins/main/public/assets/images/icons/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/main/public/assets/images/themes/dark/linux-icon.svg b/plugins/main/public/assets/images/themes/dark/linux-icon.svg new file mode 100644 index 0000000000..c76c7d6328 --- /dev/null +++ b/plugins/main/public/assets/images/themes/dark/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/main/public/assets/images/themes/dark/mac-icon.svg b/plugins/main/public/assets/images/themes/dark/mac-icon.svg new file mode 100644 index 0000000000..2eae996a06 --- /dev/null +++ b/plugins/main/public/assets/images/themes/dark/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/main/public/assets/images/themes/dark/windows-icon.svg b/plugins/main/public/assets/images/themes/dark/windows-icon.svg new file mode 100644 index 0000000000..74d5b551f8 --- /dev/null +++ b/plugins/main/public/assets/images/themes/dark/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/main/public/assets/images/themes/light/linux-icon.svg b/plugins/main/public/assets/images/themes/light/linux-icon.svg new file mode 100644 index 0000000000..85613a6872 --- /dev/null +++ b/plugins/main/public/assets/images/themes/light/linux-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/main/public/assets/images/themes/light/mac-icon.svg b/plugins/main/public/assets/images/themes/light/mac-icon.svg new file mode 100644 index 0000000000..dbfed2e61f --- /dev/null +++ b/plugins/main/public/assets/images/themes/light/mac-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/main/public/assets/images/themes/light/windows-icon.svg b/plugins/main/public/assets/images/themes/light/windows-icon.svg new file mode 100644 index 0000000000..5ef43e4d08 --- /dev/null +++ b/plugins/main/public/assets/images/themes/light/windows-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/plugins/main/public/components/common/form/hooks.test.tsx b/plugins/main/public/components/common/form/hooks.test.tsx index 38d3f19a27..283c4809bf 100644 --- a/plugins/main/public/components/common/form/hooks.test.tsx +++ b/plugins/main/public/components/common/form/hooks.test.tsx @@ -1,178 +1,219 @@ +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'; 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 CustomComponent = (props: any) => { + const { onChange, field, initialValue } = props; + const [value, setValue] = useState(initialValue || ''); + + const handleOnChange = (e: any) => { + setValue(e.target.value); + onChange(e); + }; + + return ( + <> + {field} + + + ); + }; + + const formFields: FormConfiguration = { + customField: { + type: 'custom', + initialValue: 'default value', + component: props => CustomComponent(props), + }, + }; + + const { result } = renderHook(() => useForm(formFields)); + const { container, getByRole } = render( + , + ); + + expect(container).toBeInTheDocument(); + const input = getByRole('textbox'); + expect(input).toHaveValue('default value'); + fireEvent.change(input, { target: { value: 'new value' } }); + expect(result.current.fields.customField.component).toBeInstanceOf( + Function, + ); + expect(result.current.fields.customField.value).toBe('new value'); + }); }); diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index f3837b4b32..63ff8fdb72 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -1,91 +1,143 @@ 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){ +interface IgetValueFromEventType { + [key: string]: (event: any) => any; +} + +/** + * Returns the value of the event according to the type of field + * When the type is not found, it returns the value defined in the default key + * + * @param event + * @param type + * @returns event value + */ +function getValueFromEvent( + event: any, + type: SettingTypes | CustomSettingType, +): any { return (getValueFromEventType[type] || getValueFromEventType.default)(event); -}; +} -const getValueFromEventType = { - [EpluginSettingType.switch] : (event: any) => event.target.checked, +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) => { - const [formFields, setFormFields] = useState(Object.entries(fields).reduce((accum, [fieldKey, fieldConfiguration]) => ({ - ...accum, - [fieldKey]: { - currentValue: fieldConfiguration.initialValue, - initialValue: fieldConfiguration.initialValue, - } - }), {})); +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]), ); - 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/plugins/main/public/components/common/form/index.tsx b/plugins/main/public/components/common/form/index.tsx index 2a6da4610e..d10797ca0d 100644 --- a/plugins/main/public/components/common/form/index.tsx +++ b/plugins/main/public/components/common/form/index.tsx @@ -7,6 +7,31 @@ 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'; + +export interface InputFormProps { + type: SettingTypes; + value: any; + onChange: (event: React.ChangeEvent) => void; + error?: string; + label?: string | React.ReactNode; + header?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + footer?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + preInput?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); + postInput?: + | React.ReactNode + | ((props: { value: any; error?: string }) => React.ReactNode); +} + +interface InputFormComponentProps extends InputFormProps { + rest: any; +} export const InputForm = ({ type, @@ -18,13 +43,15 @@ export const InputForm = ({ footer, preInput, postInput, -...rest}) => { - - const ComponentInput = Input[type]; + ...rest +}: InputFormComponentProps) => { + const ComponentInput = Input[ + type as keyof typeof Input + ] as React.ComponentType; - if(!ComponentInput){ + if (!ComponentInput) { return null; - }; + } const isInvalid = Boolean(error); @@ -37,23 +64,25 @@ 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; - + 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 = { @@ -64,4 +93,5 @@ const Input = { select: InputFormSelect, text: InputFormText, textarea: InputFormTextArea, + custom: ({ component, ...rest }) => component(rest), }; diff --git a/plugins/main/public/components/common/form/input_select.tsx b/plugins/main/public/components/common/form/input_select.tsx index a8f02e99d7..b212f3f068 100644 --- a/plugins/main/public/components/common/form/input_select.tsx +++ b/plugins/main/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/plugins/main/public/components/common/form/input_text.tsx b/plugins/main/public/components/common/form/input_text.tsx index feb0d218ee..c8e3d730d4 100644 --- a/plugins/main/public/components/common/form/input_text.tsx +++ b/plugins/main/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/plugins/main/public/components/common/form/types.ts b/plugins/main/public/components/common/form/types.ts index e064804790..762f73962f 100644 --- a/plugins/main/public/components/common/form/types.ts +++ b/plugins/main/public/components/common/form/types.ts @@ -1,19 +1,84 @@ -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 | undefined; + setInputRef: (reference: any) => void; + inputRef: any; + onChange: (event: any) => void; +} + +interface EnhancedDefaultField extends EnhancedField { + type: SettingTypes; +} + +interface EnhancedCustomField extends EnhancedField { + type: CustomSettingType; + component: (props: any) => JSX.Element; +} + +export type EnhancedFieldConfiguration = EnhancedDefaultField | EnhancedCustomField; +export interface EnhancedFields { + [key: string]: EnhancedFieldConfiguration; +} + +export interface UseFormReturn { + fields: EnhancedFields; + changed: { [key: string]: any }; + errors: { [key: string]: string }; + undoChanges: () => void; + doChanges: () => void; +} diff --git a/plugins/main/public/controllers/agent/index.js b/plugins/main/public/controllers/agent/index.js index c0fefdc072..51a445bb00 100644 --- a/plugins/main/public/controllers/agent/index.js +++ b/plugins/main/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 '../../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' +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/plugins/main/public/controllers/agent/register-agent/steps/wz-manager-address.tsx b/plugins/main/public/controllers/agent/register-agent/steps/wz-manager-address.tsx index 0c46c70676..8bfd679e2f 100644 --- a/plugins/main/public/controllers/agent/register-agent/steps/wz-manager-address.tsx +++ b/plugins/main/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/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/command-output/command-output.tsx b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx new file mode 100644 index 0000000000..ce42d7aaa0 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/command-output/command-output.tsx @@ -0,0 +1,103 @@ +import { + EuiCodeBlock, + EuiCopy, + EuiIcon, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, + EuiText, +} from '@elastic/eui'; +import React, { Fragment, useEffect, useState } from 'react'; +import { tOperatingSystem } from '../../core/config/os-commands-definitions'; + +interface ICommandSectionProps { + commandText: string; + showCommand: boolean; + onCopy: () => void; + os?: tOperatingSystem['name']; + password?: string; +} + +export default function CommandOutput(props: ICommandSectionProps) { + const { commandText, showCommand, onCopy, os, password } = props; + const getHighlightCodeLanguage = (os: 'WINDOWS' | string) => { + if (os.toLowerCase() === 'windows') { + return 'powershell'; + } else { + return 'bash'; + } + }; + const [havePassword, setHavePassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + const onHandleCopy = (command: any) => { + onCopy && onCopy(); + return command; // the return is needed to avoid a bug in EuiCopy + }; + + const [commandToShow, setCommandToShow] = useState(commandText); + + useEffect(() => { + if (password) { + setHavePassword(true); + osdfucatePassword(password); + } else { + setHavePassword(false); + setCommandToShow(commandText); + } + }, [password, commandText, showPassword]) + + const osdfucatePassword = (password: string) => { + if(!password) return; + if(!commandText) return; + + if(showPassword){ + setCommandToShow(commandText); + }else{ + // 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 ( + + + +
+ + {showCommand ? commandToShow : ''} + + {showCommand && ( + + {copy => ( +
onHandleCopy(copy())} + > +

+ Copy command +

+
+ )} +
+ )} +
+ + {showCommand && havePassword ? : null} +
+
+ ); +} 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 new file mode 100644 index 0000000000..575880c792 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/group-input/group-input.scss @@ -0,0 +1,5 @@ +.registerAgentLabels { + font-weight: 700; + font-size: 12px; + line-height: 20px; +} diff --git a/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx b/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx new file mode 100644 index 0000000000..e12c301850 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/group-input/group-input.tsx @@ -0,0 +1,102 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiComboBox, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; +import './group-input.scss'; + +const popoverAgentGroup = ( + + Learn about{' '} + + Select a group. + + +); + +const GroupInput = ({ value, options, onChange }) => { + const [isPopoverAgentGroup, setIsPopoverAgentGroup] = useState(false); + + const onButtonAgentGroup = () => + setIsPopoverAgentGroup(isPopoverAgentGroup => !isPopoverAgentGroup); + const closeAgentGroup = () => setIsPopoverAgentGroup(false); + return ( + <> + + +

+ Select one or more existing groups +

+
+ + + } + isOpen={isPopoverAgentGroup} + closePopover={closeAgentGroup} + anchorPosition='rightCenter' + > + {popoverAgentGroup} + + +
+ { + onChange({ + target: { value: group }, + }); + }} + isDisabled={!options?.groups.length} + isClearable={true} + data-test-subj='demoComboBox' + data-testid='group-input-combobox' + /> + {!options?.groups.length && ( + <> + + + )} + + ); +}; + +export default GroupInput; diff --git a/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx new file mode 100644 index 0000000000..317e3b6c41 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/optionals-inputs/optionals-inputs.tsx @@ -0,0 +1,110 @@ +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'; +import '../group-input/group-input.scss'; +interface OptionalsInputsProps { + formFields: UseFormReturn['fields']; +} + +const OptionalsInputs = (props: OptionalsInputsProps) => { + const { formFields } = props; + const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + const onButtonAgentName = () => + setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); + const closeAgentName = () => setIsPopoverAgentName(false); + const agentNameDocLink = webDocumentationLink( + 'user-manual/reference/ossec-conf/client.html#enrollment-agent-name', + PLUGIN_VERSION_SHORT, + ) + const popoverAgentName = ( + + Learn about{' '} + + Assigning an agent name. + + + ); + + const warningForAgentName = + 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; + return ( + + + {OPTIONAL_PARAMETERS_TEXT.map((data, index) => ( + + {data.subtitle} + + ))} + + + + +

Assign an agent name

+
+ + + } + isOpen={isPopoverAgentName} + closePopover={closeAgentName} + anchorPosition='rightCenter' + > + {popoverAgentName} + + +
+ + } + placeholder='Agent name' + /> + {warningForAgentName}} + iconType='iInCircle' + className='warningForAgentName' + /> + +
+ ); +}; + +export default OptionalsInputs; diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss new file mode 100644 index 0000000000..b8b985d165 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.scss @@ -0,0 +1,46 @@ +.checkbox-group-container { + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: 26px; + justify-content: center; +} + +.checkbox-item { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: left; +} + +.checkbox-group-container.single-architecture { + margin-top: 44px; + display: flex; + justify-content: center; +} + +.checkbox-group-container.double-architecture { + margin-top: 24px; + display: flex; + flex-direction: column; + .checkbox-item:first-child { + margin-bottom: 13px; + } + .checkbox-item { + display: flex; + flex-direction: row-reverse; + justify-content: left; + align-self: baseline; + } +} + +.architecture-label { + margin-left: 8px; + font-style: normal; + font-weight: 400; + font-size: 12px; +} +.first-card-four-items { + .checkbox-item:nth-child(n + 3) { + padding-top: 16px; + } +} diff --git a/plugins/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 new file mode 100644 index 0000000000..186fc0d240 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; + +describe('CheckboxGroupComponent', () => { + const data = ['Option 1', 'Option 2', 'Option 3']; + const cardIndex = 0; + const selectedOption = 'Option 1'; + const onOptionChange = jest.fn(); + + test('renders checkbox items with correct labels', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + expect(checkboxItems).toHaveLength(data.length); + + expect(checkboxItems[0]).toHaveAttribute('id', 'Option 1'); + expect(checkboxItems[1]).toHaveAttribute('id', 'Option 2'); + expect(checkboxItems[2]).toHaveAttribute('id', 'Option 3'); + + expect(checkboxItems[0]).toBeChecked(); + expect(checkboxItems[1]).not.toBeChecked(); + expect(checkboxItems[2]).not.toBeChecked(); + + expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.getByText('Option 2')).toBeInTheDocument(); + expect(screen.getByText('Option 3')).toBeInTheDocument(); + }); + + test('calls onOptionChange when a checkbox is selected', () => { + render( + , + ); + + const checkboxItems = screen.getAllByRole('radio'); + + fireEvent.click(checkboxItems[1]); + + expect(onOptionChange).toHaveBeenCalledTimes(1); + expect(onOptionChange).toHaveBeenCalledWith( + expect.objectContaining({ + target: { value: `Option 2` }, + }), + ); + }); +}); diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx new file mode 100644 index 0000000000..461e6e0943 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/checkbox-group/checkbox-group.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { EuiRadioGroup } from '@elastic/eui'; +import './checkbox-group.scss'; + +interface Props { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +const CheckboxGroupComponent: React.FC = ({ + data, + cardIndex, + selectedOption, + onOptionChange, +}) => { + const isSingleArchitecture = data.length === 1; + const isDoubleArchitecture = data.length === 2; + const isFirstCardWithFourItems = cardIndex === 0 && data.length === 4; + return ( +
+ {data.map((arch, idx) => ( +
+ + { + onOptionChange({ target: { value: id } }); + }} + /> +
+ ))} +
+ ); +}; + +export { CheckboxGroupComponent }; diff --git a/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss new file mode 100644 index 0000000000..55dd4092fa --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.scss @@ -0,0 +1,59 @@ +.card { + height: 183px; + + label { + cursor: pointer; + } +} + +.cardTitle { + display: flex; + align-items: center; + margin-top: 28px; + justify-content: center; + user-select: none; +} + +.cardIcon { + margin-right: 10px; +} + +.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; +} + +.hr { + border: 1px solid #d3dae6; +} + +.cardContent { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.checkboxGroupContainer { + flex-basis: 50%; +} + +.architectureItem { + margin-bottom: 8px; +} + +.last-card { + margin-right: 63px; +} + +.cardsCallOut { + margin-top: 16px; +} diff --git a/plugins/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 new file mode 100644 index 0000000000..d01a27c141 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { OsCard } from '../os-card/os-card'; + +jest.mock('../../../../../kibana-services', () => ({ + ...(jest.requireActual('../../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: (url) => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), + getUiSettings: jest.fn().mockReturnValue({ + get: (name) => { + return true; + }, + }), +})); + +describe('OsCard', () => { + test('renders three cards with different titles', () => { + render(); + + const cardTitles = screen.getAllByTestId('card-title'); + expect(cardTitles).toHaveLength(3); + + expect(cardTitles[0]).toHaveTextContent('LINUX'); + expect(cardTitles[1]).toHaveTextContent('WINDOWS'); + expect(cardTitles[2]).toHaveTextContent('macOS'); + }); +}); diff --git a/plugins/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 new file mode 100644 index 0000000000..1e88422e5b --- /dev/null +++ b/plugins/main/public/controllers/register-agent/components/os-selector/os-card/os-card.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLink, + EuiCheckbox, +} from '@elastic/eui'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; +import './os-card.scss'; +import { webDocumentationLink } from '../../../../../../common/services/web_documentation'; + +interface Props { + setStatusCheck: string; + onChange: React.ChangeEventHandler; + value: any; +} + +export const OsCard = ({ onChange, value }: Props) => { + return ( +
+ + {OPERATING_SYSTEMS_OPTIONS.map((data, index) => ( + + + Icon + {data.title} +
+ } + display='plain' + hasBorder + className='card' + > + {data.hr &&
} + + + + ))} + + + For additional systems and architectures, please check our{' '} + + documentation + + . + + } + > + + ); +}; diff --git a/plugins/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/register-agent/register-agent.scss b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.scss new file mode 100644 index 0000000000..f51d4384e0 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.scss @@ -0,0 +1,27 @@ +.register-agent-wizard-container { + box-sizing: border-box; + min-height: 1271px; + margin-top: 44px; + background: #ffffff; + border: 1px solid rgba(52, 55, 65, 0.2); + max-width: 1030px; + padding-left: 72px; + padding-right: 63px; +} + +.register-agent-wizard-title { + margin-top: 51px; + margin-bottom: 51px; + font-style: normal; + font-weight: 400; + font-size: 30px; + line-height: 36px; + display: flex; + justify-content: center; +} + +.register-agent-wizard-close { + display: flex; + margin-top: 17px; + float: right; +} diff --git a/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx new file mode 100644 index 0000000000..8ae23213cd --- /dev/null +++ b/plugins/main/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -0,0 +1,242 @@ +import React, { useState, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiButtonEmpty, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiProgress, + EuiButton, +} 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/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, + }: IRegisterAgentProps) => { + const configuration = useSelector( + (state: { appConfig: { data: any } }) => state.appConfig.data, + ); + const [wazuhVersion, setWazuhVersion] = useState(''); + const [haveUdpProtocol, setHaveUdpProtocol] = useState( + false, + ); + const [loading, setLoading] = useState(false); + const [wazuhPassword, setWazuhPassword] = useState(''); + const [groups, setGroups] = useState([]); + const [needsPassword, setNeedsPassword] = useState(false); + + const initialFields: FormConfiguration = { + operatingSystemSelection: { + type: 'custom', + initialValue: '', + component: props => { + return ; + }, + options: { + groups, + }, + }, + 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); + } + }; + + 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(); + 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'] || + ''; + } + const groups = await getGroups(); + setNeedsPassword(needsPassword); + 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 osCard = ( + + ); + + return ( +
+ + + + + +
+ {hasAgents() ? ( + addNewAgent(false)} + iconType='cross' + > + Close + + ) : ( + reload()} + iconType='refresh' + > + Refresh + + )} +
+ + + +

+ Deploy new agent +

+
+
+
+ + {loading ? ( + <> + + + + + + ) : ( + + + + )} + + + reload()} + > + Close + + + +
+
+
+
+
+
+ ); + }, +); diff --git a/plugins/main/public/controllers/register-agent/containers/steps/steps.scss b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss new file mode 100644 index 0000000000..337cc41298 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.scss @@ -0,0 +1,55 @@ +.register-agent-wizard-container { + .euiStep__title { + font-style: normal; + font-weight: 700; + font-size: 16px; + letter-spacing: 0.6px; + flex-direction: row; + } +} + +.stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 9px; +} + +.stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + margin-bottom: 20px; +} + +.titleAndIcon { + display: flex; + flex-direction: row; +} + +.warningForAgentName { + margin-top: 10px; +} + +.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/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx new file mode 100644 index 0000000000..596e282e86 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/containers/steps/steps.tsx @@ -0,0 +1,258 @@ +import React, { Fragment, useEffect, useState } from 'react'; +import { EuiCallOut, EuiLink, EuiSteps, EuiTitle } from '@elastic/eui'; +import './steps.scss'; +import { OPERATING_SYSTEMS_OPTIONS } from '../../utils/register-agent-data'; +import { + IParseRegisterFormValues, + getRegisterAgentFormValues, + parseRegisterAgentFormValues, +} from '../../services/register-agent-services'; + +import { useRegisterAgentCommands } from '../../hooks/use-register-agent-commands'; +import { + osCommandsDefinitions, + optionalParamsDefinitions, + tOperatingSystem, + tOptionalParameters, +} from '../../core/config/os-commands-definitions'; +import { UseFormReturn } from '../../../../components/common/form/types'; +import CommandOutput from '../../components/command-output/command-output'; +import ServerAddress from '../../components/server-address/server-address'; +import OptionalsInputs from '../../components/optionals-inputs/optionals-inputs'; +import { + getAgentCommandsStepStatus, + tFormStepsStatus, + getOSSelectorStepStatus, + getServerAddressStepStatus, + getOptionalParameterStepStatus, + showCommandsSections, + getPasswordStepStatus, + getIncompleteSteps, + getInvalidFields, + tFormFieldsLabel, + tFormStepsLabel, +} from '../../services/register-agent-steps-status-services'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; + +interface IStepsProps { + needsPassword: boolean; + form: UseFormReturn; + osCard: React.ReactElement; + connection: { + isUDP: boolean; + }; + wazuhPassword: string; +} + +export const Steps = ({ + needsPassword, + form, + osCard, + connection, + wazuhPassword, +}: IStepsProps) => { + const initialParsedFormValues = { + operatingSystem: { + name: '', + architecture: '', + }, + optionalParams: { + agentGroups: '', + agentName: '', + serverAddress: '', + wazuhPassword, + protocol: connection.isUDP ? 'UDP' : '', + }, + } as IParseRegisterFormValues; + const [missingStepsName, setMissingStepsName] = useState( + [], + ); + const [invalidFieldsName, setInvalidFieldsName] = useState< + tFormFieldsLabel[] + >([]); + const [registerAgentFormValues, setRegisterAgentFormValues] = + useState(initialParsedFormValues); + + const FORM_MESSAGE_CONJUNTION = ' and '; + + useEffect(() => { + // get form values and parse them divided in OS and optional params + const registerAgentFormValuesParsed = parseRegisterAgentFormValues( + getRegisterAgentFormValues(form), + OPERATING_SYSTEMS_OPTIONS, + initialParsedFormValues, + ); + setRegisterAgentFormValues(registerAgentFormValuesParsed); + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + setMissingStepsName(getIncompleteSteps(form.fields) || []); + setInvalidFieldsName(getInvalidFields(form.fields) || []); + }, [form.fields]); + + const { installCommand, startCommand, selectOS, setOptionalParams } = + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }); + + // install - start commands step state + const [installCommandWasCopied, setInstallCommandWasCopied] = useState(false); + const [installCommandStepStatus, setInstallCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); + const [startCommandWasCopied, setStartCommandWasCopied] = useState(false); + const [startCommandStepStatus, setStartCommandStepStatus] = + useState(getAgentCommandsStepStatus(form.fields, false)); + + useEffect(() => { + if ( + registerAgentFormValues.operatingSystem.name !== '' && + registerAgentFormValues.operatingSystem.architecture !== '' + ) { + selectOS(registerAgentFormValues.operatingSystem as tOperatingSystem); + } + setOptionalParams({ ...registerAgentFormValues.optionalParams }); + setInstallCommandWasCopied(false); + setStartCommandWasCopied(false); + }, [registerAgentFormValues]); + + useEffect(() => { + setInstallCommandStepStatus( + getAgentCommandsStepStatus(form.fields, installCommandWasCopied), + ); + }, [installCommandWasCopied]); + + useEffect(() => { + setStartCommandStepStatus( + getAgentCommandsStepStatus(form.fields, startCommandWasCopied), + ); + }, [startCommandWasCopied]); + + const registerAgentFormSteps = [ + { + title: 'Select the package to download and install on your system:', + children: osCard, + status: getOSSelectorStepStatus(form.fields), + }, + { + title: 'Server address', + children: , + status: getServerAddressStepStatus(form.fields), + }, + ...(needsPassword && !wazuhPassword + ? [ + { + title: 'Wazuh password', + children: ( + + The Wazuh password is required but wasn't defined. Please + check our{' '} + + documentation + + + } + iconType='iInCircle' + className='warningForAgentName' + /> + ), + status: getPasswordStepStatus(form.fields), + }, + ] + : []), + { + title: 'Optional settings', + children: , + status: getOptionalParameterStepStatus( + form.fields, + installCommandWasCopied, + ), + }, + { + title: + 'Run the following commands to download and install the Wazuh agent:', + children: ( + <> + {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: ( + <> + {missingStepsName?.length ? ( + + ) : null} + {invalidFieldsName?.length ? ( + + ) : null} + {!missingStepsName?.length && !invalidFieldsName?.length ? ( + setStartCommandWasCopied(true)} + /> + ) : null} + + ), + status: startCommandStepStatus, + }, + ]; + + return ; +}; 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 new file mode 100644 index 0000000000..1391c825a6 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/config/os-commands-definitions.ts @@ -0,0 +1,189 @@ +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 + +/** Linux options **/ +export interface ILinuxAMDRPM { + name: 'LINUX'; + architecture: 'RPM amd64'; +} + +export interface ILinuxAARCHRPM { + name: 'LINUX'; + architecture: 'RPM aarch64'; +} + +export interface ILinuxAMDDEB { + name: 'LINUX'; + architecture: 'DEB amd64'; +} + +export interface ILinuxAARCHDEB { + name: 'LINUX'; + architecture: 'DEB aarch64'; +} + +type ILinuxOSTypes = + | ILinuxAMDRPM + | ILinuxAARCHRPM + | ILinuxAMDDEB + | ILinuxAARCHDEB; + +/** Windows options **/ +export interface IWindowsOSTypes { + name: 'WINDOWS'; + architecture: 'MSI 32/64 bits'; +} + +/** MacOS options **/ +export interface IMacOSIntel { + name: 'macOS'; + architecture: 'Intel'; +} + +export interface IMacOSApple { + name: 'macOS'; + architecture: 'Apple silicon'; +} + +type IMacOSTypes = IMacOSApple | IMacOSIntel; + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +export type tOptionalParameters = + | 'serverAddress' + | 'agentName' + | 'agentGroups' + | 'wazuhPassword' + | 'protocol'; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +const linuxDefinition: IOSDefinition = { + name: 'LINUX', + options: [ + { + architecture: 'DEB amd64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_${props.wazuhVersion}-1_amd64.deb`, + installCommand: props => 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_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 => getRPMInstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + { + architecture: 'RPM aarch64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64.rpm`, + installCommand: props => getRPMInstallCommand(props), + startCommand: props => getLinuxStartCommand(props), + }, + ], +}; + +const windowsDefinition: IOSDefinition = { + name: 'WINDOWS', + options: [ + { + architecture: 'MSI 32/64 bits', + urlPackage: props => + `https://packages.wazuh.com/4.x/windows/wazuh-agent-${props.wazuhVersion}-1.msi`, + installCommand: props => getWindowsInstallCommand(props), + startCommand: props => getWindowsStartCommand(props), + }, + ], +}; + +const macDefinition: IOSDefinition = { + name: 'macOS', + options: [ + { + architecture: 'Intel', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.intel64.pkg`, + installCommand: props => getMacOsInstallCommand(props), + startCommand: props => getMacosStartCommand(props), + }, + { + architecture: 'Apple silicon', + urlPackage: props => + `https://packages.wazuh.com/4.x/macos/wazuh-agent-${props.wazuhVersion}-1.arm64.pkg`, + installCommand: props => getMacOsInstallCommand(props), + startCommand: props => getMacosStartCommand(props), + }, + ], +}; + +export const osCommandsDefinitions = [ + linuxDefinition, + windowsDefinition, + macDefinition, +]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = { + serverAddress: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + 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}'` : ''; + }, + }, + protocol: { + property: 'WAZUH_PROTOCOL', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, + wazuhPassword: { + property: 'WAZUH_REGISTRATION_PASSWORD', + getParamCommand: props => { + const { property, value } = props; + return value !== '' ? `${property}='${value}'` : ''; + }, + }, +}; diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/README.md b/plugins/main/public/controllers/register-agent/core/register-commands/README.md new file mode 100644 index 0000000000..02e7600561 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/README.md @@ -0,0 +1,327 @@ +# 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; +} + +/// .... + +interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; // add the necessary OS options + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +export interface IOSCommandsDefinition { + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryProps & { urlPackage: string }) => string; + startCommand: (props: tOSEntryProps) => string; +} + +``` + +This configuration will define the different OS that we want to support and the different packages that we want to support for each OS. The `urlPackage` function will be used to generate the URL to download the package, the `installCommand` function will be used to generate the command to install the package and the `startCommand` function will be used to generate the command to start the agent. + +### Configuration example + +```ts + +const osDefinitions: IOSDefinition[] = [{ + name: 'linux', + options: [ + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + { + architecture: 'amd64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + } + ], +}, +{ + name: 'windows', + options: [ + { + architecture: '32/64', + urlPackage: props => 'add url package', + installCommand: props => 'add install command', + startCommand: props => `add start command`, + }, + ], + } +}; +``` + +## Validations + +The `Command Generator` will validate the OS Definitions received and will throw an error if the configuration is not valid. The validations are the following: + +- The OS Definitions must not have duplicated OS names defined. +- The OS Definitions must not have duplicated options defined. +- The OS names would be defined in the `tOS` type. +- The Package Extensions would be defined in the `tPackageExtensions` type. + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Optional Parameters Configuration + +The optional parameters are a set of parameters that will be added to the registration commands. The parameters are the following: + +```ts + +export type tOptionalParamsName = + | 'server_address' + | 'agent_name' + | 'protocol' + | 'agent_group' + | 'wazuh_password'; + +export type tOptionalParams = { + [key in tOptionalParamsName]: { + property: string; + getParamCommand: (props) => string; + }; +} + +``` + +This configuration will define the different optional parameters that we want to support and the way how to we will process and show in the commands.The `getParamCommand` is the function that will process the props received and show the final command format. + +### Configuration example + +```ts + +export const optionalParameters: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => 'returns the optional param command' + } + }, + any_other: { + property: 'PARAM NAME IN THE COMMAND', + getParamCommand: props => 'returns the optional param command' + }, +} + +``` + +## Validations + +The `Command Generator` will validate the Optional Parameters received and will throw an error if the configuration is not valid. The validations are the following: + +- The Optional Parameters must not have duplicated names defined. +- The Optional Parameters name would be defined in the `tOptionalParamsName` type. + + +Another validations will be provided in development time and will be provided by the types added to the project. You can find the types definitions in the `types` file. + + +## Command Generator + +To use the command generator we need to import the class and create a new instance of the class. The class will receive the OS Definitions and the Optional Parameters as parameters. + +```ts +import { CommandGenerator } from 'path/command-generator'; + +// Commange Generator interface/contract + +export interface ICommandGenerator extends ICommandGeneratorMethods { + osDefinitions: IOSDefinition[]; + wazuhVersion: string; +} + +export interface ICommandGeneratorMethods { + selectOS(params: IOperationSystem): void; + addOptionalParams(props: IOptionalParameters): void; + getInstallCommand(): string; + getStartCommand(): string; + getUrlPackage(): string; + getAllCommands(): ICommandsResponse; +} + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +``` + +When the class is created the definitions provided will be validated and if the configuration is not valid an error will be thrown. The errors were mentioned in the configurations `Validations` section. + +### Get install command + +To generate the install command we need to call the `getInstallComand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', +}); + +// get install command +const installCommand = commandGenerator.getInstallCommand(); + +``` + +The `Command Generator` will search the OS provided and search in the OS Definitions and will process the command using the `installCommand` function defined in the OS Definition. If the OS is not found an error will be thrown. +If the `getInstallCommand` but the OS is not selected an error will be thrown. + +## Get start command + +To generate the install command we need to call the `getStartCommand` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', +}); + +// get start command +const installCommand = commandGenerator.getStartCommand(); + +``` + +## Get url package + +To generate the install command we need to call the `getUrlPackage` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested command. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', +}); + +const urlPackage = commandGenerator.getUrlPackage(); + +``` + +## Get all commands + +To generate the install command we need to call the `getAllCommands` function. To perform this function the `Command Generator` must receive the OS name and/or the optional parameters as parameters before. The function will return the requested commands. + +```ts + +import { CommandGenerator } from 'path/command-generator'; + +const commandGenerator = new CommandGenerator(osDefinitions, optionalParameters); + +// specify to the command generator the OS that we want to use +commandGenerator.selectOS({ + name: 'linux', + architecture: 'amd64', +}); + +// specify to the command generator the optional parameters that we want to use +commandGenerator.addOptionalParams({ + server_address: 'server-ip', + agent_name: 'agent-name', + any_parameter: 'any-value' +}); + +// get all commands +const installCommand = commandGenerator.getAllCommands(); + +``` + +If we specify the optional parameters the `Command Generator` will process the commands and will add the optional parameters to the commands. The optional parameters processing will be only applied to the commands that have the optional parameters defined in the Optional Parameters Definitions. If the OS Definition does not have the optional parameters defined the `Command Generator` will ignore the optional parameters. + +### getAllComands output + +```ts + +export interface ICommandsResponse { + wazuhVersion: string; + os: string; + architecture: string; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} + +``` \ No newline at end of file diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts new file mode 100644 index 0000000000..ae9af6d24e --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.test.ts @@ -0,0 +1,380 @@ +import { CommandGenerator } from './command-generator'; +import { + IOSDefinition, + IOptionalParameters, + tOptionalParams, +} from '../types'; +import { DuplicatedOSException, DuplicatedOSOptionException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; + +const mockedCommandValue = 'mocked command'; +const mockedCommandsResponse = jest.fn().mockReturnValue(mockedCommandValue); + +// Defined OS combinations +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +// Defined Optional Parameters + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x86', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, +]; + +const optionalParams: tOptionalParams = { + server_address: { + property: 'WAZUH_MANAGER', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_name: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => `${props.property}=${props.value}`, + }, + protocol: { + property: 'WAZUH_MANAGER_PROTOCOL', + getParamCommand: props => `${props.property}=${props.value}`, + }, + agent_group: { + property: 'WAZUH_AGENT_GROUP', + getParamCommand: props => `${props.property}=${props.value}`, + }, + wazuh_password: { + property: 'WAZUH_PASSWORD', + getParamCommand: props => `${props.property}=${props.value}`, + }, +}; + +const optionalValues: IOptionalParameters = { + server_address: '', + agent_name: '', + protocol: '', + agent_group: '', + wazuh_password: '', +}; + +describe('Command Generator', () => { + it('should create an valid instance', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + expect(commandGenerator).toBeDefined(); + }); + + it('should return the install command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getInstallCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return the start command for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + const command = commandGenerator.getStartCommand(); + expect(command).toBe(mockedCommandValue); + }); + + it('should return all the commands for the os selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + commandGenerator.selectOS({ + name: 'linux', + architecture: 'x64', + }); + commandGenerator.addOptionalParams(optionalValues); + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: 'linux', + architecture: 'x64', + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: {}, + }); + }); + + it('should return commands with the filled optional params', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: '10.10.10.121', + agent_name: 'agent1', + protocol: 'tcp', + agent_group: '', + wazuh_password: '123456', + }; + commandGenerator.addOptionalParams(optionalValues); + + const commands = commandGenerator.getAllCommands(); + expect(commands).toEqual({ + os: selectedOs.name, + architecture: selectedOs.architecture, + wazuhVersion: '4.4', + install_command: mockedCommandValue, + start_command: mockedCommandValue, + url_package: mockedCommandValue, + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + agent_name: optionalParams.agent_name.getParamCommand({ + property: optionalParams.agent_name.property, + value: optionalValues.agent_name, + name: 'agent_name', + }), + protocol: optionalParams.protocol.getParamCommand({ + property: optionalParams.protocol.property, + value: optionalValues.protocol, + name: 'protocol', + }), + wazuh_password: optionalParams.wazuh_password.getParamCommand({ + property: optionalParams.wazuh_password.property, + value: optionalValues.wazuh_password, + name: 'wazuh_password', + }), + }, + }); + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSOptionException); + } + }); + + it('should return an ERROR when the os definitions received has a os with options duplicated', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedCommandsResponse, + startCommand: mockedCommandsResponse, + urlPackage: mockedCommandsResponse, + }, + ], + }, + ]; + + try { + new CommandGenerator(osDefinitions, optionalParams, '4.4'); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(DuplicatedOSException); + } + }); + + it('should return an ERROR when we want to get commands and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getAllCommands(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the install command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getInstallCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when we want to get the start command and the os is not selected', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + try { + commandGenerator.getStartCommand(); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(NoOSSelectedException); + } + }); + + it('should return an ERROR when receive an empty version', () => { + try { + new CommandGenerator(osDefinitions, optionalParams, ''); + } catch (error) { + if (error instanceof Error) + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + + it('should receives the solved optional params when the install command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getInstallCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); + + it('should receives the solved optional params when the start command is called', () => { + const commandGenerator = new CommandGenerator( + osDefinitions, + optionalParams, + '4.4', + ); + + const selectedOs: tOperatingSystem = { + name: 'linux', + architecture: 'x64', + }; + commandGenerator.selectOS(selectedOs); + + const optionalValues = { + server_address: 'wazuh-ip', + }; + + commandGenerator.addOptionalParams(optionalValues as IOptionalParameters); + commandGenerator.getStartCommand(); + expect(mockedCommandsResponse).toHaveBeenCalledWith( + expect.objectContaining({ + optionals: { + server_address: optionalParams.server_address.getParamCommand({ + property: optionalParams.server_address.property, + value: optionalValues.server_address, + name: 'server_address', + }), + }, + }), + ); + }); +}); \ No newline at end of file diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts new file mode 100644 index 0000000000..6974478eac --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/command-generator/command-generator.ts @@ -0,0 +1,171 @@ +import { + ICommandsResponse, + IOSCommandsDefinition, + IOSDefinition, + IOperationSystem, + IOptionalParameters, + IOptionalParametersManager, + tOptionalParams, +} from '../types'; +import { ICommandGenerator } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from '../services/search-os-definitions.service'; +import { OptionalParametersManager } from '../optional-parameters-manager/optional-parameters-manager'; +import { NoArchitectureSelectedException, NoOSSelectedException, WazuhVersionUndefinedException } from '../exceptions'; +import { version } from '../../../../../../package.json'; + +export class CommandGenerator 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 } = params; + if (!name) { + throw new NoOSSelectedException(); + } + if (!architecture) { + throw new NoArchitectureSelectedException(); + } + + const option = searchOSDefinitions(this.osDefinitions, { + name, + architecture, + }); + return option; + } + + /** + * This method gets the URL package for the selected operating system + * @returns The URL package for the selected operating system + * @throws An error if the operating system is not selected + */ + getUrlPackage(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + return this.osDefinitionSelected.urlPackage({ + wazuhVersion: this.wazuhVersion, + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + name: this.os as OS['name'], + }); + } + + /** + * This method gets the install command for the selected operating system + * @returns The install command for the selected operating system + * @throws An error if the operating system is not selected + */ + getInstallCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.installCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + urlPackage: this.getUrlPackage(), + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets the start command for the selected operating system + * @returns The start command for the selected operating system + * @throws An error if the operating system is not selected + */ + getStartCommand(): string { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return this.osDefinitionSelected.startCommand({ + name: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + wazuhVersion: this.wazuhVersion, + optionals: this.optionals as IOptionalParameters, + }); + } + + /** + * This method gets all the commands for the selected operating system + * @returns An object containing all the commands for the selected operating system + * @throws An error if the operating system is not selected + */ + getAllCommands(): ICommandsResponse { + if (!this.osDefinitionSelected) { + throw new NoOSSelectedException(); + } + + return { + wazuhVersion: this.wazuhVersion, + os: this.os as OS['name'], + architecture: this.osDefinitionSelected.architecture as OS['architecture'], + url_package: this.getUrlPackage(), + install_command: this.getInstallCommand(), + start_command: this.getStartCommand(), + optionals: this.optionals, + }; + } + + /** + * Returns the optional paramaters processed + * @returns optionals + */ + getOptionalParamsCommands(): IOptionalParameters | object { + return this.optionals; + } + +} diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/exceptions/index.ts b/plugins/main/public/controllers/register-agent/core/register-commands/exceptions/index.ts new file mode 100644 index 0000000000..8ecf6d1be6 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/exceptions/index.ts @@ -0,0 +1,80 @@ +export class NoOptionFoundException extends Error { + constructor(osName: string, architecture: string) { + super( + `No OS option found for "${osName}" "${architecture}". Please check the OS definitions."`, + ); + } +} + +export class NoOSOptionFoundException extends Error { + constructor(osName: string) { + super( + `No OS option found for "${osName}". Please check the OS definitions."`, + ); + } +} + +export class NoStartCommandDefinitionException extends Error { + constructor(osName: string, architecture: string) { + super( + `No start command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, + ); + } +} + +export class NoInstallCommandDefinitionException extends Error { + constructor(osName: string, architecture: string) { + super( + `No install command definition found for "${osName}" "${architecture}". Please check the OS definitions.`, + ); + } +} + +export class NoPackageURLDefinitionException extends Error { + constructor(osName: string, architecture: string) { + super( + `No package URL definition found for "${osName}" "${architecture}". Please check the OS definitions.`, + ); + } +} + +export class NoOptionalParamFoundException extends Error { + constructor(paramName: string) { + super( + `Optional parameter "${paramName}" not found. Please check the optional parameters definitions.`, + ); + } +} + +export class DuplicatedOSException extends Error { + constructor(osName: string) { + super(`Duplicate OS name found: ${osName}`); + } +} + +export class DuplicatedOSOptionException extends Error { + constructor(osName: string, architecture: string) { + super( + `Duplicate OS option found for "${osName}" "${architecture}"`, + ); + } +} + +export class WazuhVersionUndefinedException extends Error { + constructor() { + super(`Wazuh version not defined`); + } +} + +export class NoOSSelectedException extends Error { + constructor() { + super(`OS not selected. Please select`); + } +} + +export class NoArchitectureSelectedException extends Error { + constructor() { + super(`Architecture not selected. Please select`); + } +} + diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.test.ts b/plugins/main/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/plugins/main/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/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts b/plugins/main/public/controllers/register-agent/core/register-commands/optional-parameters-manager/optional-parameters-manager.ts new file mode 100644 index 0000000000..34b943f797 --- /dev/null +++ b/plugins/main/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/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts b/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts new file mode 100644 index 0000000000..a4d16fcf32 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.test.ts @@ -0,0 +1,112 @@ +import { getInstallCommandByOS } from './get-install-command.service'; +import { IOSCommandsDefinition, IOSDefinition, IOptionalParameters } from '../types'; +import { + NoInstallCommandDefinitionException, + NoPackageURLDefinitionException, + WazuhVersionUndefinedException, +} from '../exceptions'; + + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + + +export type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password' | 'another_optional_parameter'; + +const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + installCommand: props => 'install command mocked', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', +}; +describe('getInstallCommandByOS', () => { + it('should return the correct install command for each OS', () => { + const installCommand = getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + expect(installCommand).toBe('install command mocked'); + }); + + it('should return ERROR when the version is not received', () => { + try { + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(WazuhVersionUndefinedException); + } + }); + it('should return ERROR when the OS has no install command', () => { + // @ts-ignore + const osDefinition: IOSCommandsDefinition = { + architecture: 'x64', + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + try { + getInstallCommandByOS( + osDefinition, + 'https://package-url.com', + '4.4', + 'linux', + ); + } catch (error) { + expect(error).toBeInstanceOf(NoInstallCommandDefinitionException); + } + }); + it('should return ERROR when the OS has no package url', () => { + try { + getInstallCommandByOS(validOsDefinition, '', '4.4', 'linux'); + } catch (error) { + expect(error).toBeInstanceOf(NoPackageURLDefinitionException); + } + }); + + it('should return install command with optional parameters', () => { + const mockedInstall = jest.fn(); + const validOsDefinition: IOSCommandsDefinition = { + architecture: 'x64', + installCommand: mockedInstall, + startCommand: props => 'start command mocked', + urlPackage: props => 'https://package-url.com', + }; + + const optionalParams: IOptionalParameters = { + agent_group: 'WAZUH_GROUP=agent_group', + agent_name: 'WAZUH_NAME=agent_name', + protocol: 'WAZUH_PROTOCOL=UDP', + server_address: 'WAZUH_MANAGER=server_address', + wazuh_password: 'WAZUH_PASSWORD=1231323', + another_optional_parameter: 'params value' + }; + + getInstallCommandByOS( + validOsDefinition, + 'https://package-url.com', + '4.4', + 'linux', + optionalParams + ); + expect(mockedInstall).toBeCalledTimes(1); + expect(mockedInstall).toBeCalledWith(expect.objectContaining({ optionals: optionalParams })); + }) +}); diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts b/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts new file mode 100644 index 0000000000..c8fabc3ebf --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/services/get-install-command.service.ts @@ -0,0 +1,37 @@ +import { NoInstallCommandDefinitionException, NoPackageURLDefinitionException, WazuhVersionUndefinedException } from "../exceptions"; +import { IOSCommandsDefinition, IOperationSystem, IOptionalParameters } from "../types"; + +/** + * Returns the installation command for a given operating system. + * @param {IOSCommandsDefinition} osDefinition - The definition of the operating system. + * @param {string} packageUrl - The URL of the package to install. + * @param {string} version - The version of Wazuh to install. + * @param {string} osName - The name of the operating system. + * @param {IOptionalParameters} [optionals] - Optional parameters to include in the command. + * @returns {string} The installation command for the given operating system. + * @throws {NoInstallCommandDefinitionException} If the installation command is not defined for the given operating system. + * @throws {NoPackageURLDefinitionException} If the package URL is not defined. + * @throws {WazuhVersionUndefinedException} If the Wazuh version is not defined. + */ +export function getInstallCommandByOS(osDefinition: IOSCommandsDefinition, packageUrl: string, version: string, osName: string, optionals?: IOptionalParameters) { + + if (!osDefinition.installCommand) { + throw new NoInstallCommandDefinitionException(osName, osDefinition.architecture); + } + + if(!packageUrl || packageUrl === ''){ + throw new NoPackageURLDefinitionException(osName, osDefinition.architecture); + } + + if(!version || version === ''){ + throw new WazuhVersionUndefinedException(); + } + + return osDefinition.installCommand({ + urlPackage: packageUrl, + wazuhVersion: version, + name: osName as OS['name'], + architecture: osDefinition.architecture, + optionals, + }); +} \ No newline at end of file diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts b/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts new file mode 100644 index 0000000000..73412b9fdb --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.test.ts @@ -0,0 +1,176 @@ +import { + NoOSOptionFoundException, +} from '../exceptions'; +import { IOSDefinition } from '../types'; +import { + searchOSDefinitions, + validateOSDefinitionHasDuplicatedOptions, + validateOSDefinitionsDuplicated, +} from './search-os-definitions.service'; + +const mockedInstallCommand = (props: any) => 'install command mocked'; +const mockedStartCommand = (props: any) => 'start command mocked'; +const mockedUrlPackage = (props: any) => 'https://package-url.com'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const validOSDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, +]; + +describe('search OS definitions services', () => { + describe('searchOSDefinitions', () => { + it('should return the OS definition if the OS name is found', () => { + const result = searchOSDefinitions(validOSDefinitions, { + name: 'linux', + architecture: 'x64', + }); + expect(result).toMatchObject(validOSDefinitions[0].options[0]); + }); + + it('should throw an error if the OS name is not found', () => { + expect(() => + searchOSDefinitions(validOSDefinitions, { + // @ts-ignore + name: 'invalid-os', + architecture: 'x64', + }), + ).toThrow(NoOSOptionFoundException); + }); + + }); + + describe('validateOSDefinitionsDuplicated', () => { + it('should not throw an error if there are no duplicated OS definitions', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'windows', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionsDuplicated(osDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions', () => { + const osDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + architecture: 'x64', + // @ts-ignore + packageManager: 'aix', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }; + const osDefinitions: IOSDefinition[] = [osDefinition, osDefinition]; + + expect(() => validateOSDefinitionsDuplicated(osDefinitions)).toThrow(); + }); + }); + + describe('validateOSDefinitionHasDuplicatedOptions', () => { + it('should not throw an error if there are no duplicated OS definitions with different options', () => { + expect(() => + validateOSDefinitionHasDuplicatedOptions(validOSDefinitions), + ).not.toThrow(); + }); + + it('should throw an error if there are duplicated OS definitions with different options', () => { + const osDefinitions: IOSDefinition[] = [ + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + { + name: 'linux', + options: [ + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + { + architecture: 'x64', + installCommand: mockedInstallCommand, + startCommand: mockedStartCommand, + urlPackage: mockedUrlPackage, + }, + ], + }, + ]; + + expect(() => + validateOSDefinitionHasDuplicatedOptions(osDefinitions), + ).toThrow(); + }); + }); +}); diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts b/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts new file mode 100644 index 0000000000..0ceefada65 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/services/search-os-definitions.service.ts @@ -0,0 +1,84 @@ +import { + DuplicatedOSException, + DuplicatedOSOptionException, + NoOSOptionFoundException, + NoOptionFoundException, +} from '../exceptions'; +import { IOSDefinition, IOperationSystem } from '../types'; + +/** + * Searches for the OS definition option that matches the given operation system parameters. + * Throws an exception if no matching option is found. + * + * @param osDefinitions - The list of OS definitions to search through. + * @param params - The operation system parameters to match against. + * @returns The matching OS definition option. + * @throws NoOSOptionFoundException - If no matching OS definition is found. + */ +export function searchOSDefinitions( + osDefinitions: IOSDefinition[], + params: IOperationSystem, +){ + const { name, architecture } = params; + + const osDefinition = osDefinitions.find(os => os.name === name); + if (!osDefinition) { + throw new NoOSOptionFoundException(name); + } + + const osDefinitionOption = osDefinition.options.find( + option => + option.architecture === architecture, + ); + + if (!osDefinitionOption) { + throw new NoOptionFoundException(name, architecture); + } + + return osDefinitionOption; +}; + +/** + * Validates that there are no duplicated OS definitions in the given list. + * Throws an exception if a duplicated OS definition is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSException - If a duplicated OS definition is found. + */ +export function validateOSDefinitionsDuplicated( + osDefinitions: IOSDefinition[], +){ + const osNames = new Set(); + + for (const osDefinition of osDefinitions) { + if (osNames.has(osDefinition.name)) { + throw new DuplicatedOSException(osDefinition.name); + } + osNames.add(osDefinition.name); + } +}; + +/** + * Validates that there are no duplicated OS definition options in the given list. + * Throws an exception if a duplicated OS definition option is found. + * + * @param osDefinitions - The list of OS definitions to validate. + * @throws DuplicatedOSOptionException - If a duplicated OS definition option is found. + */ +export function validateOSDefinitionHasDuplicatedOptions( + osDefinitions: IOSDefinition[], +){ + for (const osDefinition of osDefinitions) { + const options = new Set(); + for (const option of osDefinition.options) { + let ext_arch_manager = `${option.architecture}`; + if (options.has(ext_arch_manager)) { + throw new DuplicatedOSOptionException( + osDefinition.name, + option.architecture, + ); + } + options.add(ext_arch_manager); + } + } +}; diff --git a/plugins/main/public/controllers/register-agent/core/register-commands/types.ts b/plugins/main/public/controllers/register-agent/core/register-commands/types.ts new file mode 100644 index 0000000000..243477a999 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/core/register-commands/types.ts @@ -0,0 +1,96 @@ +///////////////////////////////////////////////////////// +/// Domain +///////////////////////////////////////////////////////// +export interface IOperationSystem { + name: string; + architecture: string; +} + +export type IOptionalParameters = { + [key in Params]: string; +}; + +/////////////////////////////////////////////////////////////////// +/// Operating system commands definitions +/////////////////////////////////////////////////////////////////// + +export interface IOSDefinition { + name: OS['name']; + options: IOSCommandsDefinition[]; +} + +interface IOptionalParamsWithValues { + optionals?: IOptionalParameters +} + + +export type tOSEntryProps = IOSProps & IOptionalParamsWithValues; +export type tOSEntryInstallCommand = tOSEntryProps & { urlPackage: string }; + +export interface IOSCommandsDefinition { + architecture: OS['architecture']; + urlPackage: (props: tOSEntryProps) => string; + installCommand: (props: tOSEntryInstallCommand) => string; + startCommand: (props: tOSEntryProps) => string; +} + +export interface IOSProps extends IOperationSystem { + wazuhVersion: string; +} + +/////////////////////////////////////////////////////////////////// +//// Commands optional parameters +/////////////////////////////////////////////////////////////////// +interface IOptionalParamProps { + property: string; + value: string; +} + +export type tOptionalParamsCommandProps = IOptionalParamProps & { + name: T; +}; +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOptionalParamInput { + value: any; + name: T; +} +export interface IOptionalParametersManager { + getOptionalParam(props: IOptionalParamInput): string; + getAllOptionalParams(paramsValues: IOptionalParameters): 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; + url_package: string; + install_command: string; + start_command: string; + optionals: IOptionalParameters | object; +} diff --git a/plugins/main/public/controllers/register-agent/hooks/README.md b/plugins/main/public/controllers/register-agent/hooks/README.md new file mode 100644 index 0000000000..d3ec96adc1 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/hooks/README.md @@ -0,0 +1,167 @@ +# Documentation + +- [useRegisterAgentCommand hook](#useregisteragentcommand-hook) +- [Advantages](#advantages) +- [Usage](#usage) +- [Types](#types) + - [Hook props](#hook-props) + - [Hook output](#hook-output) +- [Hook with Generic types](#hook-with-generic-types) + - [Operating systems types example](#operating-systems-types-example) + +## useRegisterAgentCommand hook + +This hook makes use of the `Command Generator class` to generate the commands to register agents in the manager and allows to use it in React components. + +## Advantages + +- Ease of use of the Command Generator class. +- The hook returns the methods envolved to create the register commands by the operating system and optionas specified. +- The commands generate are stored in the state of the hook and can be used in the component. + + +## Usage + +```ts + +import { useRegisterAgentCommands } from 'path/to/use-register-agent-commands'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + +/* + the props recived by the hook must implement types: + - OS: IOSDefinition[] + - optional parameters: tOptionalParams +*/ + +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(); + +// select OS depending on the specified OS defined in the hook configuration +selectOS({ + name: 'name-OS', + architecture: 'architecture-OS', +}) + +// add optionals params depending on the specified optional parameters in the hook configuration +setOptionalParams({ + field_1: 'value_1', + field_2: 'value_2', + ... +}) + +/** the commands and the optional params will be processed and stored in the hook state **/ + +// install command +console.log('install command for the selected OS with optionals params', installCommand); +// start command +console.log('start command for the selected OS with optionals params', startCommand); +// optionals params processed +console.log('optionals params processed', optionalParamsParsed); + +``` + +## Types + +### Hook props + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; +} + +interface IUseRegisterCommandsProps { + osDefinitions: IOSDefinition[]; + optionalParamsDefinitions: tOptionalParams; +} +``` + +### Hook output + +```ts + +export interface IOperationSystem { + name: string; + architecture: string; +} + +interface IUseRegisterCommandsOutput { + selectOS: (params: OS) => void; + setOptionalParams: (params: IOptionalParameters) => void; + installCommand: string; + startCommand: string; + optionalParamsParsed: IOptionalParameters | {}; +} +``` + +## Hook with Generic types + +We can pass the types with the OS posibilities options and the optionals params defined. +And the hook will validate and show warning in compilation and development time. + +#### Operating systems types example + +```ts +// global types + +export interface IOptionsParamConfig { + property: string; + getParamCommand: (props: tOptionalParamsCommandProps) => string; +} + +export type tOptionalParams = { + [key in T]: IOptionsParamConfig; +}; + +export interface IOperationSystem { + name: string; + architecture: string; +} + +/// .... + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +type tOptionalParameters = 'server_address' | 'agent_name' | 'agent_group' | 'protocol' | 'wazuh_password'; + +import { OSdefintions, paramsDefinitions} from 'path/config/os-definitions'; + + +// pass it to the hook and it will use the types when we are selecting the OS +const { + selectOS, + setOptionalParams, + installCommand, + startCommand, + optionalParamsParsed + } = useRegisterAgentCommands(OSdefintions, paramsDefinitions); + +// when the options are not valid depending on the types defined, the IDE will show a warning +selectOS({ + name: 'linux', + architecture: 'x64', +}) + +```` + diff --git a/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts b/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts new file mode 100644 index 0000000000..20a4de7b32 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.test.ts @@ -0,0 +1,229 @@ +import React from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { useRegisterAgentCommands } from './use-register-agent-commands'; +import { + IOSDefinition, + tOptionalParams, +} from '../core/register-commands/types'; + +type tOptionalParamsNames = 'optional1' | 'optional2'; + +export interface ILinuxOSTypes { + name: 'linux'; + architecture: 'x64' | 'x86'; +} +export interface IWindowsOSTypes { + name: 'windows'; + architecture: 'x86'; +} + +export interface IMacOSTypes { + name: 'mac'; + architecture: '32/64'; +} + +export type tOperatingSystem = ILinuxOSTypes | IMacOSTypes | IWindowsOSTypes; + +const linuxDefinition: IOSDefinition = { + name: 'linux', + options: [ + { + architecture: '32/64', + urlPackage: props => + `https://packages.wazuh.com/4.x/yum/wazuh-agent-${props.wazuhVersion}-1.x86_64`, + installCommand: props => `sudo yum install -y ${props.urlPackage}`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + { + architecture: 'x64', + urlPackage: props => + `https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/ wazuh-agent_${props.wazuhVersion}-1_${props.architecture}`, + installCommand: props => + `curl -so wazuh-agent.deb ${props.urlPackage} && sudo dpkg -i ./wazuh-agent.deb`, + startCommand: props => `sudo systemctl start wazuh-agent`, + }, + ], +}; + +export const osCommandsDefinitions = [linuxDefinition]; + +/////////////////////////////////////////////////////////////////// +/// Optional parameters definitions +/////////////////////////////////////////////////////////////////// + +export const optionalParamsDefinitions: tOptionalParams = + { + optional1: { + property: 'WAZUH_MANAGER', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + optional2: { + property: 'WAZUH_AGENT_NAME', + getParamCommand: props => { + const { property, value } = props; + return `${property}=${value}`; + }, + }, + }; + +describe('useRegisterAgentCommands hook', () => { + it('should return installCommand and startCommand null when the hook is initialized', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return ERROR when get installCommand and the OS received is NOT valid', () => { + const { + result: { + current: { selectOS }, + }, + } = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + try { + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + }); + }); + } catch (error) { + if (error instanceof Error) + expect(error.message).toContain('No OS option found for'); + } + }); + + it('should change the commands when the OS is selected successfully', async () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS } = hook.result.current; + const { result } = hook; + + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + }); + }); + expect(result.current.installCommand).not.toBe(''); + expect(result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(1); + expect(spyStart).toBeCalledTimes(1); + }); + + it('should return commands empty when set optional params and OS is NOT selected', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).toBe(''); + expect(hook.result.current.startCommand).toBe(''); + }); + + it('should return optional params empty when optional params are not added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { optionalParamsParsed } = hook.result.current; + expect(optionalParamsParsed).toEqual({}); + }); + + it('should return optional params when optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { setOptionalParams } = hook.result.current; + const spy1 = jest.spyOn( + optionalParamsDefinitions.optional1, + 'getParamCommand', + ); + const spy2 = jest.spyOn( + optionalParamsDefinitions.optional2, + 'getParamCommand', + ); + act(() => { + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(spy1).toBeCalledTimes(1); + expect(spy2).toBeCalledTimes(1); + }); + + it('should update the commands when the OS is selected and optional params are added', () => { + const hook = renderHook(() => + useRegisterAgentCommands({ + osDefinitions: osCommandsDefinitions, + optionalParamsDefinitions: optionalParamsDefinitions, + }), + ); + const { selectOS, setOptionalParams } = hook.result.current; + const optionSelected = osCommandsDefinitions + .find(os => os.name === 'linux') + ?.options.find( + item => item.architecture === 'x64', + ); + const spyInstall = jest.spyOn(optionSelected!, 'installCommand'); + const spyStart = jest.spyOn(optionSelected!, 'startCommand'); + + act(() => { + selectOS({ + name: 'linux', + architecture: 'x64', + }); + + setOptionalParams({ + optional1: 'value 1', + optional2: 'value 2', + }); + }); + + expect(hook.result.current.installCommand).not.toBe(''); + expect(hook.result.current.startCommand).not.toBe(''); + expect(spyInstall).toBeCalledTimes(2); + expect(spyStart).toBeCalledTimes(2); + }); +}); diff --git a/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts b/plugins/main/public/controllers/register-agent/hooks/use-register-agent-commands.ts new file mode 100644 index 0000000000..800c198039 --- /dev/null +++ b/plugins/main/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 + } +}; diff --git a/plugins/main/public/controllers/register-agent/index.tsx b/plugins/main/public/controllers/register-agent/index.tsx new file mode 100644 index 0000000000..146589950a --- /dev/null +++ b/plugins/main/public/controllers/register-agent/index.tsx @@ -0,0 +1 @@ +export { RegisterAgent } from './containers/register-agent/register-agent'; diff --git a/plugins/main/public/controllers/register-agent/interfaces/types.ts b/plugins/main/public/controllers/register-agent/interfaces/types.ts new file mode 100644 index 0000000000..f9fe6c02fc --- /dev/null +++ b/plugins/main/public/controllers/register-agent/interfaces/types.ts @@ -0,0 +1,18 @@ +import { tOperatingSystem } from '../config/os-commands-definitions'; + +interface RegisterAgentData { + icon: string; + title: tOperatingSystem['name']; + hr: boolean; + architecture: tOperatingSystem['architecture'][] +} + +interface CheckboxGroupComponentProps { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +export type { RegisterAgentData, CheckboxGroupComponentProps }; diff --git a/plugins/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-os-commands-services.tsx b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx new file mode 100644 index 0000000000..77adb03712 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/services/register-agent-os-commands-services.tsx @@ -0,0 +1,138 @@ +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; + }, + '', + ); +}; + + +/******* 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 + +export const getRPMInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, wazuhVersion } = props; + const packageName = `wazuh-agent-${wazuhVersion}-1.x86_64.rpm` + return `curl -o ${packageName} ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }rpm -ihv ${packageName}`; +}; + +/******* 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, wazuhVersion } = props; + const packageName = `wazuh-agent_${wazuhVersion}-1_amd64.deb` + return `wget ${urlPackage} && sudo ${ + optionals && getAllOptionals(optionals) + }dpkg -i ./${packageName}`; +}; + +/******* Linux *******/ + +// Start command +export const getLinuxStartCommand = ( + _props: tOSEntryProps, +) => { + return `sudo systemctl daemon-reload\nsudo systemctl enable wazuh-agent\nsudo systemctl start wazuh-agent`; +}; + +/******** Windows ********/ + +export const getWindowsInstallCommand = ( + props: tOSEntryInstallCommand, +) => { + const { optionals, urlPackage, name } = props; + return `Invoke-WebRequest -Uri ${urlPackage} -OutFile \${env.tmp}\\wazuh-agent; msiexec.exe /i \${env.tmp}\\wazuh-agent /q ${ + optionals && getAllOptionals(optionals, name) + }`; +}; + +export const getWindowsStartCommand = ( + _props: tOSEntryProps, +) => { + return `NET START WazuhSvc`; +}; + +/******** MacOS ********/ + +export const 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/plugins/main/public/controllers/register-agent/services/register-agent-services.tsx b/plugins/main/public/controllers/register-agent/services/register-agent-services.tsx new file mode 100644 index 0000000000..8200224bb2 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/services/register-agent-services.tsx @@ -0,0 +1,295 @@ +import { UseFormReturn } from '../../../components/common/form/types'; +import { WzRequest } from '../../../react-services/wz-request'; +import { + tOperatingSystem, + tOptionalParameters, +} from '../core/config/os-commands-definitions'; +import { RegisterAgentData } from '../interfaces/types'; + +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: any, + 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): any[] => { + 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 => { + 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: any[]): any[] => { + 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); + } +}; + +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[], + initialValues?: IParseRegisterFormValues +) => { + // return the values form the formFields and the value property + const parsedForm = initialValues || { + 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/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 new file mode 100644 index 0000000000..0136b0ec2b --- /dev/null +++ b/plugins/main/public/controllers/register-agent/services/register-agent-steps-status-services.tsx @@ -0,0 +1,200 @@ +import { EuiStepStatus } from '@elastic/eui'; +import { UseFormReturn } from '../../../components/common/form/types'; +import { + FormStepsDependencies, + RegisterAgentFormStatusManager, +} from './form-status-manager'; + +const fieldsHaveErrors = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw Error('fields to check are not defined in formFields'); + } + + const haveError = fieldsToCheck.some(key => { + return formFields[key]?.error; + }); + return haveError; +}; + +const fieldsAreEmpty = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw Error('fields to check are not defined in formFields'); + } + + const notEmpty = fieldsToCheck.some(key => { + return formFields[key]?.value?.length > 0; + }); + return !notEmpty; +}; + +const anyFieldIsComplete = ( + fieldsToCheck: string[], + formFields: UseFormReturn['fields'], +) => { + if (!fieldsToCheck) { + return true; + } + // check if the fieldsToCheck array NOT exists in formFields and get the field doesn't exists + if (!fieldsToCheck.every(key => formFields[key])) { + throw Error('fields to check are not defined in formFields'); + } + + if (fieldsHaveErrors(fieldsToCheck, formFields)) { + return false; + } + + if (fieldsAreEmpty(fieldsToCheck, formFields)) { + return false; + } + + return true; +}; + + +export const showCommandsSections = ( + formFields: UseFormReturn['fields'], +): boolean => { + if ( + !formFields.operatingSystemSelection.value || + formFields.serverAddress.value === '' || + formFields.serverAddress.error + ) { + return false; + } else if ( + formFields.serverAddress.value === '' && + formFields.agentName.value === '' + ) { + return true; + } else if (!fieldsHaveErrors(['agentGroups', 'agentName'], formFields)) { + return 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'; + } +}; + +export const getPasswordStepStatus = ( + formFields: UseFormReturn['fields'], +): tFormStepsStatus => { + if ( + !formFields.operatingSystemSelection.value || + formFields.operatingSystemSelection.error || + !formFields.serverAddress.value || + formFields.serverAddress.error + ) { + return 'disabled'; + } else { + return 'complete'; + } +}; + +export enum tFormStepsLabel { + operatingSystemSelection = 'operating system', + serverAddress = 'server address', +} + +export const getIncompleteSteps = ( + formFields: UseFormReturn['fields'], +): tFormStepsLabel[] => { + const steps: FormStepsDependencies = { + operatingSystemSelection: ['operatingSystemSelection'], + serverAddress: ['serverAddress'], + }; + const statusManager = new RegisterAgentFormStatusManager(formFields, steps); + // replace fields array using label names + return statusManager.getIncompleteSteps().map(field => { + 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; + }); +}; 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 new file mode 100644 index 0000000000..378bf61d33 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/utils/register-agent-data.tsx @@ -0,0 +1,47 @@ +import { RegisterAgentData } from '../interfaces/types'; +import LinuxDarkIcon from '../../../../public/assets/images/themes/dark/linux-icon.svg'; +import LinuxLightIcon from '../../../../public/assets/images/themes/light/linux-icon.svg'; +import WindowsDarkIcon from '../../../../public/assets/images/themes/dark/windows-icon.svg'; +import WindowsLightIcon from '../../../../public/assets/images/themes/light/windows-icon.svg'; +import MacDarkIcon from '../../../../public/assets/images/themes/dark/mac-icon.svg'; +import MacLightIcon from '../../../../public/assets/images/themes/light/mac-icon.svg'; +import { getUiSettings } from '../../../kibana-services'; + +const darkMode = getUiSettings()?.get('theme:darkMode'); + +export const OPERATING_SYSTEMS_OPTIONS: RegisterAgentData[] = [ + { + icon: darkMode ? LinuxDarkIcon : LinuxLightIcon, + title: 'LINUX', + hr: true, + architecture: ['RPM amd64', 'RPM aarch64', 'DEB amd64', 'DEB aarch64'], + }, + { + icon: darkMode ? WindowsDarkIcon : WindowsLightIcon, + title: 'WINDOWS', + hr: true, + architecture: ['MSI 32/64 bits'], + }, + { + icon: darkMode ? MacDarkIcon : MacLightIcon, + title: 'macOS', + hr: true, + architecture: ['Intel', 'Apple silicon'], + }, +]; + +export const SERVER_ADDRESS_TEXTS = [ + { + title: 'Server address', + subtitle: + 'This is the address the agent uses to communicate with the Wazuh server. Enter an IP address or a fully qualified domain name (FDQN).', + }, +]; + +export const OPTIONAL_PARAMETERS_TEXT = [ + { + 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/plugins/main/public/controllers/register-agent/utils/validations.test.tsx b/plugins/main/public/controllers/register-agent/utils/validations.test.tsx new file mode 100644 index 0000000000..edd7c4658d --- /dev/null +++ b/plugins/main/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.'; + 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/plugins/main/public/controllers/register-agent/utils/validations.tsx b/plugins/main/public/controllers/register-agent/utils/validations.tsx new file mode 100644 index 0000000000..52705b5e53 --- /dev/null +++ b/plugins/main/public/controllers/register-agent/utils/validations.tsx @@ -0,0 +1,57 @@ +//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 = + /^(?!-)(?!.*--)(?!.*\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.]+$/; + 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: any) => { + 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 ''; +}; diff --git a/plugins/main/public/services/routes.js b/plugins/main/public/services/routes.js index 6b24043526..1f352ff20e 100644 --- a/plugins/main/public/services/routes.js +++ b/plugins/main/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/plugins/main/public/styles/theme/dark/index.dark.scss b/plugins/main/public/styles/theme/dark/index.dark.scss index 4972795ea7..c7277ec5fb 100644 --- a/plugins/main/public/styles/theme/dark/index.dark.scss +++ b/plugins/main/public/styles/theme/dark/index.dark.scss @@ -326,7 +326,6 @@ md-divider { } .wzMultipleSelectorSelect { - color: #98a2b3; background-color: #16171c; border: 1px solid rgb(52, 55, 65); } diff --git a/plugins/main/public/templates/agents-prev/agents-prev.html b/plugins/main/public/templates/agents-prev/agents-prev.html index cec8cb3637..573720fea4 100644 --- a/plugins/main/public/templates/agents-prev/agents-prev.html +++ b/plugins/main/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/plugins/main/public/templates/visualize/dashboards.html b/plugins/main/public/templates/visualize/dashboards.html index 30eba4a8d5..1dff0934c2 100644 --- a/plugins/main/public/templates/visualize/dashboards.html +++ b/plugins/main/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 @@
-
-
+
+
-
+
- +
diff --git a/plugins/main/public/utils/assets.ts b/plugins/main/public/utils/assets.ts index 12d02c6029..771139a85a 100644 --- a/plugins/main/public/utils/assets.ts +++ b/plugins/main/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');