From 81616caf96e0538d03200c69e90b228cfc1e977f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 19 Sep 2024 16:56:16 +0200 Subject: [PATCH 01/77] feat(http): create http frontend client - Create HTTP client based on old services - Create HTTP client request interceptor: request - Create HTTP client generic: GenericRequest - Create HTTP client server: WzRequest, ApiCheck and WzAuthentication - Enhance server API backend client See https://github.com/wazuh/wazuh-dashboard-plugins/pull/6995 - Rename ILogger type to Logger --- plugins/main/server/controllers/wazuh-api.ts | 13 +- .../common/services/configuration.ts | 4 +- plugins/wazuh-core/public/plugin.ts | 26 +- .../wazuh-core/public/services/http/README.md | 105 ++++ .../public/services/http/constants.ts | 5 + .../public/services/http/generic-client.ts | 127 +++++ .../public/services/http/http-client.ts | 66 +++ .../wazuh-core/public/services/http/index.ts | 2 + .../services/http/request-interceptor.ts | 84 +++ .../services/http/server-client.test.ts | 110 ++++ .../public/services/http/server-client.ts | 532 ++++++++++++++++++ .../wazuh-core/public/services/http/types.ts | 49 ++ plugins/wazuh-core/public/types.ts | 3 + .../public/utils/configuration-store.ts | 4 +- .../public/utils/dashboard-security.ts | 4 +- .../server/services/server-api-client.ts | 56 +- 16 files changed, 1157 insertions(+), 33 deletions(-) create mode 100644 plugins/wazuh-core/public/services/http/README.md create mode 100644 plugins/wazuh-core/public/services/http/constants.ts create mode 100644 plugins/wazuh-core/public/services/http/generic-client.ts create mode 100644 plugins/wazuh-core/public/services/http/http-client.ts create mode 100644 plugins/wazuh-core/public/services/http/index.ts create mode 100644 plugins/wazuh-core/public/services/http/request-interceptor.ts create mode 100644 plugins/wazuh-core/public/services/http/server-client.test.ts create mode 100644 plugins/wazuh-core/public/services/http/server-client.ts create mode 100644 plugins/wazuh-core/public/services/http/types.ts diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index a5c3772c8f..f16177223e 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -76,16 +76,9 @@ export class WazuhApiCtrl { } } } - let token; - if (context.wazuh_core.manageHosts.isEnabledAuthWithRunAs(idHost)) { - token = await context.wazuh.api.client.asCurrentUser.authenticate( - idHost, - ); - } else { - token = await context.wazuh.api.client.asInternalUser.authenticate( - idHost, - ); - } + const token = await context.wazuh.api.client.asCurrentUser.authenticate( + idHost, + ); let textSecure = ''; if (context.wazuh.server.info.protocol === 'https') { diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index fcb642e2bb..3355bb96a4 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'; import { formatLabelValuePair } from './settings'; import { formatBytes } from './file-size'; -export interface ILogger { +export interface Logger { debug(message: string): void; info(message: string): void; warn(message: string): void; @@ -180,7 +180,7 @@ export class Configuration implements IConfiguration { store: IConfigurationStore | null = null; _settings: Map; _categories: Map; - constructor(private logger: ILogger, store: IConfigurationStore) { + constructor(private logger: Logger, store: IConfigurationStore) { this._settings = new Map(); this._categories = new Map(); this.setStore(store); diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index ef08e41595..a006dff3a8 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -11,6 +11,7 @@ import { } from '../common/constants'; import { DashboardSecurity } from './utils/dashboard-security'; import * as hooks from './hooks'; +import { CoreHTTPClient } from './services/http/http-client'; export class WazuhCorePlugin implements Plugin @@ -19,12 +20,21 @@ export class WazuhCorePlugin services: { [key: string]: any } = {}; public async setup(core: CoreSetup): Promise { const noop = () => {}; - const logger = { + // Debug logger + const consoleLogger = { + info: console.log, + error: console.error, + debug: console.debug, + warn: console.warn, + }; + // No operation logger + const noopLogger = { info: noop, error: noop, debug: noop, warn: noop, }; + const logger = noopLogger; this._internal.configurationStore = new ConfigurationStore( logger, core.http, @@ -44,9 +54,22 @@ export class WazuhCorePlugin this.services.configuration.registerCategory({ ...value, id: key }); }); + // Create dashboardSecurity this.services.dashboardSecurity = new DashboardSecurity(logger, core.http); + // Create http + this.services.http = new CoreHTTPClient(logger, { + getTimeout: async () => + (await this.services.configuration.get('timeout')) as number, + getURL: (path: string) => core.http.basePath.prepend(path), + getServerAPI: () => 'api-host-id', // TODO: implement + getIndexPatternTitle: async () => 'wazuh-alerts-*', // TODO: implement + http: core.http, + }); + + // Setup services await this.services.dashboardSecurity.setup(); + await this.services.http.setup(); return { ...this.services, @@ -60,6 +83,7 @@ export class WazuhCorePlugin setCore(core); setUiSettings(core.uiSettings); + // Start services await this.services.configuration.start({ http: core.http }); return { diff --git a/plugins/wazuh-core/public/services/http/README.md b/plugins/wazuh-core/public/services/http/README.md new file mode 100644 index 0000000000..96b14e9807 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/README.md @@ -0,0 +1,105 @@ +# HTTPClient + +The `HTTPClient` provides a custom mechanim to do an API request to the backend side. + +This defines a request interceptor that disables the requests when `core.http` returns a response with status code 401, avoiding a problem in the login flow (with SAML). + +The request interceptor is used in the clients: + +- generic +- server + +## Generic + +This client provides a method to run the request that injects some properties related to an index pattern and selected server API host in the headers of the API request that could be used for some backend endpoints + +### Usage + +#### Request + +```ts +plugins.wazuhCore.http.request('GET', '/api/check-api', {}); +``` + +## Server + +This client provides: + +- some methods to communicate with the Wazuh server API +- manage authentication with Wazuh server API +- store the login data + +### Usage + +#### Authentication + +```ts +plugins.wazuhCore.http.auth(); +``` + +#### Unauthentication + +```ts +plugins.wazuhCore.http.unauth(); +``` + +#### Request + +```ts +plugins.wazuhCore.http.request('GET', '/agents', {}); +``` + +#### CSV + +```ts +plugins.wazuhCore.http.csv('GET', '/agents', {}); +``` + +#### Check API id + +```ts +plugins.wazuhCore.http.checkApiById('api-host-id'); +``` + +#### Check API + +```ts +plugins.wazuhCore.http.checkApi(apiHostData); +``` + +#### Get user data + +```ts +plugins.wazuhCore.http.getUserData(); +``` + +The changes in the user data can be retrieved thourgh the `userData$` observable. + +```ts +plugins.wazuhCore.http.userData$.subscribe(userData => { + // do something with the data +}); +``` + +### Register interceptor + +In each application when this is mounted through the `mount` method, the request interceptor must be registered and when the application is unmounted must be unregistered. + +> We should research about the possibility to register/unregister the interceptor once in the `wazuh-core` plugin instead of registering/unregisting in each mount of application. + +```ts +// setup lifecycle plugin method + +// Register an application +core.application.register({ + // rest of registration properties + mount: () => { + // Register the interceptor + plugins.wazuhCore.http.register(); + return () => { + // Unregister the interceptor + plugins.wazuhCore.http.unregister(); + }; + }, +}); +``` diff --git a/plugins/wazuh-core/public/services/http/constants.ts b/plugins/wazuh-core/public/services/http/constants.ts new file mode 100644 index 0000000000..8ea7ec6d05 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/constants.ts @@ -0,0 +1,5 @@ +export const PLUGIN_PLATFORM_REQUEST_HEADERS = { + 'osd-xsrf': 'kibana', +}; + +export const HTTP_CLIENT_DEFAULT_TIMEOUT = 20000; diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts new file mode 100644 index 0000000000..7d193b3599 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -0,0 +1,127 @@ +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import { Logger } from '../../../common/services/configuration'; +import { + HTTPClientGeneric, + HTTPClientRequestInterceptor, + HTTPVerb, +} from './types'; + +interface GenericRequestServices { + request: HTTPClientRequestInterceptor['request']; + getURL(path: string): string; + getTimeout(): Promise; + getIndexPatternTitle(): Promise; + getServerAPI(): string; + checkAPIById(apiId: string): Promise; +} + +export class GenericRequest implements HTTPClientGeneric { + onErrorInterceptor?: (error: any) => Promise; + constructor( + private logger: Logger, + private services: GenericRequestServices, + ) {} + async request( + method: HTTPVerb, + path: string, + payload = null, + returnError = false, + ) { + try { + if (!method || !path) { + throw new Error('Missing parameters'); + } + const timeout = await this.services.getTimeout(); + const requestHeaders = { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }; + const url = this.services.getURL(path); + + try { + requestHeaders.pattern = await this.services.getIndexPatternTitle(); + } catch (error) {} + + try { + requestHeaders.id = this.services.getServerAPI(); + } catch (error) { + // Intended + } + var options = {}; + + if (method === 'GET') { + options = { + method: method, + headers: requestHeaders, + url: url, + timeout: timeout, + }; + } + if (method === 'PUT') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + if (method === 'POST') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + if (method === 'DELETE') { + options = { + method: method, + headers: requestHeaders, + data: payload, + url: url, + timeout: timeout, + }; + } + + const data = await this.services.request(options); + if (!data) { + throw new Error(`Error doing a request to ${url}, method: ${method}.`); + } + + return data; + } catch (error) { + //if the requests fails, we need to check if the API is down + const currentApi = this.services.getServerAPI(); //JSON.parse(AppState.getCurrentAPI() || '{}'); + if (currentApi) { + try { + await this.services.checkAPIById(currentApi); + } catch (err) { + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + // if ( + // ['/settings', '/health-check', '/blank-screen'].every( + // pathname => + // !NavigationService.getInstance() + // .getPathname() + // .startsWith(pathname), + // ) + // ) { + // NavigationService.getInstance().navigate('/health-check'); + // } + } + } + // if(this.onErrorInterceptor){ + // await this.onErrorInterceptor(error) + // } + if (returnError) return Promise.reject(error); + return (((error || {}).response || {}).data || {}).message || false + ? Promise.reject(new Error(error.response.data.message)) + : Promise.reject(error || new Error('Server did not respond')); + } + } + setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise) { + this.onErrorInterceptor = onErrorInterceptor; + } +} diff --git a/plugins/wazuh-core/public/services/http/http-client.ts b/plugins/wazuh-core/public/services/http/http-client.ts new file mode 100644 index 0000000000..041574f958 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/http-client.ts @@ -0,0 +1,66 @@ +import { Logger } from '../../../common/services/configuration'; +import { HTTP_CLIENT_DEFAULT_TIMEOUT } from './constants'; +import { GenericRequest } from './generic-client'; +import { RequestInterceptorClient } from './request-interceptor'; +import { WzRequest } from './server-client'; +import { HTTPClient, HTTPClientRequestInterceptor } from './types'; + +interface HTTPClientServices { + http: any; + getTimeout(): Promise; + getURL(path: string): string; + getServerAPI(): string; + getIndexPatternTitle(): Promise; +} + +export class CoreHTTPClient implements HTTPClient { + private requestInterceptor: HTTPClientRequestInterceptor; + public generic; + public server; + private _timeout: number = HTTP_CLIENT_DEFAULT_TIMEOUT; + constructor(private logger: Logger, private services: HTTPClientServices) { + this.logger.debug('Creating client'); + // Create request interceptor + this.requestInterceptor = new RequestInterceptorClient( + logger, + this.services.http, + ); + + const getTimeout = async () => + (await this.services.getTimeout()) || this._timeout; + + const internalServices = { + getTimeout, + getServerAPI: this.services.getServerAPI, + getURL: this.services.getURL, + }; + + // Create clients + this.server = new WzRequest(logger, { + request: options => this.requestInterceptor.request(options), + ...internalServices, + }); + this.generic = new GenericRequest(logger, { + request: options => this.requestInterceptor.request(options), + getIndexPatternTitle: this.services.getIndexPatternTitle, + ...internalServices, + checkAPIById: apiId => this.server.checkAPIById(apiId), + }); + this.logger.debug('Created client'); + } + async setup() { + this.logger.debug('Setup'); + } + async start() {} + async stop() {} + async register() { + this.logger.debug('Starting client'); + this.requestInterceptor.init(); + this.logger.debug('Started client'); + } + async unregister() { + this.logger.debug('Stopping client'); + this.requestInterceptor.destroy(); + this.logger.debug('Stopped client'); + } +} diff --git a/plugins/wazuh-core/public/services/http/index.ts b/plugins/wazuh-core/public/services/http/index.ts new file mode 100644 index 0000000000..f6c9b1c770 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export { CoreHTTPClient } from './http-client'; diff --git a/plugins/wazuh-core/public/services/http/request-interceptor.ts b/plugins/wazuh-core/public/services/http/request-interceptor.ts new file mode 100644 index 0000000000..21b2600da3 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/request-interceptor.ts @@ -0,0 +1,84 @@ +import axios, { AxiosRequestConfig } from 'axios'; +import { HTTP_STATUS_CODES } from '../../../common/constants'; +import { Logger } from '../../../common/services/configuration'; +import { HTTPClientRequestInterceptor } from './types'; + +export class RequestInterceptorClient implements HTTPClientRequestInterceptor { + // define if the request is allowed to run + private _allow: boolean = true; + // store the cancel token to abort the requests + private _source: any; + // unregister the interceptor + private unregisterInterceptor: () => void = () => {}; + constructor(private logger: Logger, private http: any) { + this.logger.debug('Creating'); + this._source = axios.CancelToken.source(); + this.logger.debug('Created'); + } + private registerInterceptor() { + this.logger.debug('Registering interceptor in core http'); + this.unregisterInterceptor = this.http.intercept({ + responseError: (httpErrorResponse, controller) => { + if ( + httpErrorResponse.response?.status === HTTP_STATUS_CODES.UNAUTHORIZED + ) { + this.cancel(); + } + }, + request: (current, controller) => { + if (!this._allow) { + throw new Error('Disable request'); + } + }, + }); + this.logger.debug('Registered interceptor in core http'); + } + init() { + this.logger.debug('Initiating'); + this.registerInterceptor(); + this.logger.debug('Initiated'); + } + destroy() { + this.logger.debug('Destroying'); + this.logger.debug('Unregistering interceptor in core http'); + this.unregisterInterceptor(); + this.unregisterInterceptor = () => {}; + this.logger.debug('Unregistered interceptor in core http'); + this.logger.debug('Destroyed'); + } + cancel() { + this.logger.debug('Disabling requests'); + this._allow = false; + this._source.cancel('Requests cancelled'); + this.logger.debug('Disabled requests'); + } + async request(options: AxiosRequestConfig = {}) { + if (!this._allow) { + return Promise.reject('Requests are disabled'); + } + if (!options.method || !options.url) { + return Promise.reject('Missing parameters'); + } + const optionsWithCancelToken = { + ...options, + cancelToken: this._source?.token, + }; + + if (this._allow) { + try { + const requestData = await axios(optionsWithCancelToken); + return Promise.resolve(requestData); + } catch (error) { + if ( + error.response?.data?.message === 'Unauthorized' || + error.response?.data?.message === 'Authentication required' + ) { + this.cancel(); + // To reduce the dependencies, we use window object instead of the NavigationService + window.location.reload(); + } + throw error; + } + } + } +} diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts new file mode 100644 index 0000000000..a958962b55 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -0,0 +1,110 @@ +import { WzRequest } from './server-client'; + +const noop = () => {}; +const logger = { + debug: noop, + info: noop, + warn: noop, + error: noop, +}; + +const USER_TOKEN = + 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3YXp1aCIsImF1ZCI6IldhenVoIEFQSSBSRVNUIiwibmJmIjoxNzI2NzM3MDY3LCJleHAiOjE3MjY3Mzc5NjcsInN1YiI6IndhenVoLXd1aSIsInJ1bl9hcyI6ZmFsc2UsInJiYWNfcm9sZXMiOlsxXSwicmJhY19tb2RlIjoid2hpdGUifQ.AOL4dDe3c4WCYXMjqbkBqfKFAChtjvD_uZ0FXfLOMnfU0n6zPo61OZ43Kt0bYhW25BQIXR9Belb49gG3_qAIZpcaAQhQv4HPcL41ESRSvZc2wsa9_HYgV8Z7gieSuT15gdnSNogLKFS7yK5gQQivLo1e4QfVsDThrG_TVdJPbCG3GPq9'; + +function createClient() { + const mockRequest = jest.fn(options => { + console.log({ options }); + if (options.url === '/api/login') { + return { + data: { + token: USER_TOKEN, + }, + }; + } else if (options.url === '/api/request') { + if (options.data.path === '/security/users/me/policies') { + return { + data: { + rbac_mode: 'white', + }, + }; + } else if ( + options.data.method === 'DELETE' && + options.data.path === '/security/user/authenticate' + ) { + return { + data: { + message: 'User wazuh-wui was successfully logged out', + error: 0, + }, + }; + } + } + // if(path === '/security/users/me/policies'){ + + // } + }); + const client = new WzRequest(logger, { + getServerAPI: () => 'test', + getTimeout: () => Promise.resolve(1000), + getURL: path => path, + request: mockRequest, + }); + return { client, mockRequest }; +} + +describe('Create client', () => { + it('Ensure the initial userData value', done => { + const { client } = createClient(); + + client.userData$.subscribe(userData => { + expect(userData).toEqual({ + logged: false, + token: null, + account: null, + policies: null, + }); + done(); + }); + }); + + it('Authentication', done => { + const { client, mockRequest } = createClient(); + + client.auth().then(data => { + expect(data).toEqual({ + token: USER_TOKEN, + policies: {}, + account: null, + logged: true, + }); + + client.userData$.subscribe(userData => { + expect(userData).toEqual({ + token: USER_TOKEN, + policies: {}, + account: null, + logged: true, + }); + done(); + }); + }); + }); + + it.only('Unauthentication', done => { + const { client } = createClient(); + + client.unauth().then(data => { + expect(data).toEqual({}); + done(); + }); + }); + + it('Request', async () => { + const { client, mockRequest } = createClient(); + + const data = await client.request('GET', '/security/users/me/policies', {}); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(data).toEqual({ data: { rbac_mode: 'white' } }); + }); +}); diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts new file mode 100644 index 0000000000..86ea5c44c3 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -0,0 +1,532 @@ +/* + * Wazuh app - API request service + * Copyright (C) 2015-2024 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ +import { + HTTPClientRequestInterceptor, + HTTPClientServer, + HTTPVerb, + HTTPClientServerUserData, +} from './types'; +import { Logger } from '../../../common/services/configuration'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import jwtDecode from 'jwt-decode'; +import { BehaviorSubject } from 'rxjs'; + +interface WzRequestServices { + request: HTTPClientRequestInterceptor['request']; + getURL(path: string): string; + getTimeout(): Promise; + getServerAPI(): string; +} + +interface ServerAPIResponseItems { + affected_items: Array; + failed_items: Array; + total_affected_items: number; + total_failed_items: number; +} + +interface ServerAPIResponseItemsData { + data: ServerAPIResponseItems; + message: string; + error: number; +} + +export interface ServerAPIResponseItemsDataHTTPClient { + data: ServerAPIResponseItemsData; +} + +export class WzRequest implements HTTPClientServer { + onErrorInterceptor?: ( + error: any, + options: { + checkCurrentApiIsUp: boolean; + shouldRetry: boolean; + overwriteHeaders?: any; + }, + ) => Promise; + private userData: HTTPClientServerUserData; + userData$: BehaviorSubject; + constructor(private logger: Logger, private services: WzRequestServices) { + this.userData = { + logged: false, + token: null, + account: null, + policies: null, + }; + this.userData$ = new BehaviorSubject(this.userData); + } + + /** + * Permorn a generic request + * @param {String} method + * @param {String} path + * @param {Object} payload + */ + private async _request( + method: HTTPVerb, + path: string, + payload: any = null, + extraOptions: { + shouldRetry?: boolean; + checkCurrentApiIsUp?: boolean; + overwriteHeaders?: any; + } = { + shouldRetry: true, + checkCurrentApiIsUp: true, + overwriteHeaders: {}, + }, + ): Promise { + const shouldRetry = + typeof extraOptions.shouldRetry === 'boolean' + ? extraOptions.shouldRetry + : true; + const checkCurrentApiIsUp = + typeof extraOptions.checkCurrentApiIsUp === 'boolean' + ? extraOptions.checkCurrentApiIsUp + : true; + const overwriteHeaders = + typeof extraOptions.overwriteHeaders === 'object' + ? extraOptions.overwriteHeaders + : {}; + try { + if (!method || !path) { + throw new Error('Missing parameters'); + } + + const timeout = await this.services.getTimeout(); + + const url = this.services.getURL(path); + const options = { + method: method, + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + ...overwriteHeaders, + }, + url: url, + data: payload, + timeout: timeout, + }; + + const data = await this.services.request(options); + + if (data['error']) { + throw new Error(data['error']); + } + + return Promise.resolve(data); + } catch (error) { + //if the requests fails, we need to check if the API is down + if (checkCurrentApiIsUp) { + const currentApi = this.services.getServerAPI(); + if (currentApi) { + try { + await this.checkAPIById(currentApi); + } catch (error) { + // TODO :implement + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + // if ( + // !NavigationService.getInstance() + // .getPathname() + // .startsWith('/settings') + // ) { + // NavigationService.getInstance().navigate('/health-check'); + // } + throw error; + } + } + } + // if(this.onErrorInterceptor){ + // await this.onErrorInterceptor(error, {checkCurrentApiIsUp, shouldRetry, overwriteHeaders}) + // } + const errorMessage = + (error && + error.response && + error.response.data && + error.response.data.message) || + (error || {}).message; + if ( + typeof errorMessage === 'string' && + errorMessage.includes('status code 401') && + shouldRetry + ) { + try { + await this.auth(true); //await WzAuthentication.refresh(true); + return this._request(method, path, payload, { shouldRetry: false }); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject( + this.returnErrorInstance(error, error.data.message), + ) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + return errorMessage + ? Promise.reject(this.returnErrorInstance(error, errorMessage)) + : Promise.reject( + this.returnErrorInstance(error, 'Server did not respond'), + ); + } + } + + /** + * Perform a request to the Wazuh API + * @param {String} method Eg. GET, PUT, POST, DELETE + * @param {String} path API route + * @param {Object} body Request body + */ + async request( + method: HTTPVerb, + path: string, + body: any, + options: { + checkCurrentApiIsUp?: boolean; + returnOriginalResponse?: boolean; + } = { checkCurrentApiIsUp: true, returnOriginalResponse: false }, + ): Promise> { + try { + if (!method || !path || !body) { + throw new Error('Missing parameters'); + } + + const { returnOriginalResponse, ...optionsToGenericReq } = options; + + const id = this.services.getServerAPI(); + const requestData = { method, path, body, id }; + const response = await this._request( + 'POST', + '/api/request', + requestData, + optionsToGenericReq, + ); + + if (returnOriginalResponse) { + return response; + } + + const hasFailed = + (((response || {}).data || {}).data || {}).total_failed_items || 0; + + if (hasFailed) { + const error = + ((((response.data || {}).data || {}).failed_items || [])[0] || {}) + .error || {}; + const failed_ids = + ((((response.data || {}).data || {}).failed_items || [])[0] || {}) + .id || {}; + const message = (response.data || {}).message || 'Unexpected error'; + const errorMessage = `${message} (${error.code}) - ${error.message} ${ + failed_ids && failed_ids.length > 1 + ? ` Affected ids: ${failed_ids} ` + : '' + }`; + return Promise.reject(this.returnErrorInstance(null, errorMessage)); + } + return Promise.resolve(response); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + + /** + * Perform a request to generate a CSV + * @param {String} path + * @param {Object} filters + */ + async csv(path: string, filters: any) { + try { + if (!path || !filters) { + throw new Error('Missing parameters'); + } + const id = this.services.getServerAPI(); + const requestData = { path, id, filters }; + const data = await this._request('POST', '/api/csv', requestData); + return Promise.resolve(data); + } catch (error) { + return ((error || {}).data || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.data.message)) + : Promise.reject(this.returnErrorInstance(error, error.message)); + } + } + + /** + * Customize message and return an error object + * @param error + * @param message + * @returns error + */ + private returnErrorInstance(error: any, message: string | undefined) { + if (!error || typeof error === 'string') { + return new Error(message || error); + } + error.message = message; + return error; + } + + setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise) { + this.onErrorInterceptor = onErrorInterceptor; + } + + /** + * Requests and returns an user token to the API. + * + * @param {boolean} force + * @returns {string} token as string or Promise.reject error + */ + private async login(force = false) { + try { + let idHost = this.services.getServerAPI(); + while (!idHost) { + await new Promise(r => setTimeout(r, 500)); + idHost = this.services.getServerAPI(); + } + + const response = await this._request('POST', '/api/login', { + idHost, + force, + }); + + const token = ((response || {}).data || {}).token; + return token as string; + } catch (error) { + throw error; + } + } + + /** + * Refresh the user's token + * + * @param {boolean} force + * @returns {void} nothing or Promise.reject error + */ + async auth(force = false) { + try { + // Get user token + const token: string = await this.login(force); + if (!token) { + // Remove old existent token + // await this.unauth(); + return; + } + + // Decode token and get expiration time + const jwtPayload = jwtDecode(token); + + // Get user Policies + const userPolicies = await this.getUserPolicies(); + + // Dispatch actions to set permissions and administrator consideration + // TODO: implement + // store.dispatch(updateUserPermissions(userPolicies)); + + // store.dispatch( + // updateUserAccount( + // getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + // jwtPayload, + // ), + // ), + // ); + // store.dispatch(updateWithUserLogged(true)); + const data = { + token, + policies: userPolicies, + account: null, // TODO: implement + logged: true, + }; + + this.updateUserData(data); + return data; + } catch (error) { + // TODO: implement + // const options: UIErrorLog = { + // context: `${WzAuthentication.name}.refresh`, + // level: UI_LOGGER_LEVELS.ERROR as UILogLevel, + // severity: UI_ERROR_SEVERITIES.BUSINESS as UIErrorSeverity, + // error: { + // error: error, + // message: error.message || error, + // title: `${error.name}: Error getting the authorization token`, + // }, + // }; + // getErrorOrchestrator().handleError(options); + // store.dispatch( + // updateUserAccount( + // getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( + // {}, // This value should cause the user is not considered as an administrator + // ), + // ), + // ); + // store.dispatch(updateWithUserLogged(true)); + this.updateUserData({ + token: null, + policies: null, + account: null, // TODO: implement + logged: true, + }); + throw error; + } + } + + /** + * Get current user's policies + * + * @returns {Object} user's policies or Promise.reject error + */ + private async getUserPolicies() { + try { + let idHost = this.services.getServerAPI(); + while (!idHost) { + await new Promise(r => setTimeout(r, 500)); + idHost = this.services.getServerAPI(); + } + const response = await this.request( + 'GET', + '/security/users/me/policies', + { idHost }, + ); + return response?.data?.data || {}; + } catch (error) { + throw error; + } + } + + getUserData() { + return this.userData; + } + + /** + * Sends a request to the Wazuh's API to delete the user's token. + * + * @returns {Object} + */ + async unauth() { + try { + const response = await this.request( + 'DELETE', + '/security/user/authenticate', + { delay: 5000 }, + ); + + return response?.data?.data || {}; + } catch (error) { + throw error; + } + } + + /** + * Update the internal user data and emit the value to the subscribers of userData$ + * @param data + */ + private updateUserData(data: HTTPClientServerUserData) { + this.userData = data; + this.userData$.next(this.getUserData()); + } + + async checkAPIById(serverHostId: string, idChanged = false) { + try { + const timeout = await this.services.getTimeout(); + const payload = { id: serverHostId }; + if (idChanged) { + payload.idChanged = serverHostId; + } + + const url = this.services.getURL('/api/check-stored-api'); + const options = { + method: 'POST', + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }, + url: url, + data: payload, + timeout: timeout, + }; + + // TODO: implement + // if (Object.keys(configuration).length) { + // AppState.setPatternSelector(configuration['ip.selector']); + // } + + const response = await this.services.request(options); + + if (response.error) { + return Promise.reject(this.returnErrorInstance(response)); + } + + return response; + } catch (error) { + if (error.response) { + // TODO: implement + // const wzMisc = new WzMisc(); + // wzMisc.setApiIsDown(true); + const response = (error.response.data || {}).message || error.message; + return Promise.reject(this.returnErrorInstance(response)); + } else { + return (error || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.message)) + : Promise.reject( + this.returnErrorInstance( + error, + error || 'Server did not respond', + ), + ); + } + } + } + + /** + * Check the status of an API entry + * @param {String} apiObject + */ + async checkAPI(apiEntry: any, forceRefresh = false) { + try { + const timeout = await this.services.getTimeout(); + const url = this.services.getURL('/api/check-api'); + + const options = { + method: 'POST', + headers: { + ...PLUGIN_PLATFORM_REQUEST_HEADERS, + 'content-type': 'application/json', + }, + url: url, + data: { ...apiEntry, forceRefresh }, + timeout: timeout, + }; + + const response = await this.services.request(options); + + if (response.error) { + return Promise.reject(this.returnErrorInstance(response)); + } + + return response; + } catch (error) { + if (error.response) { + const response = (error.response.data || {}).message || error.message; + return Promise.reject(this.returnErrorInstance(response)); + } else { + return (error || {}).message || false + ? Promise.reject(this.returnErrorInstance(error, error.message)) + : Promise.reject( + this.returnErrorInstance( + error, + error || 'Server did not respond', + ), + ); + } + } + } +} diff --git a/plugins/wazuh-core/public/services/http/types.ts b/plugins/wazuh-core/public/services/http/types.ts new file mode 100644 index 0000000000..574670eb32 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/types.ts @@ -0,0 +1,49 @@ +import { AxiosRequestConfig } from 'axios'; +import { BehaviorSubject } from 'rxjs'; + +export interface HTTPClientRequestInterceptor { + init(): void; + destroy(): void; + cancel(): void; + request(options: AxiosRequestConfig): Promise; +} + +export type HTTPVerb = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; + +export interface HTTPClientGeneric { + request( + method: HTTPVerb, + path: string, + payload?: any, + returnError?: boolean, + ): Promise; +} + +export type HTTPClientServerUserData = { + token: string | null; + policies: any | null; + account: any | null; + logged: boolean; +}; + +export interface HTTPClientServer { + request( + method: HTTPVerb, + path: string, + body: any, + options: { + checkCurrentApiIsUp?: boolean; + returnOriginalResponse?: boolean; + }, + ): Promise; + csv(path: string, filters: any): Promise; + auth(force: boolean): Promise; + unauth(force: boolean): Promise; + userData$: BehaviorSubject; + getUserData(): HTTPClientServerUserData; +} + +export interface HTTPClient { + generic: HTTPClientGeneric; + server: HTTPClientServer; +} diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index a3acfa7c4d..942c5d6fe4 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -1,5 +1,6 @@ import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; +import { HTTPClient } from './services/http/types'; import { DashboardSecurity } from './utils/dashboard-security'; export interface WazuhCorePluginSetup { @@ -7,6 +8,7 @@ export interface WazuhCorePluginSetup { API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurity; + http: HTTPClient; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginStart { @@ -15,6 +17,7 @@ export interface WazuhCorePluginStart { API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurity; + http: HTTPClient; } export interface AppPluginStartDependencies {} diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts index 305291c21d..ab91cf002c 100644 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ b/plugins/wazuh-core/public/utils/configuration-store.ts @@ -1,6 +1,6 @@ import { IConfigurationStore, - ILogger, + Logger, IConfiguration, } from '../../common/services/configuration'; @@ -8,7 +8,7 @@ export class ConfigurationStore implements IConfigurationStore { private _stored: any; file: string = ''; configuration: IConfiguration | null = null; - constructor(private logger: ILogger, private http: any) { + constructor(private logger: Logger, private http: any) { this._stored = {}; } setConfiguration(configuration: IConfiguration) { diff --git a/plugins/wazuh-core/public/utils/dashboard-security.ts b/plugins/wazuh-core/public/utils/dashboard-security.ts index 669ef6fbae..dd0889c192 100644 --- a/plugins/wazuh-core/public/utils/dashboard-security.ts +++ b/plugins/wazuh-core/public/utils/dashboard-security.ts @@ -1,9 +1,9 @@ import { WAZUH_ROLE_ADMINISTRATOR_ID } from '../../common/constants'; -import { ILogger } from '../../common/services/configuration'; +import { Logger } from '../../common/services/configuration'; export class DashboardSecurity { private securityPlatform: string = ''; - constructor(private logger: ILogger, private http) {} + constructor(private logger: Logger, private http) {} private async fetchCurrentPlatform() { try { this.logger.debug('Fetching the security platform'); diff --git a/plugins/wazuh-core/server/services/server-api-client.ts b/plugins/wazuh-core/server/services/server-api-client.ts index be9622b642..cd87333fb1 100644 --- a/plugins/wazuh-core/server/services/server-api-client.ts +++ b/plugins/wazuh-core/server/services/server-api-client.ts @@ -64,6 +64,11 @@ export interface ServerAPIScopedUserClient { ) => Promise>; } +export interface ServerAPIAuthenticateOptions { + useRunAs: boolean; + authContext?: any; +} + /** * This service communicates with the Wazuh server APIs */ @@ -86,7 +91,8 @@ export class ServerAPIClient { // Create internal user client this.asInternalUser = { - authenticate: async apiHostID => await this._authenticate(apiHostID), + authenticate: async apiHostID => + await this._authenticateInternalUser(apiHostID), request: async ( method: RequestHTTPMethod, path: RequestPath, @@ -158,7 +164,7 @@ export class ServerAPIClient { */ private async _authenticate( apiHostID: string, - authContext?: any, + options: ServerAPIAuthenticateOptions, ): Promise { const api: APIHost = await this.manageHosts.get(apiHostID); const optionsRequest = { @@ -171,16 +177,24 @@ export class ServerAPIClient { password: api.password, }, url: `${api.url}:${api.port}/security/user/authenticate${ - !!authContext ? '/run_as' : '' + options.useRunAs ? '/run_as' : '' }`, - ...(!!authContext ? { data: authContext } : {}), + ...(options?.authContext ? { data: options?.authContext } : {}), }; const response: AxiosResponse = await this._axios(optionsRequest); const token: string = (((response || {}).data || {}).data || {}).token; - if (!authContext) { - this._CacheInternalUserAPIHostToken.set(apiHostID, token); - } + return token; + } + + /** + * Get the authentication token for the internal user and cache it + * @param apiHostID Server API ID + * @returns + */ + private async _authenticateInternalUser(apiHostID: string): Promise { + const token = await this._authenticate(apiHostID, { useRunAs: false }); + this._CacheInternalUserAPIHostToken.set(apiHostID, token); return token; } @@ -192,13 +206,21 @@ export class ServerAPIClient { */ asScoped(context: any, request: any): ServerAPIScopedUserClient { return { - authenticate: async (apiHostID: string) => - await this._authenticate( - apiHostID, - ( - await this.dashboardSecurity.getCurrentUser(request, context) - ).authContext, - ), + authenticate: async (apiHostID: string) => { + const useRunAs = this.manageHosts.isEnabledAuthWithRunAs(apiHostID); + + const token = useRunAs + ? await this._authenticate(apiHostID, { + useRunAs: true, + authContext: ( + await this.dashboardSecurity.getCurrentUser(request, context) + ).authContext, + }) + : await this._authenticate(apiHostID, { + useRunAs: false, + }); + return token; + }, request: async ( method: RequestHTTPMethod, path: string, @@ -232,11 +254,13 @@ export class ServerAPIClient { this._CacheInternalUserAPIHostToken.has(options.apiHostID) && !options.forceRefresh ? this._CacheInternalUserAPIHostToken.get(options.apiHostID) - : await this._authenticate(options.apiHostID); + : await this._authenticateInternalUser(options.apiHostID); return await this._request(method, path, data, { ...options, token }); } catch (error) { if (error.response && error.response.status === 401) { - const token: string = await this._authenticate(options.apiHostID); + const token: string = await this._authenticateInternalUser( + options.apiHostID, + ); return await this._request(method, path, data, { ...options, token }); } throw error; From 9189e8e17f3ff75f87d4f1dbf64c4727172fa4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 19 Sep 2024 17:09:37 +0200 Subject: [PATCH 02/77] fix: add VSCode settings file --- .vscode/settings.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..b96fcd2154 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "diffEditor.ignoreTrimWhitespace": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.inlineSuggest.enabled": true, + "editor.insertSpaces": true, + "editor.minimap.enabled": true, + "editor.rulers": [80, 100], + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.wordWrap": "on", + "explorer.confirmDelete": true, + "files.autoSave": "off", + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always" +} From af7e81266490a6a0fe9f4c6199d47991a5ba64dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 19 Sep 2024 17:14:40 +0200 Subject: [PATCH 03/77] chore: remove comment --- plugins/wazuh-core/public/services/http/server-client.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts index a958962b55..5c63534b0b 100644 --- a/plugins/wazuh-core/public/services/http/server-client.test.ts +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -39,9 +39,6 @@ function createClient() { }; } } - // if(path === '/security/users/me/policies'){ - - // } }); const client = new WzRequest(logger, { getServerAPI: () => 'test', From 3fe48aa48dc7aea3716937c81a31cd2ee2da553d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 27 Sep 2024 16:40:55 +0200 Subject: [PATCH 04/77] feat: add suggestions of code review --- .../public/services/http/server-client.ts | 90 ++++++++----------- 1 file changed, 36 insertions(+), 54 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 86ea5c44c3..e89fce0f2b 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -149,12 +149,7 @@ export class WzRequest implements HTTPClientServer { // if(this.onErrorInterceptor){ // await this.onErrorInterceptor(error, {checkCurrentApiIsUp, shouldRetry, overwriteHeaders}) // } - const errorMessage = - (error && - error.response && - error.response.data && - error.response.data.message) || - (error || {}).message; + const errorMessage = error?.response?.data?.message || error?.message; if ( typeof errorMessage === 'string' && errorMessage.includes('status code 401') && @@ -164,18 +159,16 @@ export class WzRequest implements HTTPClientServer { await this.auth(true); //await WzAuthentication.refresh(true); return this._request(method, path, payload, { shouldRetry: false }); } catch (error) { - return ((error || {}).data || {}).message || false - ? Promise.reject( - this.returnErrorInstance(error, error.data.message), - ) - : Promise.reject(this.returnErrorInstance(error, error.message)); + throw this.returnErrorInstance( + error, + error?.data?.message || error.message, + ); } } - return errorMessage - ? Promise.reject(this.returnErrorInstance(error, errorMessage)) - : Promise.reject( - this.returnErrorInstance(error, 'Server did not respond'), - ); + throw this.returnErrorInstance( + error, + errorMessage || 'Server did not respond', + ); } } @@ -214,29 +207,25 @@ export class WzRequest implements HTTPClientServer { return response; } - const hasFailed = - (((response || {}).data || {}).data || {}).total_failed_items || 0; + const hasFailed = response?.data?.data?.total_failed_items || 0; if (hasFailed) { - const error = - ((((response.data || {}).data || {}).failed_items || [])[0] || {}) - .error || {}; - const failed_ids = - ((((response.data || {}).data || {}).failed_items || [])[0] || {}) - .id || {}; + const error = response?.data?.data?.failed_items?.[0]?.error || {}; + const failedIds = response?.data?.data?.failed_items?.[0]?.id || {}; const message = (response.data || {}).message || 'Unexpected error'; const errorMessage = `${message} (${error.code}) - ${error.message} ${ - failed_ids && failed_ids.length > 1 - ? ` Affected ids: ${failed_ids} ` + failedIds && failedIds.length > 1 + ? ` Affected ids: ${failedIds} ` : '' }`; - return Promise.reject(this.returnErrorInstance(null, errorMessage)); + throw this.returnErrorInstance(null, errorMessage); } - return Promise.resolve(response); + return response; } catch (error) { - return ((error || {}).data || {}).message || false - ? Promise.reject(this.returnErrorInstance(error, error.data.message)) - : Promise.reject(this.returnErrorInstance(error, error.message)); + throw this.returnErrorInstance( + error, + error?.data?.message || error.message, + ); } } @@ -255,9 +244,10 @@ export class WzRequest implements HTTPClientServer { const data = await this._request('POST', '/api/csv', requestData); return Promise.resolve(data); } catch (error) { - return ((error || {}).data || {}).message || false - ? Promise.reject(this.returnErrorInstance(error, error.data.message)) - : Promise.reject(this.returnErrorInstance(error, error.message)); + throw this.returnErrorInstance( + error, + error?.data?.message || error?.message, + ); } } @@ -462,7 +452,7 @@ export class WzRequest implements HTTPClientServer { const response = await this.services.request(options); if (response.error) { - return Promise.reject(this.returnErrorInstance(response)); + throw this.returnErrorInstance(response); } return response; @@ -472,16 +462,12 @@ export class WzRequest implements HTTPClientServer { // const wzMisc = new WzMisc(); // wzMisc.setApiIsDown(true); const response = (error.response.data || {}).message || error.message; - return Promise.reject(this.returnErrorInstance(response)); + throw this.returnErrorInstance(response); } else { - return (error || {}).message || false - ? Promise.reject(this.returnErrorInstance(error, error.message)) - : Promise.reject( - this.returnErrorInstance( - error, - error || 'Server did not respond', - ), - ); + throw this.returnErrorInstance( + error, + error?.message || error || 'Server did not respond', + ); } } } @@ -509,23 +495,19 @@ export class WzRequest implements HTTPClientServer { const response = await this.services.request(options); if (response.error) { - return Promise.reject(this.returnErrorInstance(response)); + throw this.returnErrorInstance(response); } return response; } catch (error) { if (error.response) { const response = (error.response.data || {}).message || error.message; - return Promise.reject(this.returnErrorInstance(response)); + throw this.returnErrorInstance(response); } else { - return (error || {}).message || false - ? Promise.reject(this.returnErrorInstance(error, error.message)) - : Promise.reject( - this.returnErrorInstance( - error, - error || 'Server did not respond', - ), - ); + throw this.returnErrorInstance( + error, + error?.message || error || 'Server did not respond', + ); } } } From 7b4ed5b433e58aecc72d13f3a6b148fff5f4bde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 14:17:54 +0200 Subject: [PATCH 05/77] feat(core): add TableData and ServerTable components to core plugin - Add TableData component (based on TableData of main plugin) - Add ServerTable component (based on TableWzAPI of main plugin) - Add SearchBar (copied from main plugin) - Add FileSaver (copied from main plugin) --- plugins/wazuh-core/public/components/index.ts | 2 + .../public/components/search-bar/README.md | 201 +++ .../__snapshots__/index.test.tsx.snap | 59 + .../components/eui-suggest/index.js | 3 + .../components/eui-suggest/suggest.js | 84 ++ .../components/eui-suggest/suggest_input.js | 143 ++ .../components/search-bar/index.test.tsx | 57 + .../public/components/search-bar/index.tsx | 264 ++++ .../__snapshots__/aql.test.tsx.snap | 103 ++ .../__snapshots__/wql.test.tsx.snap | 59 + .../search-bar/query-language/aql.md | 204 +++ .../search-bar/query-language/aql.test.tsx | 211 +++ .../search-bar/query-language/aql.tsx | 545 ++++++++ .../search-bar/query-language/index.ts | 32 + .../search-bar/query-language/wql.md | 269 ++++ .../search-bar/query-language/wql.test.tsx | 472 +++++++ .../search-bar/query-language/wql.tsx | 1203 +++++++++++++++++ .../public/components/table-data/README.md | 28 + .../public/components/table-data/index.ts | 2 + .../components/table-data/table-data.tsx | 360 +++++ .../public/components/table-data/types.ts | 86 ++ plugins/wazuh-core/public/hooks/index.ts | 1 + .../public/hooks/use-state-storage.ts | 30 + plugins/wazuh-core/public/plugin.ts | 16 +- .../public/services/http/http-client.ts | 6 +- .../services/http/ui/components/README.md | 19 + .../ui/components/export-table-csv.test.tsx | 38 + .../http/ui/components/export-table-csv.tsx | 73 + .../http/ui/components/server-table-data.tsx | 81 ++ .../services/http/ui/components/types.ts | 44 + .../public/services/http/ui/create.tsx | 30 + .../http/ui/services/fetch-server-data.ts | 33 + .../public/services/http/ui/withServices.tsx | 3 + plugins/wazuh-core/public/utils/file-saver.js | 198 +++ 34 files changed, 4956 insertions(+), 3 deletions(-) create mode 100644 plugins/wazuh-core/public/components/index.ts create mode 100644 plugins/wazuh-core/public/components/search-bar/README.md create mode 100644 plugins/wazuh-core/public/components/search-bar/__snapshots__/index.test.tsx.snap create mode 100644 plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js create mode 100644 plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest.js create mode 100644 plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js create mode 100644 plugins/wazuh-core/public/components/search-bar/index.test.tsx create mode 100644 plugins/wazuh-core/public/components/search-bar/index.tsx create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/aql.md create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/index.ts create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/wql.md create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx create mode 100644 plugins/wazuh-core/public/components/table-data/README.md create mode 100644 plugins/wazuh-core/public/components/table-data/index.ts create mode 100644 plugins/wazuh-core/public/components/table-data/table-data.tsx create mode 100644 plugins/wazuh-core/public/components/table-data/types.ts create mode 100644 plugins/wazuh-core/public/hooks/use-state-storage.ts create mode 100644 plugins/wazuh-core/public/services/http/ui/components/README.md create mode 100644 plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx create mode 100644 plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx create mode 100644 plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx create mode 100644 plugins/wazuh-core/public/services/http/ui/components/types.ts create mode 100644 plugins/wazuh-core/public/services/http/ui/create.tsx create mode 100644 plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts create mode 100644 plugins/wazuh-core/public/services/http/ui/withServices.tsx create mode 100644 plugins/wazuh-core/public/utils/file-saver.js diff --git a/plugins/wazuh-core/public/components/index.ts b/plugins/wazuh-core/public/components/index.ts new file mode 100644 index 0000000000..564af2e44e --- /dev/null +++ b/plugins/wazuh-core/public/components/index.ts @@ -0,0 +1,2 @@ +export * from './table-data'; +export * from './search-bar'; diff --git a/plugins/wazuh-core/public/components/search-bar/README.md b/plugins/wazuh-core/public/components/search-bar/README.md new file mode 100644 index 0000000000..ce9fd0d65b --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/README.md @@ -0,0 +1,201 @@ +# Component + +The `SearchBar` component is a base component of a search bar. + +It is designed to be extensible through the self-contained query language implementations. This means +the behavior of the search bar depends on the business logic of each query language. For example, a +query language can display suggestions according to the user input or prepend some buttons to the search bar. + +It is based on a custom `EuiSuggest` component defined in `public/components/eui-suggest/suggest.js`. So the +abilities are restricted by this one. + +## Features + +- Supports multiple query languages. +- Switch the selected query language. +- Self-contained query language implementation and ability to interact with the search bar component. +- React to external changes to set the new input. This enables to change the input from external components. + +# Usage + +Basic usage: + +```tsx + { + switch (field) { + case 'configSum': + return [ + { label: 'configSum1' }, + { label: 'configSum2' }, + ]; + break; + case 'dateAdd': + return [ + { label: 'dateAdd1' }, + { label: 'dateAdd2' }, + ]; + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + label: status, + }), + ); + break; + default: + return []; + break; + } + }, + } + }, + ]} + // Handler fired when the input handler changes. Optional. + onChange={onChange} + // Handler fired when the user press the Enter key or custom implementations. Required. + onSearch={onSearch} + // Used to define the internal input. Optional. + // This could be used to change the input text from the external components. + // Use the UQL (Unified Query Language) syntax. + input='' + // Define the default mode. Optional. If not defined, it will use the first one mode. + defaultMode='' +> +``` + +# Query languages + +The built-in query languages are: + +- AQL: API Query Language. Based on https://documentation.wazuh.com/current/user-manual/api/queries.html. + +## How to add a new query language + +### Definition + +The language expects to take the interface: + +```ts +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + apiQuery: string, + query: string + } + }>; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; +}; +``` + +where: + +- `description`: is the description of the query language. This is displayed in a query language popover + on the right side of the search bar. Required. +- `documentationLink`: URL to the documentation link. Optional. +- `id`: identification of the query language. +- `label`: name +- `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. +- `run`: method that returns: + - `searchBarProps`: properties to be passed to the search bar component. This allows the + customization the properties that will used by the base search bar component and the output used when searching + - `output`: + - `language`: query language ID + - `apiQuery`: API query. + - `query`: current query in the specified language +- `transformInput`: method that transforms the UQL (Unified Query Language) to the specific query + language. This is used when receives a external input in the Unified Query Language, the returned + value is converted to the specific query language to set the new input text of the search bar + component. + +Create a new file located in `public/components/search-bar/query-language` and define the expected interface; + +### Register + +Go to `public/components/search-bar/query-language/index.ts` and add the new query language: + +```ts +import { AQL } from './aql'; + +// Import the custom query language +import { CustomQL } from './custom'; + +// [...] + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [ + AQL, + CustomQL, // Add the new custom query language +].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; +}, {}); +``` + +## Unified Query Language - UQL + +This is an unified syntax used by the search bar component that provides a way to communicate +with the different query language implementations. + +The input and output parameters of the search bar component must use this syntax. + +This is used in: +- input: + - `input` component property +- output: + - `onChange` component handler + - `onSearch` component handler + +Its syntax is equal to Wazuh API Query Language +https://wazuh.com/./user-manual/api/queries.html + +> The AQL query language is a implementation of this syntax. \ No newline at end of file diff --git a/plugins/wazuh-core/public/components/search-bar/__snapshots__/index.test.tsx.snap b/plugins/wazuh-core/public/components/search-bar/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..5602512bd0 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/__snapshots__/index.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly the initial render 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js new file mode 100644 index 0000000000..93ac8c3eab --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js @@ -0,0 +1,3 @@ +export { EuiSuggestInput } from './suggest_input'; + +export { EuiSuggest } from './suggest'; diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest.js new file mode 100644 index 0000000000..4298a003d7 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSuggestItem } from '@elastic/eui'; +import { EuiSuggestInput } from './suggest_input'; + +export class EuiSuggest extends Component { + state = { + value: '', + status: 'unsaved', + }; + + getValue = val => { + this.setState({ + value: val, + }); + }; + + onChange = e => { + this.props.onInputChange(e.target.value); + }; + + render() { + const { + onItemClick, + onInputChange, + status, + append, + tooltipContent, + suggestions, + ...rest + } = this.props; + + const suggestionList = suggestions.map((item, index) => ( + onItemClick(item) : null} + description={item.description} + /> + )); + + const suggestInput = ( + + ); + return
{suggestInput}
; + } +} + +EuiSuggest.propTypes = { + className: PropTypes.string, + /** + * Status of the current query 'notYetSaved', 'saved', 'unchanged' or 'loading'. + */ + status: PropTypes.oneOf(['unsaved', 'saved', 'unchanged', 'loading']), + tooltipContent: PropTypes.string, + /** + * Element to be appended to the input bar (e.g. hashtag popover). + */ + append: PropTypes.node, + /** + * List of suggestions to display using 'suggestItem'. + */ + suggestions: PropTypes.array, + /** + * Handler for click on a suggestItem. + */ + onItemClick: PropTypes.func, + onInputChange: PropTypes.func, + isOpen: PropTypes.bool, + onClosePopover: PropTypes.func, + onPopoverFocus: PropTypes.func, +}; + +EuiSuggestInput.defaultProps = { + status: 'unchanged', +}; diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js new file mode 100644 index 0000000000..7a4f5df6f2 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js @@ -0,0 +1,143 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { + EuiFilterButton, + EuiFieldText, + EuiToolTip, + EuiIcon, + EuiPopover +} from '@elastic/eui'; +import { EuiInputPopover } from '@elastic/eui'; + +const statusMap = { + unsaved: { + icon: 'dot', + color: 'accent', + tooltip: 'Changes have not been saved.' + }, + saved: { + icon: 'checkInCircleFilled', + color: 'secondary', + tooltip: 'Saved.' + }, + unchanged: { + icon: '', + color: 'secondary' + } +}; + +export class EuiSuggestInput extends Component { + state = { + value: '', + isPopoverOpen: false + }; + + onFieldChange = e => { + this.setState({ + value: e.target.value, + isPopoverOpen: e.target.value !== '' ? true : false + }); + this.props.sendValue(e.target.value); + }; + + render() { + const { + className, + status, + append, + tooltipContent, + suggestions, + sendValue, + onPopoverFocus, + isPopoverOpen, + onClosePopover, + disableFocusTrap = false, + ...rest + } = this.props; + + let icon; + let color; + + if (statusMap[status]) { + icon = statusMap[status].icon; + color = statusMap[status].color; + } + const classes = classNames('euiSuggestInput', className); + + // EuiFieldText's append accepts an array of elements so start by creating an empty array + const appendArray = []; + + const statusElement = (status === 'saved' || status === 'unsaved') && ( + + + + ); + + // Push the status element to the array if it is not undefined + if (statusElement) appendArray.push(statusElement); + + // Check to see if consumer passed an append item and if so, add it to the array + if (append) appendArray.push(append); + + const customInput = ( + + ); + + return ( +
+ +
{suggestions}
+
+
+ ); + } +} + +EuiSuggestInput.propTypes = { + className: PropTypes.string, + /** + * Status of the current query 'unsaved', 'saved', 'unchanged' or 'loading'. + */ + status: PropTypes.oneOf(['unsaved', 'saved', 'unchanged', 'loading']), + tooltipContent: PropTypes.string, + /** + * Element to be appended to the input bar. + */ + append: PropTypes.node, + /** + * List of suggestions to display using 'suggestItem'. + */ + suggestions: PropTypes.array, + isOpen: PropTypes.bool, + onClosePopover: PropTypes.func, + onPopoverFocus: PropTypes.func +}; + +EuiSuggestInput.defaultProps = { + status: 'unchanged' +}; diff --git a/plugins/wazuh-core/public/components/search-bar/index.test.tsx b/plugins/wazuh-core/public/components/search-bar/index.test.tsx new file mode 100644 index 0000000000..31f18f6dda --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/index.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SearchBar } from './index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: 'wql', + input: '', + modes: [ + { + id: 'aql', + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }){ + return []; + }, + }, + }, + { + id: 'wql', + implicitQuery: { + query: 'id!=000', + conjunction: ';' + }, + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }){ + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {} + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly the initial render', async () => { + const wrapper = render( + + ); + + /* This test causes a warning about act. This is intentional, because the test pretends to get + the first rendering of the component that doesn't have the component properties coming of the + selected query language */ + expect(wrapper.container).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/plugins/wazuh-core/public/components/search-bar/index.tsx b/plugins/wazuh-core/public/components/search-bar/index.tsx new file mode 100644 index 0000000000..71c56447ce --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/index.tsx @@ -0,0 +1,264 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFormRow, + EuiLink, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiSelect, + EuiText, + EuiFlexGroup, + EuiFlexItem, + // EuiSuggest, +} from '@elastic/eui'; +import { EuiSuggest } from './components/eui-suggest'; +import { searchBarQueryLanguages } from './query-language'; +import _ from 'lodash'; +import { ISearchBarModeWQL } from './query-language/wql'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; + +export interface SearchBarProps { + defaultMode?: string; + modes: ISearchBarModeWQL[]; + onChange?: (params: any) => void; + onSearch: (params: any) => void; + buttonsRender?: () => React.ReactNode; + input?: string; +} + +export const SearchBar = ({ + defaultMode, + modes, + onChange, + onSearch, + ...rest +}: SearchBarProps) => { + // Query language ID and configuration + const [queryLanguage, setQueryLanguage] = useState<{ + id: string; + configuration: any; + }>({ + id: defaultMode || modes[0].id, + configuration: + searchBarQueryLanguages[ + defaultMode || modes[0].id + ]?.getConfiguration?.() || {}, + }); + // Popover query language is open + const [isOpenPopoverQueryLanguage, setIsOpenPopoverQueryLanguage] = + useState(false); + // Input field + const [input, setInput] = useState(rest.input || ''); + // Query language output of run method + const [queryLanguageOutputRun, setQueryLanguageOutputRun] = useState({ + searchBarProps: { suggestions: [] }, + output: undefined, + }); + // Cache the previous output + const queryLanguageOutputRunPreviousOutput = useRef( + queryLanguageOutputRun.output, + ); + // Controls when the suggestion popover is open/close + const [isOpenSuggestionPopover, setIsOpenSuggestionPopover] = + useState(false); + // Reference to the input + const inputRef = useRef(); + // Debounce update timer + const debounceUpdateSearchBarTimer = useRef(); + + // Handler when searching + const _onSearch = (output: any) => { + // TODO: fix when searching + onSearch(output); + setIsOpenSuggestionPopover(false); + }; + + // Handler on change the input field text + const onChangeInput = (event: React.ChangeEvent) => + setInput(event.target.value); + + // Handler when pressing a key + const onKeyPressHandler = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + _onSearch(queryLanguageOutputRun.output); + } + }; + + const selectedQueryLanguageParameters = modes.find( + ({ id }) => id === queryLanguage.id, + ); + + useEffect(() => { + // React to external changes and set the internal input text. Use the `transformInput` of + // the query language in use + rest.input && + searchBarQueryLanguages[queryLanguage.id]?.transformInput && + setInput( + searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( + rest.input, + { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + ), + ); + }, [rest.input]); + + useEffect(() => { + (async () => { + // Set the query language output + debounceUpdateSearchBarTimer.current && + clearTimeout(debounceUpdateSearchBarTimer.current); + // Debounce the updating of the search bar state + debounceUpdateSearchBarTimer.current = setTimeout(async () => { + const queryLanguageOutput = await searchBarQueryLanguages[ + queryLanguage.id + ].run(input, { + onSearch: _onSearch, + setInput, + closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), + openSuggestionPopover: () => setIsOpenSuggestionPopover(true), + setQueryLanguageConfiguration: (configuration: any) => + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), + setQueryLanguageOutput: setQueryLanguageOutputRun, + inputRef, + queryLanguage: { + configuration: queryLanguage.configuration, + parameters: selectedQueryLanguageParameters, + }, + }); + queryLanguageOutputRunPreviousOutput.current = { + ...queryLanguageOutputRun.output, + }; + setQueryLanguageOutputRun(queryLanguageOutput); + }, SEARCH_BAR_DEBOUNCE_UPDATE_TIME); + })(); + }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); + + useEffect(() => { + onChange && + // Ensure the previous output is different to the new one + !_.isEqual( + queryLanguageOutputRun.output, + queryLanguageOutputRunPreviousOutput.current, + ) && + onChange(queryLanguageOutputRun.output); + }, [queryLanguageOutputRun.output]); + + const onQueryLanguagePopoverSwitch = () => + setIsOpenPopoverQueryLanguage(state => !state); + + const searchBar = ( + <> + {}} /* This method is run by EuiSuggest when there is a change in + a div wrapper of the input and should be defined. Defining this + property prevents an error. */ + suggestions={[]} + isPopoverOpen={ + queryLanguageOutputRun?.searchBarProps?.suggestions?.length > 0 && + isOpenSuggestionPopover + } + onClosePopover={() => setIsOpenSuggestionPopover(false)} + onPopoverFocus={() => setIsOpenSuggestionPopover(true)} + placeholder={'Search'} + append={ + + {searchBarQueryLanguages[queryLanguage.id].label} + + } + isOpen={isOpenPopoverQueryLanguage} + closePopover={onQueryLanguagePopoverSwitch} + > + SYNTAX OPTIONS +
+ + {searchBarQueryLanguages[queryLanguage.id].description} + + {searchBarQueryLanguages[queryLanguage.id].documentationLink && ( + <> + +
+ + Documentation + +
+ + )} + {modes?.length > 1 && ( + <> + + + ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} + value={queryLanguage.id} + onChange={( + event: React.ChangeEvent, + ) => { + const queryLanguageID: string = event.target.value; + setQueryLanguage({ + id: queryLanguageID, + configuration: + searchBarQueryLanguages[ + queryLanguageID + ]?.getConfiguration?.() || {}, + }); + setInput(''); + }} + aria-label='query-language-selector' + /> + + + )} +
+
+ } + {...queryLanguageOutputRun.searchBarProps} + {...(queryLanguageOutputRun.searchBarProps?.onItemClick + ? { + onItemClick: + queryLanguageOutputRun.searchBarProps?.onItemClick(input), + } + : {})} + /> + + ); + return rest.buttonsRender || queryLanguageOutputRun.filterButtons ? ( + + {searchBar} + {rest.buttonsRender && ( + {rest.buttonsRender()} + )} + {queryLanguageOutputRun.filterButtons && ( + + {queryLanguageOutputRun.filterButtons} + + )} + + ) : ( + searchBar + ); +}; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap b/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap new file mode 100644 index 0000000000..3f9bd54fc2 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/aql.test.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap b/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap new file mode 100644 index 0000000000..8d16ea7342 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/__snapshots__/wql.test.tsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchBar component Renders correctly to match the snapshot of query language 1`] = ` +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.md b/plugins/wazuh-core/public/components/search-bar/query-language/aql.md new file mode 100644 index 0000000000..9d144e3b15 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.md @@ -0,0 +1,204 @@ +**WARNING: The search bar was changed and this language needs some adaptations to work.** + +# Query Language - AQL + +AQL (API Query Language) is a query language based in the `q` query parameters of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +## Features +- Suggestions for `fields` (configurable), `operators` and `values` (configurable) +- Support implicit query + +# Language syntax + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +# Developer notes + +## Options + +- `implicitQuery`: add an implicit query that is added to the user input. Optional. +Use UQL (Unified Query Language). +This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + +```ts +// language options +// ID is not equal to 000 and . This is defined in UQL that is transformed internally to the specific query language. +implicitQuery: 'id!=000;' +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + return [ + { label: 'configSum', description: 'Config sum' }, + { label: 'dateAdd', description: 'Date add' }, + { label: 'id', description: 'ID' }, + { label: 'ip', description: 'IP address' }, + { label: 'group', description: 'Group' }, + { label: 'group_config_status', description: 'Synced configuration status' }, + { label: 'lastKeepAline', description: 'Date add' }, + { label: 'manager', description: 'Manager' }, + { label: 'mergedSum', description: 'Merged sum' }, + { label: 'name', description: 'Agent name' }, + { label: 'node_name', description: 'Node name' }, + { label: 'os.platform', description: 'Operating system platform' }, + { label: 'status', description: 'Status' }, + { label: 'version', description: 'Version' }, + ]; + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { previousField }) => { + switch (previousField) { + case 'configSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'dateAdd': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'id': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'ip': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'group_config_status': + return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'lastKeepAline': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'manager': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'mergedSum': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'node_name': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'os.platform': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + case 'status': + return UI_ORDER_AGENT_STATUS.map( + (status) => ({ + type: 'value', + label: status, + }), + ); + break; + case 'version': + return await getAgentFilterValuesMapToSearchBarSuggestion( + previousField, + currentValue, + {q: 'id!=000'} + ); + break; + default: + return []; + break; + } + } + ``` + +## Language workflow + +```mermaid +graph TD; + user_input[User input]-->tokenizer; + subgraph tokenizer + tokenize_regex[Wazuh API `q` regular expression] + end + + tokenizer-->tokens; + + tokens-->searchBarProps; + subgraph searchBarProps; + searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] + searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{implicitQuery} + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion onclick handler]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + end + + tokens-->output; + subgraph output[output]; + output_result[implicitFilter + user input] + end + + output-->output_search_bar[Output] +``` \ No newline at end of file diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx new file mode 100644 index 0000000000..3c6a57caf3 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx @@ -0,0 +1,211 @@ +import { AQL, getSuggestions, tokenizer } from './aql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: AQL.id, + input: '', + modes: [ + { + id: AQL.id, + implicitQuery: 'id!=000;', + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { previousField }) { + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {}, + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render(); + + await waitFor(() => { + const elementImplicitQuery = wrapper.container.querySelector( + '.euiCodeBlock__code', + ); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); + +describe('Query language - AQL', () => { + // Tokenize the input + it.each` + input | tokens + ${''} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'f'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'f' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with spaces<'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with spaces<' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value with (parenthesis)'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value with (parenthesis)' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!='} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'field=value;field2!=value2'} | ${[{ type: 'operator_group', value: undefined }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '!=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'('} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2);'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2='} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=value2)'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'value2' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + ${'(field>2;field2=custom value())'} | ${[{ type: 'operator_group', value: '(' }, { type: 'field', value: 'field' }, { type: 'operator_compare', value: '>' }, { type: 'value', value: '2' }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: ';' }, { type: 'operator_group', value: undefined }, { type: 'field', value: 'field2' }, { type: 'operator_compare', value: '=' }, { type: 'value', value: 'custom value()' }, { type: 'operator_group', value: ')' }, { type: 'conjunction', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'field', value: undefined }, { type: 'operator_compare', value: undefined }, { type: 'value', value: undefined }, { type: 'operator_group', value: undefined }, { type: 'conjunction', value: undefined }]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { + expect(tokenizer(input)).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value;'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'field=value;field2'} | ${[{ description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field=value;field2='} | ${[{ label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { label: '190.0.0.1', type: 'value' }, { label: '190.0.0.2', type: 'value' }]} + ${'field=value;field2=127'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: '127.0.0.1', type: 'value' }, { label: '127.0.0.2', type: 'value' }, { description: 'and', label: ';', type: 'conjunction' }, { description: 'or', label: ',', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer(input), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { previousField }) { + switch (previousField) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); + + // When a suggestion is clicked, change the input text + it.each` + AQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ';' }} | ${'field=value;'} + ${'field=value;'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value;field2'} + ${'field=value;field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value;field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field=with spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field=with "spaces'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="value'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: ',' }} | ${'(field=value,'} + ${'(field=value,'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value,field2'} + ${'(field=value,field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value,field2>'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value,field2~'} + ${'(field=value,field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value,field2>value3'} + ${'(field=value,field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value,field2>value2)'} + `( + 'click suggestion - AQL "$AQL" => "$changedInput"', + async ({ AQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; + + const qlOutput = await AQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + implicitQuery: '', + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | AQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value;'} + ${'field=value;field2'} | ${'field=value;field2'} + ${'field="'} | ${'field="'} + ${'field=with spaces'} | ${'field=with spaces'} + ${'field=with "spaces'} | ${'field=with "spaces'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value,'} + ${'(field=value,field2'} | ${'(field=value,field2'} + ${'(field=value,field2>'} | ${'(field=value,field2>'} + ${'(field=value,field2>value2'} | ${'(field=value,field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $AQL', + async ({ UQL, AQL: changedInput }) => { + expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); + }, + ); +}); diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx new file mode 100644 index 0000000000..68d1292a23 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -0,0 +1,545 @@ +import React from 'react'; +import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; +import { webDocumentationLink } from '../../../../common/services/web_documentation'; + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction'; +type IToken = { type: ITokenType; value: string }; +type ITokens = IToken[]; + +/* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: +??? + +Implemented schema: +?????? +*/ + +// Language definition +export const language = { + // Tokens + tokens: { + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + ';': 'and', + ',': 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, +}; + +/** + * Creator of intermediate interface of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType) { + return function ({ ...params }) { + return { + type, + ...params, + }; + }; +} + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param input + * @returns + */ +export function tokenizer(input: string): ITokens { + // API regular expression + // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 + // self.query_regex = re.compile( + // # A ( character. + // r"(\()?" + + // # Field name: name of the field to look on DB. + // r"([\w.]+)" + + // # Operator: looks for '=', '!=', '<', '>' or '~'. + // rf"([{''.join(self.query_operators.keys())}]{{1,2}})" + + // # Value: A string. + // r"((?:(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*" + // r"(?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]+)" + // r"(?:\((?:\[[\[\]\w _\-.,:?\\/'\"=@%<>{}]*]|[\[\]\w _\-.:?\\/'\"=@%<>{}]*)\))*)+)" + + // # A ) character. + // r"(\))?" + + // # Separator: looks for ';', ',' or nothing. + // rf"([{''.join(self.query_separators.keys())}])?" + // ) + + const re = new RegExp( + // The following regular expression is based in API one but was modified to use named groups + // and added the optional operator to allow matching the entities when the query is not + // completed. This helps to tokenize the query and manage when the input is not completed. + // A ( character. + '(?\\()?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Value: A string. + '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find + // A ) character. + '(?\\))?' + + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + 'g', + ); + + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value, + })), + ) + .flat(); +} + +type QLOptionSuggestionEntityItem = { + description?: string; + label: string; +}; + +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction'; +}; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string; color: string }; +}; + +type QLOptionSuggestionHandler = ( + currentValue: string | undefined, + { + previousField, + previousOperatorCompare, + }: { previousField: string; previousOperatorCompare: string }, +) => Promise; + +type optionsQL = { + suggestions: { + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; + }; +}; + +/** + * Get the last token with value + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValue(tokens: ITokens): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find(({ value }) => value); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenWithValueByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type === tokenType && value, + ); + return tokenFound; +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions( + tokens: ITokens, + options: optionsQL, +): Promise { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = getLastTokenWithValue(tokens); + + // If it can't get a token with value, then returns fields and open operator group + if (!lastToken?.type) { + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + } + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + ...( + await options.suggestions.value(undefined, { + previousField: getLastTokenWithValueByType(tokens, 'field')! + .value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), + ] + : []), + ]; + break; + case 'value': + return [ + ...(lastToken.value + ? [ + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] + : []), + ...( + await options.suggestions.value(lastToken.value, { + previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(mapSuggestionCreatorValue), + ...Object.entries(language.tokens.conjunction.literal).map( + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest, + }; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @returns + */ +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[], +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); +} + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: { implicitQuery?: string } = {}) { + const unifiedQuery = `${options?.implicitQuery ?? ''}${ + options?.implicitQuery ? `(${input})` : input + }`; + return { + language: AQL.id, + query: unifiedQuery, + unifiedQuery, + }; +} + +export const AQL = { + id: 'aql', + label: 'AQL', + description: 'API Query Language (AQL) allows to do queries.', + documentationLink: webDocumentationLink('user-manual/api/queries.html'), + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); + + return { + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToEuiSuggestItem( + await getSuggestions(tokens, params.queryLanguage.parameters), + ), + // Handler to manage when clicking in a suggestion item + onItemClick: currentInput => item => { + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + params.onSearch( + getOutput(currentInput, params.queryLanguage.parameters), + ); + } else { + // When the clicked item has another iconType + const lastToken: IToken = getLastTokenWithValue(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token + lastToken.value = item.label; + } else { + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: item.label, + }); + } + + // Change the input + params.setInput( + tokens + .filter(({ value }) => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); + } + }, + prepend: params.queryLanguage.parameters.implicitQuery ? ( + + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) + } + iconType='filter' + > + + {params.queryLanguage.parameters.implicitQuery} + + + } + isOpen={ + params.queryLanguage.configuration.isOpenPopoverImplicitFilter + } + closePopover={() => + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) + } + > + + Implicit query:{' '} + {params.queryLanguage.parameters.implicitQuery} + + This query is added to the input. + + ) : null, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true, + }, + output: getOutput(input, params.queryLanguage.parameters), + }; + }, + transformUQLToQL(unifiedQuery: string): string { + return unifiedQuery; + }, +}; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts new file mode 100644 index 0000000000..5a897d1d34 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts @@ -0,0 +1,32 @@ +import { AQL } from './aql'; +import { WQL } from './wql'; + +type SearchBarQueryLanguage = { + description: string; + documentationLink?: string; + id: string; + label: string; + getConfiguration?: () => any; + run: (input: string | undefined, params: any) => Promise<{ + searchBarProps: any, + output: { + language: string, + unifiedQuery: string, + query: string + } + }>; + transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; +}; + +// Register the query languages +export const searchBarQueryLanguages: { + [key: string]: SearchBarQueryLanguage; +} = [AQL, WQL].reduce((accum, item) => { + if (accum[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + return { + ...accum, + [item.id]: item, + }; +}, {}); diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.md b/plugins/wazuh-core/public/components/search-bar/query-language/wql.md new file mode 100644 index 0000000000..108c942d32 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.md @@ -0,0 +1,269 @@ +# Query Language - WQL + +WQL (Wazuh Query Language) is a query language based in the `q` query parameter of the Wazuh API +endpoints. + +Documentation: https://wazuh.com/./user-manual/api/queries.html + +The implementation is adapted to work with the search bar component defined +`public/components/search-bar/index.tsx`. + +# Language syntax + +It supports 2 modes: + +- `explicit`: define the field, operator and value +- `search term`: use a term to search in the available fields + +Theses modes can not be combined. + +`explicit` mode is enabled when it finds a field and operator tokens. + +## Mode: explicit + +### Schema + +``` +???????????? +``` + +### Fields + +Regular expression: /[\\w.]+/ + +Examples: + +``` +field +field.custom +``` + +### Operators + +#### Compare + +- `=` equal to +- `!=` not equal to +- `>` bigger +- `<` smaller +- `~` like + +#### Group + +- `(` open +- `)` close + +#### Conjunction (logical) + +- `and` intersection +- `or` union + +#### Values + +- Value without spaces can be literal +- Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. + +Examples: +``` +value_without_whitespace +"value with whitespaces" +"value with whitespaces and escaped \"quotes\"" +``` + +### Notes + +- The tokens can be separated by whitespaces. + +### Examples + +- Simple query + +``` +id=001 +id = 001 +``` + +- Complex query (logical operator) +``` +status=active and os.platform~linux +status = active and os.platform ~ linux +``` + +``` +status!=never_connected and ip~240 or os.platform~linux +status != never_connected and ip ~ 240 or os.platform ~ linux +``` + +- Complex query (logical operators and group operator) +``` +(status!=never_connected and ip~240) or id=001 +( status != never_connected and ip ~ 240 ) or id = 001 +``` + +## Mode: search term + +Search the term in the available fields. + +This mode is used when there is no a `field` and `operator` according to the regular expression +of the **explicit** mode. + +### Examples: + +``` +linux +``` + +If the available fields are `id` and `ip`, then the input will be translated under the hood to the +following UQL syntax: + +``` +id~linux,ip~linux +``` + +## Developer notes + +## Features +- Support suggestions for each token entity. `fields` and `values` are customizable. +- Support implicit query. +- Support for search term mode. It enables to search a term in multiple fields. + The query is built under the hoods. This mode requires there are `field` and `operator_compare`. + +### Implicit query + +This a query that can't be added, edited or removed by the user. It is added to the user input. + +### Search term mode + +This mode enables to search in multiple fields using a search term. The fields to use must be defined. + +Use an union expression of each field with the like as operation `~`. + +The user input is transformed to something as: +``` +field1~user_input,field2~user_input,field3~user_input +``` + +## Options + +- `options`: options + + - `implicitQuery`: add an implicit query that is added to the user input. Optional. + This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + - `query`: query string in UQL (Unified Query Language) +Use UQL (Unified Query Language). + - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + - `searchTermFields`: define the fields used to build the query for the search term mode + - `filterButtons`: define a list of buttons to filter in the search bar + + +```ts +// language options +options: { + // ID is not equal to 000 and . This is defined in UQL that is transformed internally to + // the specific query language. + implicitQuery: { + query: 'id!=000', + conjunction: ';' + } + searchTermFields: ['id', 'ip'] + filterButtons: [ + {id: 'status-active', input: 'status=active', label: 'Active'} + ] +} +``` + +- `suggestions`: define the suggestion handlers. This is required. + + - `field`: method that returns the suggestions for the fields + + ```ts + // language options + field(currentValue) { + // static or async fetching is allowed + return [ + { label: 'field1', description: 'Description' }, + { label: 'field2', description: 'Description' } + ]; + } + ``` + + - `value`: method that returns the suggestion for the values + ```ts + // language options + value: async (currentValue, { field }) => { + // static or async fetching is allowed + // async fetching data + // const response = await fetchData(); + return [ + { label: 'value1' }, + { label: 'value2' } + ] + } + ``` + +- `validate`: define validation methods for the field types. Optional + - `value`: method to validate the value token + + ```ts + validate: { + value: (token, {field, operator_compare}) => { + if(field === 'field1'){ + const value = token.formattedValue || token.value + return /\d+/ ? undefined : `Invalid value for field ${field}, only digits are supported: "${value}"` + } + } + } + ``` + +## Language workflow + +```mermaid +graph TD; + user_input[User input]-->ql_run; + ql_run-->filterButtons[filterButtons]; + ql_run-->tokenizer-->tokens; + tokens-->searchBarProps; + tokens-->output; + + subgraph tokenizer + tokenize_regex[Query language regular expression: decomposition and extract quoted values] + end + + subgraph searchBarProps; + searchBarProps_suggestions[suggestions]-->searchBarProps_suggestions_input_isvalid{Input is valid} + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_success[Yes] + searchBarProps_suggestions_input_isvalid{Is input valid?}-->searchBarProps_suggestions_input_isvalid_fail[No] + searchBarProps_suggestions_input_isvalid_success[Yes]--->searchBarProps_suggestions_get_last_token_with_value[Get last token with value]-->searchBarProps_suggestions__result[Suggestions] + searchBarProps_suggestions__result[Suggestions]-->EuiSuggestItem + searchBarProps_suggestions_input_isvalid_fail[No]-->searchBarProps_suggestions_invalid[Invalid with error message] + searchBarProps_suggestions_invalid[Invalid with error message]-->EuiSuggestItem + searchBarProps_prepend[prepend]-->searchBarProps_prepend_implicitQuery{options.implicitQuery} + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_yes[Yes]-->EuiButton + searchBarProps_prepend_implicitQuery{options.implicitQuery}-->searchBarProps_prepend_implicitQuery_no[No]-->null + searchBarProps_disableFocusTrap:true[disableFocusTrap = true] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_search[Search suggestion] + searchBarProps_onItemClick_suggestion_search[Search suggestion]-->searchBarProps_onItemClick_suggestion_search_run[Run search] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_edit_current_token[Edit current token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_add_new_token[Add new token]-->searchBarProps_onItemClick_build_input[Build input] + searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] + searchBarProps_isInvalid[isInvalid]-->searchBarProps_validate_input[validate input] + end + + subgraph output[output]; + output_input_options_implicitFilter[options.implicitFilter]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + output_input_user_input_QL[User input in QL]-->output_input_user_input_UQL[User input in UQL]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] + end + + subgraph filterButtons; + filterButtons_optional{options.filterButtons}-->filterButtons_optional_yes[Yes]-->filterButtons_optional_yes_component[Render fitter button] + filterButtons_optional{options.filterButtons}-->filterButtons_optional_no[No]-->filterButtons_optional_no_null[null] + end +``` + +## Notes + +- The value that contains the following characters: `!`, `~` are not supported by the AQL and this +could cause problems when do the request to the API. +- The value with spaces are wrapped with `"`. If the value contains the `\"` sequence this is +replaced by `"`. This could cause a problem with values that are intended to have the mentioned +sequence. \ No newline at end of file diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx new file mode 100644 index 0000000000..a803a79ecf --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -0,0 +1,472 @@ +import { + getSuggestions, + tokenizer, + transformSpecificQLToUnifiedQL, + WQL, +} from './wql'; +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; + +describe('SearchBar component', () => { + const componentProps = { + defaultMode: WQL.id, + input: '', + modes: [ + { + id: WQL.id, + options: { + implicitQuery: { + query: 'id!=000', + conjunction: ';', + }, + }, + suggestions: { + field(currentValue) { + return []; + }, + value(currentValue, { field }) { + return []; + }, + }, + }, + ], + /* eslint-disable @typescript-eslint/no-empty-function */ + onChange: () => {}, + onSearch: () => {}, + /* eslint-enable @typescript-eslint/no-empty-function */ + }; + + it('Renders correctly to match the snapshot of query language', async () => { + const wrapper = render(); + + await waitFor(() => { + expect(wrapper.container).toMatchSnapshot(); + }); + }); +}); + +/* eslint-disable max-len */ +describe('Query language - WQL', () => { + // Tokenize the input + function tokenCreator({ type, value, formattedValue }) { + return { type, value, ...(formattedValue ? { formattedValue } : {}) }; + } + + const t = { + opGroup: (value = undefined) => + tokenCreator({ type: 'operator_group', value }), + opCompare: (value = undefined) => + tokenCreator({ type: 'operator_compare', value }), + field: (value = undefined) => tokenCreator({ type: 'field', value }), + value: (value = undefined, formattedValue = undefined) => + tokenCreator({ + type: 'value', + value, + formattedValue: formattedValue ?? value, + }), + whitespace: (value = undefined) => + tokenCreator({ type: 'whitespace', value }), + conjunction: (value = undefined) => + tokenCreator({ type: 'conjunction', value }), + }; + + // Token undefined + const tu = { + opGroup: tokenCreator({ type: 'operator_group', value: undefined }), + opCompare: tokenCreator({ type: 'operator_compare', value: undefined }), + whitespace: tokenCreator({ type: 'whitespace', value: undefined }), + field: tokenCreator({ type: 'field', value: undefined }), + value: tokenCreator({ + type: 'value', + value: undefined, + formattedValue: undefined, + }), + conjunction: tokenCreator({ type: 'conjunction', value: undefined }), + }; + + const tuBlankSerie = [ + tu.opGroup, + tu.whitespace, + tu.field, + tu.whitespace, + tu.opCompare, + tu.whitespace, + tu.value, + tu.whitespace, + tu.opGroup, + tu.whitespace, + tu.conjunction, + tu.whitespace, + ]; + + it.each` + input | tokens + ${''} | ${tuBlankSerie} + ${'f'} | ${[tu.opGroup, tu.whitespace, t.field('f'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=or'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('or'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueand'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueand'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=valueor'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('valueor'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value!='), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value>'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value>'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value<'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value<'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value~'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value~'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value and value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value and value2"', 'value and value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value or value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value or value2"', 'value or value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value = value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value = value2"', 'value = value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value != value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value != value2"', 'value != value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value > value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value > value2"', 'value > value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value < value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value < value2"', 'value < value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field="value ~ value2"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('"value ~ value2"', 'value ~ value2'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie] /** ~ character is not supported as value in the q query parameter */} + ${'field=value and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + ${'field=value and '} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), ...tuBlankSerie]} + ${'field=value and field2'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, tu.opCompare, tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!='} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, tu.value, tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!="value"'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('"value"', 'value'), tu.whitespace, tu.opGroup, tu.whitespace, tu.conjunction, tu.whitespace, ...tuBlankSerie]} + ${'field=value and field2!=value2 and'} | ${[tu.opGroup, tu.whitespace, t.field('field'), tu.whitespace, t.opCompare('='), tu.whitespace, t.value('value'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.field('field2'), tu.whitespace, t.opCompare('!='), tu.whitespace, t.value('value2'), t.whitespace(' '), tu.opGroup, tu.whitespace, t.conjunction('and'), tu.whitespace, ...tuBlankSerie]} + `(`Tokenizer API input $input`, ({ input, tokens }) => { + expect(tokenizer(input)).toEqual(tokens); + }); + + // Get suggestions + it.each` + input | suggestions + ${''} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + ${'w'} | ${[]} + ${'f'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }]} + ${'field'} | ${[{ description: 'Field2', label: 'field2', type: 'field' }, { description: 'equality', label: '=', type: 'operator_compare' }, { description: 'not equality', label: '!=', type: 'operator_compare' }, { description: 'bigger', label: '>', type: 'operator_compare' }, { description: 'smaller', label: '<', type: 'operator_compare' }, { description: 'like as', label: '~', type: 'operator_compare' }]} + ${'field='} | ${[{ label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }]} + ${'field=v'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value'} | ${[{ description: 'run the search query', label: 'Search', type: 'function_search' }, { label: 'value', type: 'value' }, { label: 'value2', type: 'value' }, { label: 'value3', type: 'value' }, { label: 'value4', type: 'value' }, { description: 'and', label: 'and', type: 'conjunction' }, { description: 'or', label: 'or', type: 'conjunction' }, { description: 'close group', label: ')', type: 'operator_group' }]} + ${'field=value and'} | ${[{ description: 'Field', label: 'field', type: 'field' }, { description: 'Field2', label: 'field2', type: 'field' }, { description: 'open group', label: '(', type: 'operator_group' }]} + `('Get suggestion from the input: $input', async ({ input, suggestions }) => { + expect( + await getSuggestions(tokenizer(input), { + id: 'aql', + suggestions: { + field(currentValue) { + return [ + { label: 'field', description: 'Field' }, + { label: 'field2', description: 'Field2' }, + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); + }, + value(currentValue = '', { field }) { + switch (field) { + case 'field': + return ['value', 'value2', 'value3', 'value4'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + case 'field2': + return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] + .filter(value => value.startsWith(currentValue)) + .map(value => ({ type: 'value', label: value })); + break; + default: + return []; + break; + } + }, + }, + }), + ).toEqual(suggestions); + }); + + // Transform specific query language to UQL (Unified Query Language) + it.each` + WQL | UQL + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=value'} | ${'field=value'} + ${'field=value()'} | ${'field=value()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom value"'} | ${'field=custom value'} + ${'field="custom value()"'} | ${'field=custom value()'} + ${'field="value and value2"'} | ${'field=value and value2'} + ${'field="value or value2"'} | ${'field=value or value2'} + ${'field="value = value2"'} | ${'field=value = value2'} + ${'field="value != value2"'} | ${'field=value != value2'} + ${'field="value > value2"'} | ${'field=value > value2'} + ${'field="value < value2"'} | ${'field=value < value2'} + ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field="custom \\"value"'} | ${'field=custom "value'} + ${'field="custom \\"value\\""'} | ${'field=custom "value"'} + ${'field=value and'} | ${'field=value;'} + ${'field="custom value" and'} | ${'field=custom value;'} + ${'(field=value'} | ${'(field=value'} + ${'(field=value)'} | ${'(field=value)'} + ${'(field=value) and'} | ${'(field=value);'} + ${'(field=value) and field2'} | ${'(field=value);field2'} + ${'(field=value) and field2>'} | ${'(field=value);field2>'} + ${'(field=value) and field2>"wrappedcommas"'} | ${'(field=value);field2>wrappedcommas'} + ${'(field=value) and field2>"value with spaces"'} | ${'(field=value);field2>value with spaces'} + ${'field ='} | ${'field='} + ${'field = value'} | ${'field=value'} + ${'field = value()'} | ${'field=value()'} + ${'field = valueand'} | ${'field=valueand'} + ${'field = valueor'} | ${'field=valueor'} + ${'field = value='} | ${'field=value='} + ${'field = value!='} | ${'field=value!='} + ${'field = value>'} | ${'field=value>'} + ${'field = value<'} | ${'field=value<'} + ${'field = value~'} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field = "custom value"'} | ${'field=custom value'} + ${'field = "custom value()"'} | ${'field=custom value()'} + ${'field = "value and value2"'} | ${'field=value and value2'} + ${'field = "value or value2"'} | ${'field=value or value2'} + ${'field = "value = value2"'} | ${'field=value = value2'} + ${'field = "value != value2"'} | ${'field=value != value2'} + ${'field = "value > value2"'} | ${'field=value > value2'} + ${'field = "value < value2"'} | ${'field=value < value2'} + ${'field = "value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} + ${'field = value or'} | ${'field=value,'} + ${'field = value or field2'} | ${'field=value,field2'} + ${'field = value or field2 <'} | ${'field=value,field2<'} + ${'field = value or field2 < value2'} | ${'field=value,field2 "custom value" '} | ${'(field=value);field2>custom value'} + `('transformSpecificQLToUnifiedQL - WQL $WQL TO UQL $UQL', ({ WQL, UQL }) => { + expect(transformSpecificQLToUnifiedQL(WQL)).toEqual(UQL); + }); + + // When a suggestion is clicked, change the input text + it.each` + WQL | clikedSuggestion | changedInput + ${''} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'field'} + ${'field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field2'} + ${'field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'field='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value()' }} | ${'field=value()'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueand' }} | ${'field=valueand'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'valueor' }} | ${'field=valueor'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value=' }} | ${'field=value='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value!=' }} | ${'field=value!='} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value>' }} | ${'field=value>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value<' }} | ${'field=value<'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value~' }} | ${'field=value~' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '!=' }} | ${'field!='} + ${'field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'field=value2'} + ${'field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'field=value and '} + ${'field=value and'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or'} + ${'field=value and'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'field=value or '} + ${'field=value and '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} + ${'field=value and field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value and field2>'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field="with spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field="with \\"spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()' }} | ${'field="with value()"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value' }} | ${'field="with and value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value' }} | ${'field="with or value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with = value' }} | ${'field="with = value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with != value' }} | ${'field="with != value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value' }} | ${'field="with > value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value' }} | ${'field="with < value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value' }} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="\\"value"'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} + ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces' }} | ${'field="other spaces"'} + ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} + ${'('} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field' }} | ${'(field'} + ${'(field'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field2'} + ${'(field'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '=' }} | ${'(field='} + ${'(field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'(field=value'} + ${'(field=value'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value2'} + ${'(field=value'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'or' }} | ${'(field=value or '} + ${'(field=value or'} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and'} + ${'(field=value or'} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or '} | ${{ type: { iconType: 'kqlSelector', color: 'tint3' }, label: 'and' }} | ${'(field=value and '} + ${'(field=value or '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'(field=value or field2'} + ${'(field=value or field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'(field=value or field2>'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '~' }} | ${'(field=value or field2~'} + ${'(field=value or field2>'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value2' }} | ${'(field=value or field2>value2'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value3' }} | ${'(field=value or field2>value3'} + ${'(field=value or field2>value2'} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: ')' }} | ${'(field=value or field2>value2 )'} + `( + 'click suggestion - WQL "$WQL" => "$changedInput"', + async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { + // Mock input + let input = currentInput; + + const qlOutput = await WQL.run(input, { + setInput: (value: string): void => { + input = value; + }, + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => [], + value: () => [], + }, + }, + }, + }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); + expect(input).toEqual(changedInput); + }, + ); + + // Transform the external input in UQL (Unified Query Language) to QL + it.each` + UQL | WQL + ${''} | ${''} + ${'field'} | ${'field'} + ${'field='} | ${'field='} + ${'field=()'} | ${'field=()'} + ${'field=valueand'} | ${'field=valueand'} + ${'field=valueor'} | ${'field=valueor'} + ${'field=value='} | ${'field=value='} + ${'field=value!='} | ${'field=value!='} + ${'field=value>'} | ${'field=value>'} + ${'field=value<'} | ${'field=value<'} + ${'field=value~'} | ${'field=value~'} + ${'field!='} | ${'field!='} + ${'field>'} | ${'field>'} + ${'field<'} | ${'field<'} + ${'field~'} | ${'field~'} + ${'field=value'} | ${'field=value'} + ${'field=value;'} | ${'field=value and '} + ${'field=value;field2'} | ${'field=value and field2'} + ${'field="'} | ${'field="\\""'} + ${'field=with spaces'} | ${'field="with spaces"'} + ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'field=value ()'} | ${'field="value ()"'} + ${'field=with and value'} | ${'field="with and value"'} + ${'field=with or value'} | ${'field="with or value"'} + ${'field=with = value'} | ${'field="with = value"'} + ${'field=with > value'} | ${'field="with > value"'} + ${'field=with < value'} | ${'field="with < value"'} + ${'('} | ${'('} + ${'(field'} | ${'(field'} + ${'(field='} | ${'(field='} + ${'(field=value'} | ${'(field=value'} + ${'(field=value,'} | ${'(field=value or '} + ${'(field=value,field2'} | ${'(field=value or field2'} + ${'(field=value,field2>'} | ${'(field=value or field2>'} + ${'(field=value,field2>value2'} | ${'(field=value or field2>value2'} + ${'(field=value,field2>value2)'} | ${'(field=value or field2>value2)'} + ${'implicit=value;'} | ${''} + ${'implicit=value;field'} | ${'field'} + `( + 'Transform the external input UQL to QL - UQL $UQL => $WQL', + async ({ UQL, WQL: changedInput }) => { + expect( + WQL.transformInput(UQL, { + parameters: { + options: { + implicitQuery: { + query: 'implicit=value', + conjunction: ';', + }, + }, + }, + }), + ).toEqual(changedInput); + }, + ); + + /* The ! and ~ characters can't be part of a value that contains examples. The tests doesn't + include these cases. + + Value examples: + - with != value + - with ~ value + */ + + // Validate the tokens + // Some examples of value tokens are based on this API test: https://github.com/wazuh/wazuh/blob/813595cf58d753c1066c3e7c2018dbb4708df088/framework/wazuh/core/tests/test_utils.py#L987-L1050 + it.each` + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field_not_number=1'} | ${['Numbers are not valid for field_not_number']} + ${'field_not_number=value1'} | ${['Numbers are not valid for field_not_number']} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} + ${'field1=value,'} | ${['"value," is not a valid value.']} + ${'field1="Mozilla Firefox 53.0 (x64 en-US)"'} | ${undefined} + ${'field1="[\\"https://example-link@<>=,%?\\"]"'} | ${undefined} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} + ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} + `( + 'validate the tokens - WQL $WQL => $validationError', + async ({ WQL: currentInput, validationError }) => { + const qlOutput = await WQL.run(currentInput, { + queryLanguage: { + parameters: { + options: {}, + suggestions: { + field: () => + ['field1', 'field2', 'field_not_number'].map(label => ({ + label, + })), + value: () => [], + }, + validate: { + value: (token, { field, operator_compare }) => { + if (field === 'field_not_number') { + const value = token.formattedValue || token.value; + return /\d/.test(value) + ? `Numbers are not valid for ${field}` + : undefined; + } + }, + }, + }, + }, + }); + expect(qlOutput.output.error).toEqual(validationError); + }, + ); +}); diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx new file mode 100644 index 0000000000..7d139db27b --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -0,0 +1,1203 @@ +import React from 'react'; +import { + EuiButtonEmpty, + EuiButtonGroup, + EuiPopover, + EuiText, + EuiCode, +} from '@elastic/eui'; +import { tokenizer as tokenizerUQL } from './aql'; +import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT } from '../../../../common/constants'; +import { webDocumentationLink } from '../../../../common/services/web_documentation'; + +/* UI Query language +https://documentation.wazuh.com/current/user-manual/api/queries.html + +// Example of another query language definition +*/ + +type ITokenType = + | 'field' + | 'operator_compare' + | 'operator_group' + | 'value' + | 'conjunction' + | 'whitespace'; +type IToken = { type: ITokenType; value: string; formattedValue?: string }; +type ITokens = IToken[]; + +/* API Query Language +Define the API Query Language to use in the search bar. +It is based in the language used by the q query parameter. +https://documentation.wazuh.com/current/user-manual/api/queries.html + +Use the regular expression of API with some modifications to allow the decomposition of +input in entities that doesn't compose a valid query. It allows get not-completed queries. + +API schema: +??? + +Implemented schema: +???????????? +*/ + +// Language definition +const language = { + // Tokens + tokens: { + // eslint-disable-next-line camelcase + operator_compare: { + literal: { + '=': 'equality', + '!=': 'not equality', + '>': 'bigger', + '<': 'smaller', + '~': 'like as', + }, + }, + conjunction: { + literal: { + and: 'and', + or: 'or', + }, + }, + // eslint-disable-next-line camelcase + operator_group: { + literal: { + '(': 'open group', + ')': 'close group', + }, + }, + }, + equivalencesToUQL: { + conjunction: { + literal: { + and: ';', + or: ',', + }, + }, + }, +}; + +// Suggestion mapper by language token type +const suggestionMappingLanguageTokenType = { + field: { iconType: 'kqlField', color: 'tint4' }, + // eslint-disable-next-line camelcase + operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, + value: { iconType: 'kqlValue', color: 'tint0' }, + conjunction: { iconType: 'kqlSelector', color: 'tint3' }, + // eslint-disable-next-line camelcase + operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, + // eslint-disable-next-line camelcase + function_search: { iconType: 'search', color: 'tint5' }, + // eslint-disable-next-line camelcase + validation_error: { iconType: 'alert', color: 'tint2' }, +}; + +/** + * Creator of intermediate interface of EuiSuggestItem + * @param type + * @returns + */ +function mapSuggestionCreator(type: ITokenType) { + return function ({ label, ...params }) { + return { + type, + ...params, + /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is + displayed in the console related to prop types + */ + ...(typeof label !== 'undefined' ? { label: String(label) } : {}), + }; + }; +} + +const mapSuggestionCreatorField = mapSuggestionCreator('field'); +const mapSuggestionCreatorValue = mapSuggestionCreator('value'); + +/** + * Transform the conjunction to the query language syntax + * @param conjunction + * @returns + */ +function transformQLConjunction(conjunction: string): string { + // If the value has a whitespace or comma, then + return conjunction === language.equivalencesToUQL.conjunction.literal['and'] + ? ` ${language.tokens.conjunction.literal['and']} ` + : ` ${language.tokens.conjunction.literal['or']} `; +} + +/** + * Transform the value to the query language syntax + * @param value + * @returns + */ +function transformQLValue(value: string): string { + // If the value has a whitespace or comma, then + return /[\s|"]/.test(value) + ? // Escape the commas (") => (\") and wraps the string with commas ("") + `"${value.replace(/"/, '\\"')}"` + : // Raw value + value; +} + +/** + * Tokenize the input string. Returns an array with the tokens. + * @param input + * @returns + */ +export function tokenizer(input: string): ITokens { + const re = new RegExp( + // A ( character. + '(?\\()?' + + // Whitespace + '(?\\s+)?' + + // Field name: name of the field to look on DB. + '(?[\\w.]+)?' + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Operator: looks for '=', '!=', '<', '>' or '~'. + // This seems to be a bug because is not searching the literal valid operators. + // I guess the operator is validated after the regular expression matches + `(?[${Object.keys( + language.tokens.operator_compare.literal, + )}]{1,2})?` + // Added an optional find + // Whitespace + '(?\\s+)?' + + // Value: A string. + // Simple value + // Quoted ", "value, "value", "escaped \"quote" + // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes + '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + + // Whitespace + '(?\\s+)?' + + // A ) character. + '(?\\))?' + + // Whitespace + '(?\\s+)?' + + `(?${Object.keys(language.tokens.conjunction.literal).join( + '|', + )})?` + + // Whitespace + '(?\\s+)?', + 'g', + ); + + return [...input.matchAll(re)] + .map(({ groups }) => + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key, + value, + ...(key === 'value' && + (value && /^"([\s\S]+)"$/.test(value) + ? { formattedValue: value.match(/^"([\s\S]+)"$/)[1] } + : { formattedValue: value })), + })), + ) + .flat(); +} + +type QLOptionSuggestionEntityItem = { + description?: string; + label: string; +}; + +type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { + type: + | 'operator_group' + | 'field' + | 'operator_compare' + | 'value' + | 'conjunction' + | 'function_search'; +}; + +type SuggestItem = QLOptionSuggestionEntityItem & { + type: { iconType: string; color: string }; +}; + +type QLOptionSuggestionHandler = ( + currentValue: string | undefined, + { field, operatorCompare }: { field: string; operatorCompare: string }, +) => Promise; + +type OptionsQLImplicitQuery = { + query: string; + conjunction: string; +}; +type OptionsQL = { + options?: { + implicitQuery?: OptionsQLImplicitQuery; + searchTermFields?: string[]; + filterButtons: { id: string; label: string; input: string }[]; + }; + suggestions: { + field: QLOptionSuggestionHandler; + value: QLOptionSuggestionHandler; + }; + validate?: { + value?: { + [key: string]: ( + token: IToken, + nearTokens: { field: string; operator: string }, + ) => string | undefined; + }; + }; +}; + +export interface ISearchBarModeWQL extends OptionsQL { + id: 'wql'; +} + +/** + * Get the last token with value + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenDefined(tokens: ITokens): IToken | undefined { + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type !== 'whitespace' && value, + ); + return tokenFound; +} + +/** + * Get the last token with value by type + * @param tokens Tokens + * @param tokenType token type to search + * @returns + */ +function getLastTokenDefinedByType( + tokens: ITokens, + tokenType: ITokenType, +): IToken | undefined { + // Find the last token by type + // Reverse the tokens array and use the Array.protorype.find method + const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArrayReversed = shallowCopyArray.reverse(); + const tokenFound = shallowCopyArrayReversed.find( + ({ type, value }) => type === tokenType && value, + ); + return tokenFound; +} + +/** + * Get the token that is near to a token position of the token type. + * @param tokens + * @param tokenReferencePosition + * @param tokenType + * @param mode + * @returns + */ +function getTokenNearTo( + tokens: ITokens, + tokenType: ITokenType, + mode: 'previous' | 'next' = 'previous', + options: { + tokenReferencePosition?: number; + tokenFoundShouldHaveValue?: boolean; + } = {}, +): IToken | undefined { + const shallowCopyTokens = Array.from([...tokens]); + const computedShallowCopyTokens = + mode === 'previous' + ? shallowCopyTokens + .slice(0, options?.tokenReferencePosition || tokens.length) + .reverse() + : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); + return computedShallowCopyTokens.find( + ({ type, value }) => + type === tokenType && (options?.tokenFoundShouldHaveValue ? value : true), + ); +} + +/** + * It returns the regular expression that validate the token of type value + * @returns The regular expression + */ +function getTokenValueRegularExpression() { + return new RegExp( + // Value: A string. + '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + + '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + + '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + ); +} + +/** + * It filters the values that matche the validation regular expression and returns the first items + * defined by SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT constant. + * @param suggestions Suggestions provided by the suggestions.value method of each instance of the + * search bar + * @returns + */ +function filterTokenValueSuggestion( + suggestions: QLOptionSuggestionEntityItemTyped[], +) { + return suggestions + ? suggestions + .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { + const re = getTokenValueRegularExpression(); + return re.test(label); + }) + .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT) + : []; +} + +/** + * Get the suggestions from the tokens + * @param tokens + * @param language + * @param options + * @returns + */ +export async function getSuggestions( + tokens: ITokens, + options: OptionsQL, +): Promise { + if (!tokens.length) { + return []; + } + + // Get last token + const lastToken = getLastTokenDefined(tokens); + + // If it can't get a token with value, then returns fields and open operator group + if (!lastToken?.type) { + return [ + // Search function + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + } + + switch (lastToken.type) { + case 'field': + return [ + // fields that starts with the input but is not equals + ...(await options.suggestions.field()) + .filter( + ({ label }) => + label.startsWith(lastToken.value) && label !== lastToken.value, + ) + .map(mapSuggestionCreatorField), + // operators if the input field is exact + ...((await options.suggestions.field()).some( + ({ label }) => label === lastToken.value, + ) + ? [ + ...Object.keys(language.tokens.operator_compare.literal).map( + operator => ({ + type: 'operator_compare', + label: operator, + description: + language.tokens.operator_compare.literal[operator], + }), + ), + ] + : []), + ]; + break; + case 'operator_compare': { + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( + tokens, + 'operator_compare', + )?.value; + + // If there is no a previous field, then no return suggestions because it would be an syntax + // error + if (!field) { + return []; + } + + return [ + ...Object.keys(language.tokens.operator_compare.literal) + .filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ) + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), + ...(Object.keys(language.tokens.operator_compare.literal).some( + operator => operator === lastToken.value, + ) + ? [ + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( + await options.suggestions.value(undefined, { + field, + operatorCompare, + }), + ).map(mapSuggestionCreatorValue), + ] + : []), + ]; + break; + } + case 'value': { + const field = getLastTokenDefinedByType(tokens, 'field')?.value; + const operatorCompare = getLastTokenDefinedByType( + tokens, + 'operator_compare', + )?.value; + + /* If there is no a previous field or operator_compare, then no return suggestions because + it would be an syntax error */ + if (!field || !operatorCompare) { + return []; + } + + return [ + ...(lastToken.formattedValue + ? [ + { + type: 'function_search', + label: 'Search', + description: 'run the search query', + }, + ] + : []), + /* + WORKAROUND: When getting suggestions for the distinct values for any field, the API + could reply some values that doesn't match the expected regular expression. If the + value is invalid, a validation message is displayed and avoid the search can be run. + The goal of this filter is that the suggested values can be used to search. This + causes some values could not be displayed as suggestions. + */ + ...filterTokenValueSuggestion( + await options.suggestions.value(lastToken.formattedValue, { + field, + operatorCompare, + }), + ).map(mapSuggestionCreatorValue), + ...Object.entries(language.tokens.conjunction.literal).map( + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), + ), + { + type: 'operator_group', + label: ')', + description: language.tokens.operator_group.literal[')'], + }, + ]; + break; + } + case 'conjunction': + return [ + ...Object.keys(language.tokens.conjunction.literal) + .filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ) + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), + // fields if the input field is exact + ...(Object.keys(language.tokens.conjunction.literal).some( + conjunction => conjunction === lastToken.value, + ) + ? [ + ...(await options.suggestions.field()).map( + mapSuggestionCreatorField, + ), + ] + : []), + { + type: 'operator_group', + label: '(', + description: language.tokens.operator_group.literal['('], + }, + ]; + break; + case 'operator_group': + if (lastToken.value === '(') { + return [ + // fields + ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ]; + } else if (lastToken.value === ')') { + return [ + // conjunction + ...Object.keys(language.tokens.conjunction.literal).map( + conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }), + ), + ]; + } + break; + default: + return []; + break; + } + + return []; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param param0 + * @returns + */ +export function transformSuggestionToEuiSuggestItem( + suggestion: QLOptionSuggestionEntityItemTyped, +): SuggestItem { + const { type, ...rest } = suggestion; + return { + type: { ...suggestionMappingLanguageTokenType[type] }, + ...rest, + }; +} + +/** + * Transform the suggestion object to the expected object by EuiSuggestItem + * @param suggestions + * @returns + */ +function transformSuggestionsToEuiSuggestItem( + suggestions: QLOptionSuggestionEntityItemTyped[], +): SuggestItem[] { + return suggestions.map(transformSuggestionToEuiSuggestItem); +} + +/** + * Transform the UQL (Unified Query Language) to QL + * @param input + * @returns + */ +export function transformUQLToQL(input: string) { + const tokens = tokenizerUQL(input); + return tokens + .filter(({ value }) => value) + .map(({ type, value }) => { + switch (type) { + case 'conjunction': + return transformQLConjunction(value); + break; + case 'value': + return transformQLValue(value); + break; + default: + return value; + break; + } + }) + .join(''); +} + +export function shouldUseSearchTerm(tokens: ITokens): boolean { + return !( + tokens.some(({ type, value }) => type === 'operator_compare' && value) && + tokens.some(({ type, value }) => type === 'field' && value) + ); +} + +export function transformToSearchTerm( + searchTermFields: string[], + input: string, +): string { + return searchTermFields + .map(searchTermField => `${searchTermField}~${input}`) + .join(','); +} + +/** + * Transform the input in QL to UQL (Unified Query Language) + * @param input + * @returns + */ +export function transformSpecificQLToUnifiedQL( + input: string, + searchTermFields: string[], +) { + const tokens = tokenizer(input); + + if (input && searchTermFields && shouldUseSearchTerm(tokens)) { + return transformToSearchTerm(searchTermFields, input); + } + + return tokens + .filter( + ({ type, value, formattedValue }) => + type !== 'whitespace' && (formattedValue ?? value), + ) + .map(({ type, value, formattedValue }) => { + switch (type) { + case 'value': { + // If the value is wrapped with ", then replace the escaped double quotation mark (\") + // by double quotation marks (") + // WARN: This could cause a problem with value that contains this sequence \" + const extractedValue = + formattedValue !== value + ? formattedValue.replace(/\\"/g, '"') + : formattedValue; + return extractedValue || value; + break; + } + case 'conjunction': + return value === 'and' + ? language.equivalencesToUQL.conjunction.literal['and'] + : language.equivalencesToUQL.conjunction.literal['or']; + break; + default: + return value; + break; + } + }) + .join(''); +} + +/** + * Get the output from the input + * @param input + * @returns + */ +function getOutput(input: string, options: OptionsQL) { + // Implicit query + const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; + const implicitQueryAsQL = transformUQLToQL(implicitQueryAsUQL); + + // Implicit query conjunction + const implicitQueryConjunctionAsUQL = + options?.options?.implicitQuery?.conjunction ?? ''; + const implicitQueryConjunctionAsQL = transformUQLToQL( + implicitQueryConjunctionAsUQL, + ); + + // User input query + const inputQueryAsQL = input; + const inputQueryAsUQL = transformSpecificQLToUnifiedQL( + inputQueryAsQL, + options?.options?.searchTermFields ?? [], + ); + + return { + language: WQL.id, + apiQuery: { + q: [ + implicitQueryAsUQL, + implicitQueryAsUQL && inputQueryAsUQL + ? implicitQueryConjunctionAsUQL + : '', + implicitQueryAsUQL && inputQueryAsUQL + ? `(${inputQueryAsUQL})` + : inputQueryAsUQL, + ].join(''), + }, + query: [ + implicitQueryAsQL, + implicitQueryAsQL && inputQueryAsQL ? implicitQueryConjunctionAsQL : '', + implicitQueryAsQL && inputQueryAsQL + ? `(${inputQueryAsQL})` + : inputQueryAsQL, + ].join(''), + }; +} + +/** + * Validate the token value + * @param token + * @returns + */ +function validateTokenValue(token: IToken): string | undefined { + const re = getTokenValueRegularExpression(); + + const value = token.formattedValue ?? token.value; + const match = value.match(re); + + if (match?.groups?.value === value) { + return undefined; + } + + const invalidCharacters: string[] = token.value + .split('') + .filter((value, index, array) => array.indexOf(value) === index) + .filter( + character => + !new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test( + character, + ), + ); + + return [ + `"${value}" is not a valid value.`, + ...(invalidCharacters.length + ? [`Invalid characters found: ${invalidCharacters.join('')}`] + : []), + ].join(' '); +} + +type ITokenValidator = ( + tokenValue: IToken, + proximityTokens: any, +) => string | undefined; +/** + * Validate the tokens while the user is building the query + * @param tokens + * @param validate + * @returns + */ +function validatePartial( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string { + // Ensure is not in search term mode + if (!shouldUseSearchTerm(tokens)) { + return ( + tokens + .map((token: IToken, index) => { + if (token.value) { + if (token.type === 'field') { + // Ensure there is a operator next to field to check if the fields is valid or not. + // This allows the user can type the field token and get the suggestions for the field. + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return tokenOperatorNearToField + ? validate.field(token) + : undefined; + } + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + return ( + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined) + ); + } + } + }) + .filter(t => typeof t !== 'undefined') + .join('\n') || undefined + ); + } +} + +/** + * Validate the tokens if they are a valid syntax + * @param tokens + * @param validate + * @returns + */ +function validate( + tokens: ITokens, + validate: { field: ITokenValidator; value: ITokenValidator }, +): undefined | string[] { + if (!shouldUseSearchTerm(tokens)) { + const errors = tokens + .map((token: IToken, index) => { + const errors = []; + if (token.value) { + if (token.type === 'field') { + const tokenOperatorNearToField = getTokenNearTo( + tokens, + 'operator_compare', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenValueNearToField = getTokenNearTo( + tokens, + 'value', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + if (validate.field(token)) { + errors.push(`"${token.value}" is not a valid field.`); + } else if (!tokenOperatorNearToField) { + errors.push( + `The operator for field "${token.value}" is missing.`, + ); + } else if (!tokenValueNearToField) { + errors.push(`The value for field "${token.value}" is missing.`); + } + } + // Check if the value is allowed + if (token.type === 'value') { + const tokenFieldNearToValue = getTokenNearTo( + tokens, + 'field', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const tokenOperatorCompareNearToValue = getTokenNearTo( + tokens, + 'operator_compare', + 'previous', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + const validationError = + validateTokenValue(token) || + (tokenFieldNearToValue && + tokenOperatorCompareNearToValue && + validate.value + ? validate.value(token, { + field: tokenFieldNearToValue?.value, + operator: tokenOperatorCompareNearToValue?.value, + }) + : undefined); + + validationError && errors.push(validationError); + } + + // Check if the value is allowed + if (token.type === 'conjunction') { + const tokenWhitespaceNearToFieldNext = getTokenNearTo( + tokens, + 'whitespace', + 'next', + { tokenReferencePosition: index }, + ); + const tokenFieldNearToFieldNext = getTokenNearTo( + tokens, + 'field', + 'next', + { + tokenReferencePosition: index, + tokenFoundShouldHaveValue: true, + }, + ); + !tokenWhitespaceNearToFieldNext?.value?.length && + errors.push( + `There is no whitespace after conjunction "${token.value}".`, + ); + !tokenFieldNearToFieldNext?.value?.length && + errors.push( + `There is no sentence after conjunction "${token.value}".`, + ); + } + } + return errors.length ? errors : undefined; + }) + .filter(errors => errors) + .flat(); + return errors.length ? errors : undefined; + } + return undefined; +} + +export const WQL = { + id: 'wql', + label: 'WQL', + description: + 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', + documentationLink: webDocumentationLink( + 'user-manual/wazuh-dashboard/queries.html', + ), + getConfiguration() { + return { + isOpenPopoverImplicitFilter: false, + }; + }, + async run(input, params) { + // Get the tokens from the input + const tokens: ITokens = tokenizer(input); + + // Get the implicit query as query language syntax + const implicitQueryAsQL = params.queryLanguage.parameters?.options + ?.implicitQuery + ? transformUQLToQL( + params.queryLanguage.parameters.options.implicitQuery.query + + params.queryLanguage.parameters.options.implicitQuery.conjunction, + ) + : ''; + + const fieldsSuggestion: string[] = + await params.queryLanguage.parameters.suggestions + .field() + .map(({ label }) => label); + + const validators = { + field: ({ value }) => + fieldsSuggestion.includes(value) + ? undefined + : `"${value}" is not valid field.`, + ...(params.queryLanguage.parameters?.validate?.value + ? { + value: params.queryLanguage.parameters?.validate?.value, + } + : {}), + }; + + // Validate the user input + const validationPartial = validatePartial(tokens, validators); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + const onSearch = output => { + if (output?.error) { + params.setQueryLanguageOutput(state => ({ + ...state, + searchBarProps: { + ...state.searchBarProps, + suggestions: transformSuggestionsToEuiSuggestItem( + output.error.map(error => ({ + type: 'validation_error', + label: 'Invalid', + description: error, + })), + ), + isInvalid: true, + }, + })); + } else { + params.onSearch(output); + } + }; + + return { + filterButtons: params.queryLanguage.parameters?.options?.filterButtons ? ( + ({ id, label }), + )} + idToSelectedMap={{}} + type='multi' + onChange={(id: string) => { + const buttonParams = + params.queryLanguage.parameters?.options?.filterButtons.find( + ({ id: buttonID }) => buttonID === id, + ); + if (buttonParams) { + params.setInput(buttonParams.input); + const output = { + ...getOutput( + buttonParams.input, + params.queryLanguage.parameters, + ), + error: undefined, + }; + params.onSearch(output); + } + }} + /> + ) : null, + searchBarProps: { + // Props that will be used by the EuiSuggest component + // Suggestions + suggestions: transformSuggestionsToEuiSuggestItem( + validationPartial + ? [ + { + type: 'validation_error', + label: 'Invalid', + description: validationPartial, + }, + ] + : await getSuggestions(tokens, params.queryLanguage.parameters), + ), + // Handler to manage when clicking in a suggestion item + onItemClick: currentInput => item => { + // There is an error, clicking on the item does nothing + if (item.type.iconType === 'alert') { + return; + } + // When the clicked item has the `search` iconType, run the `onSearch` function + if (item.type.iconType === 'search') { + // Execute the search action + // Get the tokens from the input + const tokens: ITokens = tokenizer(currentInput); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(currentInput, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); + } else { + // When the clicked item has another iconType + const lastToken: IToken | undefined = getLastTokenDefined(tokens); + // if the clicked suggestion is of same type of last token + if ( + lastToken && + suggestionMappingLanguageTokenType[lastToken.type].iconType === + item.type.iconType + ) { + // replace the value of last token with the current one. + // if the current token is a value, then transform it + lastToken.value = + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label; + } else { + // add a whitespace for conjunction + // add a whitespace for grouping operator ) + !/\s$/.test(input) && + (item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType || + lastToken?.type === 'conjunction' || + (item.type.iconType === + suggestionMappingLanguageTokenType.operator_group + .iconType && + item.label === ')')) && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + + // add a new token of the selected type and value + tokens.push({ + type: Object.entries(suggestionMappingLanguageTokenType).find( + ([, { iconType }]) => iconType === item.type.iconType, + )[0], + value: + item.type.iconType === + suggestionMappingLanguageTokenType.value.iconType + ? transformQLValue(item.label) + : item.label, + }); + + // add a whitespace for conjunction + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType && + tokens.push({ + type: 'whitespace', + value: ' ', + }); + } + + // Change the input + params.setInput( + tokens + .filter(value => value) // Ensure the input is rebuilt using tokens with value. + // The input tokenization can contain tokens with no value due to the used + // regular expression. + .map(({ value }) => value) + .join(''), + ); + } + }, + // Disable the focus trap in the EuiInputPopover. + // This causes when using the Search suggestion, the suggestion popover can be closed. + // If this is disabled, then the suggestion popover is open after a short time for this + // use case. + disableFocusTrap: true, + // Show the input is invalid + isInvalid: Boolean(validationStrict), + // Define the handler when the a key is pressed while the input is focused + onKeyPress: event => { + if (event.key === 'Enter') { + // Get the tokens from the input + const input = event.currentTarget.value; + const tokens: ITokens = tokenizer(input); + + const validationStrict = validate(tokens, validators); + + // Get the output of query language + const output = { + ...getOutput(input, params.queryLanguage.parameters), + error: validationStrict, + }; + + onSearch(output); + } + }, + }, + output, + }; + }, + transformInput: (unifiedQuery: string, { parameters }) => { + const input = + unifiedQuery && parameters?.options?.implicitQuery + ? unifiedQuery.replace( + new RegExp( + `^${parameters.options.implicitQuery.query}${parameters.options.implicitQuery.conjunction}`, + ), + '', + ) + : unifiedQuery; + + return transformUQLToQL(input); + }, +}; diff --git a/plugins/wazuh-core/public/components/table-data/README.md b/plugins/wazuh-core/public/components/table-data/README.md new file mode 100644 index 0000000000..0a8a87e57e --- /dev/null +++ b/plugins/wazuh-core/public/components/table-data/README.md @@ -0,0 +1,28 @@ +# TableData + +This is a generic table data component that represents the data in pages that is obtained using a parameter. When the pagination or sorting changes, the parameter to get the data is executed. + +# Layout + +``` +title? (totalItems?) postTitle? preActionButtons? actionReload postActionButtons? +description? +preTable? +table +postTable? +``` + +# Features + +- Ability to reload the data +- Ability to select the visible columns (persist data in localStorage or sessionStorage) +- Customizable: + - Title + - Post title + - Description + - Pre action buttons + - Post action buttons + - Above table + - Below table + - Table columns + - Table initial sorting column diff --git a/plugins/wazuh-core/public/components/table-data/index.ts b/plugins/wazuh-core/public/components/table-data/index.ts new file mode 100644 index 0000000000..7f30d5b41c --- /dev/null +++ b/plugins/wazuh-core/public/components/table-data/index.ts @@ -0,0 +1,2 @@ +export * from './table-data'; +export * from './types'; diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx new file mode 100644 index 0000000000..584ddb040f --- /dev/null +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -0,0 +1,360 @@ +/* + * Wazuh app - Table with search bar + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import React, { useEffect, useState, useRef } from 'react'; +import { + EuiTitle, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButtonEmpty, + EuiToolTip, + EuiIcon, + EuiCheckboxGroup, + EuiBasicTable, +} from '@elastic/eui'; +import { useStateStorage } from '../../hooks'; +import { isEqual } from 'lodash'; +import { TableDataProps } from './types'; + +const getColumMetaField = item => item.field || item.name; + +export function TableData({ + preActionButtons, + postActionButtons, + postTitle, + onReload, + fetchData, + tablePageSizeOptions = [15, 25, 50, 100], + tableInitialSortingDirection = 'asc', + tableInitialSortingField = '', + ...rest +}: TableDataProps) { + const [isLoading, setIsLoading] = useState(false); + const [items, setItems] = useState([]); + const [totalItems, setTotalItems] = useState(0); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: tablePageSizeOptions[0], + }); + const [sorting, setSorting] = useState({ + sort: { + field: tableInitialSortingField, + direction: tableInitialSortingDirection, + }, + }); + const [refresh, setRefresh] = useState(rest.reload || 0); + const [fetchContext, setFetchContext] = useState(rest.fetchContext || {}); + + const isMounted = useRef(false); + const tableRef = useRef(); + + const [selectedFields, setSelectedFields] = useStateStorage( + rest.tableColumns.some(({ show }) => show) + ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) + : rest.tableColumns.map(({ field }) => field), + rest?.saveStateStorage?.system, + rest?.saveStateStorage?.key + ? `${rest?.saveStateStorage?.key}-visible-fields` + : undefined, + ); + const [isOpenFieldSelector, setIsOpenFieldSelector] = useState(false); + + const onFetch = async ({ pagination, sorting }) => { + try { + const enhancedFetchContext = { + pagination, + sorting, + fetchContext, + }; + setIsLoading(true); + rest?.onFetchContextChange?.(enhancedFetchContext); + + const { items, totalItems } = await fetchData(enhancedFetchContext); + + setIsLoading(false); + setItems(items); + setTotalItems(totalItems); + + const result = { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; + + rest?.onDataChange?.(result); + } catch (error) { + setIsLoading(false); + setTotalItems(0); + if (error?.name) { + /* This replaces the error name. The intention is that an AxiosError + doesn't appear in the toast message. + TODO: This should be managed by the service that does the request instead of only changing + the name in this case. + */ + error.name = 'RequestError'; + } + throw error; + } + }; + + const tableColumns = rest.tableColumns.filter(item => + selectedFields.includes(getColumMetaField(item)), + ); + + const renderActionButtons = actionButtons => { + if (Array.isArray(actionButtons)) { + return actionButtons.map((button, key) => ( + + {button} + + )); + } + + if (typeof actionButtons === 'object') { + return {actionButtons}; + } + + if (typeof actionButtons === 'function') { + return actionButtons({ + fetchContext, + pagination, + sorting, + items, + totalItems, + tableColumns, + }); + } + }; + + /** + * Generate a new reload footprint and set reload to propagate refresh + */ + const triggerReload = () => { + setRefresh(Date.now()); + if (onReload) { + onReload(Date.now()); + } + }; + + function updateRefresh() { + setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); + setRefresh(Date.now()); + } + + function tableOnChange({ page = {}, sort = {} }) { + if (isMounted.current) { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort; + setPagination({ + pageIndex, + pageSize, + }); + setSorting({ + sort: { + field, + direction, + }, + }); + } + } + + useEffect(() => { + // This effect is triggered when the component is mounted because of how to the useEffect hook works. + // We don't want to set the pagination state because there is another effect that has this dependency + // and will cause the effect is triggered (redoing the onFetch function). + if (isMounted.current) { + // Reset the page index when the reload changes. + // This will cause that onFetch function is triggered because to changes in pagination in the another effect. + updateRefresh(); + } + }, [rest?.reload]); + + useEffect(() => { + onFetch({ pagination, sorting }); + }, [fetchContext, pagination, sorting, refresh]); + + useEffect(() => { + // This effect is triggered when the component is mounted because of how to the useEffect hook works. + // We don't want to set the searchParams state because there is another effect that has this dependency + // and will cause the effect is triggered (redoing the onFetch function). + if (isMounted.current && !isEqual(rest.fetchContext, fetchContext)) { + setFetchContext(rest.fetchContext); + updateRefresh(); + } + }, [rest?.fetchContext]); + + useEffect(() => { + if (rest.reload) triggerReload(); + }, [rest.reload]); + + // It is required that this effect runs after other effects that use isMounted + // to avoid that these effects run when the component is mounted, only running + // when one of its dependencies changes. + useEffect(() => { + isMounted.current = true; + }, []); + + const tablePagination = { + ...pagination, + totalItemCount: totalItems, + pageSizeOptions: tablePageSizeOptions, + }; + + const ReloadButton = ( + + triggerReload()}> + Refresh + + + ); + + const header = ( + <> + + + + + {rest.title && ( + +

+ {rest.title}{' '} + {isLoading ? ( + + ) : ( + ({totalItems}) + )} +

+
+ )} +
+ {postTitle ? ( + + {postTitle} + + ) : null} +
+
+ + + {/* Render optional custom action button */} + {renderActionButtons(preActionButtons)} + {/* Render optional reload button */} + {rest.showActionReload && ReloadButton} + {/* Render optional post custom action button */} + {renderActionButtons(postActionButtons)} + {rest.showFieldSelector && ( + + + setIsOpenFieldSelector(state => !state)} + > + + + + + )} + + +
+ {isOpenFieldSelector && ( + + + { + const metaField = getColumMetaField(item); + return { + id: metaField, + label: item.name, + checked: selectedFields.includes(metaField), + }; + })} + onChange={optionID => { + setSelectedFields(state => { + if (state.includes(optionID)) { + if (state.length > 1) { + return state.filter(field => field !== optionID); + } + return state; + } + return [...state, optionID]; + }); + }} + className='columnsSelectedCheckboxs' + idToSelectedMap={{}} + /> + + + )} + + ); + + const tableDataRenderElementsProps = { + ...rest, + tableColumns, + isOpenFieldSelector, + selectedFields, + refresh, + updateRefresh, + fetchContext, + setFetchContext, + pagination, + setPagination, + sorting, + setSorting, + tableRef, + }; + + return ( + + {header} + {rest.description && ( + + {rest.description} + + )} + + + ({ ...rest }), + )} + items={items} + loading={isLoading} + pagination={tablePagination} + sorting={sorting} + onChange={tableOnChange} + rowProps={rest.rowProps} + {...rest.tableProps} + /> + + + + ); +} + +const TableDataRenderElement = ({ render, ...rest }) => { + if (typeof render === 'function') { + return {render(rest)}; + } + if (typeof render === 'object') { + return {render}; + } + return null; +}; diff --git a/plugins/wazuh-core/public/components/table-data/types.ts b/plugins/wazuh-core/public/components/table-data/types.ts new file mode 100644 index 0000000000..bd5d6580ab --- /dev/null +++ b/plugins/wazuh-core/public/components/table-data/types.ts @@ -0,0 +1,86 @@ +import { ReactNode } from 'react'; +import { EuiBasicTableProps } from '@elastic/eui'; + +export interface TableDataProps { + preActionButtons?: ReactNode | ((options: any) => ReactNode); + postActionButtons?: ReactNode | ((options: any) => ReactNode); + title?: string; + postTitle?: ReactNode; + description?: string; + /** + * Define a render above to the table + */ + preTable?: ReactNode | ((options: any) => ReactNode); + /** + * Define a render below to the table + */ + postTable?: ReactNode | ((options: any) => ReactNode); + /** + * Enable the action to reload the data + */ + showActionReload?: boolean; + onDataChange?: Function; + onReload?: (newValue: number) => void; + /** + * Fetch context + */ + fetchContext: any; + /** + * Function to fetch the data + */ + fetchData: (params: { + fetchContext: any; + pagination: EuiBasicTableProps['pagination']; + sorting: EuiBasicTableProps['sorting']; + }) => Promise<{ items: any[]; totalItems: number }>; + onFetchContextChange?: Function; + /** + * Columns for the table + */ + tableColumns: EuiBasicTableProps['columns'] & { + composeField?: string[]; + searchable?: string; + show?: boolean; + }; + /** + * Table row properties for the table + */ + rowProps?: EuiBasicTableProps['rowProps']; + /** + * Table page size options + */ + tablePageSizeOptions?: number[]; + /** + * Table initial sorting direction + */ + tableInitialSortingDirection?: 'asc' | 'desc'; + /** + * Table initial sorting field + */ + tableInitialSortingField?: string; + /** + * Table properties + */ + tableProps?: Omit< + EuiBasicTableProps, + | 'columns' + | 'items' + | 'loading' + | 'pagination' + | 'sorting' + | 'onChange' + | 'rowProps' + >; + /** + * Refresh the fetch of data + */ + reload?: number; + saveStateStorage?: { + system: 'localStorage' | 'sessionStorage'; + key: string; + }; + /** + * Show the field selector + */ + showFieldSelector?: boolean; +} diff --git a/plugins/wazuh-core/public/hooks/index.ts b/plugins/wazuh-core/public/hooks/index.ts index d06d93425c..808261cd95 100644 --- a/plugins/wazuh-core/public/hooks/index.ts +++ b/plugins/wazuh-core/public/hooks/index.ts @@ -1 +1,2 @@ export { useDockedSideNav } from './use-docked-side-nav'; +export * from './use-state-storage'; diff --git a/plugins/wazuh-core/public/hooks/use-state-storage.ts b/plugins/wazuh-core/public/hooks/use-state-storage.ts new file mode 100644 index 0000000000..3251ea1be5 --- /dev/null +++ b/plugins/wazuh-core/public/hooks/use-state-storage.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +function transformValueToStorage(value: any){ + return typeof value !== 'string' ? JSON.stringify(value) : value; +}; + +function transformValueFromStorage(value: any){ + return typeof value === 'string' ? JSON.parse(value) : value; +}; + +export function useStateStorage(initialValue: any, storageSystem?: 'sessionStorage' | 'localStorage', storageKey?: string){ + const [state, setState] = useState( + (storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey)) + ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) + : initialValue + ); + + function setStateStorage(value: any){ + setState((state) => { + const formattedValue = typeof value === 'function' + ? value(state) + : value; + + storageSystem && storageKey && window?.[storageSystem]?.setItem(storageKey, transformValueToStorage(formattedValue)); + return formattedValue; + }); + }; + + return [state, setStateStorage]; +}; diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index a006dff3a8..bf2a7c1c31 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -2,6 +2,7 @@ import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; import { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; import { setChrome, setCore, setUiSettings } from './plugin-services'; import * as utils from './utils'; +import * as uiComponents from './components'; import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; import { ConfigurationStore } from './utils/configuration-store'; @@ -16,6 +17,7 @@ import { CoreHTTPClient } from './services/http/http-client'; export class WazuhCorePlugin implements Plugin { + runtime = { setup: {} }; _internal: { [key: string]: any } = {}; services: { [key: string]: any } = {}; public async setup(core: CoreSetup): Promise { @@ -62,19 +64,23 @@ export class WazuhCorePlugin getTimeout: async () => (await this.services.configuration.get('timeout')) as number, getURL: (path: string) => core.http.basePath.prepend(path), - getServerAPI: () => 'api-host-id', // TODO: implement + getServerAPI: () => 'imposter', // TODO: implement getIndexPatternTitle: async () => 'wazuh-alerts-*', // TODO: implement http: core.http, }); // Setup services await this.services.dashboardSecurity.setup(); - await this.services.http.setup(); + this.runtime.setup.http = await this.services.http.setup({ core }); return { ...this.services, utils, API_USER_STATUS_RUN_AS, + ui: { + ...uiComponents, + ...this.runtime.setup.http.ui, + }, }; } @@ -85,12 +91,18 @@ export class WazuhCorePlugin // Start services await this.services.configuration.start({ http: core.http }); + await this.services.dashboardSecurity.start(); + await this.services.http.start(); return { ...this.services, utils, API_USER_STATUS_RUN_AS, hooks, + ui: { + ...uiComponents, + ...this.runtime.setup.http.ui, + }, }; } diff --git a/plugins/wazuh-core/public/services/http/http-client.ts b/plugins/wazuh-core/public/services/http/http-client.ts index 041574f958..e7d8df3729 100644 --- a/plugins/wazuh-core/public/services/http/http-client.ts +++ b/plugins/wazuh-core/public/services/http/http-client.ts @@ -4,6 +4,7 @@ import { GenericRequest } from './generic-client'; import { RequestInterceptorClient } from './request-interceptor'; import { WzRequest } from './server-client'; import { HTTPClient, HTTPClientRequestInterceptor } from './types'; +import { createUI } from './ui/create'; interface HTTPClientServices { http: any; @@ -48,8 +49,11 @@ export class CoreHTTPClient implements HTTPClient { }); this.logger.debug('Created client'); } - async setup() { + async setup(deps) { this.logger.debug('Setup'); + return { + ui: createUI({ ...deps, http: this }), + }; } async start() {} async stop() {} diff --git a/plugins/wazuh-core/public/services/http/ui/components/README.md b/plugins/wazuh-core/public/services/http/ui/components/README.md new file mode 100644 index 0000000000..2a9fe4d7e9 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/README.md @@ -0,0 +1,19 @@ +# ServerTable + +This is a specific table data component that represents the data from the "server". + +It is based in the `TableData` and adds some features: + +- Ability to export the data +- Ability to render a search bar + +# Layout + +``` +title? (totalItems?) postTitle? preActionButtons? actionReload actionExportFormatted? postActionButtons? +description? +searchBar? +preTable? +table +postTable? +``` diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx new file mode 100644 index 0000000000..2d18457bc8 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx @@ -0,0 +1,38 @@ +/* + * Wazuh app - React test for Export Table Csv component. + * + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + * + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { ExportTableCsv } from './export-table-csv'; + +jest.mock('../../../../kibana-services', () => ({ + getHttp: () => ({ + basePath: { + prepend: str => str, + }, + }), +})); + +jest.mock('../../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: options => {}, + }), +})); + +describe('Export Table Csv component', () => { + it('renders correctly to match the snapshot', () => { + const wrapper = mount(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx new file mode 100644 index 0000000000..2deebbe375 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx @@ -0,0 +1,73 @@ +/* + * Wazuh app - Table with search bar + * Copyright (C) 2015-2022 Wazuh, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Find more information about this on the LICENSE file. + */ + +import React from 'react'; +import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; + +export function ExportTableCsv({ + fetchContext: { endpoint, filters }, + totalItems, + title, + showToast, + exportCSV, +}) { + const downloadCSV = async () => { + try { + const formatedFilters = Object.entries(filters || []).map( + ([name, value]) => ({ + name, + value, + }), + ); + showToast({ + color: 'success', + title: 'Your download should begin automatically...', + toastLifeTimeMs: 3000, + }); + + await exportCSV(endpoint, [...formatedFilters], `${title.toLowerCase()}`); + } catch (error) { + // TODO: implement + // const options = { + // context: `${ExportTableCsv.name}.downloadCsv`, + // level: UI_LOGGER_LEVELS.ERROR, + // severity: UI_ERROR_SEVERITIES.BUSINESS, + // error: { + // error: error, + // message: error.message || error, + // title: `${error.name}: Error downloading csv`, + // }, + // }; + // getErrorOrchestrator().handleError(options); + } + }; + + return ( + + + Export formatted + + + ); +} + +// Set default props +ExportTableCsv.defaultProps = { + endpoint: '/', + totalItems: 0, + filters: [], + title: '', +}; diff --git a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx new file mode 100644 index 0000000000..91d30df500 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { SearchBar, TableData } from '../../../../components'; +import { EuiSpacer } from '@elastic/eui'; +import { ServerDataProps } from './types'; + +export function ServerTableData({ + showActionExportFormatted, + postActionButtons, + ActionExportFormatted, + ...props +}: ServerDataProps) { + return ( + ( + <> + {showActionExportFormatted && ( + + )} + {postActionButtons && postActionButtons(params)} + + )} + preTable={ + props.showSearchBar && + (({ tableColumns, ...rest }) => { + /* Render search bar*/ + const searchBarWQLOptions = useMemo( + () => ({ + searchTermFields: tableColumns + .filter( + ({ field, searchable }) => + searchable && rest.selectedFields.includes(field), + ) + .map(({ field, composeField }) => + [composeField || field].flat(), + ) + .flat(), + ...(rest?.searchBarWQL?.options || {}), + }), + [rest?.searchBarWQL?.options, rest?.selectedFields], + ); + return ( + <> + { + // Set the query, reset the page index and update the refresh + rest.setFetchContext({ + ...rest.fetchContext, + filters: apiQuery, + }); + rest.updateRefresh(); + }} + /> + + + ); + }) + } + /> + ); +} diff --git a/plugins/wazuh-core/public/services/http/ui/components/types.ts b/plugins/wazuh-core/public/services/http/ui/components/types.ts new file mode 100644 index 0000000000..ae940174cd --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/types.ts @@ -0,0 +1,44 @@ +import { SearchBarProps } from '../../../../components'; +import { TableDataProps } from '../../../../components/table-data/types'; + +export interface ServerDataProps extends TableDataProps { + /** + * Component to render the export formatted action + */ + ActionExportFormatted: any; + /** + * Properties for the search bar + */ + searchBarProps?: Omit< + SearchBarProps, + 'defaultMode' | 'modes' | 'onSearch' | 'input' + >; + /** + * Options releated to WQL. This is a shortcut that add properties to the WQL language. + */ + searchBarWQL?: { + options: { + searchTermFields: string[]; + implicitQuery: { + query: string; + conjunction: ';' | ','; + }; + }; + suggestions?: { + field?: () => any; + value?: () => any; + }; + validate?: { + field?: () => any; + value?: () => any; + }; + }; + /** + * Show the search bar + */ + showSearchBar?: boolean; + /** + * Show the the export formatted action + */ + showActionExportFormatted?: boolean; +} diff --git a/plugins/wazuh-core/public/services/http/ui/create.tsx b/plugins/wazuh-core/public/services/http/ui/create.tsx new file mode 100644 index 0000000000..0fd0c23178 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/create.tsx @@ -0,0 +1,30 @@ +import { fetchServerTableDataCreator } from './services/fetch-server-data'; +import { withServices } from './withServices'; +import { ExportTableCsv } from './components/export-table-csv'; +import * as FileSaver from '../../../utils/file-saver'; +import { ServerTableData } from './components/server-table-data'; + +export const createUI = deps => { + const serverDataFetch = fetchServerTableDataCreator( + deps.http.server.request.bind(deps.http.server), + ); + + const ActionExportFormatted = withServices({ + showToast: deps.core.notifications.toasts.add.bind( + deps.core.notifications.toasts, + ), + exportCSV: async (path, filters = [], exportName = 'data') => { + const data = await deps.http.server.csv(path, filters); + const output = data.data ? [data.data] : []; + const blob = new Blob(output, { type: 'text/csv' }); + FileSaver.saveAs(blob, `${exportName}.csv`); + }, + })(ExportTableCsv); + + return { + ServerTable: withServices({ + ActionExportFormatted, + fetchData: serverDataFetch, + })(ServerTableData), + }; +}; diff --git a/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts new file mode 100644 index 0000000000..e76815772b --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts @@ -0,0 +1,33 @@ +const getFilters = filters => { + if (!filters) { + return {}; + } + const { default: defaultFilters, ...restFilters } = filters; + return Object.keys(restFilters).length ? restFilters : defaultFilters; +}; + +export const fetchServerTableDataCreator = + fetchData => + async ({ pagination, sorting, fetchContext }) => { + console.log(fetchContext); + const { pageIndex, pageSize } = pagination; + const { field, direction } = sorting.sort; + const params = { + ...getFilters(fetchContext.filters), + offset: pageIndex * pageSize, + limit: pageSize, + sort: `${direction === 'asc' ? '+' : '-'}${field}`, + }; + + const response = await fetchData( + fetchContext.method, + fetchContext.endpoint, + { + params, + }, + ); + return { + items: response?.data?.data?.affected_items, + totalItems: response?.data?.data?.total_affected_items, + }; + }; diff --git a/plugins/wazuh-core/public/services/http/ui/withServices.tsx b/plugins/wazuh-core/public/services/http/ui/withServices.tsx new file mode 100644 index 0000000000..4ee5ac081f --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/withServices.tsx @@ -0,0 +1,3 @@ +import React from 'react'; +export const withServices = services => WrappedComponent => props => + ; diff --git a/plugins/wazuh-core/public/utils/file-saver.js b/plugins/wazuh-core/public/utils/file-saver.js new file mode 100644 index 0000000000..6de85f79dd --- /dev/null +++ b/plugins/wazuh-core/public/utils/file-saver.js @@ -0,0 +1,198 @@ +/* eslint-disable */ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 1.3.8 + * 2018-03-22 14:03:47 + * + * By Eli Grey, https://eligrey.com + * License: MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/src/FileSaver.js */ + +export var saveAs = + saveAs || + (function(view) { + 'use strict'; + // IE <10 is explicitly unsupported + if ( + typeof view === 'undefined' || + (typeof navigator !== 'undefined' && + /MSIE [1-9]\./.test(navigator.userAgent)) + ) { + return; + } + var doc = view.document, + // only get URL when necessary in case Blob.js hasn't overridden it yet + get_URL = function() { + return view.URL || view.webkitURL || view; + }, + save_link = doc.createElementNS('http://www.w3.org/1999/xhtml', 'a'), + can_use_save_link = 'download' in save_link, + click = function(node) { + var event = new MouseEvent('click'); + node.dispatchEvent(event); + }, + is_safari = /constructor/i.test(view.HTMLElement) || view.safari, + is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent), + setImmediate = view.setImmediate || view.setTimeout, + throw_outside = function(ex) { + setImmediate(function() { + throw ex; + }, 0); + }, + force_saveable_type = 'application/octet-stream', + // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to + arbitrary_revoke_timeout = 1000 * 40, // in ms + revoke = function(file) { + var revoker = function() { + if (typeof file === 'string') { + // file is an object URL + get_URL().revokeObjectURL(file); + } else { + // file is a File + file.remove(); + } + }; + setTimeout(revoker, arbitrary_revoke_timeout); + }, + dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver['on' + event_types[i]]; + if (typeof listener === 'function') { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + }, + auto_bom = function(blob) { + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + if ( + /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test( + blob.type + ) + ) { + return new Blob([String.fromCharCode(0xfeff), blob], { + type: blob.type + }); + } + return blob; + }, + FileSaver = function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + // First try a.download, then web filesystem, then object URLs + var filesaver = this, + type = blob.type, + force = type === force_saveable_type, + object_url, + dispatch_all = function() { + dispatch( + filesaver, + 'writestart progress write writeend'.split(' ') + ); + }, + // on any filesys errors revert to saving with object URLs + fs_error = function() { + if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { + // Safari doesn't allow downloading of blob urls + var reader = new FileReader(); + reader.onloadend = function() { + var url = is_chrome_ios + ? reader.result + : reader.result.replace( + /^data:[^;]*;/, + 'data:attachment/file;' + ); + var popup = view.open(url, '_blank'); + if (!popup) view.location.href = url; + url = undefined; // release reference before dispatching + filesaver.readyState = filesaver.DONE; + dispatch_all(); + }; + reader.readAsDataURL(blob); + filesaver.readyState = filesaver.INIT; + return; + } + // don't create more object URLs than needed + if (!object_url) { + object_url = get_URL().createObjectURL(blob); + } + if (force) { + view.location.href = object_url; + } else { + var opened = view.open(object_url, '_blank'); + if (!opened) { + // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html + view.location.href = object_url; + } + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + }; + filesaver.readyState = filesaver.INIT; + + if (can_use_save_link) { + object_url = get_URL().createObjectURL(blob); + setImmediate(function() { + save_link.href = object_url; + save_link.download = name; + click(save_link); + dispatch_all(); + revoke(object_url); + filesaver.readyState = filesaver.DONE; + }, 0); + return; + } + + fs_error(); + }, + FS_proto = FileSaver.prototype, + saveAs = function(blob, name, no_auto_bom) { + return new FileSaver( + blob, + name || blob.name || 'download', + no_auto_bom + ); + }; + + // IE 10+ (native saveAs) + if (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { + return function(blob, name, no_auto_bom) { + name = name || blob.name || 'download'; + + if (!no_auto_bom) { + blob = auto_bom(blob); + } + return navigator.msSaveOrOpenBlob(blob, name); + }; + } + + // todo: detect chrome extensions & packaged apps + //save_link.target = "_blank"; + + FS_proto.abort = function() {}; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = FS_proto.onwritestart = FS_proto.onprogress = FS_proto.onwrite = FS_proto.onabort = FS_proto.onerror = FS_proto.onwriteend = null; + + return saveAs; + })( + (typeof self !== 'undefined' && self) || + (typeof window !== 'undefined' && window) || + this + ); From 54d3fc08e6a835d8f27afb1a003582eac5b7c82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 14:38:48 +0200 Subject: [PATCH 06/77] test(core): fix ExportTableCsv test --- .../export-table-csv.test.tsx.snap | 181 ++++++++++++++++++ .../ui/components/export-table-csv.test.tsx | 40 ++-- .../http/ui/components/export-table-csv.tsx | 7 +- 3 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap diff --git a/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap b/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap new file mode 100644 index 0000000000..d6e60119af --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Export Table Csv component renders correctly to match the snapshot when the button is disabled 1`] = ` + + +
+ + + +
+
+
+`; + +exports[`Export Table Csv component renders correctly to match the snapshot when the button is enabled 1`] = ` + + +
+ + + +
+
+
+`; diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx index 2d18457bc8..4d81a1cfb6 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx @@ -15,24 +15,30 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExportTableCsv } from './export-table-csv'; - -jest.mock('../../../../kibana-services', () => ({ - getHttp: () => ({ - basePath: { - prepend: str => str, - }, - }), -})); - -jest.mock('../../../../react-services/common-services', () => ({ - getErrorOrchestrator: () => ({ - handleError: options => {}, - }), -})); - +const noop = () => {}; describe('Export Table Csv component', () => { - it('renders correctly to match the snapshot', () => { - const wrapper = mount(); + it('renders correctly to match the snapshot when the button is disabled', () => { + const wrapper = mount( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + it('renders correctly to match the snapshot when the button is enabled', () => { + const wrapper = mount( + , + ); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx index 2deebbe375..2665ccbc92 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; export function ExportTableCsv({ - fetchContext: { endpoint, filters }, + fetchContext, totalItems, title, showToast, @@ -22,7 +22,8 @@ export function ExportTableCsv({ }) { const downloadCSV = async () => { try { - const formatedFilters = Object.entries(filters || []).map( + const { endpoint, filters } = fetchContext; + const formattedFilters = Object.entries(filters || []).map( ([name, value]) => ({ name, value, @@ -34,7 +35,7 @@ export function ExportTableCsv({ toastLifeTimeMs: 3000, }); - await exportCSV(endpoint, [...formatedFilters], `${title.toLowerCase()}`); + await exportCSV(endpoint, formattedFilters, title.toLowerCase()); } catch (error) { // TODO: implement // const options = { From 2634eeb092890cf13d0f6f8582c5e67608d82d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 14:41:39 +0200 Subject: [PATCH 07/77] chore(prettier): fix some code syntax --- .../public/components/search-bar/README.md | 56 ++++++++++--------- .../components/eui-suggest/suggest_input.js | 26 ++++----- .../components/search-bar/index.test.tsx | 17 ++---- .../search-bar/query-language/aql.md | 54 +++++++++--------- .../search-bar/query-language/index.ts | 20 ++++--- .../search-bar/query-language/wql.md | 45 ++++++++------- plugins/wazuh-core/public/utils/file-saver.js | 55 ++++++++++-------- 7 files changed, 143 insertions(+), 130 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/README.md b/plugins/wazuh-core/public/components/search-bar/README.md index ce9fd0d65b..42d0f92bb0 100644 --- a/plugins/wazuh-core/public/components/search-bar/README.md +++ b/plugins/wazuh-core/public/components/search-bar/README.md @@ -34,9 +34,9 @@ Basic usage: // Use the UQL (Unified Query Language) syntax. implicitQuery: { query: 'id!=000', - conjunction: ';' + conjunction: ';', }, - searchTermFields: ['id', 'ip'] + searchTermFields: ['id', 'ip'], }, suggestions: { field(currentValue) { @@ -46,7 +46,10 @@ Basic usage: { label: 'id', description: 'ID' }, { label: 'ip', description: 'IP address' }, { label: 'group', description: 'Group' }, - { label: 'group_config_status', description: 'Synced configuration status' }, + { + label: 'group_config_status', + description: 'Synced configuration status', + }, { label: 'lastKeepAline', description: 'Date add' }, { label: 'manager', description: 'Manager' }, { label: 'mergedSum', description: 'Merged sum' }, @@ -60,30 +63,22 @@ Basic usage: value: async (currentValue, { field }) => { switch (field) { case 'configSum': - return [ - { label: 'configSum1' }, - { label: 'configSum2' }, - ]; + return [{ label: 'configSum1' }, { label: 'configSum2' }]; break; case 'dateAdd': - return [ - { label: 'dateAdd1' }, - { label: 'dateAdd2' }, - ]; + return [{ label: 'dateAdd1' }, { label: 'dateAdd2' }]; break; case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - label: status, - }), - ); + return UI_ORDER_AGENT_STATUS.map(status => ({ + label: status, + })); break; default: return []; break; } }, - } + }, }, ]} // Handler fired when the input handler changes. Optional. @@ -118,15 +113,21 @@ type SearchBarQueryLanguage = { id: string; label: string; getConfiguration?: () => any; - run: (input: string | undefined, params: any) => Promise<{ - searchBarProps: any, + run: ( + input: string | undefined, + params: any, + ) => Promise<{ + searchBarProps: any; output: { - language: string, - apiQuery: string, - query: string - } + language: string; + apiQuery: string; + query: string; + }; }>; - transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; + transformInput: ( + unifiedQuery: string, + options: { configuration: any; parameters: any }, + ) => string; }; ``` @@ -140,7 +141,7 @@ where: - `getConfiguration`: method that returns the configuration of the language. This allows custom behavior. - `run`: method that returns: - `searchBarProps`: properties to be passed to the search bar component. This allows the - customization the properties that will used by the base search bar component and the output used when searching + customization the properties that will used by the base search bar component and the output used when searching - `output`: - `language`: query language ID - `apiQuery`: API query. @@ -189,13 +190,14 @@ with the different query language implementations. The input and output parameters of the search bar component must use this syntax. This is used in: + - input: - `input` component property - output: - - `onChange` component handler + - `onChange` component handler - `onSearch` component handler Its syntax is equal to Wazuh API Query Language https://wazuh.com/./user-manual/api/queries.html -> The AQL query language is a implementation of this syntax. \ No newline at end of file +> The AQL query language is a implementation of this syntax. diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js index 7a4f5df6f2..3cb28b935f 100644 --- a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js @@ -7,7 +7,7 @@ import { EuiFieldText, EuiToolTip, EuiIcon, - EuiPopover + EuiPopover, } from '@elastic/eui'; import { EuiInputPopover } from '@elastic/eui'; @@ -15,29 +15,29 @@ const statusMap = { unsaved: { icon: 'dot', color: 'accent', - tooltip: 'Changes have not been saved.' + tooltip: 'Changes have not been saved.', }, saved: { icon: 'checkInCircleFilled', color: 'secondary', - tooltip: 'Saved.' + tooltip: 'Saved.', }, unchanged: { icon: '', - color: 'secondary' - } + color: 'secondary', + }, }; export class EuiSuggestInput extends Component { state = { value: '', - isPopoverOpen: false + isPopoverOpen: false, }; onFieldChange = e => { this.setState({ value: e.target.value, - isPopoverOpen: e.target.value !== '' ? true : false + isPopoverOpen: e.target.value !== '' ? true : false, }); this.props.sendValue(e.target.value); }; @@ -71,11 +71,11 @@ export class EuiSuggestInput extends Component { const statusElement = (status === 'saved' || status === 'unsaved') && ( @@ -103,10 +103,10 @@ export class EuiSuggestInput extends Component { return (
{ field(currentValue) { return []; }, - value(currentValue, { field }){ + value(currentValue, { field }) { return []; }, }, @@ -23,13 +23,13 @@ describe('SearchBar component', () => { id: 'wql', implicitQuery: { query: 'id!=000', - conjunction: ';' + conjunction: ';', }, suggestions: { field(currentValue) { return []; }, - value(currentValue, { field }){ + value(currentValue, { field }) { return []; }, }, @@ -37,21 +37,16 @@ describe('SearchBar component', () => { ], /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, - onSearch: () => {} + onSearch: () => {}, /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly the initial render', async () => { - const wrapper = render( - - ); + const wrapper = render(); /* This test causes a warning about act. This is intentional, because the test pretends to get the first rendering of the component that doesn't have the component properties coming of the selected query language */ expect(wrapper.container).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.md b/plugins/wazuh-core/public/components/search-bar/query-language/aql.md index 9d144e3b15..d052a2f4b1 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.md +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.md @@ -7,10 +7,11 @@ endpoints. Documentation: https://wazuh.com/./user-manual/api/queries.html -The implementation is adapted to work with the search bar component defined +The implementation is adapted to work with the search bar component defined `public/components/search-bar/index.tsx`. ## Features + - Suggestions for `fields` (configurable), `operators` and `values` (configurable) - Support implicit query @@ -23,19 +24,19 @@ Documentation: https://wazuh.com/./user-manual/api ## Options - `implicitQuery`: add an implicit query that is added to the user input. Optional. -Use UQL (Unified Query Language). -This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. + Use UQL (Unified Query Language). + This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. ```ts // language options // ID is not equal to 000 and . This is defined in UQL that is transformed internally to the specific query language. -implicitQuery: 'id!=000;' +implicitQuery: 'id!=000;'; ``` - `suggestions`: define the suggestion handlers. This is required. - `field`: method that returns the suggestions for the fields - + ```ts // language options field(currentValue) { @@ -59,6 +60,7 @@ implicitQuery: 'id!=000;' ``` - `value`: method that returns the suggestion for the values + ```ts // language options value: async (currentValue, { previousField }) => { @@ -67,40 +69,40 @@ implicitQuery: 'id!=000;' return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'dateAdd': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'id': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'ip': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'group': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'group_config_status': return [AGENT_SYNCED_STATUS.SYNCED, AGENT_SYNCED_STATUS.NOT_SYNCED].map( - (status) => ({ + status => ({ type: 'value', label: status, }), @@ -110,64 +112,62 @@ implicitQuery: 'id!=000;' return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'manager': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'mergedSum': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'name': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'node_name': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'os.platform': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; case 'status': - return UI_ORDER_AGENT_STATUS.map( - (status) => ({ - type: 'value', - label: status, - }), - ); + return UI_ORDER_AGENT_STATUS.map(status => ({ + type: 'value', + label: status, + })); break; case 'version': return await getAgentFilterValuesMapToSearchBarSuggestion( previousField, currentValue, - {q: 'id!=000'} + { q: 'id!=000' }, ); break; default: return []; break; } - } + }; ``` ## Language workflow @@ -180,7 +180,7 @@ graph TD; end tokenizer-->tokens; - + tokens-->searchBarProps; subgraph searchBarProps; searchBarProps_suggestions-->searchBarProps_suggestions_get_last_token_with_value[Get last token with value] @@ -201,4 +201,4 @@ graph TD; end output-->output_search_bar[Output] -``` \ No newline at end of file +``` diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts index 5a897d1d34..ba9a0554c4 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts +++ b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts @@ -7,15 +7,21 @@ type SearchBarQueryLanguage = { id: string; label: string; getConfiguration?: () => any; - run: (input: string | undefined, params: any) => Promise<{ - searchBarProps: any, + run: ( + input: string | undefined, + params: any, + ) => Promise<{ + searchBarProps: any; output: { - language: string, - unifiedQuery: string, - query: string - } + language: string; + unifiedQuery: string; + query: string; + }; }>; - transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; + transformInput: ( + unifiedQuery: string, + options: { configuration: any; parameters: any }, + ) => string; }; // Register the query languages diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.md b/plugins/wazuh-core/public/components/search-bar/query-language/wql.md index 108c942d32..f29230e564 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.md +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.md @@ -5,7 +5,7 @@ endpoints. Documentation: https://wazuh.com/./user-manual/api/queries.html -The implementation is adapted to work with the search bar component defined +The implementation is adapted to work with the search bar component defined `public/components/search-bar/index.tsx`. # Language syntax @@ -64,6 +64,7 @@ field.custom - Value with spaces should be wrapped by `"`. The `"` can be escaped using `\"`. Examples: + ``` value_without_whitespace "value with whitespaces" @@ -84,6 +85,7 @@ id = 001 ``` - Complex query (logical operator) + ``` status=active and os.platform~linux status = active and os.platform ~ linux @@ -95,6 +97,7 @@ status != never_connected and ip ~ 240 or os.platform ~ linux ``` - Complex query (logical operators and group operator) + ``` (status!=never_connected and ip~240) or id=001 ( status != never_connected and ip ~ 240 ) or id = 001 @@ -123,6 +126,7 @@ id~linux,ip~linux ## Developer notes ## Features + - Support suggestions for each token entity. `fields` and `values` are customizable. - Support implicit query. - Support for search term mode. It enables to search a term in multiple fields. @@ -139,6 +143,7 @@ This mode enables to search in multiple fields using a search term. The fields t Use an union expression of each field with the like as operation `~`. The user input is transformed to something as: + ``` field1~user_input,field2~user_input,field3~user_input ``` @@ -148,13 +153,10 @@ field1~user_input,field2~user_input,field3~user_input - `options`: options - `implicitQuery`: add an implicit query that is added to the user input. Optional. - This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. - - `query`: query string in UQL (Unified Query Language) -Use UQL (Unified Query Language). - - `conjunction`: query string of the conjunction in UQL (Unified Query Language) + This can't be changed by the user. If this is defined, will be displayed as a prepend of the search bar. - `query`: query string in UQL (Unified Query Language) + Use UQL (Unified Query Language). - `conjunction`: query string of the conjunction in UQL (Unified Query Language) - `searchTermFields`: define the fields used to build the query for the search term mode - `filterButtons`: define a list of buttons to filter in the search bar - ```ts // language options @@ -175,7 +177,7 @@ options: { - `suggestions`: define the suggestion handlers. This is required. - `field`: method that returns the suggestions for the fields - + ```ts // language options field(currentValue) { @@ -188,30 +190,31 @@ options: { ``` - `value`: method that returns the suggestion for the values + ```ts // language options value: async (currentValue, { field }) => { // static or async fetching is allowed // async fetching data // const response = await fetchData(); - return [ - { label: 'value1' }, - { label: 'value2' } - ] - } + return [{ label: 'value1' }, { label: 'value2' }]; + }; ``` - `validate`: define validation methods for the field types. Optional + - `value`: method to validate the value token ```ts validate: { - value: (token, {field, operator_compare}) => { - if(field === 'field1'){ - const value = token.formattedValue || token.value - return /\d+/ ? undefined : `Invalid value for field ${field}, only digits are supported: "${value}"` + value: (token, { field, operator_compare }) => { + if (field === 'field1') { + const value = token.formattedValue || token.value; + return /\d+/ + ? undefined + : `Invalid value for field ${field}, only digits are supported: "${value}"`; } - } + }; } ``` @@ -248,7 +251,7 @@ graph TD; searchBarProps_onItemClick[onItemClickSuggestion]-->searchBarProps_onItemClick_suggestion_error[Error] searchBarProps_isInvalid[isInvalid]-->searchBarProps_validate_input[validate input] end - + subgraph output[output]; output_input_options_implicitFilter[options.implicitFilter]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] output_input_user_input_QL[User input in QL]-->output_input_user_input_UQL[User input in UQL]-->output_input_options_result["{apiQuery: { q }, error, language: 'wql', query}"] @@ -263,7 +266,7 @@ graph TD; ## Notes - The value that contains the following characters: `!`, `~` are not supported by the AQL and this -could cause problems when do the request to the API. + could cause problems when do the request to the API. - The value with spaces are wrapped with `"`. If the value contains the `\"` sequence this is -replaced by `"`. This could cause a problem with values that are intended to have the mentioned -sequence. \ No newline at end of file + replaced by `"`. This could cause a problem with values that are intended to have the mentioned + sequence. diff --git a/plugins/wazuh-core/public/utils/file-saver.js b/plugins/wazuh-core/public/utils/file-saver.js index 6de85f79dd..92beea7e6a 100644 --- a/plugins/wazuh-core/public/utils/file-saver.js +++ b/plugins/wazuh-core/public/utils/file-saver.js @@ -16,7 +16,7 @@ export var saveAs = saveAs || - (function(view) { + (function (view) { 'use strict'; // IE <10 is explicitly unsupported if ( @@ -28,28 +28,28 @@ export var saveAs = } var doc = view.document, // only get URL when necessary in case Blob.js hasn't overridden it yet - get_URL = function() { + get_URL = function () { return view.URL || view.webkitURL || view; }, save_link = doc.createElementNS('http://www.w3.org/1999/xhtml', 'a'), can_use_save_link = 'download' in save_link, - click = function(node) { + click = function (node) { var event = new MouseEvent('click'); node.dispatchEvent(event); }, is_safari = /constructor/i.test(view.HTMLElement) || view.safari, is_chrome_ios = /CriOS\/[\d]+/.test(navigator.userAgent), setImmediate = view.setImmediate || view.setTimeout, - throw_outside = function(ex) { - setImmediate(function() { + throw_outside = function (ex) { + setImmediate(function () { throw ex; }, 0); }, force_saveable_type = 'application/octet-stream', // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to arbitrary_revoke_timeout = 1000 * 40, // in ms - revoke = function(file) { - var revoker = function() { + revoke = function (file) { + var revoker = function () { if (typeof file === 'string') { // file is an object URL get_URL().revokeObjectURL(file); @@ -60,7 +60,7 @@ export var saveAs = }; setTimeout(revoker, arbitrary_revoke_timeout); }, - dispatch = function(filesaver, event_types, event) { + dispatch = function (filesaver, event_types, event) { event_types = [].concat(event_types); var i = event_types.length; while (i--) { @@ -74,21 +74,21 @@ export var saveAs = } } }, - auto_bom = function(blob) { + auto_bom = function (blob) { // prepend BOM for UTF-8 XML and text/* types (including HTML) // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF if ( /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test( - blob.type + blob.type, ) ) { return new Blob([String.fromCharCode(0xfeff), blob], { - type: blob.type + type: blob.type, }); } return blob; }, - FileSaver = function(blob, name, no_auto_bom) { + FileSaver = function (blob, name, no_auto_bom) { if (!no_auto_bom) { blob = auto_bom(blob); } @@ -97,23 +97,23 @@ export var saveAs = type = blob.type, force = type === force_saveable_type, object_url, - dispatch_all = function() { + dispatch_all = function () { dispatch( filesaver, - 'writestart progress write writeend'.split(' ') + 'writestart progress write writeend'.split(' '), ); }, // on any filesys errors revert to saving with object URLs - fs_error = function() { + fs_error = function () { if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { // Safari doesn't allow downloading of blob urls var reader = new FileReader(); - reader.onloadend = function() { + reader.onloadend = function () { var url = is_chrome_ios ? reader.result : reader.result.replace( /^data:[^;]*;/, - 'data:attachment/file;' + 'data:attachment/file;', ); var popup = view.open(url, '_blank'); if (!popup) view.location.href = url; @@ -146,7 +146,7 @@ export var saveAs = if (can_use_save_link) { object_url = get_URL().createObjectURL(blob); - setImmediate(function() { + setImmediate(function () { save_link.href = object_url; save_link.download = name; click(save_link); @@ -160,17 +160,17 @@ export var saveAs = fs_error(); }, FS_proto = FileSaver.prototype, - saveAs = function(blob, name, no_auto_bom) { + saveAs = function (blob, name, no_auto_bom) { return new FileSaver( blob, name || blob.name || 'download', - no_auto_bom + no_auto_bom, ); }; // IE 10+ (native saveAs) if (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { - return function(blob, name, no_auto_bom) { + return function (blob, name, no_auto_bom) { name = name || blob.name || 'download'; if (!no_auto_bom) { @@ -183,16 +183,23 @@ export var saveAs = // todo: detect chrome extensions & packaged apps //save_link.target = "_blank"; - FS_proto.abort = function() {}; + FS_proto.abort = function () {}; FS_proto.readyState = FS_proto.INIT = 0; FS_proto.WRITING = 1; FS_proto.DONE = 2; - FS_proto.error = FS_proto.onwritestart = FS_proto.onprogress = FS_proto.onwrite = FS_proto.onabort = FS_proto.onerror = FS_proto.onwriteend = null; + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; return saveAs; })( (typeof self !== 'undefined' && self) || (typeof window !== 'undefined' && window) || - this + this, ); From 1bde3582ea77c0abec9c345f28c28e58991667ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 14:46:17 +0200 Subject: [PATCH 08/77] chore(prettier): fix some code syntax --- .../public/hooks/use-state-storage.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/plugins/wazuh-core/public/hooks/use-state-storage.ts b/plugins/wazuh-core/public/hooks/use-state-storage.ts index 3251ea1be5..01829a6a19 100644 --- a/plugins/wazuh-core/public/hooks/use-state-storage.ts +++ b/plugins/wazuh-core/public/hooks/use-state-storage.ts @@ -1,30 +1,37 @@ import { useState } from 'react'; -function transformValueToStorage(value: any){ +function transformValueToStorage(value: any) { return typeof value !== 'string' ? JSON.stringify(value) : value; -}; +} -function transformValueFromStorage(value: any){ +function transformValueFromStorage(value: any) { return typeof value === 'string' ? JSON.parse(value) : value; -}; +} -export function useStateStorage(initialValue: any, storageSystem?: 'sessionStorage' | 'localStorage', storageKey?: string){ +export function useStateStorage( + initialValue: any, + storageSystem?: 'sessionStorage' | 'localStorage', + storageKey?: string, +) { const [state, setState] = useState( - (storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey)) + storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey) ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) - : initialValue + : initialValue, ); - function setStateStorage(value: any){ - setState((state) => { - const formattedValue = typeof value === 'function' - ? value(state) - : value; + function setStateStorage(value: any) { + setState(state => { + const formattedValue = typeof value === 'function' ? value(state) : value; - storageSystem && storageKey && window?.[storageSystem]?.setItem(storageKey, transformValueToStorage(formattedValue)); + storageSystem && + storageKey && + window?.[storageSystem]?.setItem( + storageKey, + transformValueToStorage(formattedValue), + ); return formattedValue; }); - }; + } return [state, setStateStorage]; -}; +} From 57f213ce6bc516e1578fe7b14eebc48e513f84c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 14:49:35 +0200 Subject: [PATCH 09/77] chore: remove console.log --- .../public/services/http/ui/services/fetch-server-data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts index e76815772b..8baa159794 100644 --- a/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts +++ b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts @@ -9,7 +9,6 @@ const getFilters = filters => { export const fetchServerTableDataCreator = fetchData => async ({ pagination, sorting, fetchContext }) => { - console.log(fetchContext); const { pageIndex, pageSize } = pagination; const { field, direction } = sorting.sort; const params = { From aad99307d3dbe590fb81a0ada25723027a1f02cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 2 Oct 2024 15:52:43 +0200 Subject: [PATCH 10/77] feat(core): enhance typing --- .../components/table-data/table-data.tsx | 2 +- .../public/hooks/use-docked-side-nav.tsx | 2 ++ .../public/hooks/use-state-storage.ts | 20 ++++++++++---- plugins/wazuh-core/public/types.ts | 27 ++++++++++++++++++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx index 584ddb040f..5ab60623f6 100644 --- a/plugins/wazuh-core/public/components/table-data/table-data.tsx +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -59,7 +59,7 @@ export function TableData({ const isMounted = useRef(false); const tableRef = useRef(); - const [selectedFields, setSelectedFields] = useStateStorage( + const [selectedFields, setSelectedFields] = useStateStorage( rest.tableColumns.some(({ show }) => show) ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) : rest.tableColumns.map(({ field }) => field), diff --git a/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx b/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx index ad33997d4c..18894b861b 100644 --- a/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx +++ b/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx @@ -51,3 +51,5 @@ export const useDockedSideNav = () => { return isDockedSideNavVisible; }; + +export type UseDockedSideNav = () => boolean; diff --git a/plugins/wazuh-core/public/hooks/use-state-storage.ts b/plugins/wazuh-core/public/hooks/use-state-storage.ts index 01829a6a19..4aacbd55b0 100644 --- a/plugins/wazuh-core/public/hooks/use-state-storage.ts +++ b/plugins/wazuh-core/public/hooks/use-state-storage.ts @@ -1,5 +1,15 @@ import { useState } from 'react'; +export type UseStateStorageSystem = 'sessionStorage' | 'localStorage'; +export type UseStateStorageReturn = [T, (value: T) => void]; +export type UseStateStorage = { + ( + initialValue: T, + storageSystem?: UseStateStorageSystem, + storageKey?: string, + ): [T, (value: T) => void]; +}; + function transformValueToStorage(value: any) { return typeof value !== 'string' ? JSON.stringify(value) : value; } @@ -8,18 +18,18 @@ function transformValueFromStorage(value: any) { return typeof value === 'string' ? JSON.parse(value) : value; } -export function useStateStorage( - initialValue: any, - storageSystem?: 'sessionStorage' | 'localStorage', +export function useStateStorage( + initialValue: T, + storageSystem?: UseStateStorageSystem, storageKey?: string, -) { +): UseStateStorageReturn { const [state, setState] = useState( storageSystem && storageKey && window?.[storageSystem]?.getItem(storageKey) ? transformValueFromStorage(window?.[storageSystem]?.getItem(storageKey)) : initialValue, ); - function setStateStorage(value: any) { + function setStateStorage(value: T) { setState(state => { const formattedValue = typeof value === 'function' ? value(state) : value; diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index 942c5d6fe4..411d3fbda0 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -1,6 +1,10 @@ import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; +import { TableDataProps } from './components'; +import { UseStateStorage, UseStateStorageSystem } from './hooks'; +import { UseDockedSideNav } from './hooks/use-docked-side-nav'; import { HTTPClient } from './services/http/types'; +import { ServerDataProps } from './services/http/ui/components/types'; import { DashboardSecurity } from './utils/dashboard-security'; export interface WazuhCorePluginSetup { @@ -9,15 +13,36 @@ export interface WazuhCorePluginSetup { configuration: Configuration; dashboardSecurity: DashboardSecurity; http: HTTPClient; + ui: { + TableData( + prop: TableDataProps, + ): React.ComponentType>; + SearchBar(prop: any): React.ComponentType; + ServerTable( + prop: ServerDataProps, + ): React.ComponentType>; + }; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginStart { - hooks: { useDockedSideNav: () => boolean }; + hooks: { + useDockedSideNav: UseDockedSideNav; + useStateStorage: UseStateStorage; // TODO: enhance + }; utils: { formatUIDate: (date: Date) => string }; API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; configuration: Configuration; dashboardSecurity: DashboardSecurity; http: HTTPClient; + ui: { + TableData( + prop: TableDataProps, + ): React.ComponentType>; + SearchBar(prop: any): React.ComponentType; + ServerTable( + prop: ServerDataProps, + ): React.ComponentType>; + }; } export interface AppPluginStartDependencies {} From 5c965f0c2e50088d89d0072982c26a0eb839ca1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 27 Nov 2024 16:43:38 +0100 Subject: [PATCH 11/77] fix(http): move type definitions --- .../public/services/http/server-client.ts | 23 ++----------------- .../wazuh-core/public/services/http/types.ts | 20 ++++++++++++++++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index e89fce0f2b..168c18f395 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -10,36 +10,17 @@ * Find more information about this on the LICENSE file. */ import { - HTTPClientRequestInterceptor, HTTPClientServer, HTTPVerb, HTTPClientServerUserData, + WzRequestServices, + ServerAPIResponseItemsData, } from './types'; import { Logger } from '../../../common/services/configuration'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; import jwtDecode from 'jwt-decode'; import { BehaviorSubject } from 'rxjs'; -interface WzRequestServices { - request: HTTPClientRequestInterceptor['request']; - getURL(path: string): string; - getTimeout(): Promise; - getServerAPI(): string; -} - -interface ServerAPIResponseItems { - affected_items: Array; - failed_items: Array; - total_affected_items: number; - total_failed_items: number; -} - -interface ServerAPIResponseItemsData { - data: ServerAPIResponseItems; - message: string; - error: number; -} - export interface ServerAPIResponseItemsDataHTTPClient { data: ServerAPIResponseItemsData; } diff --git a/plugins/wazuh-core/public/services/http/types.ts b/plugins/wazuh-core/public/services/http/types.ts index 574670eb32..3b9990343f 100644 --- a/plugins/wazuh-core/public/services/http/types.ts +++ b/plugins/wazuh-core/public/services/http/types.ts @@ -47,3 +47,23 @@ export interface HTTPClient { generic: HTTPClientGeneric; server: HTTPClientServer; } + +export interface WzRequestServices { + request: HTTPClientRequestInterceptor['request']; + getURL(path: string): string; + getTimeout(): Promise; + getServerAPI(): string; +} + +export interface ServerAPIResponseItems { + affected_items: Array; + failed_items: Array; + total_affected_items: number; + total_failed_items: number; +} + +export interface ServerAPIResponseItemsData { + data: ServerAPIResponseItems; + message: string; + error: number; +} From bfff8dd31552cb027330dda890fb8481f1aaa1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 27 Nov 2024 17:48:45 +0100 Subject: [PATCH 12/77] fix: tests --- plugins/wazuh-core/public/services/http/server-client.test.ts | 1 - .../ui/components/__snapshots__/export-table-csv.test.tsx.snap | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts index 5c63534b0b..155af5ba5e 100644 --- a/plugins/wazuh-core/public/services/http/server-client.test.ts +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -13,7 +13,6 @@ const USER_TOKEN = function createClient() { const mockRequest = jest.fn(options => { - console.log({ options }); if (options.url === '/api/login') { return { data: { diff --git a/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap b/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap index d6e60119af..b5a8744e57 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap +++ b/plugins/wazuh-core/public/services/http/ui/components/__snapshots__/export-table-csv.test.tsx.snap @@ -34,6 +34,7 @@ exports[`Export Table Csv component renders correctly to match the snapshot when > Date: Thu, 28 Nov 2024 10:38:08 +0100 Subject: [PATCH 13/77] fix: move types --- plugins/wazuh-core/public/services/http/server-client.ts | 6 +----- plugins/wazuh-core/public/services/http/types.ts | 4 ++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 168c18f395..1a8b75316a 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -14,17 +14,13 @@ import { HTTPVerb, HTTPClientServerUserData, WzRequestServices, - ServerAPIResponseItemsData, + ServerAPIResponseItemsDataHTTPClient, } from './types'; import { Logger } from '../../../common/services/configuration'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; import jwtDecode from 'jwt-decode'; import { BehaviorSubject } from 'rxjs'; -export interface ServerAPIResponseItemsDataHTTPClient { - data: ServerAPIResponseItemsData; -} - export class WzRequest implements HTTPClientServer { onErrorInterceptor?: ( error: any, diff --git a/plugins/wazuh-core/public/services/http/types.ts b/plugins/wazuh-core/public/services/http/types.ts index 3b9990343f..ee64b68300 100644 --- a/plugins/wazuh-core/public/services/http/types.ts +++ b/plugins/wazuh-core/public/services/http/types.ts @@ -67,3 +67,7 @@ export interface ServerAPIResponseItemsData { message: string; error: number; } + +export interface ServerAPIResponseItemsDataHTTPClient { + data: ServerAPIResponseItemsData; +} From cbd5584add263af63947cf6f4d8a42f1222c680e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 28 Nov 2024 13:44:17 +0100 Subject: [PATCH 14/77] chore(changelog): add entry) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb7cf1177..c2fa78bcc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh 5.0.0 - Added creation of report definition when creating dashboard by reference and the button to reset the report [#7091](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7091) +- Added a http client to core plugin [#7000](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7000) ### Removed From 6dedffc3f3cb857db27762e3eef28799c1a692fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 3 Dec 2024 16:33:47 +0100 Subject: [PATCH 15/77] fix(lint): code lint --- .eslintrc.js | 4 + .../search-bar/query-language/index.ts | 33 +- .../common/services/configuration.ts | 240 ++++++----- .../components/eui-suggest/index.js | 2 +- .../{suggest_input.js => suggest-input.js} | 29 +- .../components/eui-suggest/suggest.js | 16 +- .../components/search-bar/index.test.tsx | 1 + .../public/components/search-bar/index.tsx | 51 ++- .../search-bar/query-language/aql.test.tsx | 44 +- .../search-bar/query-language/aql.tsx | 228 ++++++----- .../search-bar/query-language/wql.test.tsx | 186 +++++---- .../search-bar/query-language/wql.tsx | 387 ++++++++++-------- .../components/table-data/table-data.tsx | 58 ++- .../public/hooks/use-docked-side-nav.tsx | 12 +- .../public/hooks/use-state-storage.ts | 25 +- plugins/wazuh-core/public/plugin.ts | 40 +- .../public/services/http/generic-client.ts | 46 ++- .../public/services/http/http-client.ts | 30 +- .../services/http/request-interceptor.ts | 47 ++- .../services/http/server-client.test.ts | 6 +- .../public/services/http/server-client.ts | 78 ++-- .../wazuh-core/public/services/http/types.ts | 38 +- .../ui/components/export-table-csv.test.tsx | 5 + .../http/ui/components/export-table-csv.tsx | 27 +- .../http/ui/components/server-table-data.tsx | 20 +- .../public/services/http/ui/create.tsx | 6 +- .../http/ui/services/fetch-server-data.ts | 6 +- .../public/services/http/ui/with-services.tsx | 6 + .../public/services/http/ui/withServices.tsx | 3 - plugins/wazuh-core/public/types.ts | 32 +- .../public/utils/configuration-store.ts | 56 ++- .../public/utils/dashboard-security.ts | 23 +- plugins/wazuh-core/public/utils/file-saver.js | 1 + .../server/services/server-api-client.ts | 104 +++-- 34 files changed, 1125 insertions(+), 765 deletions(-) rename plugins/wazuh-core/public/components/search-bar/components/eui-suggest/{suggest_input.js => suggest-input.js} (87%) create mode 100644 plugins/wazuh-core/public/services/http/ui/with-services.tsx delete mode 100644 plugins/wazuh-core/public/services/http/ui/withServices.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 28118e1374..cd4a3b7dec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -217,6 +217,10 @@ module.exports = { types: ['function'], format: ['camelCase', 'PascalCase'], }, + { + selector: 'function', + format: ['camelCase', 'PascalCase'], + }, { selector: ['objectLiteralProperty', 'typeProperty'], format: null, diff --git a/plugins/main/public/components/search-bar/query-language/index.ts b/plugins/main/public/components/search-bar/query-language/index.ts index 5a897d1d34..95dbaf9f73 100644 --- a/plugins/main/public/components/search-bar/query-language/index.ts +++ b/plugins/main/public/components/search-bar/query-language/index.ts @@ -1,30 +1,39 @@ import { AQL } from './aql'; import { WQL } from './wql'; -type SearchBarQueryLanguage = { +interface SearchBarQueryLanguage { description: string; documentationLink?: string; id: string; label: string; getConfiguration?: () => any; - run: (input: string | undefined, params: any) => Promise<{ - searchBarProps: any, + run: ( + input: string | undefined, + params: any, + ) => Promise<{ + searchBarProps: any; output: { - language: string, - unifiedQuery: string, - query: string - } + language: string; + unifiedQuery: string; + query: string; + }; }>; - transformInput: (unifiedQuery: string, options: {configuration: any, parameters: any}) => string; -}; + transformInput: ( + unifiedQuery: string, + options: { configuration: any; parameters: any }, + ) => string; +} // Register the query languages -export const searchBarQueryLanguages: { - [key: string]: SearchBarQueryLanguage; -} = [AQL, WQL].reduce((accum, item) => { +export const searchBarQueryLanguages: Record = [ + AQL, + WQL, + // eslint-disable-next-line unicorn/no-array-reduce +].reduce((accum, item) => { if (accum[item.id]) { throw new Error(`Query language with id: ${item.id} already registered.`); } + return { ...accum, [item.id]: item, diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 3355bb96a4..5251403fe8 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -3,35 +3,35 @@ import { formatLabelValuePair } from './settings'; import { formatBytes } from './file-size'; export interface Logger { - debug(message: string): void; - info(message: string): void; - warn(message: string): void; - error(message: string): void; + debug: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; } -type TConfigurationSettingOptionsPassword = { +interface TConfigurationSettingOptionsPassword { password: { dual?: 'text' | 'password' | 'dual'; }; -}; +} -type TConfigurationSettingOptionsTextArea = { +interface TConfigurationSettingOptionsTextArea { maxRows?: number; minRows?: number; maxLength?: number; -}; +} -type TConfigurationSettingOptionsSelect = { +interface TConfigurationSettingOptionsSelect { select: { text: string; value: any }[]; -}; +} -type TConfigurationSettingOptionsEditor = { +interface TConfigurationSettingOptionsEditor { editor: { language: string; }; -}; +} -type TConfigurationSettingOptionsFile = { +interface TConfigurationSettingOptionsFile { file: { type: 'image'; extensions?: string[]; @@ -52,37 +52,46 @@ type TConfigurationSettingOptionsFile = { resolveStaticURL: (filename: string) => string; }; }; -}; +} -type TConfigurationSettingOptionsNumber = { +interface TConfigurationSettingOptionsNumber { number: { min?: number; max?: number; integer?: boolean; }; -}; +} -type TConfigurationSettingOptionsSwitch = { +interface TConfigurationSettingOptionsSwitch { switch: { values: { disabled: { label?: string; value: any }; enabled: { label?: string; value: any }; }; }; -}; +} +// eslint-disable-next-line @typescript-eslint/naming-convention export enum EpluginSettingType { + // eslint-disable-next-line @typescript-eslint/naming-convention text = 'text', + // eslint-disable-next-line @typescript-eslint/naming-convention password = 'password', + // eslint-disable-next-line @typescript-eslint/naming-convention textarea = 'textarea', + // eslint-disable-next-line @typescript-eslint/naming-convention switch = 'switch', + // eslint-disable-next-line @typescript-eslint/naming-convention number = 'number', + // eslint-disable-next-line @typescript-eslint/naming-convention editor = 'editor', + // eslint-disable-next-line @typescript-eslint/naming-convention select = 'select', + // eslint-disable-next-line @typescript-eslint/naming-convention filepicker = 'filepicker', } -export type TConfigurationSetting = { +export interface TConfigurationSetting { // Define the text displayed in the UI. title: string; // Description. @@ -133,19 +142,19 @@ export type TConfigurationSetting = { validateUIForm?: (value: any) => string | undefined; // Validate function creator to validate the setting in the backend. validate?: (schema: any) => (value: unknown) => string | undefined; -}; +} export type TConfigurationSettingWithKey = TConfigurationSetting & { key: string; }; -export type TConfigurationSettingCategory = { +export interface TConfigurationSettingCategory { title: string; description?: string; documentationLink?: string; renderOrder?: number; -}; +} -type TConfigurationSettings = { [key: string]: any }; +type TConfigurationSettings = Record; export interface IConfigurationStore { setup: () => Promise; start: () => Promise; @@ -153,70 +162,79 @@ export interface IConfigurationStore { get: (...settings: string[]) => Promise; set: (settings: TConfigurationSettings) => Promise; clear: (...settings: string[]) => Promise; + // eslint-disable-next-line no-use-before-define setConfiguration: (configuration: IConfiguration) => void; } export interface IConfiguration { - setStore(store: IConfigurationStore): void; - setup(): Promise; - start(): Promise; - stop(): Promise; - register(id: string, value: any): void; - get(...settings: string[]): Promise; - set(settings: TConfigurationSettings): Promise; - clear(...settings: string[]): Promise; - reset(...settings: string[]): Promise; - _settings: Map< - string, - { - [key: string]: TConfigurationSetting; - } - >; - getSettingValue(settingKey: string, value?: any): any; - getSettingValueIfNotSet(settingKey: string, value?: any): any; + setStore: (store: IConfigurationStore) => void; + setup: () => Promise; + start: () => Promise; + stop: () => Promise; + register: (id: string, value: any) => void; + get: (...settings: string[]) => Promise; + set: (settings: TConfigurationSettings) => Promise; + clear: (...settings: string[]) => Promise; + reset: (...settings: string[]) => Promise; + _settings: Map>; + getSettingValue: (settingKey: string, value?: any) => any; + getSettingValueIfNotSet: (settingKey: string, value?: any) => any; } export class Configuration implements IConfiguration { store: IConfigurationStore | null = null; - _settings: Map; - _categories: Map; - constructor(private logger: Logger, store: IConfigurationStore) { + // eslint-disable-next-line @typescript-eslint/naming-convention + _settings: Map>; + // eslint-disable-next-line @typescript-eslint/naming-convention + _categories: Map>; + + constructor( + private readonly logger: Logger, + store: IConfigurationStore, + ) { this._settings = new Map(); this._categories = new Map(); this.setStore(store); } + setStore(store: IConfigurationStore) { this.store = store; this.store.setConfiguration(this); } + async setup(dependencies: any = {}) { return this.store.setup(dependencies); } + async start(dependencies: any = {}) { return this.store.start(dependencies); } + async stop(dependencies: any = {}) { return this.store.stop(dependencies); } + /** * Register a setting * @param id * @param value */ register(id: string, value: any) { - if (!this._settings.has(id)) { + if (this._settings.has(id)) { + const message = `Setting ${id} exists`; + + this.logger.error(message); + throw new Error(message); + } else { // Enhance the setting const enhancedValue = value; + // Enhance the description enhancedValue._description = value.description; enhancedValue.description = this.enhanceSettingDescription(value); // Register the setting this._settings.set(id, enhancedValue); this.logger.debug(`Registered ${id}`); - } else { - const message = `Setting ${id} exists`; - this.logger.error(message); - throw new Error(message); } } @@ -245,20 +263,25 @@ export class Configuration implements IConfiguration { this.logger.debug( `Getting value for [${settingKey}]: stored [${JSON.stringify(value)}]`, ); + if (!this._settings.has(settingKey)) { throw new Error(`${settingKey} is not registered`); } - if (typeof value !== 'undefined') { + + if (value !== undefined) { return value; } + const setting = this._settings.get(settingKey); const finalValue = - typeof setting.defaultValueIfNotSet !== 'undefined' - ? setting.defaultValueIfNotSet - : setting.defaultValue; + setting.defaultValueIfNotSet === undefined + ? setting.defaultValue + : setting.defaultValueIfNotSet; + this.logger.debug( `Value for [${settingKey}]: [${JSON.stringify(finalValue)}]`, ); + return finalValue; } @@ -273,19 +296,25 @@ export class Configuration implements IConfiguration { this.logger.debug( `Getting value for [${settingKey}]: stored [${JSON.stringify(value)}]`, ); + if (!this._settings.has(settingKey)) { throw new Error(`${settingKey} is not registered`); } - if (typeof value !== 'undefined') { + + if (value !== undefined) { return value; } + const setting = this._settings.get(settingKey); const finalValue = setting.defaultValue; + this.logger.debug( `Value for [${settingKey}]: [${JSON.stringify(finalValue)}]`, ); + return finalValue; } + /** * Get the value for all settings or a subset of them * @param rest @@ -293,57 +322,60 @@ export class Configuration implements IConfiguration { */ async get(...settings: string[]) { this.logger.debug( - settings.length + settings.length > 0 ? `Getting settings [${settings.join(',')}]` : 'Getting settings', ); + const stored = await this.store.get(...settings); + this.logger.debug(`configuration stored: ${JSON.stringify({ stored })}`); const result = settings && settings.length === 1 ? this.getSettingValue(settings[0], stored[settings[0]]) - : (settings.length > 1 - ? settings - : Array.from(this._settings.keys()) - ).reduce( - (accum, key) => ({ - ...accum, - [key]: this.getSettingValue(key, stored[key]), - }), - {}, + : Object.fromEntries( + (settings.length > 1 ? settings : [...this._settings.keys()]).map( + key => [key, this.getSettingValue(key, stored[key])], + ), ); // Clone the result. This avoids the object reference can be changed when managing the result. return cloneDeep(result); } + /** * Set a the value for a subset of settings * @param settings * @returns */ - async set(settings: { [key: string]: any }) { + async set(settings: Record) { const settingsAreRegistered = Object.entries(settings) .map(([key]) => this._settings.has(key) ? null : `${key} is not registered`, ) - .filter(value => value); - if (settingsAreRegistered.length) { + .filter(Boolean); + + if (settingsAreRegistered.length > 0) { throw new Error(`${settingsAreRegistered.join(', ')} are not registered`); } const validationErrors = Object.entries(settings) .map(([key, value]) => { const validationError = this._settings.get(key)?.validate?.(value); + return validationError ? `setting [${key}]: ${validationError}` : undefined; }) - .filter(value => value); - if (validationErrors.length) { + .filter(Boolean); + + if (validationErrors.length > 0) { throw new Error(`Validation errors: ${validationErrors.join('\n')}`); } + const responseStore = await this.store.set(settings); + return { requirements: this.checkRequirementsOnUpdatedSettings( Object.keys(responseStore), @@ -358,10 +390,13 @@ export class Configuration implements IConfiguration { * @returns */ async clear(...settings: string[]) { - if (settings.length) { + if (settings.length > 0) { this.logger.debug(`Clean settings: ${settings.join(', ')}`); + const responseStore = await this.store.clear(...settings); + this.logger.info('Settings were cleared'); + return { requirements: this.checkRequirementsOnUpdatedSettings( Object.keys(responseStore), @@ -369,7 +404,7 @@ export class Configuration implements IConfiguration { update: responseStore, }; } else { - return await this.clear(...Array.from(this._settings.keys())); + return await this.clear(...this._settings.keys()); } } @@ -379,8 +414,10 @@ export class Configuration implements IConfiguration { * @returns */ async reset(...settings: string[]) { - if (settings.length) { + if (settings.length > 0) { this.logger.debug(`Reset settings: ${settings.join(', ')}`); + + // eslint-disable-next-line unicorn/no-array-reduce const updatedSettings = settings.reduce((accum, settingKey: string) => { return { ...accum, @@ -388,7 +425,9 @@ export class Configuration implements IConfiguration { }; }, {}); const responseStore = await this.store.set(updatedSettings); + this.logger.info('Settings were reset'); + return { requirements: this.checkRequirementsOnUpdatedSettings( Object.keys(responseStore), @@ -405,6 +444,7 @@ export class Configuration implements IConfiguration { this.logger.error(`Registered category [${id}]`); throw new Error(`Category exists [${id}]`); } + this._categories.set(id, rest); this.logger.debug(`Registered category [${id}]`); } @@ -412,7 +452,7 @@ export class Configuration implements IConfiguration { getUniqueCategories() { return [ ...new Set( - Array.from(this._settings.entries()) + [...this._settings.entries()] .filter( ([, { isConfigurableFromSettings }]) => isConfigurableFromSettings, ) @@ -426,11 +466,14 @@ export class Configuration implements IConfiguration { } else if (categoryA.title < categoryB.title) { return -1; } + return 0; }); } + private enhanceSettingDescription(setting: TConfigurationSetting) { const { description, options } = setting; + return [ description, ...(options?.select @@ -473,27 +516,27 @@ export class Configuration implements IConfiguration { ] : []), // File size - ...(options?.file?.size && - typeof options.file.size.minBytes !== 'undefined' + ...(options?.file?.size && options.file.size.minBytes !== undefined ? [`Minimum file size: ${formatBytes(options.file.size.minBytes)}.`] : []), - ...(options?.file?.size && - typeof options.file.size.maxBytes !== 'undefined' + ...(options?.file?.size && options.file.size.maxBytes !== undefined ? [`Maximum file size: ${formatBytes(options.file.size.maxBytes)}.`] : []), // Multi line text - ...(options?.maxRows && typeof options.maxRows !== 'undefined' + ...(options?.maxRows && options.maxRows !== undefined ? [`Maximum amount of lines: ${options.maxRows}.`] : []), - ...(options?.minRows && typeof options.minRows !== 'undefined' + ...(options?.minRows && options.minRows !== undefined ? [`Minimum amount of lines: ${options.minRows}.`] : []), - ...(options?.maxLength && typeof options.maxLength !== 'undefined' + ...(options?.maxLength && options.maxLength !== undefined ? [`Maximum lines length is ${options.maxLength} characters.`] : []), ].join(' '); } + groupSettingsByCategory( + // eslint-disable-next-line @typescript-eslint/naming-convention _settings: string[] | null = null, filterFunction: | ((setting: TConfigurationSettingWithKey) => boolean) @@ -501,35 +544,40 @@ export class Configuration implements IConfiguration { ) { const settings = ( _settings && Array.isArray(_settings) - ? Array.from(this._settings.entries()).filter(([key]) => + ? [...this._settings.entries()].filter(([key]) => _settings.includes(key), ) - : Array.from(this._settings.entries()) - ).map(([key, value]) => ({ - ...value, - key, - })); - + : [...this._settings.entries()] + ).map(([key, value]) => { + return { + ...value, + key, + }; + }); const settingsSortedByCategories = ( - filterFunction ? settings.filter(filterFunction) : settings + filterFunction + ? settings.filter(element => filterFunction(element)) + : settings ) .sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)) - .reduce( - (accum, pluginSettingConfiguration) => ({ + // eslint-disable-next-line unicorn/no-array-reduce + .reduce((accum, pluginSettingConfiguration) => { + return { ...accum, [pluginSettingConfiguration.category]: [ ...(accum[pluginSettingConfiguration.category] || []), { ...pluginSettingConfiguration }, ], - }), - {}, - ); + }; + }, {}); return Object.entries(settingsSortedByCategories) - .map(([category, settings]) => ({ - category: this._categories.get(String(category)), - settings, - })) + .map(([category, settings]) => { + return { + category: this._categories.get(String(category)), + settings, + }; + }) .filter(categoryEntry => categoryEntry.settings.length); } } diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js index 93ac8c3eab..3d3a15b048 100644 --- a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/index.js @@ -1,3 +1,3 @@ -export { EuiSuggestInput } from './suggest_input'; +export { EuiSuggestInput } from './suggest-input'; export { EuiSuggest } from './suggest'; diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js similarity index 87% rename from plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js rename to plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js index 3cb28b935f..36b4c45eaf 100644 --- a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest_input.js +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js @@ -3,13 +3,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { - EuiFilterButton, EuiFieldText, EuiToolTip, EuiIcon, - EuiPopover, + EuiInputPopover, } from '@elastic/eui'; -import { EuiInputPopover } from '@elastic/eui'; const statusMap = { unsaved: { @@ -34,13 +32,13 @@ export class EuiSuggestInput extends Component { isPopoverOpen: false, }; - onFieldChange = e => { + onFieldChange(event) { this.setState({ - value: e.target.value, - isPopoverOpen: e.target.value !== '' ? true : false, + value: event.target.value, + isPopoverOpen: event.target.value === '' ? false : true, }); - this.props.sendValue(e.target.value); - }; + this.props.sendValue(event.target.value); + } render() { const { @@ -56,7 +54,6 @@ export class EuiSuggestInput extends Component { disableFocusTrap = false, ...rest } = this.props; - let icon; let color; @@ -64,11 +61,10 @@ export class EuiSuggestInput extends Component { icon = statusMap[status].icon; color = statusMap[status].color; } - const classes = classNames('euiSuggestInput', className); + const classes = classNames('euiSuggestInput', className); // EuiFieldText's append accepts an array of elements so start by creating an empty array const appendArray = []; - const statusElement = (status === 'saved' || status === 'unsaved') && ( { + getValue(val) { this.setState({ value: val, }); - }; + } - onChange = e => { - this.props.onInputChange(e.target.value); - }; + onChange(event) { + this.props.onInputChange(event.target.value); + } render() { const { onItemClick, - onInputChange, status, append, tooltipContent, suggestions, ...rest } = this.props; - const suggestionList = suggestions.map((item, index) => ( )); - const suggestInput = ( ); + return
{suggestInput}
; } } diff --git a/plugins/wazuh-core/public/components/search-bar/index.test.tsx b/plugins/wazuh-core/public/components/search-bar/index.test.tsx index 9e847941e6..9500613ee8 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { render } from '@testing-library/react'; import { SearchBar } from './index'; diff --git a/plugins/wazuh-core/public/components/search-bar/index.tsx b/plugins/wazuh-core/public/components/search-bar/index.tsx index 71c56447ce..d10d2ef2a1 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.tsx @@ -12,11 +12,11 @@ import { EuiFlexItem, // EuiSuggest, } from '@elastic/eui'; +import { isEqual } from 'lodash'; +import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; import { EuiSuggest } from './components/eui-suggest'; import { searchBarQueryLanguages } from './query-language'; -import _ from 'lodash'; import { ISearchBarModeWQL } from './query-language/wql'; -import { SEARCH_BAR_DEBOUNCE_UPDATE_TIME } from '../../../common/constants'; export interface SearchBarProps { defaultMode?: string; @@ -68,6 +68,7 @@ export const SearchBar = ({ const debounceUpdateSearchBarTimer = useRef(); // Handler when searching + // eslint-disable-next-line @typescript-eslint/naming-convention const _onSearch = (output: any) => { // TODO: fix when searching onSearch(output); @@ -92,8 +93,10 @@ export const SearchBar = ({ useEffect(() => { // React to external changes and set the internal input text. Use the `transformInput` of // the query language in use - rest.input && - searchBarQueryLanguages[queryLanguage.id]?.transformInput && + if ( + rest.input && + searchBarQueryLanguages[queryLanguage.id]?.transformInput + ) { setInput( searchBarQueryLanguages[queryLanguage.id]?.transformInput?.( rest.input, @@ -103,13 +106,16 @@ export const SearchBar = ({ }, ), ); + } }, [rest.input]); useEffect(() => { (async () => { // Set the query language output - debounceUpdateSearchBarTimer.current && + if (debounceUpdateSearchBarTimer.current) { clearTimeout(debounceUpdateSearchBarTimer.current); + } + // Debounce the updating of the search bar state debounceUpdateSearchBarTimer.current = setTimeout(async () => { const queryLanguageOutput = await searchBarQueryLanguages[ @@ -120,11 +126,13 @@ export const SearchBar = ({ closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), openSuggestionPopover: () => setIsOpenSuggestionPopover(true), setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => ({ - ...state, - configuration: - configuration?.(state.configuration) || configuration, - })), + setQueryLanguage(state => { + return { + ...state, + configuration: + configuration?.(state.configuration) || configuration, + }; + }), setQueryLanguageOutput: setQueryLanguageOutputRun, inputRef, queryLanguage: { @@ -132,6 +140,7 @@ export const SearchBar = ({ parameters: selectedQueryLanguageParameters, }, }); + queryLanguageOutputRunPreviousOutput.current = { ...queryLanguageOutputRun.output, }; @@ -141,18 +150,20 @@ export const SearchBar = ({ }, [input, queryLanguage, selectedQueryLanguageParameters?.options]); useEffect(() => { - onChange && + if ( + onChange && // Ensure the previous output is different to the new one - !_.isEqual( + !isEqual( queryLanguageOutputRun.output, queryLanguageOutputRunPreviousOutput.current, - ) && + ) + ) { onChange(queryLanguageOutputRun.output); + } }, [queryLanguageOutputRun.output]); const onQueryLanguagePopoverSwitch = () => setIsOpenPopoverQueryLanguage(state => !state); - const searchBar = ( <> ({ - value: id, - text: searchBarQueryLanguages[id].label, - }))} + options={modes.map(({ id }) => { + return { + value: id, + text: searchBarQueryLanguages[id].label, + }; + })} value={queryLanguage.id} onChange={( event: React.ChangeEvent, ) => { const queryLanguageID: string = event.target.value; + setQueryLanguage({ id: queryLanguageID, configuration: @@ -246,6 +260,7 @@ export const SearchBar = ({ /> ); + return rest.buttonsRender || queryLanguageOutputRun.filterButtons ? ( {searchBar} diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx index 3c6a57caf3..a2a726cd7f 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx @@ -1,7 +1,8 @@ -import { AQL, getSuggestions, tokenizer } from './aql'; +/* eslint-disable @typescript-eslint/naming-convention */ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SearchBar } from '../index'; +import { AQL, getSuggestions, tokenizer } from './aql'; describe('SearchBar component', () => { const componentProps = { @@ -12,9 +13,11 @@ describe('SearchBar component', () => { id: AQL.id, implicitQuery: 'id!=000;', suggestions: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars field(currentValue) { return []; }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars value(currentValue, { previousField }) { return []; }, @@ -34,6 +37,7 @@ describe('SearchBar component', () => { const elementImplicitQuery = wrapper.container.querySelector( '.euiCodeBlock__code', ); + expect(elementImplicitQuery?.innerHTML).toEqual('id!=000;'); expect(wrapper.container).toMatchSnapshot(); }); @@ -91,31 +95,41 @@ describe('Query language - AQL', () => { await getSuggestions(tokenizer(input), { id: 'aql', suggestions: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars field(currentValue) { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => ({ - type: 'field', - label, - description, - })); + ].map(({ label, description }) => { + return { + type: 'field', + label, + description, + }; + }); }, + // eslint-disable-next-line default-param-last value(currentValue = '', { previousField }) { switch (previousField) { - case 'field': + case 'field': { return ['value', 'value2', 'value3', 'value4'] .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - case 'field2': + .map(value => { + return { type: 'value', label: value }; + }); + } + + case 'field2': { return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - default: + .map(value => { + return { type: 'value', label: value }; + }); + } + + default: { return []; - break; + } } }, }, @@ -157,7 +171,6 @@ describe('Query language - AQL', () => { async ({ AQL: currentInput, clikedSuggestion, changedInput }) => { // Mock input let input = currentInput; - const qlOutput = await AQL.run(input, { setInput: (value: string): void => { input = value; @@ -172,6 +185,7 @@ describe('Query language - AQL', () => { }, }, }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); expect(input).toEqual(changedInput); }, diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx index 68d1292a23..66f41586cd 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-await-expression-member */ import React from 'react'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; @@ -8,7 +9,10 @@ type ITokenType = | 'operator_group' | 'value' | 'conjunction'; -type IToken = { type: ITokenType; value: string }; +interface IToken { + type: ITokenType; + value: string; +} type ITokens = IToken[]; /* API Query Language @@ -116,9 +120,9 @@ export function tokenizer(input: string): ITokens { // and added the optional operator to allow matching the entities when the query is not // completed. This helps to tokenize the query and manage when the input is not completed. // A ( character. - '(?\\()?' + + String.raw`(?\()?` + // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find + String.raw`(?[\w.]+)?` + // Added an optional find // Operator: looks for '=', '!=', '<', '>' or '~'. // This seems to be a bug because is not searching the literal valid operators. // I guess the operator is validated after the regular expression matches @@ -126,29 +130,29 @@ export function tokenizer(input: string): ITokens { language.tokens.operator_compare.literal, )}]{1,2})?` + // Added an optional find // Value: A string. - '(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)?' + // Added an optional find + String.raw`(?(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + + String.raw`(?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]+)` + + String.raw`(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]*)\))*)+)?` + // Added an optional find // A ) character. - '(?\\))?' + + String.raw`(?\))?` + `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, 'g', ); - return [...input.matchAll(re)] - .map(({ groups }) => - Object.entries(groups).map(([key, value]) => ({ + return [...input.matchAll(re)].flatMap(({ groups }) => + Object.entries(groups).map(([key, value]) => { + return { type: key.startsWith('operator_group') ? 'operator_group' : key, value, - })), - ) - .flat(); + }; + }), + ); } -type QLOptionSuggestionEntityItem = { +interface QLOptionSuggestionEntityItem { description?: string; label: string; -}; +} type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { type: @@ -171,12 +175,12 @@ type QLOptionSuggestionHandler = ( }: { previousField: string; previousOperatorCompare: string }, ) => Promise; -type optionsQL = { +interface OptionsQL { suggestions: { field: QLOptionSuggestionHandler; value: QLOptionSuggestionHandler; }; -}; +} /** * Get the last token with value @@ -186,9 +190,10 @@ type optionsQL = { */ function getLastTokenWithValue(tokens: ITokens): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find(({ value }) => value); + return tokenFound; } @@ -204,11 +209,12 @@ function getLastTokenWithValueByType( ): IToken | undefined { // Find the last token by type // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find( ({ type, value }) => type === tokenType && value, ); + return tokenFound; } @@ -221,9 +227,9 @@ function getLastTokenWithValueByType( */ export async function getSuggestions( tokens: ITokens, - options: optionsQL, + options: OptionsQL, ): Promise { - if (!tokens.length) { + if (tokens.length === 0) { return []; } @@ -234,7 +240,9 @@ export async function getSuggestions( if (!lastToken?.type) { return [ // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ...(await options.suggestions.field()).map((element, index, array) => + mapSuggestionCreatorField(element, index, array), + ), { type: 'operator_group', label: '(', @@ -244,7 +252,7 @@ export async function getSuggestions( } switch (lastToken.type) { - case 'field': + case 'field': { return [ // fields that starts with the input but is not equals ...(await options.suggestions.field()) @@ -252,25 +260,28 @@ export async function getSuggestions( ({ label }) => label.startsWith(lastToken.value) && label !== lastToken.value, ) - .map(mapSuggestionCreatorField), + .map((element, index, array) => + mapSuggestionCreatorField(element, index, array), + ), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, ) - ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ + ? Object.keys(language.tokens.operator_compare.literal).map( + operator => { + return { type: 'operator_compare', label: operator, description: language.tokens.operator_compare.literal[operator], - }), - ), - ] + }; + }, + ) : []), ]; - break; - case 'operator_compare': + } + + case 'operator_compare': { return [ ...Object.keys(language.tokens.operator_compare.literal) .filter( @@ -278,30 +289,33 @@ export async function getSuggestions( operator.startsWith(lastToken.value) && operator !== lastToken.value, ) - .map(operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - })), - ...(Object.keys(language.tokens.operator_compare.literal).some( - operator => operator === lastToken.value, + .map(operator => { + return { + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + }; + }), + ...(Object.keys(language.tokens.operator_compare.literal).includes( + lastToken.value, ) - ? [ - ...( - await options.suggestions.value(undefined, { - previousField: getLastTokenWithValueByType(tokens, 'field')! - .value, - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, - }) - ).map(mapSuggestionCreatorValue), - ] + ? ( + await options.suggestions.value(undefined, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + previousField: getLastTokenWithValueByType(tokens, 'field')! + .value, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + previousOperatorCompare: getLastTokenWithValueByType( + tokens, + 'operator_compare', + )!.value, + }) + ).map(element => mapSuggestionCreatorValue(element)) : []), ]; - break; - case 'value': + } + + case 'value': { return [ ...(lastToken.value ? [ @@ -314,19 +328,23 @@ export async function getSuggestions( : []), ...( await options.suggestions.value(lastToken.value, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion previousField: getLastTokenWithValueByType(tokens, 'field')!.value, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion previousOperatorCompare: getLastTokenWithValueByType( tokens, 'operator_compare', )!.value, }) - ).map(mapSuggestionCreatorValue), + ).map(element => mapSuggestionCreatorValue(element)), ...Object.entries(language.tokens.conjunction.literal).map( - ([conjunction, description]) => ({ - type: 'conjunction', - label: conjunction, - description, - }), + ([conjunction, description]) => { + return { + type: 'conjunction', + label: conjunction, + description, + }; + }, ), { type: 'operator_group', @@ -334,8 +352,9 @@ export async function getSuggestions( description: language.tokens.operator_group.literal[')'], }, ]; - break; - case 'conjunction': + } + + case 'conjunction': { return [ ...Object.keys(language.tokens.conjunction.literal) .filter( @@ -343,20 +362,20 @@ export async function getSuggestions( conjunction.startsWith(lastToken.value) && conjunction !== lastToken.value, ) - .map(conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - })), + .map(conjunction => { + return { + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }; + }), // fields if the input field is exact - ...(Object.keys(language.tokens.conjunction.literal).some( - conjunction => conjunction === lastToken.value, + ...(Object.keys(language.tokens.conjunction.literal).includes( + lastToken.value, ) - ? [ - ...(await options.suggestions.field()).map( - mapSuggestionCreatorField, - ), - ] + ? (await options.suggestions.field()).map(element => + mapSuggestionCreatorField(element), + ) : []), { type: 'operator_group', @@ -364,29 +383,35 @@ export async function getSuggestions( description: language.tokens.operator_group.literal['('], }, ]; - break; - case 'operator_group': + } + + case 'operator_group': { if (lastToken.value === '(') { - return [ + return ( // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ]; + (await options.suggestions.field()).map(element => + mapSuggestionCreatorField(element), + ) + ); } else if (lastToken.value === ')') { - return [ + return ( // conjunction - ...Object.keys(language.tokens.conjunction.literal).map( - conjunction => ({ + Object.keys(language.tokens.conjunction.literal).map(conjunction => { + return { type: 'conjunction', label: conjunction, description: language.tokens.conjunction.literal[conjunction], - }), - ), - ]; + }; + }) + ); } + break; - default: + } + + default: { return []; - break; + } } return []; @@ -401,6 +426,7 @@ export function transformSuggestionToEuiSuggestItem( suggestion: QLOptionSuggestionEntityItemTyped, ): SuggestItem { const { type, ...rest } = suggestion; + return { type: { ...suggestionMappingLanguageTokenType[type] }, ...rest, @@ -415,7 +441,9 @@ export function transformSuggestionToEuiSuggestItem( function transformSuggestionsToEuiSuggestItem( suggestions: QLOptionSuggestionEntityItemTyped[], ): SuggestItem[] { - return suggestions.map(transformSuggestionToEuiSuggestItem); + return suggestions.map(element => + transformSuggestionToEuiSuggestItem(element), + ); } /** @@ -427,13 +455,16 @@ function getOutput(input: string, options: { implicitQuery?: string } = {}) { const unifiedQuery = `${options?.implicitQuery ?? ''}${ options?.implicitQuery ? `(${input})` : input }`; + return { + // eslint-disable-next-line no-use-before-define language: AQL.id, query: unifiedQuery, unifiedQuery, }; } +// eslint-disable-next-line @typescript-eslint/naming-convention export const AQL = { id: 'aql', label: 'AQL', @@ -466,6 +497,7 @@ export const AQL = { } else { // When the clicked item has another iconType const lastToken: IToken = getLastTokenWithValue(tokens); + // if the clicked suggestion is of same type of last token if ( lastToken && @@ -500,11 +532,13 @@ export const AQL = { button={ - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: - !state.isOpenPopoverImplicitFilter, - })) + params.setQueryLanguageConfiguration(state => { + return { + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + }; + }) } iconType='filter' > @@ -517,10 +551,12 @@ export const AQL = { params.queryLanguage.configuration.isOpenPopoverImplicitFilter } closePopover={() => - params.setQueryLanguageConfiguration(state => ({ - ...state, - isOpenPopoverImplicitFilter: false, - })) + params.setQueryLanguageConfiguration(state => { + return { + ...state, + isOpenPopoverImplicitFilter: false, + }; + }) } > diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index a803a79ecf..c827524f24 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -1,12 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { SearchBar } from '../index'; import { getSuggestions, tokenizer, transformSpecificQLToUnifiedQL, WQL, } from './wql'; -import React from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { SearchBar } from '../index'; describe('SearchBar component', () => { const componentProps = { @@ -22,9 +23,11 @@ describe('SearchBar component', () => { }, }, suggestions: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars field(currentValue) { return []; }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars value(currentValue, { field }) { return []; }, @@ -46,31 +49,26 @@ describe('SearchBar component', () => { }); }); +// Tokenize the input +function tokenCreator({ type, value, formattedValue }) { + return { type, value, ...(formattedValue ? { formattedValue } : {}) }; +} + /* eslint-disable max-len */ describe('Query language - WQL', () => { - // Tokenize the input - function tokenCreator({ type, value, formattedValue }) { - return { type, value, ...(formattedValue ? { formattedValue } : {}) }; - } - const t = { - opGroup: (value = undefined) => - tokenCreator({ type: 'operator_group', value }), - opCompare: (value = undefined) => - tokenCreator({ type: 'operator_compare', value }), - field: (value = undefined) => tokenCreator({ type: 'field', value }), - value: (value = undefined, formattedValue = undefined) => + opGroup: (value?) => tokenCreator({ type: 'operator_group', value }), + opCompare: (value?) => tokenCreator({ type: 'operator_compare', value }), + field: (value?) => tokenCreator({ type: 'field', value }), + value: (value?, formattedValue?) => tokenCreator({ type: 'value', value, formattedValue: formattedValue ?? value, }), - whitespace: (value = undefined) => - tokenCreator({ type: 'whitespace', value }), - conjunction: (value = undefined) => - tokenCreator({ type: 'conjunction', value }), + whitespace: (value?) => tokenCreator({ type: 'whitespace', value }), + conjunction: (value?) => tokenCreator({ type: 'conjunction', value }), }; - // Token undefined const tu = { opGroup: tokenCreator({ type: 'operator_group', value: undefined }), @@ -84,7 +82,6 @@ describe('Query language - WQL', () => { }), conjunction: tokenCreator({ type: 'conjunction', value: undefined }), }; - const tuBlankSerie = [ tu.opGroup, tu.whitespace, @@ -153,31 +150,41 @@ describe('Query language - WQL', () => { await getSuggestions(tokenizer(input), { id: 'aql', suggestions: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars field(currentValue) { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => ({ - type: 'field', - label, - description, - })); + ].map(({ label, description }) => { + return { + type: 'field', + label, + description, + }; + }); }, + // eslint-disable-next-line default-param-last value(currentValue = '', { field }) { switch (field) { - case 'field': + case 'field': { return ['value', 'value2', 'value3', 'value4'] .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - case 'field2': + .map(value => { + return { type: 'value', label: value }; + }); + } + + case 'field2': { return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] .filter(value => value.startsWith(currentValue)) - .map(value => ({ type: 'value', label: value })); - break; - default: + .map(value => { + return { type: 'value', label: value }; + }); + } + + default: { return []; - break; + } } }, }, @@ -208,8 +215,8 @@ describe('Query language - WQL', () => { ${'field="value > value2"'} | ${'field=value > value2'} ${'field="value < value2"'} | ${'field=value < value2'} ${'field="value ~ value2"'} | ${'field=value ~ value2' /** ~ character is not supported as value in the q query parameter */} - ${'field="custom \\"value"'} | ${'field=custom "value'} - ${'field="custom \\"value\\""'} | ${'field=custom "value"'} + ${String.raw`field="custom \"value"`} | ${'field=custom "value'} + ${String.raw`field="custom \"value\""`} | ${'field=custom "value"'} ${'field=value and'} | ${'field=value;'} ${'field="custom value" and'} | ${'field=custom value;'} ${'(field=value'} | ${'(field=value'} @@ -271,7 +278,7 @@ describe('Query language - WQL', () => { ${'field=value and '} | ${{ type: { iconType: 'kqlField', color: 'tint4' }, label: 'field2' }} | ${'field=value and field2'} ${'field=value and field2'} | ${{ type: { iconType: 'kqlOperand', color: 'tint1' }, label: '>' }} | ${'field=value and field2>'} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with spaces' }} | ${'field="with spaces"'} - ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${'field="with \\"spaces"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with "spaces' }} | ${String.raw`field="with \"spaces"`} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with value()' }} | ${'field="with value()"'} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with and value' }} | ${'field="with and value"'} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with or value' }} | ${'field="with or value"'} @@ -280,7 +287,7 @@ describe('Query language - WQL', () => { ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with > value' }} | ${'field="with > value"'} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with < value' }} | ${'field="with < value"'} ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'with ~ value' }} | ${'field="with ~ value"' /** ~ character is not supported as value in the q query parameter */} - ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${'field="\\"value"'} + ${'field='} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: '"value' }} | ${String.raw`field="\"value"`} ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'value' }} | ${'field=value'} ${'field="with spaces"'} | ${{ type: { iconType: 'kqlValue', color: 'tint0' }, label: 'other spaces' }} | ${'field="other spaces"'} ${''} | ${{ type: { iconType: 'tokenDenseVector', color: 'tint3' }, label: '(' }} | ${'('} @@ -305,7 +312,6 @@ describe('Query language - WQL', () => { async ({ WQL: currentInput, clikedSuggestion, changedInput }) => { // Mock input let input = currentInput; - const qlOutput = await WQL.run(input, { setInput: (value: string): void => { input = value; @@ -320,6 +326,7 @@ describe('Query language - WQL', () => { }, }, }); + qlOutput.searchBarProps.onItemClick('')(clikedSuggestion); expect(input).toEqual(changedInput); }, @@ -346,9 +353,9 @@ describe('Query language - WQL', () => { ${'field=value'} | ${'field=value'} ${'field=value;'} | ${'field=value and '} ${'field=value;field2'} | ${'field=value and field2'} - ${'field="'} | ${'field="\\""'} + ${'field="'} | ${String.raw`field="\""`} ${'field=with spaces'} | ${'field="with spaces"'} - ${'field=with "spaces'} | ${'field="with \\"spaces"'} + ${'field=with "spaces'} | ${String.raw`field="with \"spaces"`} ${'field=value ()'} | ${'field="value ()"'} ${'field=with and value'} | ${'field="with and value"'} ${'field=with or value'} | ${'field="with or value"'} @@ -395,50 +402,50 @@ describe('Query language - WQL', () => { // Validate the tokens // Some examples of value tokens are based on this API test: https://github.com/wazuh/wazuh/blob/813595cf58d753c1066c3e7c2018dbb4708df088/framework/wazuh/core/tests/test_utils.py#L987-L1050 it.each` - WQL | validationError - ${''} | ${undefined} - ${'field1'} | ${undefined} - ${'field2'} | ${undefined} - ${'field1='} | ${['The value for field "field1" is missing.']} - ${'field2='} | ${['The value for field "field2" is missing.']} - ${'field='} | ${['"field" is not a valid field.']} - ${'custom='} | ${['"custom" is not a valid field.']} - ${'field1=value'} | ${undefined} - ${'field_not_number=1'} | ${['Numbers are not valid for field_not_number']} - ${'field_not_number=value1'} | ${['Numbers are not valid for field_not_number']} - ${'field2=value'} | ${undefined} - ${'field=value'} | ${['"field" is not a valid field.']} - ${'custom=value'} | ${['"custom" is not a valid field.']} - ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} - ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} - ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} - ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} - ${'field1=value,'} | ${['"value," is not a valid value.']} - ${'field1="Mozilla Firefox 53.0 (x64 en-US)"'} | ${undefined} - ${'field1="[\\"https://example-link@<>=,%?\\"]"'} | ${undefined} - ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} - ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} - ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} - ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} - ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} - ${'field1=value and field'} | ${['"field" is not a valid field.']} - ${'field2=value and field'} | ${['"field" is not a valid field.']} - ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} - ${'('} | ${undefined} - ${'(field'} | ${undefined} - ${'(field='} | ${['"field" is not a valid field.']} - ${'(field=value'} | ${['"field" is not a valid field.']} - ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} - ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} - ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} - ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} - ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} + WQL | validationError + ${''} | ${undefined} + ${'field1'} | ${undefined} + ${'field2'} | ${undefined} + ${'field1='} | ${['The value for field "field1" is missing.']} + ${'field2='} | ${['The value for field "field2" is missing.']} + ${'field='} | ${['"field" is not a valid field.']} + ${'custom='} | ${['"custom" is not a valid field.']} + ${'field1=value'} | ${undefined} + ${'field_not_number=1'} | ${['Numbers are not valid for field_not_number']} + ${'field_not_number=value1'} | ${['Numbers are not valid for field_not_number']} + ${'field2=value'} | ${undefined} + ${'field=value'} | ${['"field" is not a valid field.']} + ${'custom=value'} | ${['"custom" is not a valid field.']} + ${'field1=value!test'} | ${['"value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value&test'} | ${['"value&test" is not a valid value. Invalid characters found: &']} + ${'field1=value!value&test'} | ${['"value!value&test" is not a valid value. Invalid characters found: !&']} + ${'field1=value!value!test'} | ${['"value!value!test" is not a valid value. Invalid characters found: !']} + ${'field1=value!value!t$&st'} | ${['"value!value!t$&st" is not a valid value. Invalid characters found: !$&']} + ${'field1=value,'} | ${['"value," is not a valid value.']} + ${'field1="Mozilla Firefox 53.0 (x64 en-US)"'} | ${undefined} + ${String.raw`field1="[\"https://example-link@<>=,%?\"]"`} | ${undefined} + ${'field1=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field2=value and'} | ${['There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field=value and'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'custom=value and'} | ${['"custom" is not a valid field.', 'There is no whitespace after conjunction "and".', 'There is no sentence after conjunction "and".']} + ${'field1=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field2=value and '} | ${['There is no sentence after conjunction "and".']} + ${'field=value and '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'custom=value and '} | ${['"custom" is not a valid field.', 'There is no sentence after conjunction "and".']} + ${'field1=value and field2'} | ${['The operator for field "field2" is missing.']} + ${'field2=value and field1'} | ${['The operator for field "field1" is missing.']} + ${'field1=value and field'} | ${['"field" is not a valid field.']} + ${'field2=value and field'} | ${['"field" is not a valid field.']} + ${'field=value and custom'} | ${['"field" is not a valid field.', '"custom" is not a valid field.']} + ${'('} | ${undefined} + ${'(field'} | ${undefined} + ${'(field='} | ${['"field" is not a valid field.']} + ${'(field=value'} | ${['"field" is not a valid field.']} + ${'(field=value or'} | ${['"field" is not a valid field.', 'There is no whitespace after conjunction "or".', 'There is no sentence after conjunction "or".']} + ${'(field=value or '} | ${['"field" is not a valid field.', 'There is no sentence after conjunction "or".']} + ${'(field=value or field2'} | ${['"field" is not a valid field.', 'The operator for field "field2" is missing.']} + ${'(field=value or field2>'} | ${['"field" is not a valid field.', 'The value for field "field2" is missing.']} + ${'(field=value or field2>value2'} | ${['"field" is not a valid field.']} `( 'validate the tokens - WQL $WQL => $validationError', async ({ WQL: currentInput, validationError }) => { @@ -448,15 +455,19 @@ describe('Query language - WQL', () => { options: {}, suggestions: { field: () => - ['field1', 'field2', 'field_not_number'].map(label => ({ - label, - })), + ['field1', 'field2', 'field_not_number'].map(label => { + return { + label, + }; + }), value: () => [], }, validate: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars value: (token, { field, operator_compare }) => { if (field === 'field_not_number') { const value = token.formattedValue || token.value; + return /\d/.test(value) ? `Numbers are not valid for ${field}` : undefined; @@ -466,6 +477,7 @@ describe('Query language - WQL', () => { }, }, }); + expect(qlOutput.output.error).toEqual(validationError); }, ); diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 7d139db27b..1c763e6d9d 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -1,14 +1,9 @@ +/* eslint-disable unicorn/no-await-expression-member */ import React from 'react'; -import { - EuiButtonEmpty, - EuiButtonGroup, - EuiPopover, - EuiText, - EuiCode, -} from '@elastic/eui'; -import { tokenizer as tokenizerUQL } from './aql'; +import { EuiButtonGroup } from '@elastic/eui'; import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT } from '../../../../common/constants'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; +import { tokenizer as tokenizerUQL } from './aql'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -23,7 +18,11 @@ type ITokenType = | 'value' | 'conjunction' | 'whitespace'; -type IToken = { type: ITokenType; value: string; formattedValue?: string }; +interface IToken { + type: ITokenType; + value: string; + formattedValue?: string; +} type ITokens = IToken[]; /* API Query Language @@ -78,7 +77,6 @@ const language = { }, }, }; - // Suggestion mapper by language token type const suggestionMappingLanguageTokenType = { field: { iconType: 'kqlField', color: 'tint4' }, @@ -107,7 +105,7 @@ function mapSuggestionCreator(type: ITokenType) { /* WORKAROUND: ensure the label is a string. If it is not a string, an warning is displayed in the console related to prop types */ - ...(typeof label !== 'undefined' ? { label: String(label) } : {}), + ...(label === undefined ? {} : { label: String(label) }), }; }; } @@ -134,9 +132,9 @@ function transformQLConjunction(conjunction: string): string { */ function transformQLValue(value: string): string { // If the value has a whitespace or comma, then - return /[\s|"]/.test(value) + return /[\s"|]/.test(value) ? // Escape the commas (") => (\") and wraps the string with commas ("") - `"${value.replace(/"/, '\\"')}"` + `"${value.replace(/"/, String.raw`\"`)}"` : // Raw value value; } @@ -149,13 +147,13 @@ function transformQLValue(value: string): string { export function tokenizer(input: string): ITokens { const re = new RegExp( // A ( character. - '(?\\()?' + + String.raw`(?\()?` + // Whitespace - '(?\\s+)?' + + String.raw`(?\s+)?` + // Field name: name of the field to look on DB. - '(?[\\w.]+)?' + // Added an optional find + String.raw`(?[\w.]+)?` + // Added an optional find // Whitespace - '(?\\s+)?' + + String.raw`(?\s+)?` + // Operator: looks for '=', '!=', '<', '>' or '~'. // This seems to be a bug because is not searching the literal valid operators. // I guess the operator is validated after the regular expression matches @@ -163,48 +161,48 @@ export function tokenizer(input: string): ITokens { language.tokens.operator_compare.literal, )}]{1,2})?` + // Added an optional find // Whitespace - '(?\\s+)?' + + String.raw`(?\s+)?` + // Value: A string. // Simple value // Quoted ", "value, "value", "escaped \"quote" // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes - '(?(?:(?:[^"\\s]+|(?:"(?:[^"\\\\]|\\\\")*")|(?:"(?:[^"\\\\]|\\\\")*)|")))?' + + String.raw`(?(?:(?:[^"\s]+|(?:"(?:[^"\\]|\\")*")|(?:"(?:[^"\\]|\\")*)|")))?` + // Whitespace - '(?\\s+)?' + + String.raw`(?\s+)?` + // A ) character. - '(?\\))?' + + String.raw`(?\))?` + // Whitespace - '(?\\s+)?' + + String.raw`(?\s+)?` + `(?${Object.keys(language.tokens.conjunction.literal).join( '|', )})?` + // Whitespace - '(?\\s+)?', + String.raw`(?\s+)?`, 'g', ); - return [...input.matchAll(re)] - .map(({ groups }) => - Object.entries(groups).map(([key, value]) => ({ + return [...input.matchAll(re)].flatMap(({ groups }) => + Object.entries(groups).map(([key, value]) => { + return { type: key.startsWith('operator_group') // Transform operator_group group match ? 'operator_group' : key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' - : key, + ? 'whitespace' + : key, value, ...(key === 'value' && - (value && /^"([\s\S]+)"$/.test(value) - ? { formattedValue: value.match(/^"([\s\S]+)"$/)[1] } + (value && /^"([\S\s]+)"$/.test(value) + ? { formattedValue: value.match(/^"([\S\s]+)"$/)[1] } : { formattedValue: value })), - })), - ) - .flat(); + }; + }), + ); } -type QLOptionSuggestionEntityItem = { +interface QLOptionSuggestionEntityItem { description?: string; label: string; -}; +} type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { type: @@ -225,11 +223,11 @@ type QLOptionSuggestionHandler = ( { field, operatorCompare }: { field: string; operatorCompare: string }, ) => Promise; -type OptionsQLImplicitQuery = { +interface OptionsQLImplicitQuery { query: string; conjunction: string; -}; -type OptionsQL = { +} +interface OptionsQL { options?: { implicitQuery?: OptionsQLImplicitQuery; searchTermFields?: string[]; @@ -240,14 +238,15 @@ type OptionsQL = { value: QLOptionSuggestionHandler; }; validate?: { - value?: { - [key: string]: ( + value?: Record< + string, + ( token: IToken, nearTokens: { field: string; operator: string }, - ) => string | undefined; - }; + ) => string | undefined + >; }; -}; +} export interface ISearchBarModeWQL extends OptionsQL { id: 'wql'; @@ -261,11 +260,12 @@ export interface ISearchBarModeWQL extends OptionsQL { */ function getLastTokenDefined(tokens: ITokens): IToken | undefined { // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find( ({ type, value }) => type !== 'whitespace' && value, ); + return tokenFound; } @@ -281,11 +281,12 @@ function getLastTokenDefinedByType( ): IToken | undefined { // Find the last token by type // Reverse the tokens array and use the Array.protorype.find method - const shallowCopyArray = Array.from([...tokens]); + const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find( ({ type, value }) => type === tokenType && value, ); + return tokenFound; } @@ -306,13 +307,17 @@ function getTokenNearTo( tokenFoundShouldHaveValue?: boolean; } = {}, ): IToken | undefined { - const shallowCopyTokens = Array.from([...tokens]); + const shallowCopyTokens = [...tokens]; const computedShallowCopyTokens = mode === 'previous' ? shallowCopyTokens - .slice(0, options?.tokenReferencePosition || tokens.length) + .slice( + 0, + options?.tokenReferencePosition || (tokens.length as number), + ) .reverse() : shallowCopyTokens.slice(options?.tokenReferencePosition || 0); + return computedShallowCopyTokens.find( ({ type, value }) => type === tokenType && (options?.tokenFoundShouldHaveValue ? value : true), @@ -326,9 +331,9 @@ function getTokenNearTo( function getTokenValueRegularExpression() { return new RegExp( // Value: A string. - '^(?(?:(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\/\'"=@%<>{}]*)\\))*' + - '(?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|^[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]+)' + - '(?:\\((?:\\[[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}]*]|[\\[\\]\\w _\\-.:?\\\\/\'"=@%<>{}]*)\\))*)+)$', + String.raw`^(?(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + + String.raw`(?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|^[\[\]\w _\-.:?\\/'"=@%<>{}]+)` + + String.raw`(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]*)\))*)+)$`, ); } @@ -346,6 +351,7 @@ function filterTokenValueSuggestion( ? suggestions .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { const re = getTokenValueRegularExpression(); + return re.test(label); }) .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT) @@ -363,7 +369,7 @@ export async function getSuggestions( tokens: ITokens, options: OptionsQL, ): Promise { - if (!tokens.length) { + if (tokens.length === 0) { return []; } @@ -380,7 +386,9 @@ export async function getSuggestions( description: 'run the search query', }, // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), + ...(await options.suggestions.field()).map((element, index, array) => + mapSuggestionCreatorField(element, index, array), + ), { type: 'operator_group', label: '(', @@ -390,7 +398,7 @@ export async function getSuggestions( } switch (lastToken.type) { - case 'field': + case 'field': { return [ // fields that starts with the input but is not equals ...(await options.suggestions.field()) @@ -398,24 +406,27 @@ export async function getSuggestions( ({ label }) => label.startsWith(lastToken.value) && label !== lastToken.value, ) - .map(mapSuggestionCreatorField), + .map((element, index, array) => + mapSuggestionCreatorField(element, index, array), + ), // operators if the input field is exact ...((await options.suggestions.field()).some( ({ label }) => label === lastToken.value, ) - ? [ - ...Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ + ? Object.keys(language.tokens.operator_compare.literal).map( + operator => { + return { type: 'operator_compare', label: operator, description: language.tokens.operator_compare.literal[operator], - }), - ), - ] + }; + }, + ) : []), ]; - break; + } + case 'operator_compare': { const field = getLastTokenDefinedByType(tokens, 'field')?.value; const operatorCompare = getLastTokenDefinedByType( @@ -436,33 +447,35 @@ export async function getSuggestions( operator.startsWith(lastToken.value) && operator !== lastToken.value, ) - .map(operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - })), - ...(Object.keys(language.tokens.operator_compare.literal).some( - operator => operator === lastToken.value, + .map(operator => { + return { + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + }; + }), + ...(Object.keys(language.tokens.operator_compare.literal).includes( + lastToken.value, ) - ? [ - /* + ? /* WORKAROUND: When getting suggestions for the distinct values for any field, the API could reply some values that doesn't match the expected regular expression. If the value is invalid, a validation message is displayed and avoid the search can be run. The goal of this filter is that the suggested values can be used to search. This causes some values could not be displayed as suggestions. */ - ...filterTokenValueSuggestion( - await options.suggestions.value(undefined, { - field, - operatorCompare, - }), - ).map(mapSuggestionCreatorValue), - ] + filterTokenValueSuggestion( + await options.suggestions.value(undefined, { + field, + operatorCompare, + }), + ).map((element, index, array) => + mapSuggestionCreatorValue(element, index, array), + ) : []), ]; - break; } + case 'value': { const field = getLastTokenDefinedByType(tokens, 'field')?.value; const operatorCompare = getLastTokenDefinedByType( @@ -498,13 +511,17 @@ export async function getSuggestions( field, operatorCompare, }), - ).map(mapSuggestionCreatorValue), + ).map((element, index, array) => + mapSuggestionCreatorValue(element, index, array), + ), ...Object.entries(language.tokens.conjunction.literal).map( - ([conjunction, description]) => ({ - type: 'conjunction', - label: conjunction, - description, - }), + ([conjunction, description]) => { + return { + type: 'conjunction', + label: conjunction, + description, + }; + }, ), { type: 'operator_group', @@ -512,9 +529,9 @@ export async function getSuggestions( description: language.tokens.operator_group.literal[')'], }, ]; - break; } - case 'conjunction': + + case 'conjunction': { return [ ...Object.keys(language.tokens.conjunction.literal) .filter( @@ -522,20 +539,20 @@ export async function getSuggestions( conjunction.startsWith(lastToken.value) && conjunction !== lastToken.value, ) - .map(conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - })), + .map(conjunction => { + return { + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + }; + }), // fields if the input field is exact - ...(Object.keys(language.tokens.conjunction.literal).some( - conjunction => conjunction === lastToken.value, + ...(Object.keys(language.tokens.conjunction.literal).includes( + lastToken.value, ) - ? [ - ...(await options.suggestions.field()).map( - mapSuggestionCreatorField, - ), - ] + ? (await options.suggestions.field()).map((element, index, array) => + mapSuggestionCreatorField(element, index, array), + ) : []), { type: 'operator_group', @@ -543,29 +560,35 @@ export async function getSuggestions( description: language.tokens.operator_group.literal['('], }, ]; - break; - case 'operator_group': + } + + case 'operator_group': { if (lastToken.value === '(') { - return [ + return ( // fields - ...(await options.suggestions.field()).map(mapSuggestionCreatorField), - ]; + (await options.suggestions.field()).map(element => + mapSuggestionCreatorField(element), + ) + ); } else if (lastToken.value === ')') { - return [ + return ( // conjunction - ...Object.keys(language.tokens.conjunction.literal).map( - conjunction => ({ + Object.keys(language.tokens.conjunction.literal).map(conjunction => { + return { type: 'conjunction', label: conjunction, description: language.tokens.conjunction.literal[conjunction], - }), - ), - ]; + }; + }) + ); } + break; - default: + } + + default: { return []; - break; + } } return []; @@ -580,6 +603,7 @@ export function transformSuggestionToEuiSuggestItem( suggestion: QLOptionSuggestionEntityItemTyped, ): SuggestItem { const { type, ...rest } = suggestion; + return { type: { ...suggestionMappingLanguageTokenType[type] }, ...rest, @@ -594,7 +618,9 @@ export function transformSuggestionToEuiSuggestItem( function transformSuggestionsToEuiSuggestItem( suggestions: QLOptionSuggestionEntityItemTyped[], ): SuggestItem[] { - return suggestions.map(transformSuggestionToEuiSuggestItem); + return suggestions.map(element => + transformSuggestionToEuiSuggestItem(element), + ); } /** @@ -604,19 +630,22 @@ function transformSuggestionsToEuiSuggestItem( */ export function transformUQLToQL(input: string) { const tokens = tokenizerUQL(input); + return tokens .filter(({ value }) => value) .map(({ type, value }) => { switch (type) { - case 'conjunction': + case 'conjunction': { return transformQLConjunction(value); - break; - case 'value': + } + + case 'value': { return transformQLValue(value); - break; - default: + } + + default: { return value; - break; + } } }) .join(''); @@ -665,20 +694,22 @@ export function transformSpecificQLToUnifiedQL( // by double quotation marks (") // WARN: This could cause a problem with value that contains this sequence \" const extractedValue = - formattedValue !== value - ? formattedValue.replace(/\\"/g, '"') - : formattedValue; + formattedValue === value + ? formattedValue + : formattedValue.replaceAll(String.raw`\"`, '"'); + return extractedValue || value; - break; } - case 'conjunction': + + case 'conjunction': { return value === 'and' ? language.equivalencesToUQL.conjunction.literal['and'] : language.equivalencesToUQL.conjunction.literal['or']; - break; - default: + } + + default: { return value; - break; + } } }) .join(''); @@ -693,14 +724,12 @@ function getOutput(input: string, options: OptionsQL) { // Implicit query const implicitQueryAsUQL = options?.options?.implicitQuery?.query ?? ''; const implicitQueryAsQL = transformUQLToQL(implicitQueryAsUQL); - // Implicit query conjunction const implicitQueryConjunctionAsUQL = options?.options?.implicitQuery?.conjunction ?? ''; const implicitQueryConjunctionAsQL = transformUQLToQL( implicitQueryConjunctionAsUQL, ); - // User input query const inputQueryAsQL = input; const inputQueryAsUQL = transformSpecificQLToUnifiedQL( @@ -709,6 +738,7 @@ function getOutput(input: string, options: OptionsQL) { ); return { + // eslint-disable-next-line no-use-before-define language: WQL.id, apiQuery: { q: [ @@ -738,7 +768,6 @@ function getOutput(input: string, options: OptionsQL) { */ function validateTokenValue(token: IToken): string | undefined { const re = getTokenValueRegularExpression(); - const value = token.formattedValue ?? token.value; const match = value.match(re); @@ -746,19 +775,18 @@ function validateTokenValue(token: IToken): string | undefined { return undefined; } - const invalidCharacters: string[] = token.value - .split('') + const invalidCharacters: string[] = [...token.value] .filter((value, index, array) => array.indexOf(value) === index) .filter( character => - !new RegExp('[\\[\\]\\w _\\-.,:?\\\\/\'"=@%<>{}\\(\\)]').test( + !new RegExp(String.raw`[\[\]\w _\-.,:?\\/'"=@%<>{}\(\)]`).test( character, ), ); return [ `"${value}" is not a valid value.`, - ...(invalidCharacters.length + ...(invalidCharacters.length > 0 ? [`Invalid characters found: ${invalidCharacters.join('')}`] : []), ].join(' '); @@ -768,6 +796,7 @@ type ITokenValidator = ( tokenValue: IToken, proximityTokens: any, ) => string | undefined; + /** * Validate the tokens while the user is building the query * @param tokens @@ -796,10 +825,12 @@ function validatePartial( tokenFoundShouldHaveValue: true, }, ); + return tokenOperatorNearToField ? validate.field(token) : undefined; } + // Check if the value is allowed if (token.type === 'value') { const tokenFieldNearToValue = getTokenNearTo( @@ -820,6 +851,7 @@ function validatePartial( tokenFoundShouldHaveValue: true, }, ); + return ( validateTokenValue(token) || (tokenFieldNearToValue && @@ -834,7 +866,7 @@ function validatePartial( } } }) - .filter(t => typeof t !== 'undefined') + .filter(t => t !== undefined) .join('\n') || undefined ); } @@ -854,6 +886,7 @@ function validate( const errors = tokens .map((token: IToken, index) => { const errors = []; + if (token.value) { if (token.type === 'field') { const tokenOperatorNearToField = getTokenNearTo( @@ -874,6 +907,7 @@ function validate( tokenFoundShouldHaveValue: true, }, ); + if (validate.field(token)) { errors.push(`"${token.value}" is not a valid field.`); } else if (!tokenOperatorNearToField) { @@ -884,6 +918,7 @@ function validate( errors.push(`The value for field "${token.value}" is missing.`); } } + // Check if the value is allowed if (token.type === 'value') { const tokenFieldNearToValue = getTokenNearTo( @@ -915,7 +950,9 @@ function validate( }) : undefined); - validationError && errors.push(validationError); + if (validationError) { + errors.push(validationError); + } } // Check if the value is allowed @@ -935,25 +972,33 @@ function validate( tokenFoundShouldHaveValue: true, }, ); - !tokenWhitespaceNearToFieldNext?.value?.length && + + if (!tokenWhitespaceNearToFieldNext?.value?.length) { errors.push( `There is no whitespace after conjunction "${token.value}".`, ); - !tokenFieldNearToFieldNext?.value?.length && + } + + if (!tokenFieldNearToFieldNext?.value?.length) { errors.push( `There is no sentence after conjunction "${token.value}".`, ); + } } } - return errors.length ? errors : undefined; + + return errors.length > 0 ? errors : undefined; }) - .filter(errors => errors) + .filter(Boolean) .flat(); - return errors.length ? errors : undefined; + + return errors.length > 0 ? errors : undefined; } + return undefined; } +// eslint-disable-next-line @typescript-eslint/naming-convention export const WQL = { id: 'wql', label: 'WQL', @@ -970,8 +1015,8 @@ export const WQL = { async run(input, params) { // Get the tokens from the input const tokens: ITokens = tokenizer(input); - // Get the implicit query as query language syntax + // eslint-disable-next-line @typescript-eslint/no-unused-vars const implicitQueryAsQL = params.queryLanguage.parameters?.options ?.implicitQuery ? transformUQLToQL( @@ -979,12 +1024,10 @@ export const WQL = { params.queryLanguage.parameters.options.implicitQuery.conjunction, ) : ''; - const fieldsSuggestion: string[] = await params.queryLanguage.parameters.suggestions .field() .map(({ label }) => label); - const validators = { field: ({ value }) => fieldsSuggestion.includes(value) @@ -996,12 +1039,9 @@ export const WQL = { } : {}), }; - // Validate the user input const validationPartial = validatePartial(tokens, validators); - const validationStrict = validate(tokens, validators); - // Get the output of query language const output = { ...getOutput(input, params.queryLanguage.parameters), @@ -1010,20 +1050,24 @@ export const WQL = { const onSearch = output => { if (output?.error) { - params.setQueryLanguageOutput(state => ({ - ...state, - searchBarProps: { - ...state.searchBarProps, - suggestions: transformSuggestionsToEuiSuggestItem( - output.error.map(error => ({ - type: 'validation_error', - label: 'Invalid', - description: error, - })), - ), - isInvalid: true, - }, - })); + params.setQueryLanguageOutput(state => { + return { + ...state, + searchBarProps: { + ...state.searchBarProps, + suggestions: transformSuggestionsToEuiSuggestItem( + output.error.map(error => { + return { + type: 'validation_error', + label: 'Invalid', + description: error, + }; + }), + ), + isInvalid: true, + }, + }; + }); } else { params.onSearch(output); } @@ -1036,7 +1080,9 @@ export const WQL = { name='textAlign' buttonSize='m' options={params.queryLanguage.parameters?.options?.filterButtons.map( - ({ id, label }) => ({ id, label }), + ({ id, label }) => { + return { id, label }; + }, )} idToSelectedMap={{}} type='multi' @@ -1045,8 +1091,10 @@ export const WQL = { params.queryLanguage.parameters?.options?.filterButtons.find( ({ id: buttonID }) => buttonID === id, ); + if (buttonParams) { params.setInput(buttonParams.input); + const output = { ...getOutput( buttonParams.input, @@ -1054,6 +1102,7 @@ export const WQL = { ), error: undefined, }; + params.onSearch(output); } }} @@ -1079,14 +1128,13 @@ export const WQL = { if (item.type.iconType === 'alert') { return; } + // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action // Get the tokens from the input const tokens: ITokens = tokenizer(currentInput); - const validationStrict = validate(tokens, validators); - // Get the output of query language const output = { ...getOutput(currentInput, params.queryLanguage.parameters), @@ -1097,6 +1145,7 @@ export const WQL = { } else { // When the clicked item has another iconType const lastToken: IToken | undefined = getLastTokenDefined(tokens); + // if the clicked suggestion is of same type of last token if ( lastToken && @@ -1113,6 +1162,7 @@ export const WQL = { } else { // add a whitespace for conjunction // add a whitespace for grouping operator ) + // eslint-disable-next-line @typescript-eslint/no-unused-expressions !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || @@ -1139,18 +1189,21 @@ export const WQL = { }); // add a whitespace for conjunction - item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType && + if ( + item.type.iconType === + suggestionMappingLanguageTokenType.conjunction.iconType + ) { tokens.push({ type: 'whitespace', value: ' ', }); + } } // Change the input params.setInput( tokens - .filter(value => value) // Ensure the input is rebuilt using tokens with value. + .filter(Boolean) // Ensure the input is rebuilt using tokens with value. // The input tokenization can contain tokens with no value due to the used // regular expression. .map(({ value }) => value) @@ -1171,9 +1224,7 @@ export const WQL = { // Get the tokens from the input const input = event.currentTarget.value; const tokens: ITokens = tokenizer(input); - const validationStrict = validate(tokens, validators); - // Get the output of query language const output = { ...getOutput(input, params.queryLanguage.parameters), diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx index 5ab60623f6..474b6aebc8 100644 --- a/plugins/wazuh-core/public/components/table-data/table-data.tsx +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -23,12 +23,29 @@ import { EuiCheckboxGroup, EuiBasicTable, } from '@elastic/eui'; -import { useStateStorage } from '../../hooks'; import { isEqual } from 'lodash'; +import { useStateStorage } from '../../hooks'; import { TableDataProps } from './types'; const getColumMetaField = item => item.field || item.name; +const TableDataRenderElement = ({ + render, + ...rest +}: { + render: ((params: any) => React.JSXElement) | React.JSXElement; +}) => { + if (typeof render === 'function') { + return {render(rest)}; + } + + if (typeof render === 'object') { + return {render}; + } + + return null; +}; + export function TableData({ preActionButtons, postActionButtons, @@ -55,10 +72,8 @@ export function TableData({ }); const [refresh, setRefresh] = useState(rest.reload || 0); const [fetchContext, setFetchContext] = useState(rest.fetchContext || {}); - const isMounted = useRef(false); const tableRef = useRef(); - const [selectedFields, setSelectedFields] = useStateStorage( rest.tableColumns.some(({ show }) => show) ? rest.tableColumns.filter(({ show }) => show).map(({ field }) => field) @@ -77,6 +92,7 @@ export function TableData({ sorting, fetchContext, }; + setIsLoading(true); rest?.onFetchContextChange?.(enhancedFetchContext); @@ -87,7 +103,11 @@ export function TableData({ setTotalItems(totalItems); const result = { - items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + items: rest.mapResponseItem + ? items.map((element, index, array) => + rest.mapResponseItem(element, index, array), + ) + : items, totalItems, }; @@ -95,6 +115,7 @@ export function TableData({ } catch (error) { setIsLoading(false); setTotalItems(0); + if (error?.name) { /* This replaces the error name. The intention is that an AxiosError doesn't appear in the toast message. @@ -103,6 +124,7 @@ export function TableData({ */ error.name = 'RequestError'; } + throw error; } }; @@ -141,6 +163,7 @@ export function TableData({ */ const triggerReload = () => { setRefresh(Date.now()); + if (onReload) { onReload(Date.now()); } @@ -155,6 +178,7 @@ export function TableData({ if (isMounted.current) { const { index: pageIndex, size: pageSize } = page; const { field, direction } = sort; + setPagination({ pageIndex, pageSize, @@ -194,7 +218,9 @@ export function TableData({ }, [rest?.fetchContext]); useEffect(() => { - if (rest.reload) triggerReload(); + if (rest.reload) { + triggerReload(); + } }, [rest.reload]); // It is required that this effect runs after other effects that use isMounted @@ -209,7 +235,7 @@ export function TableData({ totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions, }; - + // eslint-disable-next-line @typescript-eslint/naming-convention const ReloadButton = ( triggerReload()}> @@ -217,7 +243,6 @@ export function TableData({ ); - const header = ( <> @@ -272,6 +297,7 @@ export function TableData({ { const metaField = getColumMetaField(item); + return { id: metaField, label: item.name, @@ -284,8 +310,10 @@ export function TableData({ if (state.length > 1) { return state.filter(field => field !== optionID); } + return state; } + return [...state, optionID]; }); }} @@ -297,7 +325,6 @@ export function TableData({ )} ); - const tableDataRenderElementsProps = { ...rest, tableColumns, @@ -330,7 +357,10 @@ export function TableData({ ({ ...rest }), + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ searchable, show, composeField, ...rest }) => { + return { ...rest }; + }, )} items={items} loading={isLoading} @@ -348,13 +378,3 @@ export function TableData({ ); } - -const TableDataRenderElement = ({ render, ...rest }) => { - if (typeof render === 'function') { - return {render(rest)}; - } - if (typeof render === 'object') { - return {render}; - } - return null; -}; diff --git a/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx b/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx index 18894b861b..5f1af3870f 100644 --- a/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx +++ b/plugins/wazuh-core/public/hooks/use-docked-side-nav.tsx @@ -2,9 +2,12 @@ import { useEffect, useState, useRef, useCallback } from 'react'; import { getChrome } from '../plugin-services'; export const useDockedSideNav = () => { - const [sideNavDocked, _setSideNavDocked] = useState(false); + const [sideNavDocked, setSideNavDockedState] = useState(false); const [isDockedSideNavVisible, setIsDockedSideNavVisible] = useState(false); + // Create references so the event handler can read the latest values + const timeoutID = useRef(0); + const currentSideNavDocked = useRef(sideNavDocked); /* We have to create a reference to the state of the react component, @@ -12,13 +15,9 @@ export const useDockedSideNav = () => { */ const setSideNavDocked = (value: boolean) => { currentSideNavDocked.current = value; - _setSideNavDocked(value); + setSideNavDockedState(value); }; - // Create references so the event handler can read the latest values - let timeoutID = useRef(0); - const currentSideNavDocked = useRef(sideNavDocked); - // If the inner width of the window is less than 992px, the side nav is always hidden. // The use of useCallback is to keep the function reference the same so we can remove it in the event listener const onWindowResize = useCallback(() => { @@ -43,6 +42,7 @@ export const useDockedSideNav = () => { }); window.addEventListener('resize', onWindowResize, true); + return () => { isNavDrawerSubscription.unsubscribe(); window.removeEventListener('resize', onWindowResize, true); diff --git a/plugins/wazuh-core/public/hooks/use-state-storage.ts b/plugins/wazuh-core/public/hooks/use-state-storage.ts index 4aacbd55b0..845e36b726 100644 --- a/plugins/wazuh-core/public/hooks/use-state-storage.ts +++ b/plugins/wazuh-core/public/hooks/use-state-storage.ts @@ -2,22 +2,26 @@ import { useState } from 'react'; export type UseStateStorageSystem = 'sessionStorage' | 'localStorage'; export type UseStateStorageReturn = [T, (value: T) => void]; -export type UseStateStorage = { - ( - initialValue: T, - storageSystem?: UseStateStorageSystem, - storageKey?: string, - ): [T, (value: T) => void]; -}; +export type UseStateStorage = ( + initialValue: T, + storageSystem?: UseStateStorageSystem, + storageKey?: string, +) => [T, (value: T) => void]; function transformValueToStorage(value: any) { - return typeof value !== 'string' ? JSON.stringify(value) : value; + return typeof value === 'string' ? value : JSON.stringify(value); } function transformValueFromStorage(value: any) { return typeof value === 'string' ? JSON.parse(value) : value; } +export type UseStateStorageHook = ( + initialValue: T, + storageSystem?: UseStateStorageSystem, + storageKey?: string, +) => UseStateStorageReturn; + export function useStateStorage( initialValue: T, storageSystem?: UseStateStorageSystem, @@ -33,12 +37,13 @@ export function useStateStorage( setState(state => { const formattedValue = typeof value === 'function' ? value(state) : value; - storageSystem && - storageKey && + if (storageSystem && storageKey) { window?.[storageSystem]?.setItem( storageKey, transformValueToStorage(formattedValue), ); + } + return formattedValue; }); } diff --git a/plugins/wazuh-core/public/plugin.ts b/plugins/wazuh-core/public/plugin.ts index bf2a7c1c31..8b9135fc44 100644 --- a/plugins/wazuh-core/public/plugin.ts +++ b/plugins/wazuh-core/public/plugin.ts @@ -1,34 +1,29 @@ import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; -import { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; -import { setChrome, setCore, setUiSettings } from './plugin-services'; -import * as utils from './utils'; -import * as uiComponents from './components'; import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; -import { ConfigurationStore } from './utils/configuration-store'; import { PLUGIN_SETTINGS, PLUGIN_SETTINGS_CATEGORIES, } from '../common/constants'; +import { WazuhCorePluginSetup, WazuhCorePluginStart } from './types'; +import { setChrome, setCore, setUiSettings } from './plugin-services'; +import * as utils from './utils'; +import * as uiComponents from './components'; +import { ConfigurationStore } from './utils/configuration-store'; import { DashboardSecurity } from './utils/dashboard-security'; import * as hooks from './hooks'; import { CoreHTTPClient } from './services/http/http-client'; +const noop = () => {}; + export class WazuhCorePlugin implements Plugin { runtime = { setup: {} }; - _internal: { [key: string]: any } = {}; - services: { [key: string]: any } = {}; + internal: Record = {}; + services: Record = {}; + public async setup(core: CoreSetup): Promise { - const noop = () => {}; - // Debug logger - const consoleLogger = { - info: console.log, - error: console.error, - debug: console.debug, - warn: console.warn, - }; // No operation logger const noopLogger = { info: noop, @@ -37,24 +32,25 @@ export class WazuhCorePlugin warn: noop, }; const logger = noopLogger; - this._internal.configurationStore = new ConfigurationStore( + + this.internal.configurationStore = new ConfigurationStore( logger, core.http, ); this.services.configuration = new Configuration( logger, - this._internal.configurationStore, + this.internal.configurationStore, ); // Register the plugin settings - Object.entries(PLUGIN_SETTINGS).forEach(([key, value]) => - this.services.configuration.register(key, value), - ); + for (const [key, value] of Object.entries(PLUGIN_SETTINGS)) { + this.services.configuration.register(key, value); + } // Add categories to the configuration - Object.entries(PLUGIN_SETTINGS_CATEGORIES).forEach(([key, value]) => { + for (const [key, value] of Object.entries(PLUGIN_SETTINGS_CATEGORIES)) { this.services.configuration.registerCategory({ ...value, id: key }); - }); + } // Create dashboardSecurity this.services.dashboardSecurity = new DashboardSecurity(logger, core.http); diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts index 7d193b3599..92dc9fca23 100644 --- a/plugins/wazuh-core/public/services/http/generic-client.ts +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -1,5 +1,5 @@ -import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; import { Logger } from '../../../common/services/configuration'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; import { HTTPClientGeneric, HTTPClientRequestInterceptor, @@ -8,19 +8,21 @@ import { interface GenericRequestServices { request: HTTPClientRequestInterceptor['request']; - getURL(path: string): string; - getTimeout(): Promise; - getIndexPatternTitle(): Promise; - getServerAPI(): string; - checkAPIById(apiId: string): Promise; + getURL: (path: string) => string; + getTimeout: () => Promise; + getIndexPatternTitle: () => Promise; + getServerAPI: () => string; + checkAPIById: (apiId: string) => Promise; } export class GenericRequest implements HTTPClientGeneric { onErrorInterceptor?: (error: any) => Promise; + constructor( - private logger: Logger, - private services: GenericRequestServices, + private readonly logger: Logger, + private readonly services: GenericRequestServices, ) {} + async request( method: HTTPVerb, path: string, @@ -31,6 +33,7 @@ export class GenericRequest implements HTTPClientGeneric { if (!method || !path) { throw new Error('Missing parameters'); } + const timeout = await this.services.getTimeout(); const requestHeaders = { ...PLUGIN_PLATFORM_REQUEST_HEADERS, @@ -40,14 +43,17 @@ export class GenericRequest implements HTTPClientGeneric { try { requestHeaders.pattern = await this.services.getIndexPatternTitle(); - } catch (error) {} + } catch { + /* empty */ + } try { requestHeaders.id = this.services.getServerAPI(); - } catch (error) { + } catch { // Intended } - var options = {}; + + let options = {}; if (method === 'GET') { options = { @@ -57,6 +63,7 @@ export class GenericRequest implements HTTPClientGeneric { timeout: timeout, }; } + if (method === 'PUT') { options = { method: method, @@ -66,6 +73,7 @@ export class GenericRequest implements HTTPClientGeneric { timeout: timeout, }; } + if (method === 'POST') { options = { method: method, @@ -75,6 +83,7 @@ export class GenericRequest implements HTTPClientGeneric { timeout: timeout, }; } + if (method === 'DELETE') { options = { method: method, @@ -86,18 +95,20 @@ export class GenericRequest implements HTTPClientGeneric { } const data = await this.services.request(options); + if (!data) { throw new Error(`Error doing a request to ${url}, method: ${method}.`); } return data; } catch (error) { - //if the requests fails, we need to check if the API is down - const currentApi = this.services.getServerAPI(); //JSON.parse(AppState.getCurrentAPI() || '{}'); + // if the requests fails, we need to check if the API is down + const currentApi = this.services.getServerAPI(); // JSON.parse(AppState.getCurrentAPI() || '{}'); + if (currentApi) { try { await this.services.checkAPIById(currentApi); - } catch (err) { + } catch { // const wzMisc = new WzMisc(); // wzMisc.setApiIsDown(true); // if ( @@ -112,15 +123,20 @@ export class GenericRequest implements HTTPClientGeneric { // } } } + // if(this.onErrorInterceptor){ // await this.onErrorInterceptor(error) // } - if (returnError) return Promise.reject(error); + if (returnError) { + throw error; + } + return (((error || {}).response || {}).data || {}).message || false ? Promise.reject(new Error(error.response.data.message)) : Promise.reject(error || new Error('Server did not respond')); } } + setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise) { this.onErrorInterceptor = onErrorInterceptor; } diff --git a/plugins/wazuh-core/public/services/http/http-client.ts b/plugins/wazuh-core/public/services/http/http-client.ts index e7d8df3729..bbf2a84a1b 100644 --- a/plugins/wazuh-core/public/services/http/http-client.ts +++ b/plugins/wazuh-core/public/services/http/http-client.ts @@ -8,18 +8,22 @@ import { createUI } from './ui/create'; interface HTTPClientServices { http: any; - getTimeout(): Promise; - getURL(path: string): string; - getServerAPI(): string; - getIndexPatternTitle(): Promise; + getTimeout: () => Promise; + getURL: (path: string) => string; + getServerAPI: () => string; + getIndexPatternTitle: () => Promise; } export class CoreHTTPClient implements HTTPClient { - private requestInterceptor: HTTPClientRequestInterceptor; + private readonly requestInterceptor: HTTPClientRequestInterceptor; public generic; public server; - private _timeout: number = HTTP_CLIENT_DEFAULT_TIMEOUT; - constructor(private logger: Logger, private services: HTTPClientServices) { + private readonly TIMEOUT: number = HTTP_CLIENT_DEFAULT_TIMEOUT; + + constructor( + private readonly logger: Logger, + private readonly services: HTTPClientServices, + ) { this.logger.debug('Creating client'); // Create request interceptor this.requestInterceptor = new RequestInterceptorClient( @@ -27,11 +31,9 @@ export class CoreHTTPClient implements HTTPClient { this.services.http, ); - const getTimeout = async () => - (await this.services.getTimeout()) || this._timeout; - + const { getTimeout } = this.services; const internalServices = { - getTimeout, + getTimeout: async () => (await getTimeout()) || this.TIMEOUT, getServerAPI: this.services.getServerAPI, getURL: this.services.getURL, }; @@ -49,19 +51,25 @@ export class CoreHTTPClient implements HTTPClient { }); this.logger.debug('Created client'); } + async setup(deps) { this.logger.debug('Setup'); + return { ui: createUI({ ...deps, http: this }), }; } + async start() {} + async stop() {} + async register() { this.logger.debug('Starting client'); this.requestInterceptor.init(); this.logger.debug('Started client'); } + async unregister() { this.logger.debug('Stopping client'); this.requestInterceptor.destroy(); diff --git a/plugins/wazuh-core/public/services/http/request-interceptor.ts b/plugins/wazuh-core/public/services/http/request-interceptor.ts index 21b2600da3..a6f1e4fdea 100644 --- a/plugins/wazuh-core/public/services/http/request-interceptor.ts +++ b/plugins/wazuh-core/public/services/http/request-interceptor.ts @@ -5,19 +5,25 @@ import { HTTPClientRequestInterceptor } from './types'; export class RequestInterceptorClient implements HTTPClientRequestInterceptor { // define if the request is allowed to run - private _allow: boolean = true; + private allow = true; // store the cancel token to abort the requests - private _source: any; + private readonly cancelTokenSource: any; // unregister the interceptor - private unregisterInterceptor: () => void = () => {}; - constructor(private logger: Logger, private http: any) { + private unregisterInterceptor: () => void = function () {}; + + constructor( + private readonly logger: Logger, + private readonly http: any, + ) { this.logger.debug('Creating'); - this._source = axios.CancelToken.source(); + this.cancelTokenSource = axios.CancelToken.source(); this.logger.debug('Created'); } + private registerInterceptor() { this.logger.debug('Registering interceptor in core http'); this.unregisterInterceptor = this.http.intercept({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars responseError: (httpErrorResponse, controller) => { if ( httpErrorResponse.response?.status === HTTP_STATUS_CODES.UNAUTHORIZED @@ -25,49 +31,59 @@ export class RequestInterceptorClient implements HTTPClientRequestInterceptor { this.cancel(); } }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars request: (current, controller) => { - if (!this._allow) { + if (!this.allow) { throw new Error('Disable request'); } }, }); this.logger.debug('Registered interceptor in core http'); } + init() { this.logger.debug('Initiating'); this.registerInterceptor(); this.logger.debug('Initiated'); } + destroy() { this.logger.debug('Destroying'); this.logger.debug('Unregistering interceptor in core http'); this.unregisterInterceptor(); + this.unregisterInterceptor = () => {}; + this.logger.debug('Unregistered interceptor in core http'); this.logger.debug('Destroyed'); } + cancel() { this.logger.debug('Disabling requests'); - this._allow = false; - this._source.cancel('Requests cancelled'); + this.allow = false; + this.cancelTokenSource.cancel('Requests cancelled'); this.logger.debug('Disabled requests'); } + async request(options: AxiosRequestConfig = {}) { - if (!this._allow) { - return Promise.reject('Requests are disabled'); + if (!this.allow) { + throw 'Requests are disabled'; } + if (!options.method || !options.url) { - return Promise.reject('Missing parameters'); + throw 'Missing parameters'; } + const optionsWithCancelToken = { ...options, - cancelToken: this._source?.token, + cancelToken: this.cancelTokenSource?.token, }; - if (this._allow) { + if (this.allow) { try { const requestData = await axios(optionsWithCancelToken); - return Promise.resolve(requestData); + + return requestData; } catch (error) { if ( error.response?.data?.message === 'Unauthorized' || @@ -75,8 +91,9 @@ export class RequestInterceptorClient implements HTTPClientRequestInterceptor { ) { this.cancel(); // To reduce the dependencies, we use window object instead of the NavigationService - window.location.reload(); + globalThis.location.reload(); } + throw error; } } diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts index 155af5ba5e..be8a1d8608 100644 --- a/plugins/wazuh-core/public/services/http/server-client.test.ts +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -1,13 +1,13 @@ import { WzRequest } from './server-client'; const noop = () => {}; + const logger = { debug: noop, info: noop, warn: noop, error: noop, }; - const USER_TOKEN = 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ3YXp1aCIsImF1ZCI6IldhenVoIEFQSSBSRVNUIiwibmJmIjoxNzI2NzM3MDY3LCJleHAiOjE3MjY3Mzc5NjcsInN1YiI6IndhenVoLXd1aSIsInJ1bl9hcyI6ZmFsc2UsInJiYWNfcm9sZXMiOlsxXSwicmJhY19tb2RlIjoid2hpdGUifQ.AOL4dDe3c4WCYXMjqbkBqfKFAChtjvD_uZ0FXfLOMnfU0n6zPo61OZ43Kt0bYhW25BQIXR9Belb49gG3_qAIZpcaAQhQv4HPcL41ESRSvZc2wsa9_HYgV8Z7gieSuT15gdnSNogLKFS7yK5gQQivLo1e4QfVsDThrG_TVdJPbCG3GPq9'; @@ -45,6 +45,7 @@ function createClient() { getURL: path => path, request: mockRequest, }); + return { client, mockRequest }; } @@ -64,7 +65,7 @@ describe('Create client', () => { }); it('Authentication', done => { - const { client, mockRequest } = createClient(); + const { client } = createClient(); client.auth().then(data => { expect(data).toEqual({ @@ -97,7 +98,6 @@ describe('Create client', () => { it('Request', async () => { const { client, mockRequest } = createClient(); - const data = await client.request('GET', '/security/users/me/policies', {}); expect(mockRequest).toHaveBeenCalledTimes(1); diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 1a8b75316a..56ce444b19 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -9,6 +9,9 @@ * * Find more information about this on the LICENSE file. */ +import jwtDecode from 'jwt-decode'; +import { BehaviorSubject } from 'rxjs'; +import { Logger } from '../../../common/services/configuration'; import { HTTPClientServer, HTTPVerb, @@ -16,10 +19,7 @@ import { WzRequestServices, ServerAPIResponseItemsDataHTTPClient, } from './types'; -import { Logger } from '../../../common/services/configuration'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; -import jwtDecode from 'jwt-decode'; -import { BehaviorSubject } from 'rxjs'; export class WzRequest implements HTTPClientServer { onErrorInterceptor?: ( @@ -32,7 +32,11 @@ export class WzRequest implements HTTPClientServer { ) => Promise; private userData: HTTPClientServerUserData; userData$: BehaviorSubject; - constructor(private logger: Logger, private services: WzRequestServices) { + + constructor( + private readonly logger: Logger, + private readonly services: WzRequestServices, + ) { this.userData = { logged: false, token: null, @@ -48,10 +52,11 @@ export class WzRequest implements HTTPClientServer { * @param {String} path * @param {Object} payload */ - private async _request( + private async requestInternal( method: HTTPVerb, path: string, payload: any = null, + // eslint-disable-next-line unicorn/no-object-as-default-parameter extraOptions: { shouldRetry?: boolean; checkCurrentApiIsUp?: boolean; @@ -74,13 +79,13 @@ export class WzRequest implements HTTPClientServer { typeof extraOptions.overwriteHeaders === 'object' ? extraOptions.overwriteHeaders : {}; + try { if (!method || !path) { throw new Error('Missing parameters'); } const timeout = await this.services.getTimeout(); - const url = this.services.getURL(path); const options = { method: method, @@ -93,19 +98,20 @@ export class WzRequest implements HTTPClientServer { data: payload, timeout: timeout, }; - const data = await this.services.request(options); if (data['error']) { throw new Error(data['error']); } - return Promise.resolve(data); + return data; } catch (error) { - //if the requests fails, we need to check if the API is down + // if the requests fails, we need to check if the API is down if (checkCurrentApiIsUp) { const currentApi = this.services.getServerAPI(); + if (currentApi) { + // eslint-disable-next-line no-useless-catch try { await this.checkAPIById(currentApi); } catch (error) { @@ -123,18 +129,23 @@ export class WzRequest implements HTTPClientServer { } } } + // if(this.onErrorInterceptor){ // await this.onErrorInterceptor(error, {checkCurrentApiIsUp, shouldRetry, overwriteHeaders}) // } const errorMessage = error?.response?.data?.message || error?.message; + if ( typeof errorMessage === 'string' && errorMessage.includes('status code 401') && shouldRetry ) { try { - await this.auth(true); //await WzAuthentication.refresh(true); - return this._request(method, path, payload, { shouldRetry: false }); + await this.auth(true); // await WzAuthentication.refresh(true); + + return this.requestInternal(method, path, payload, { + shouldRetry: false, + }); } catch (error) { throw this.returnErrorInstance( error, @@ -142,6 +153,7 @@ export class WzRequest implements HTTPClientServer { ); } } + throw this.returnErrorInstance( error, errorMessage || 'Server did not respond', @@ -159,6 +171,7 @@ export class WzRequest implements HTTPClientServer { method: HTTPVerb, path: string, body: any, + // eslint-disable-next-line unicorn/no-object-as-default-parameter options: { checkCurrentApiIsUp?: boolean; returnOriginalResponse?: boolean; @@ -170,10 +183,9 @@ export class WzRequest implements HTTPClientServer { } const { returnOriginalResponse, ...optionsToGenericReq } = options; - const id = this.services.getServerAPI(); const requestData = { method, path, body, id }; - const response = await this._request( + const response = await this.requestInternal( 'POST', '/api/request', requestData, @@ -195,8 +207,10 @@ export class WzRequest implements HTTPClientServer { ? ` Affected ids: ${failedIds} ` : '' }`; + throw this.returnErrorInstance(null, errorMessage); } + return response; } catch (error) { throw this.returnErrorInstance( @@ -216,10 +230,12 @@ export class WzRequest implements HTTPClientServer { if (!path || !filters) { throw new Error('Missing parameters'); } + const id = this.services.getServerAPI(); const requestData = { path, id, filters }; - const data = await this._request('POST', '/api/csv', requestData); - return Promise.resolve(data); + const data = await this.requestInternal('POST', '/api/csv', requestData); + + return data; } catch (error) { throw this.returnErrorInstance( error, @@ -238,7 +254,9 @@ export class WzRequest implements HTTPClientServer { if (!error || typeof error === 'string') { return new Error(message || error); } + error.message = message; + return error; } @@ -255,19 +273,22 @@ export class WzRequest implements HTTPClientServer { private async login(force = false) { try { let idHost = this.services.getServerAPI(); + while (!idHost) { + // eslint-disable-next-line no-await-in-loop await new Promise(r => setTimeout(r, 500)); idHost = this.services.getServerAPI(); } - const response = await this._request('POST', '/api/login', { + const response = await this.requestInternal('POST', '/api/login', { idHost, force, }); - const token = ((response || {}).data || {}).token; + return token as string; } catch (error) { + this.logger.error(`Error in the login: ${error.message}`); throw error; } } @@ -282,6 +303,7 @@ export class WzRequest implements HTTPClientServer { try { // Get user token const token: string = await this.login(force); + if (!token) { // Remove old existent token // await this.unauth(); @@ -289,15 +311,13 @@ export class WzRequest implements HTTPClientServer { } // Decode token and get expiration time + // eslint-disable-next-line @typescript-eslint/no-unused-vars const jwtPayload = jwtDecode(token); - // Get user Policies const userPolicies = await this.getUserPolicies(); - // Dispatch actions to set permissions and administrator consideration // TODO: implement // store.dispatch(updateUserPermissions(userPolicies)); - // store.dispatch( // updateUserAccount( // getWazuhCorePlugin().dashboardSecurity.getAccountFromJWTAPIDecodedToken( @@ -314,6 +334,7 @@ export class WzRequest implements HTTPClientServer { }; this.updateUserData(data); + return data; } catch (error) { // TODO: implement @@ -354,17 +375,22 @@ export class WzRequest implements HTTPClientServer { private async getUserPolicies() { try { let idHost = this.services.getServerAPI(); + while (!idHost) { + // eslint-disable-next-line no-await-in-loop await new Promise(r => setTimeout(r, 500)); idHost = this.services.getServerAPI(); } + const response = await this.request( 'GET', '/security/users/me/policies', { idHost }, ); + return response?.data?.data || {}; } catch (error) { + this.logger.error(`Error getting the user policies: ${error.message}`); throw error; } } @@ -388,6 +414,7 @@ export class WzRequest implements HTTPClientServer { return response?.data?.data || {}; } catch (error) { + this.logger.error(`Error in the unauthentication: ${error.message}`); throw error; } } @@ -405,6 +432,7 @@ export class WzRequest implements HTTPClientServer { try { const timeout = await this.services.getTimeout(); const payload = { id: serverHostId }; + if (idChanged) { payload.idChanged = serverHostId; } @@ -420,12 +448,6 @@ export class WzRequest implements HTTPClientServer { data: payload, timeout: timeout, }; - - // TODO: implement - // if (Object.keys(configuration).length) { - // AppState.setPatternSelector(configuration['ip.selector']); - // } - const response = await this.services.request(options); if (response.error) { @@ -439,6 +461,7 @@ export class WzRequest implements HTTPClientServer { // const wzMisc = new WzMisc(); // wzMisc.setApiIsDown(true); const response = (error.response.data || {}).message || error.message; + throw this.returnErrorInstance(response); } else { throw this.returnErrorInstance( @@ -457,7 +480,6 @@ export class WzRequest implements HTTPClientServer { try { const timeout = await this.services.getTimeout(); const url = this.services.getURL('/api/check-api'); - const options = { method: 'POST', headers: { @@ -468,7 +490,6 @@ export class WzRequest implements HTTPClientServer { data: { ...apiEntry, forceRefresh }, timeout: timeout, }; - const response = await this.services.request(options); if (response.error) { @@ -479,6 +500,7 @@ export class WzRequest implements HTTPClientServer { } catch (error) { if (error.response) { const response = (error.response.data || {}).message || error.message; + throw this.returnErrorInstance(response); } else { throw this.returnErrorInstance( diff --git a/plugins/wazuh-core/public/services/http/types.ts b/plugins/wazuh-core/public/services/http/types.ts index ee64b68300..ce0e47f558 100644 --- a/plugins/wazuh-core/public/services/http/types.ts +++ b/plugins/wazuh-core/public/services/http/types.ts @@ -2,32 +2,32 @@ import { AxiosRequestConfig } from 'axios'; import { BehaviorSubject } from 'rxjs'; export interface HTTPClientRequestInterceptor { - init(): void; - destroy(): void; - cancel(): void; - request(options: AxiosRequestConfig): Promise; + init: () => void; + destroy: () => void; + cancel: () => void; + request: (options: AxiosRequestConfig) => Promise; } export type HTTPVerb = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT'; export interface HTTPClientGeneric { - request( + request: ( method: HTTPVerb, path: string, payload?: any, returnError?: boolean, - ): Promise; + ) => Promise; } -export type HTTPClientServerUserData = { +export interface HTTPClientServerUserData { token: string | null; policies: any | null; account: any | null; logged: boolean; -}; +} export interface HTTPClientServer { - request( + request: ( method: HTTPVerb, path: string, body: any, @@ -35,12 +35,12 @@ export interface HTTPClientServer { checkCurrentApiIsUp?: boolean; returnOriginalResponse?: boolean; }, - ): Promise; - csv(path: string, filters: any): Promise; - auth(force: boolean): Promise; - unauth(force: boolean): Promise; + ) => Promise; + csv: (path: string, filters: any) => Promise; + auth: (force: boolean) => Promise; + unauth: (force: boolean) => Promise; userData$: BehaviorSubject; - getUserData(): HTTPClientServerUserData; + getUserData: () => HTTPClientServerUserData; } export interface HTTPClient { @@ -50,14 +50,14 @@ export interface HTTPClient { export interface WzRequestServices { request: HTTPClientRequestInterceptor['request']; - getURL(path: string): string; - getTimeout(): Promise; - getServerAPI(): string; + getURL: (path: string) => string; + getTimeout: () => Promise; + getServerAPI: () => string; } export interface ServerAPIResponseItems { - affected_items: Array; - failed_items: Array; + affected_items: T[]; + failed_items: any[]; total_affected_items: number; total_failed_items: number; } diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx index 4d81a1cfb6..5f2bb2953e 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.test.tsx @@ -15,7 +15,9 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExportTableCsv } from './export-table-csv'; + const noop = () => {}; + describe('Export Table Csv component', () => { it('renders correctly to match the snapshot when the button is disabled', () => { const wrapper = mount( @@ -27,8 +29,10 @@ describe('Export Table Csv component', () => { title='example' />, ); + expect(wrapper).toMatchSnapshot(); }); + it('renders correctly to match the snapshot when the button is enabled', () => { const wrapper = mount( { title='example' />, ); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx index 2665ccbc92..7089ce166c 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx @@ -19,16 +19,33 @@ export function ExportTableCsv({ title, showToast, exportCSV, +}: { + fetchContext: { endpoint: string; filters: Record }; + totalItems: number; + title: string; + showToast: (options: { + color: string; + title: string; + toastLifeTimeMs: number; + }) => any; + exportCSV: ( + endpoint: string, + formattedFilters: any[], + title: string, + ) => Promise; }) { const downloadCSV = async () => { try { const { endpoint, filters } = fetchContext; const formattedFilters = Object.entries(filters || []).map( - ([name, value]) => ({ - name, - value, - }), + ([name, value]) => { + return { + name, + value, + }; + }, ); + showToast({ color: 'success', title: 'Your download should begin automatically...', @@ -36,7 +53,7 @@ export function ExportTableCsv({ }); await exportCSV(endpoint, formattedFilters, title.toLowerCase()); - } catch (error) { + } catch { // TODO: implement // const options = { // context: `${ExportTableCsv.name}.downloadCsv`, diff --git a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx index 91d30df500..8e0014ab03 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx @@ -1,11 +1,12 @@ import React, { useMemo } from 'react'; -import { SearchBar, TableData } from '../../../../components'; import { EuiSpacer } from '@elastic/eui'; +import { SearchBar, TableData } from '../../../../components'; import { ServerDataProps } from './types'; export function ServerTableData({ showActionExportFormatted, postActionButtons, + // eslint-disable-next-line @typescript-eslint/naming-convention ActionExportFormatted, ...props }: ServerDataProps) { @@ -29,21 +30,20 @@ export function ServerTableData({ props.showSearchBar && (({ tableColumns, ...rest }) => { /* Render search bar*/ - const searchBarWQLOptions = useMemo( - () => ({ + const searchBarWQLOptions = useMemo(() => { + return { searchTermFields: tableColumns .filter( ({ field, searchable }) => searchable && rest.selectedFields.includes(field), ) - .map(({ field, composeField }) => + .flatMap(({ field, composeField }) => [composeField || field].flat(), - ) - .flat(), - ...(rest?.searchBarWQL?.options || {}), - }), - [rest?.searchBarWQL?.options, rest?.selectedFields], - ); + ), + ...rest?.searchBarWQL?.options, + }; + }, [rest?.searchBarWQL?.options, rest?.selectedFields]); + return ( <> { const serverDataFetch = fetchServerTableDataCreator( deps.http.server.request.bind(deps.http.server), ); - const ActionExportFormatted = withServices({ showToast: deps.core.notifications.toasts.add.bind( deps.core.notifications.toasts, @@ -17,6 +16,7 @@ export const createUI = deps => { const data = await deps.http.server.csv(path, filters); const output = data.data ? [data.data] : []; const blob = new Blob(output, { type: 'text/csv' }); + FileSaver.saveAs(blob, `${exportName}.csv`); }, })(ExportTableCsv); diff --git a/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts index 8baa159794..ad1701ce3e 100644 --- a/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts +++ b/plugins/wazuh-core/public/services/http/ui/services/fetch-server-data.ts @@ -2,8 +2,10 @@ const getFilters = filters => { if (!filters) { return {}; } + const { default: defaultFilters, ...restFilters } = filters; - return Object.keys(restFilters).length ? restFilters : defaultFilters; + + return Object.keys(restFilters).length > 0 ? restFilters : defaultFilters; }; export const fetchServerTableDataCreator = @@ -17,7 +19,6 @@ export const fetchServerTableDataCreator = limit: pageSize, sort: `${direction === 'asc' ? '+' : '-'}${field}`, }; - const response = await fetchData( fetchContext.method, fetchContext.endpoint, @@ -25,6 +26,7 @@ export const fetchServerTableDataCreator = params, }, ); + return { items: response?.data?.data?.affected_items, totalItems: response?.data?.data?.total_affected_items, diff --git a/plugins/wazuh-core/public/services/http/ui/with-services.tsx b/plugins/wazuh-core/public/services/http/ui/with-services.tsx new file mode 100644 index 0000000000..4e5d665568 --- /dev/null +++ b/plugins/wazuh-core/public/services/http/ui/with-services.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +// eslint-disable-next-line @typescript-eslint/naming-convention, react/display-name +export const withServices = services => WrappedComponent => props => ( + +); diff --git a/plugins/wazuh-core/public/services/http/ui/withServices.tsx b/plugins/wazuh-core/public/services/http/ui/withServices.tsx deleted file mode 100644 index 4ee5ac081f..0000000000 --- a/plugins/wazuh-core/public/services/http/ui/withServices.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; -export const withServices = services => WrappedComponent => props => - ; diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index 411d3fbda0..d3cb3a5b6c 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -1,7 +1,7 @@ import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; import { TableDataProps } from './components'; -import { UseStateStorage, UseStateStorageSystem } from './hooks'; +import { UseStateStorageHook } from './hooks'; import { UseDockedSideNav } from './hooks/use-docked-side-nav'; import { HTTPClient } from './services/http/types'; import { ServerDataProps } from './services/http/ui/components/types'; @@ -14,20 +14,23 @@ export interface WazuhCorePluginSetup { dashboardSecurity: DashboardSecurity; http: HTTPClient; ui: { - TableData( + // eslint-disable-next-line @typescript-eslint/naming-convention + TableData: ( prop: TableDataProps, - ): React.ComponentType>; - SearchBar(prop: any): React.ComponentType; - ServerTable( + ) => React.ComponentType>; + // eslint-disable-next-line @typescript-eslint/naming-convention + SearchBar: (prop: any) => React.ComponentType; + // eslint-disable-next-line @typescript-eslint/naming-convention + ServerTable: ( prop: ServerDataProps, - ): React.ComponentType>; + ) => React.ComponentType>; }; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WazuhCorePluginStart { hooks: { useDockedSideNav: UseDockedSideNav; - useStateStorage: UseStateStorage; // TODO: enhance + useStateStorage: UseStateStorageHook; // TODO: enhance }; utils: { formatUIDate: (date: Date) => string }; API_USER_STATUS_RUN_AS: API_USER_STATUS_RUN_AS; @@ -35,14 +38,17 @@ export interface WazuhCorePluginStart { dashboardSecurity: DashboardSecurity; http: HTTPClient; ui: { - TableData( + // eslint-disable-next-line @typescript-eslint/naming-convention + TableData: ( prop: TableDataProps, - ): React.ComponentType>; - SearchBar(prop: any): React.ComponentType; - ServerTable( + ) => React.ComponentType>; + // eslint-disable-next-line @typescript-eslint/naming-convention + SearchBar: (prop: any) => React.ComponentType; + // eslint-disable-next-line @typescript-eslint/naming-convention + ServerTable: ( prop: ServerDataProps, - ): React.ComponentType>; + ) => React.ComponentType>; }; } -export interface AppPluginStartDependencies {} +export type AppPluginStartDependencies = object; diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts index ab91cf002c..51ba05f557 100644 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ b/plugins/wazuh-core/public/utils/configuration-store.ts @@ -5,73 +5,97 @@ import { } from '../../common/services/configuration'; export class ConfigurationStore implements IConfigurationStore { - private _stored: any; - file: string = ''; + private stored: any; + file = ''; configuration: IConfiguration | null = null; - constructor(private logger: Logger, private http: any) { - this._stored = {}; + + constructor( + private readonly logger: Logger, + private readonly http: any, + ) { + this.stored = {}; } + setConfiguration(configuration: IConfiguration) { this.configuration = configuration; } + async setup() { this.logger.debug('Setup'); } + async start() { try { this.logger.debug('Start'); + const response = await this.http.get('/api/setup'); + this.file = response.data.configuration_file; } catch (error) { this.logger.error(`Error on start: ${error.message}`); } } + async stop() { this.logger.debug('Stop'); } + private storeGet() { - return this._stored; + return this.stored; } + private storeSet(value: any) { - this._stored = value; + this.stored = value; } - async get(...settings: string[]): Promise { + + async get(...settings: string[]): Promise> { const stored = this.storeGet(); - return settings.length - ? settings.reduce( - (accum, settingKey: string) => ({ - ...accum, - [settingKey]: stored[settingKey], - }), - {}, + return settings.length > 0 + ? Object.fromEntries( + settings.map((settingKey: string) => [ + settingKey, + stored[settingKey], + ]), ) : stored; } - async set(settings: { [key: string]: any }): Promise { + + async set(settings: Record): Promise { try { const attributes = this.storeGet(); const newSettings = { ...attributes, ...settings, }; + this.logger.debug(`Updating store with ${JSON.stringify(newSettings)}`); + const response = this.storeSet(newSettings); + this.logger.debug('Store was updated'); + return response; } catch (error) { this.logger.error(error.message); throw error; } } + async clear(...settings: string[]): Promise { try { const attributes = await this.get(); const updatedSettings = { ...attributes, }; - settings.forEach(setting => delete updatedSettings[setting]); + + for (const setting of settings) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete updatedSettings[setting]; + } + const response = this.storeSet(updatedSettings); + return response; } catch (error) { this.logger.error(error.message); diff --git a/plugins/wazuh-core/public/utils/dashboard-security.ts b/plugins/wazuh-core/public/utils/dashboard-security.ts index dd0889c192..d03b530fca 100644 --- a/plugins/wazuh-core/public/utils/dashboard-security.ts +++ b/plugins/wazuh-core/public/utils/dashboard-security.ts @@ -2,21 +2,30 @@ import { WAZUH_ROLE_ADMINISTRATOR_ID } from '../../common/constants'; import { Logger } from '../../common/services/configuration'; export class DashboardSecurity { - private securityPlatform: string = ''; - constructor(private logger: Logger, private http) {} + private securityPlatform = ''; + + constructor( + private readonly logger: Logger, + private readonly http: { get: (path: string) => any }, + ) {} + private async fetchCurrentPlatform() { try { this.logger.debug('Fetching the security platform'); + const response = await this.http.get( '/elastic/security/current-platform', ); + this.logger.debug(`Security platform: ${this.securityPlatform}`); + return response.platform; } catch (error) { this.logger.error(error.message); throw error; } } + async setup() { try { this.logger.debug('Setup'); @@ -26,17 +35,21 @@ export class DashboardSecurity { this.logger.error(error.message); } } + async start() {} + async stop() {} + getAccountFromJWTAPIDecodedToken(decodedToken: number[]) { const isAdministrator = decodedToken?.rbac_roles?.some?.( (role: number) => role === WAZUH_ROLE_ADMINISTRATOR_ID, ); + return { administrator: isAdministrator, - administrator_requirements: !isAdministrator - ? 'User has no administrator role in the selected API connection.' - : null, + administrator_requirements: isAdministrator + ? null + : 'User has no administrator role in the selected API connection.', }; } } diff --git a/plugins/wazuh-core/public/utils/file-saver.js b/plugins/wazuh-core/public/utils/file-saver.js index 92beea7e6a..136d1e9ffe 100644 --- a/plugins/wazuh-core/public/utils/file-saver.js +++ b/plugins/wazuh-core/public/utils/file-saver.js @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-abusive-eslint-disable */ /* eslint-disable */ /* FileSaver.js * A saveAs() FileSaver implementation. diff --git a/plugins/wazuh-core/server/services/server-api-client.ts b/plugins/wazuh-core/server/services/server-api-client.ts index cd87333fb1..e279582d68 100644 --- a/plugins/wazuh-core/server/services/server-api-client.ts +++ b/plugins/wazuh-core/server/services/server-api-client.ts @@ -10,8 +10,8 @@ * Find more information about this on the LICENSE file. */ +import https from 'node:https'; import axios, { AxiosInstance, AxiosResponse } from 'axios'; -import https from 'https'; import { Logger } from 'opensearch-dashboards/server'; import { getCookieValueByName } from './cookie'; import { ManageHosts } from './manage-hosts'; @@ -73,32 +73,33 @@ export interface ServerAPIAuthenticateOptions { * This service communicates with the Wazuh server APIs */ export class ServerAPIClient { - private _CacheInternalUserAPIHostToken: Map; - private _axios: typeof axios; - private asInternalUser: ServerAPIInternalUserClient; - private _axios: AxiosInstance; + private readonly cacheInternalUserAPIHostToken: Map; + private readonly client: AxiosInstance; + private readonly asInternalUser: ServerAPIInternalUserClient; + constructor( - private logger: Logger, // TODO: add logger as needed - private manageHosts: ManageHosts, - private dashboardSecurity: ISecurityFactory, + private readonly logger: Logger, // TODO: add logger as needed + private readonly manageHosts: ManageHosts, + private readonly dashboardSecurity: ISecurityFactory, ) { const httpsAgent = new https.Agent({ rejectUnauthorized: false, }); - this._axios = axios.create({ httpsAgent }); + + this.client = axios.create({ httpsAgent }); // Cache to save the token for the internal user by API host ID - this._CacheInternalUserAPIHostToken = new Map(); + this.cacheInternalUserAPIHostToken = new Map(); // Create internal user client this.asInternalUser = { authenticate: async apiHostID => - await this._authenticateInternalUser(apiHostID), + await this.authenticateInternalUser(apiHostID), request: async ( method: RequestHTTPMethod, path: RequestPath, data: any, options, - ) => await this._requestAsInternalUser(method, path, data, options), + ) => await this.requestAsInternalUser(method, path, data, options), }; } @@ -110,7 +111,7 @@ export class ServerAPIClient { * @param options Options. Data about the Server API ID and the token * @returns */ - private async _request( + private async request( method: RequestHTTPMethod, path: RequestPath, data: any, @@ -118,13 +119,14 @@ export class ServerAPIClient { | APIInterceptorRequestOptionsInternalUser | APIInterceptorRequestOptionsScopedUser, ): Promise { - const optionsRequest = await this._buildRequestOptions( + const optionsRequest = await this.buildRequestOptions( method, path, data, options, ); - return await this._axios(optionsRequest); + + return await this.client(optionsRequest); } /** @@ -135,20 +137,21 @@ export class ServerAPIClient { * @param options Options. Data about the Server API ID and the token * @returns */ - private async _buildRequestOptions( + private async buildRequestOptions( method: RequestHTTPMethod, path: RequestPath, data: any, { apiHostID, token }: APIInterceptorRequestOptions, ) { const api = await this.manageHosts.get(apiHostID); - const { body, params, headers, ...rest } = data; + const { body, params, headers = {}, ...rest } = data; + return { method: method, headers: { 'content-type': 'application/json', Authorization: 'Bearer ' + token, - ...(headers ? headers : {}), + ...headers, }, data: body || rest || {}, params: params || {}, @@ -162,7 +165,7 @@ export class ServerAPIClient { * @param authContext Authentication context to get the token * @returns */ - private async _authenticate( + private async authenticate( apiHostID: string, options: ServerAPIAuthenticateOptions, ): Promise { @@ -181,9 +184,9 @@ export class ServerAPIClient { }`, ...(options?.authContext ? { data: options?.authContext } : {}), }; - - const response: AxiosResponse = await this._axios(optionsRequest); + const response: AxiosResponse = await this.client(optionsRequest); const token: string = (((response || {}).data || {}).data || {}).token; + return token; } @@ -192,9 +195,11 @@ export class ServerAPIClient { * @param apiHostID Server API ID * @returns */ - private async _authenticateInternalUser(apiHostID: string): Promise { - const token = await this._authenticate(apiHostID, { useRunAs: false }); - this._CacheInternalUserAPIHostToken.set(apiHostID, token); + private async authenticateInternalUser(apiHostID: string): Promise { + const token = await this.authenticate(apiHostID, { useRunAs: false }); + + this.cacheInternalUserAPIHostToken.set(apiHostID, token); + return token; } @@ -208,17 +213,24 @@ export class ServerAPIClient { return { authenticate: async (apiHostID: string) => { const useRunAs = this.manageHosts.isEnabledAuthWithRunAs(apiHostID); + let token: string; + + if (useRunAs) { + const { authContext } = await this.dashboardSecurity.getCurrentUser( + request, + context, + ); + + token = await this.authenticate(apiHostID, { + useRunAs: true, + authContext, + }); + } else { + token = await this.authenticate(apiHostID, { + useRunAs: false, + }); + } - const token = useRunAs - ? await this._authenticate(apiHostID, { - useRunAs: true, - authContext: ( - await this.dashboardSecurity.getCurrentUser(request, context) - ).authContext, - }) - : await this._authenticate(apiHostID, { - useRunAs: false, - }); return token; }, request: async ( @@ -226,12 +238,11 @@ export class ServerAPIClient { path: string, data: any, options: APIInterceptorRequestOptionsScopedUser, - ) => { - return await this._request(method, path, data, { + ) => + await this.request(method, path, data, { ...options, token: getCookieValueByName(request.headers.cookie, 'wz-token'), - }); - }, + }), }; } @@ -243,7 +254,7 @@ export class ServerAPIClient { * @param options Options. Data about the Server API ID and the token * @returns */ - private async _requestAsInternalUser( + private async requestAsInternalUser( method: RequestHTTPMethod, path: RequestPath, data: any, @@ -251,18 +262,21 @@ export class ServerAPIClient { ) { try { const token = - this._CacheInternalUserAPIHostToken.has(options.apiHostID) && + this.cacheInternalUserAPIHostToken.has(options.apiHostID) && !options.forceRefresh - ? this._CacheInternalUserAPIHostToken.get(options.apiHostID) - : await this._authenticateInternalUser(options.apiHostID); - return await this._request(method, path, data, { ...options, token }); + ? this.cacheInternalUserAPIHostToken.get(options.apiHostID) + : await this.authenticateInternalUser(options.apiHostID); + + return await this.request(method, path, data, { ...options, token }); } catch (error) { if (error.response && error.response.status === 401) { - const token: string = await this._authenticateInternalUser( + const token: string = await this.authenticateInternalUser( options.apiHostID, ); - return await this._request(method, path, data, { ...options, token }); + + return await this.request(method, path, data, { ...options, token }); } + throw error; } } From 85c331cabd0f0675bf8c52733388186fdbc53a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 3 Dec 2024 16:39:33 +0100 Subject: [PATCH 16/77] fix(prettier): code prettier --- plugins/main/server/controllers/wazuh-api.ts | 194 +++++++++++++------ 1 file changed, 134 insertions(+), 60 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index f16177223e..9e7badb1b9 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -1,3 +1,5 @@ +/* eslint-disable unicorn/no-abusive-eslint-disable */ +/* eslint-disable */ /* * Wazuh app - Class for Wazuh-API functions * Copyright (C) 2015-2022 Wazuh, Inc. @@ -11,30 +13,28 @@ */ // Require some libraries -import { ErrorResponse } from '../lib/error-response'; +import fs from 'node:fs'; +import path from 'node:path'; import { Parser } from 'json2csv'; -import { KeyEquivalence } from '../../common/csv-key-equivalence'; -import { ApiErrorEquivalence } from '../lib/api-errors-equivalence'; -import apiRequestList from '../../common/api-info/endpoints'; -import { HTTP_STATUS_CODES } from '../../common/constants'; -import { addJobToQueue } from '../start/queue'; import jwtDecode from 'jwt-decode'; import { OpenSearchDashboardsRequest, RequestHandlerContext, OpenSearchDashboardsResponseFactory, } from 'src/core/server'; +import { ErrorResponse } from '../lib/error-response'; +import { KeyEquivalence } from '../../common/csv-key-equivalence'; +import { ApiErrorEquivalence } from '../lib/api-errors-equivalence'; +import apiRequestList from '../../common/api-info/endpoints'; +import { HTTP_STATUS_CODES } from '../../common/constants'; +import { addJobToQueue } from '../start/queue'; import { getCookieValueByName } from '../lib/cookie'; import { version as pluginVersion, revision as pluginRevision, } from '../../package.json'; -import fs from 'fs'; -import path from 'path'; export class WazuhApiCtrl { - constructor() {} - async getToken( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, @@ -46,6 +46,7 @@ export class WazuhApiCtrl { request, context, ); + if ( !force && request.headers.cookie && @@ -59,11 +60,13 @@ export class WazuhApiCtrl { request.headers.cookie, 'wz-token', ); + if (wzToken) { try { // if the current token is not a valid jwt token we ask for a new one const decodedToken = jwtDecode(wzToken); const expirationTime = decodedToken.exp - Date.now() / 1000; + if (wzToken && expirationTime > 0) { return response.ok({ body: { token: wzToken }, @@ -76,15 +79,17 @@ export class WazuhApiCtrl { } } } - const token = await context.wazuh.api.client.asCurrentUser.authenticate( - idHost, - ); + const token = + await context.wazuh.api.client.asCurrentUser.authenticate(idHost); let textSecure = ''; + if (context.wazuh.server.info.protocol === 'https') { textSecure = ';Secure'; } + const encodedUser = encodeURIComponent(username); + return response.ok({ headers: { 'set-cookie': [ @@ -99,7 +104,9 @@ export class WazuhApiCtrl { const errorMessage = `Error getting the authorization token: ${ ((error.response || {}).data || {}).detail || error.message || error }`; + context.wazuh.logger.error(errorMessage); + return ErrorResponse( errorMessage, 3000, @@ -124,11 +131,14 @@ export class WazuhApiCtrl { try { // Get config from configuration const id = request.body.id; + context.wazuh.logger.debug(`Getting server API host by ID: ${id}`); + const apiHostData = await context.wazuh_core.manageHosts.get(id, { excludePassword: true, }); const api = { ...apiHostData }; + context.wazuh.logger.debug( `Server API host data: ${JSON.stringify(api)}`, ); @@ -163,6 +173,7 @@ export class WazuhApiCtrl { ) { // Clear and update cluster information before being sent back to frontend delete api.cluster_info; + const responseAgents = await context.wazuh.api.client.asInternalUser.request( 'GET', @@ -174,7 +185,6 @@ export class WazuhApiCtrl { if (responseAgents.status === HTTP_STATUS_CODES.OK) { const managerName = responseAgents.data.data.affected_items[0].manager; - const responseClusterStatus = await context.wazuh.api.client.asInternalUser.request( 'GET', @@ -182,6 +192,7 @@ export class WazuhApiCtrl { {}, { apiHostID: id }, ); + if (responseClusterStatus.status === HTTP_STATUS_CODES.OK) { if (responseClusterStatus.data.data.enabled === 'yes') { const responseClusterLocalInfo = @@ -191,9 +202,11 @@ export class WazuhApiCtrl { {}, { apiHostID: id }, ); + if (responseClusterLocalInfo.status === HTTP_STATUS_CODES.OK) { const clusterEnabled = responseClusterStatus.data.data.enabled === 'yes'; + api.cluster_info = { status: clusterEnabled ? 'enabled' : 'disabled', manager: managerName, @@ -263,10 +276,10 @@ export class WazuhApiCtrl { } else { try { const apis = await context.wazuh_core.manageHosts.get(); + for (const api of apis) { try { const { id } = api; - const responseManagerInfo = await context.wazuh.api.client.asInternalUser.request( 'GET', @@ -285,15 +298,18 @@ export class WazuhApiCtrl { response, ); } + if (responseManagerInfo.status === HTTP_STATUS_CODES.OK) { request.body.id = id; request.body.idChanged = id; + return await this.checkStoredAPI(context, request, response); } } catch (error) {} // eslint-disable-line } } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse( error.message || error, 3020, @@ -301,7 +317,9 @@ export class WazuhApiCtrl { response, ); } + context.wazuh.logger.error(error.message || error); + return ErrorResponse( error.message || error, 3002, @@ -354,18 +372,23 @@ export class WazuhApiCtrl { ) { try { let apiAvailable = null; + // const notValid = this.validateCheckApiParams(request.body); // if (notValid) return ErrorResponse(notValid, 3003, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response); context.wazuh.logger.debug(`${request.body.id} is valid`); + // Check if a Wazuh API id is given (already stored API) const data = await context.wazuh_core.manageHosts.get(request.body.id, { excludePassword: true, }); + if (data) { apiAvailable = data; } else { const errorMessage = `The server API host entry with ID ${request.body.id} was not found`; + context.wazuh.logger.debug(errorMessage); + return ErrorResponse( errorMessage, 3029, @@ -373,11 +396,15 @@ export class WazuhApiCtrl { response, ); } + const options = { apiHostID: request.body.id }; + if (request.body.forceRefresh) { options['forceRefresh'] = request.body.forceRefresh; } + let responseManagerInfo; + try { responseManagerInfo = await context.wazuh.api.client.asInternalUser.request( @@ -396,13 +423,16 @@ export class WazuhApiCtrl { response, ); } + context.wazuh.logger.debug(`${request.body.id} credentials are valid`); + if ( responseManagerInfo.status === HTTP_STATUS_CODES.OK && responseManagerInfo.data ) { const result = await context.wazuh_core.manageHosts.getRegistryDataByHost(data); + return response.ok({ body: result, }); @@ -422,6 +452,7 @@ export class WazuhApiCtrl { response, ); } + if ( error && error.response && @@ -435,6 +466,7 @@ export class WazuhApiCtrl { response, ); } + if (error.code === 'EPROTO') { return ErrorResponse( 'Wrong protocol being used to connect to the API', @@ -443,6 +475,7 @@ export class WazuhApiCtrl { response, ); } + return ErrorResponse( error.message || error, 3005, @@ -466,6 +499,7 @@ export class WazuhApiCtrl { return isDown; } + return false; } @@ -483,23 +517,19 @@ export class WazuhApiCtrl { {}, { apiHostID: api.id }, ); - const daemons = ((((response || {}).data || {}).data || {}).affected_items || [])[0] || {}; - const isCluster = ((api || {}).cluster_info || {}).status === 'enabled' && - typeof daemons['wazuh-clusterd'] !== 'undefined'; - const wazuhdbExists = typeof daemons['wazuh-db'] !== 'undefined'; - + daemons['wazuh-clusterd'] !== undefined; + const wazuhdbExists = daemons['wazuh-db'] !== undefined; const execd = daemons['wazuh-execd'] === 'running'; const modulesd = daemons['wazuh-modulesd'] === 'running'; const wazuhdb = wazuhdbExists ? daemons['wazuh-db'] === 'running' : true; const clusterd = isCluster ? daemons['wazuh-clusterd'] === 'running' : true; - const isValid = execd && modulesd && wazuhdb && clusterd; isValid && context.wazuh.logger.debug('Wazuh is ready'); @@ -513,6 +543,7 @@ export class WazuhApiCtrl { } } catch (error) { context.wazuh.logger.error(error.message || error); + return Promise.reject(error); } } @@ -535,15 +566,18 @@ export class WazuhApiCtrl { */ async makeRequest(context, method, path, data, id, response) { const devTools = !!(data || {}).devTools; + try { let api; + try { api = await context.wazuh_core.manageHosts.get(id, { excludePassword: true, }); - } catch (error) { + } catch { context.wazuh.logger.error('Could not get host credentials'); - //Can not get credentials from wazuh-hosts + + // Can not get credentials from wazuh-hosts return ErrorResponse( 'Could not get host credentials', 3011, @@ -592,7 +626,9 @@ export class WazuhApiCtrl { data.headers['content-type'] = 'application/octet-stream'; delete data.origin; } + const delay = (data || {}).delay || 0; + if (delay) { // Remove the delay parameter that is used to add the sever API request to the queue job. // This assumes the delay parameter is not used as part of the server API request. If it @@ -618,6 +654,7 @@ export class WazuhApiCtrl { } }, }); + return response.ok({ body: { error: 0, message: 'Success' }, }); @@ -626,13 +663,16 @@ export class WazuhApiCtrl { if (path === '/ping') { try { const check = await this.checkDaemons(context, api, path); + return check; } catch (error) { const isDown = (error || {}).code === 'ECONNREFUSED'; + if (!isDown) { context.wazuh.logger.error( 'Server API is online but the server is not ready yet', ); + return ErrorResponse( `ERROR3099 - ${error.message || 'Server not ready yet'}`, 3099, @@ -653,6 +693,7 @@ export class WazuhApiCtrl { options, ); const responseIsDown = this.checkResponseIsDown(context, responseToken); + if (responseIsDown) { return ErrorResponse( `ERROR3099 - ${response.body.message || 'Server not ready yet'}`, @@ -661,7 +702,9 @@ export class WazuhApiCtrl { response, ); } + let responseBody = (responseToken || {}).data || {}; + if (!responseBody) { responseBody = typeof responseBody === 'string' && @@ -671,8 +714,9 @@ export class WazuhApiCtrl { : false; response.data = responseBody; } + const responseError = - response.status !== HTTP_STATUS_CODES.OK ? response.status : false; + response.status === HTTP_STATUS_CODES.OK ? false : response.status; if (!responseError && responseBody) { return response.ok({ @@ -685,6 +729,7 @@ export class WazuhApiCtrl { body: response.data, }); } + throw responseError && responseBody.detail ? { message: responseBody.detail, code: responseError } : new Error('Unexpected error fetching data from the API'); @@ -701,8 +746,11 @@ export class WazuhApiCtrl { response, ); } + const errorMsg = (error.response || {}).data || error.message; + context.wazuh.logger.error(errorMsg || error); + if (devTools) { return response.ok({ body: { error: '3013', message: errorMsg || error }, @@ -711,6 +759,7 @@ export class WazuhApiCtrl { if ((error || {}).code && ApiErrorEquivalence[error.code]) { error.message = ApiErrorEquivalence[error.code]; } + return ErrorResponse( errorMsg.detail || error, error.code ? `API error: ${error.code}` : 3013, @@ -734,6 +783,7 @@ export class WazuhApiCtrl { response: OpenSearchDashboardsResponseFactory, ) { const idApi = getCookieValueByName(request.headers.cookie, 'wz-api'); + if (idApi !== request.body.id) { // if the current token belongs to a different API id, we relogin to obtain a new token return ErrorResponse( @@ -743,6 +793,7 @@ export class WazuhApiCtrl { response, ); } + if (!request.body.method) { return ErrorResponse( 'Missing param: method', @@ -750,9 +801,10 @@ export class WazuhApiCtrl { HTTP_STATUS_CODES.BAD_REQUEST, response, ); - } else if (!request.body.method.match(/^(?:GET|PUT|POST|DELETE)$/)) { + } else if (!/^(?:GET|PUT|POST|DELETE)$/.test(request.body.method)) { context.wazuh.logger.error('Request method is not valid.'); - //Method is not a valid HTTP request method + + // Method is not a valid HTTP request method return ErrorResponse( 'Request method is not valid.', 3015, @@ -766,16 +818,7 @@ export class WazuhApiCtrl { HTTP_STATUS_CODES.BAD_REQUEST, response, ); - } else if (!request.body.path.startsWith('/')) { - context.wazuh.logger.error('Request path is not valid.'); - //Path doesn't start with '/' - return ErrorResponse( - 'Request path is not valid.', - 3015, - HTTP_STATUS_CODES.BAD_REQUEST, - response, - ); - } else { + } else if (request.body.path.startsWith('/')) { return this.makeRequest( context, request.body.method, @@ -784,6 +827,16 @@ export class WazuhApiCtrl { request.body.id, response, ); + } else { + context.wazuh.logger.error('Request path is not valid.'); + + // Path doesn't start with '/' + return ErrorResponse( + 'Request path is not valid.', + 3015, + HTTP_STATUS_CODES.BAD_REQUEST, + response, + ); } } @@ -800,62 +853,71 @@ export class WazuhApiCtrl { response: OpenSearchDashboardsResponseFactory, ) { try { - if (!request.body || !request.body.path) + if (!request.body || !request.body.path) { throw new Error('Field path is required'); - if (!request.body.id) throw new Error('Field id is required'); + } + + if (!request.body.id) { + throw new Error('Field id is required'); + } const filters = Array.isArray(((request || {}).body || {}).filters) ? request.body.filters : []; - let tmpPath = request.body.path; if (tmpPath && typeof tmpPath === 'string') { - tmpPath = tmpPath[0] === '/' ? tmpPath.substr(1) : tmpPath; + tmpPath = tmpPath[0] === '/' ? tmpPath.slice(1) : tmpPath; } - if (!tmpPath) throw new Error('An error occurred parsing path field'); + if (!tmpPath) { + throw new Error('An error occurred parsing path field'); + } context.wazuh.logger.debug(`Report ${tmpPath}`); + // Real limit, regardless the user query const params = { limit: 500 }; - if (filters.length) { + if (filters.length > 0) { for (const filter of filters) { - if (!filter.name || !filter.value) continue; + if (!filter.name || !filter.value) { + continue; + } + params[filter.name] = filter.value; } } let itemsArray = []; - const output = await context.wazuh.api.client.asCurrentUser.request( 'GET', `/${tmpPath}`, { params: params }, { apiHostID: request.body.id }, ); - const isList = request.body.path.includes('/lists') && request.body.filters && request.body.filters.length && request.body.filters.find(filter => filter._isCDBList); - const totalItems = (((output || {}).data || {}).data || {}) .total_affected_items; if (totalItems && !isList) { params.offset = 0; itemsArray.push(...output.data.data.affected_items); + while (itemsArray.length < totalItems && params.offset < totalItems) { params.offset += params.limit; + const tmpData = await context.wazuh.api.client.asCurrentUser.request( 'GET', `/${tmpPath}`, { params: params }, { apiHostID: request.body.id }, ); + itemsArray.push(...tmpData.data.data.affected_items); } } @@ -898,16 +960,21 @@ export class WazuhApiCtrl { if (isArrayOfLists) { const flatLists = []; + for (const list of itemsArray) { const { relative_dirname, items } = list; + flatLists.push( - ...items.map(item => ({ - relative_dirname, - key: item.key, - value: item.value, - })), + ...items.map(item => { + return { + relative_dirname, + key: item.key, + value: item.value, + }; + }), ); } + fields = ['relative_dirname', 'key', 'value']; itemsArray = [...flatLists]; } @@ -916,13 +983,17 @@ export class WazuhApiCtrl { fields = ['key', 'value']; itemsArray = output.data.data.affected_items[0].items; } - fields = fields.map(item => ({ value: item, default: '-' })); - const json2csvParser = new Parser({ fields }); + fields = fields.map(item => { + return { value: item, default: '-' }; + }); + const json2csvParser = new Parser({ fields }); let csv = json2csvParser.parse(itemsArray); + for (const field of fields) { const { value } = field; + if (csv.includes(value)) { csv = csv.replace(value, KeyEquivalence[value] || value); } @@ -950,6 +1021,7 @@ export class WazuhApiCtrl { } } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse( error.message || error, 3034, @@ -965,7 +1037,7 @@ export class WazuhApiCtrl { request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory, ) { - //Read a static JSON until the api call has implemented + // Read a static JSON until the api call has implemented return response.ok({ body: apiRequestList, }); @@ -996,6 +1068,7 @@ export class WazuhApiCtrl { }); } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse( `Could not get data from wazuh-version registry due to ${ error.message || error @@ -1021,12 +1094,12 @@ export class WazuhApiCtrl { ) { try { const apiHostID = getCookieValueByName(request.headers.cookie, 'wz-api'); + if (!request.params || !apiHostID || !request.params.agent) { throw new Error('Agent ID and API ID are required'); } const { agent } = request.params; - const data = await Promise.all([ context.wazuh.api.client.asInternalUser.request( 'GET', @@ -1041,19 +1114,17 @@ export class WazuhApiCtrl { { apiHostID }, ), ]); - const result = data.map(item => (item.data || {}).data || []); const [hardwareResponse, osResponse] = result; - // Fill syscollector object const syscollector = { hardware: typeof hardwareResponse === 'object' && - Object.keys(hardwareResponse).length + Object.keys(hardwareResponse).length > 0 ? { ...hardwareResponse.affected_items[0] } : false, os: - typeof osResponse === 'object' && Object.keys(osResponse).length + typeof osResponse === 'object' && Object.keys(osResponse).length > 0 ? { ...osResponse.affected_items[0] } : false, }; @@ -1063,6 +1134,7 @@ export class WazuhApiCtrl { }); } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse( error.message || error, 3035, @@ -1086,7 +1158,6 @@ export class WazuhApiCtrl { try { const APP_LOGO = 'customization.logo.app'; const HEALTHCHECK_LOGO = 'customization.logo.healthcheck'; - const logos = { [APP_LOGO]: await context.wazuh_core.configuration.getCustomizationSetting( @@ -1103,6 +1174,7 @@ export class WazuhApiCtrl { }); } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse( error.message || error, 3035, @@ -1111,6 +1183,7 @@ export class WazuhApiCtrl { ); } } + async getAppDashboards( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, @@ -1131,6 +1204,7 @@ export class WazuhApiCtrl { }); } catch (error) { context.wazuh.logger.error(error.message || error); + return ErrorResponse(error.message || error, 5030, 500, response); } } From 2e8cfcc28625026ba085b79bb6c7bb8db7ea5249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 3 Dec 2024 16:46:38 +0100 Subject: [PATCH 17/77] fix(lint): code lint --- .../components/search-bar/query-language/index.ts | 13 ++++++++----- .../wazuh-core/public/services/http/constants.ts | 4 ++-- .../public/services/http/generic-client.ts | 4 ++-- .../public/services/http/server-client.ts | 8 ++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts index ba9a0554c4..95dbaf9f73 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts +++ b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts @@ -1,7 +1,7 @@ import { AQL } from './aql'; import { WQL } from './wql'; -type SearchBarQueryLanguage = { +interface SearchBarQueryLanguage { description: string; documentationLink?: string; id: string; @@ -22,15 +22,18 @@ type SearchBarQueryLanguage = { unifiedQuery: string, options: { configuration: any; parameters: any }, ) => string; -}; +} // Register the query languages -export const searchBarQueryLanguages: { - [key: string]: SearchBarQueryLanguage; -} = [AQL, WQL].reduce((accum, item) => { +export const searchBarQueryLanguages: Record = [ + AQL, + WQL, + // eslint-disable-next-line unicorn/no-array-reduce +].reduce((accum, item) => { if (accum[item.id]) { throw new Error(`Query language with id: ${item.id} already registered.`); } + return { ...accum, [item.id]: item, diff --git a/plugins/wazuh-core/public/services/http/constants.ts b/plugins/wazuh-core/public/services/http/constants.ts index 8ea7ec6d05..93a7e74f16 100644 --- a/plugins/wazuh-core/public/services/http/constants.ts +++ b/plugins/wazuh-core/public/services/http/constants.ts @@ -1,5 +1,5 @@ -export const PLUGIN_PLATFORM_REQUEST_HEADERS = { +export const pluginPlatformRequestHeaders = { 'osd-xsrf': 'kibana', }; -export const HTTP_CLIENT_DEFAULT_TIMEOUT = 20000; +export const HTTP_CLIENT_DEFAULT_TIMEOUT = 20_000; diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts index 92dc9fca23..a72c0d2904 100644 --- a/plugins/wazuh-core/public/services/http/generic-client.ts +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -1,5 +1,5 @@ import { Logger } from '../../../common/services/configuration'; -import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import { pluginPlatformRequestHeaders } from './constants'; import { HTTPClientGeneric, HTTPClientRequestInterceptor, @@ -36,7 +36,7 @@ export class GenericRequest implements HTTPClientGeneric { const timeout = await this.services.getTimeout(); const requestHeaders = { - ...PLUGIN_PLATFORM_REQUEST_HEADERS, + ...pluginPlatformRequestHeaders, 'content-type': 'application/json', }; const url = this.services.getURL(path); diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 56ce444b19..65e4c08c54 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -19,7 +19,7 @@ import { WzRequestServices, ServerAPIResponseItemsDataHTTPClient, } from './types'; -import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; +import { pluginPlatformRequestHeaders } from './constants'; export class WzRequest implements HTTPClientServer { onErrorInterceptor?: ( @@ -90,7 +90,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: method, headers: { - ...PLUGIN_PLATFORM_REQUEST_HEADERS, + ...pluginPlatformRequestHeaders, 'content-type': 'application/json', ...overwriteHeaders, }, @@ -441,7 +441,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: 'POST', headers: { - ...PLUGIN_PLATFORM_REQUEST_HEADERS, + ...pluginPlatformRequestHeaders, 'content-type': 'application/json', }, url: url, @@ -483,7 +483,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: 'POST', headers: { - ...PLUGIN_PLATFORM_REQUEST_HEADERS, + ...pluginPlatformRequestHeaders, 'content-type': 'application/json', }, url: url, From f61763fd0f119f83f3b477c4edbd4c4b267b8245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 4 Dec 2024 09:00:55 +0100 Subject: [PATCH 18/77] fix(lint): code lint --- plugins/wazuh-core/public/components/table-data/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-core/public/components/table-data/types.ts b/plugins/wazuh-core/public/components/table-data/types.ts index bd5d6580ab..9fe33650b3 100644 --- a/plugins/wazuh-core/public/components/table-data/types.ts +++ b/plugins/wazuh-core/public/components/table-data/types.ts @@ -19,7 +19,7 @@ export interface TableDataProps { * Enable the action to reload the data */ showActionReload?: boolean; - onDataChange?: Function; + onDataChange?: (data: any) => void; onReload?: (newValue: number) => void; /** * Fetch context @@ -33,7 +33,7 @@ export interface TableDataProps { pagination: EuiBasicTableProps['pagination']; sorting: EuiBasicTableProps['sorting']; }) => Promise<{ items: any[]; totalItems: number }>; - onFetchContextChange?: Function; + onFetchContextChange?: (context: any) => void; /** * Columns for the table */ From 38bede6c0451fdfca6ffc082af1773f18af4422e Mon Sep 17 00:00:00 2001 From: Antonio <34042064+Desvelao@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:43:38 +0100 Subject: [PATCH 19/77] Update plugins/wazuh-core/public/services/http/server-client.ts Co-authored-by: Guido Modarelli <38738725+guidomodarelli@users.noreply.github.com> --- plugins/wazuh-core/public/services/http/server-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 65e4c08c54..803964d212 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -47,7 +47,7 @@ export class WzRequest implements HTTPClientServer { } /** - * Permorn a generic request + * Perform a generic request * @param {String} method * @param {String} path * @param {Object} payload From f433f64fc893c2ba1f7c8eeeb210f5db7032451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 5 Dec 2024 09:49:09 +0100 Subject: [PATCH 20/77] fix(http): review suggestions --- plugins/wazuh-core/public/services/http/server-client.test.ts | 2 +- plugins/wazuh-core/public/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.test.ts b/plugins/wazuh-core/public/services/http/server-client.test.ts index be8a1d8608..878e30a82f 100644 --- a/plugins/wazuh-core/public/services/http/server-client.test.ts +++ b/plugins/wazuh-core/public/services/http/server-client.test.ts @@ -87,7 +87,7 @@ describe('Create client', () => { }); }); - it.only('Unauthentication', done => { + it('Unauthentication', done => { const { client } = createClient(); client.unauth().then(data => { diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index d3cb3a5b6c..c91ce07595 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -26,7 +26,7 @@ export interface WazuhCorePluginSetup { ) => React.ComponentType>; }; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface WazuhCorePluginStart { hooks: { useDockedSideNav: UseDockedSideNav; From eb3516baa7b977a6c715cf9dfb2121fbb0e33cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 5 Dec 2024 12:08:26 +0100 Subject: [PATCH 21/77] fix(http): review suggestions --- .../public/services/http/server-client.ts | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 803964d212..d85c7272a5 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -21,6 +21,16 @@ import { } from './types'; import { pluginPlatformRequestHeaders } from './constants'; +interface RequestInternalOptions { + shouldRetry?: boolean; + checkCurrentApiIsUp?: boolean; + overwriteHeaders?: any; +} + +type RequestOptions = RequestInternalOptions & { + returnOriginalResponse?: boolean; +}; + export class WzRequest implements HTTPClientServer { onErrorInterceptor?: ( error: any, @@ -55,31 +65,14 @@ export class WzRequest implements HTTPClientServer { private async requestInternal( method: HTTPVerb, path: string, + // eslint-disable-next-line default-param-last payload: any = null, - // eslint-disable-next-line unicorn/no-object-as-default-parameter - extraOptions: { - shouldRetry?: boolean; - checkCurrentApiIsUp?: boolean; - overwriteHeaders?: any; - } = { - shouldRetry: true, - checkCurrentApiIsUp: true, - overwriteHeaders: {}, - }, + { + shouldRetry = true, + checkCurrentApiIsUp = true, + overwriteHeaders = {}, + }: RequestInternalOptions, ): Promise { - const shouldRetry = - typeof extraOptions.shouldRetry === 'boolean' - ? extraOptions.shouldRetry - : true; - const checkCurrentApiIsUp = - typeof extraOptions.checkCurrentApiIsUp === 'boolean' - ? extraOptions.checkCurrentApiIsUp - : true; - const overwriteHeaders = - typeof extraOptions.overwriteHeaders === 'object' - ? extraOptions.overwriteHeaders - : {}; - try { if (!method || !path) { throw new Error('Missing parameters'); @@ -171,25 +164,24 @@ export class WzRequest implements HTTPClientServer { method: HTTPVerb, path: string, body: any, - // eslint-disable-next-line unicorn/no-object-as-default-parameter - options: { - checkCurrentApiIsUp?: boolean; - returnOriginalResponse?: boolean; - } = { checkCurrentApiIsUp: true, returnOriginalResponse: false }, + { + checkCurrentApiIsUp = true, + returnOriginalResponse = false, + ...restRequestInternalOptions + }: RequestOptions, ): Promise> { try { if (!method || !path || !body) { throw new Error('Missing parameters'); } - const { returnOriginalResponse, ...optionsToGenericReq } = options; const id = this.services.getServerAPI(); const requestData = { method, path, body, id }; const response = await this.requestInternal( 'POST', '/api/request', requestData, - optionsToGenericReq, + { ...restRequestInternalOptions, checkCurrentApiIsUp }, ); if (returnOriginalResponse) { @@ -451,7 +443,7 @@ export class WzRequest implements HTTPClientServer { const response = await this.services.request(options); if (response.error) { - throw this.returnErrorInstance(response); + throw this.returnErrorInstance(response); // FIXME: this could cause an expected error due to missing message argument or wrong response argument when this should be a string according to the implementation of returnErrorInstance } return response; @@ -460,7 +452,8 @@ export class WzRequest implements HTTPClientServer { // TODO: implement // const wzMisc = new WzMisc(); // wzMisc.setApiIsDown(true); - const response = (error.response.data || {}).message || error.message; + const response: string = + (error.response.data || {}).message || error.message; throw this.returnErrorInstance(response); } else { @@ -493,7 +486,7 @@ export class WzRequest implements HTTPClientServer { const response = await this.services.request(options); if (response.error) { - throw this.returnErrorInstance(response); + throw this.returnErrorInstance(response); // FIXME: this could cause an expected error due to missing message argument or wrong response argument when this should be a string according to the implementation of returnErrorInstance } return response; From 4fcad8f78209898dd54e02b3c3e5737c7f852895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 5 Dec 2024 16:41:32 +0100 Subject: [PATCH 22/77] fix(http): fix options paramenter in http client --- .../public/services/http/server-client.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index d85c7272a5..62c9c3823f 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -67,12 +67,15 @@ export class WzRequest implements HTTPClientServer { path: string, // eslint-disable-next-line default-param-last payload: any = null, - { - shouldRetry = true, - checkCurrentApiIsUp = true, - overwriteHeaders = {}, - }: RequestInternalOptions, + options: RequestInternalOptions, ): Promise { + const { shouldRetry, checkCurrentApiIsUp, overwriteHeaders } = { + shouldRetry: true, + checkCurrentApiIsUp: true, + overwriteHeaders: {}, + ...options, + }; + try { if (!method || !path) { throw new Error('Missing parameters'); @@ -164,12 +167,18 @@ export class WzRequest implements HTTPClientServer { method: HTTPVerb, path: string, body: any, - { - checkCurrentApiIsUp = true, - returnOriginalResponse = false, - ...restRequestInternalOptions - }: RequestOptions, + options: RequestOptions, ): Promise> { + const { + checkCurrentApiIsUp, + returnOriginalResponse, + ...restRequestInternalOptions + } = { + checkCurrentApiIsUp: true, + returnOriginalResponse: false, + ...options, + }; + try { if (!method || !path || !body) { throw new Error('Missing parameters'); From 2dcdc1011e5c9706cce4e185a774b81f829ed39b Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Thu, 5 Dec 2024 18:07:43 -0300 Subject: [PATCH 23/77] fix(lint): simplify arrow-body-style configuration in ESLint settings --- .eslintrc.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index cd4a3b7dec..37160573d5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,11 +56,7 @@ module.exports = { ], 'prefer-arrow-callback': 'error', 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], - 'arrow-body-style': [ - 'error', - 'as-needed', - { requireReturnForObjectLiteral: true }, - ], + 'arrow-body-style': ['error', 'as-needed'], 'no-unreachable': 'error', 'no-fallthrough': [ 'error', From a907cd864e889bc2b9fc9f85d5541a5779c4a66e Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Thu, 5 Dec 2024 18:09:02 -0300 Subject: [PATCH 24/77] fix(types): enhance SearchBarQueryLanguage and refine query language initialization logic --- .../search-bar/query-language/index.ts | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/index.ts b/plugins/main/public/components/search-bar/query-language/index.ts index 95dbaf9f73..abb9fcf231 100644 --- a/plugins/main/public/components/search-bar/query-language/index.ts +++ b/plugins/main/public/components/search-bar/query-language/index.ts @@ -1,41 +1,58 @@ +import { suggestItem } from '../../wz-search-bar'; import { AQL } from './aql'; import { WQL } from './wql'; +interface SearchBarProps { + suggestions: suggestItem[]; + onItemClick: (currentInput: string) => (item: suggestItem) => void; + prepend?: React.ReactNode; + disableFocusTrap?: boolean; + isInvalid?: boolean; + onKeyPress?: (event: React.KeyboardEvent) => void; +} + interface SearchBarQueryLanguage { - description: string; - documentationLink?: string; id: string; label: string; + description: string; + documentationLink?: string; getConfiguration?: () => any; run: ( input: string | undefined, params: any, ) => Promise<{ - searchBarProps: any; + filterButtons?: React.ReactElement | null; + searchBarProps: SearchBarProps; output: { language: string; - unifiedQuery: string; + unifiedQuery?: string; + apiQuery?: { + q: string; + }; query: string; }; }>; - transformInput: ( + transformInput?: ( unifiedQuery: string, options: { configuration: any; parameters: any }, ) => string; + transformUQLToQL?: (unifiedQuery: string) => string; } // Register the query languages -export const searchBarQueryLanguages: Record = [ - AQL, - WQL, - // eslint-disable-next-line unicorn/no-array-reduce -].reduce((accum, item) => { - if (accum[item.id]) { - throw new Error(`Query language with id: ${item.id} already registered.`); +function initializeSearchBarQueryLanguages() { + const languages = [AQL, WQL]; + const result: Record = {}; + + for (const item of languages) { + if (result[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + + result[item.id] = item; } - return { - ...accum, - [item.id]: item, - }; -}, {}); + return result; +} + +export const searchBarQueryLanguages = initializeSearchBarQueryLanguages(); From b4819d23186bc37418dcdfc1dcd517d89641a2c6 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Thu, 5 Dec 2024 18:47:24 -0300 Subject: [PATCH 25/77] fix(lint): update ESLint rules for unicorn and TypeScript to improve code quality and reduce false positives --- .eslintrc.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 37160573d5..e02504035e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -159,6 +159,7 @@ module.exports = { /* -------------------------------------------------------------------------- */ /* unicorn */ /* -------------------------------------------------------------------------- */ + 'unicorn/prefer-module': 'off', 'unicorn/prefer-ternary': 'off', // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/custom-error-definition.md 'unicorn/custom-error-definition': 'error', @@ -199,6 +200,31 @@ module.exports = { /* -------------------------------------------------------------------------- */ /* @typescript-eslint */ /* -------------------------------------------------------------------------- */ + '@typescript-eslint/no-unused-vars': [ + 'error', + { + // Whether to check all, some, or no arguments. + args: 'after-used', + // Regular expressions of argument names to not check for usage. + argsIgnorePattern: '^_', + // Whether to check catch block arguments. + caughtErrors: 'all', + // Regular expressions of catch block argument names to not check for usage. + caughtErrorsIgnorePattern: '^_', + // Regular expressions of destructured array variable names to not check for usage. + destructuredArrayIgnorePattern: '^_', + // Whether to ignore classes with at least one static initialization block. + ignoreClassWithStaticInitBlock: false, + // Whether to ignore sibling properties in `...` destructurings. + ignoreRestSiblings: false, + // Whether to report variables that match any of the valid ignore pattern options if they have been used. + reportUsedIgnorePattern: true, + // Whether to check all variables or only locally-declared variables. + vars: 'all', + // Regular expressions of variable names to not check for usage. + varsIgnorePattern: '[iI]gnored$', + }, + ], '@typescript-eslint/prefer-readonly': 'error', '@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/method-signature-style': ['error', 'property'], @@ -206,7 +232,11 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/naming-convention': [ 'error', - { selector: 'default', format: ['camelCase'] }, + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, { selector: 'import', format: ['camelCase', 'PascalCase'] }, { selector: 'variable', From 8d99e4125ce105799243ee230019b2e4920c87ba Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Thu, 5 Dec 2024 18:47:31 -0300 Subject: [PATCH 26/77] fix(lint): refine ESLint rules and clean up code with consistent error handling and improved variable naming conventions --- plugins/main/server/controllers/wazuh-api.ts | 63 +++++++++----------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 9e7badb1b9..2a26f1c195 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -1,5 +1,4 @@ -/* eslint-disable unicorn/no-abusive-eslint-disable */ -/* eslint-disable */ +/* eslint-disable no-await-in-loop */ /* * Wazuh app - Class for Wazuh-API functions * Copyright (C) 2015-2022 Wazuh, Inc. @@ -52,7 +51,7 @@ export class WazuhApiCtrl { request.headers.cookie && username === decodeURIComponent( - getCookieValueByName(request.headers.cookie, 'wz-user'), + getCookieValueByName(request.headers.cookie, 'wz-user') as string, ) && idHost === getCookieValueByName(request.headers.cookie, 'wz-api') ) { @@ -305,7 +304,9 @@ export class WazuhApiCtrl { return await this.checkStoredAPI(context, request, response); } - } catch (error) {} // eslint-disable-line + } catch { + /* empty */ + } } } catch (error) { context.wazuh.logger.error(error.message || error); @@ -371,8 +372,6 @@ export class WazuhApiCtrl { response: OpenSearchDashboardsResponseFactory, ) { try { - let apiAvailable = null; - // const notValid = this.validateCheckApiParams(request.body); // if (notValid) return ErrorResponse(notValid, 3003, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response); context.wazuh.logger.debug(`${request.body.id} is valid`); @@ -382,9 +381,7 @@ export class WazuhApiCtrl { excludePassword: true, }); - if (data) { - apiAvailable = data; - } else { + if (!data) { const errorMessage = `The server API host entry with ID ${request.body.id} was not found`; context.wazuh.logger.debug(errorMessage); @@ -492,10 +489,11 @@ export class WazuhApiCtrl { const status = (response.data || {}).status || 1; const isDown = socketErrorCodes.includes(status); - isDown && + if (isDown) { context.wazuh.logger.error( 'Server API is online but the server is not ready yet', ); + } return isDown; } @@ -532,7 +530,9 @@ export class WazuhApiCtrl { : true; const isValid = execd && modulesd && wazuhdb && clusterd; - isValid && context.wazuh.logger.debug('Wazuh is ready'); + if (isValid) { + context.wazuh.logger.debug('Wazuh is ready'); + } if (path === '/ping') { return { isValid }; @@ -544,13 +544,12 @@ export class WazuhApiCtrl { } catch (error) { context.wazuh.logger.error(error.message || error); - return Promise.reject(error); + throw error; } } - sleep(timeMs) { - // eslint-disable-next-line - return new Promise((resolve, reject) => { + sleep(timeMs: number) { + return new Promise(resolve => { setTimeout(resolve, timeMs); }); } @@ -899,7 +898,7 @@ export class WazuhApiCtrl { const isList = request.body.path.includes('/lists') && request.body.filters && - request.body.filters.length && + request.body.filters.length > 0 && request.body.filters.find(filter => filter._isCDBList); const totalItems = (((output || {}).data || {}).data || {}) .total_affected_items; @@ -923,7 +922,7 @@ export class WazuhApiCtrl { } if (totalItems) { - const { path, filters } = request.body; + const { path, filters: filtersIgnored } = request.body; const isArrayOfLists = path.includes('/lists') && !isList; const isAgents = path.includes('/agents') && !path.includes('groups'); const isAgentsOfGroup = path.startsWith('/agents/groups/'); @@ -962,16 +961,14 @@ export class WazuhApiCtrl { const flatLists = []; for (const list of itemsArray) { - const { relative_dirname, items } = list; + const { relative_dirname: relativeDirname, items } = list; flatLists.push( - ...items.map(item => { - return { - relative_dirname, - key: item.key, - value: item.value, - }; - }), + ...items.map(item => ({ + relative_dirname: relativeDirname, + key: item.key, + value: item.value, + })), ); } @@ -984,9 +981,7 @@ export class WazuhApiCtrl { itemsArray = output.data.data.affected_items[0].items; } - fields = fields.map(item => { - return { value: item, default: '-' }; - }); + fields = fields.map(item => ({ value: item, default: '-' })); const json2csvParser = new Parser({ fields }); let csv = json2csvParser.parse(itemsArray); @@ -1156,16 +1151,16 @@ export class WazuhApiCtrl { response: OpenSearchDashboardsResponseFactory, ) { try { - const APP_LOGO = 'customization.logo.app'; - const HEALTHCHECK_LOGO = 'customization.logo.healthcheck'; + const appLogo = 'customization.logo.app'; + const healthcheckLogo = 'customization.logo.healthcheck'; const logos = { - [APP_LOGO]: + [appLogo]: await context.wazuh_core.configuration.getCustomizationSetting( - APP_LOGO, + appLogo, ), - [HEALTHCHECK_LOGO]: + [healthcheckLogo]: await context.wazuh_core.configuration.getCustomizationSetting( - HEALTHCHECK_LOGO, + healthcheckLogo, ), }; From 0f8289e83c5d16c1499dcd66b65516a04f04230b Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:04:34 -0300 Subject: [PATCH 27/77] fix(lint): add rule for optional chaining and update WazuhApiCtrl for consistent use of optional chaining operator throughout --- .eslintrc.js | 5 +++ plugins/main/server/controllers/wazuh-api.ts | 44 +++++++------------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e02504035e..a9e09787ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,6 +53,11 @@ module.exports = { message: "Don't use arrow functions in class properties. Use a function instead.", }, + { + selector: + 'MemberExpression > LogicalExpression[operator="||"]:has(Identifier)', + message: 'Use optional chaining operator instead (?.).', + }, ], 'prefer-arrow-callback': 'error', 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 2a26f1c195..ce9bf34c13 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -101,7 +101,7 @@ export class WazuhApiCtrl { }); } catch (error) { const errorMessage = `Error getting the authorization token: ${ - ((error.response || {}).data || {}).detail || error.message || error + error.response?.data?.detail || error.message || error }`; context.wazuh.logger.error(errorMessage); @@ -486,7 +486,7 @@ export class WazuhApiCtrl { if (response.status !== HTTP_STATUS_CODES.OK) { // Avoid "Error communicating with socket" like errors const socketErrorCodes = [1013, 1014, 1017, 1018, 1019]; - const status = (response.data || {}).status || 1; + const status = response.data?.status || 1; const isDown = socketErrorCodes.includes(status); if (isDown) { @@ -515,11 +515,9 @@ export class WazuhApiCtrl { {}, { apiHostID: api.id }, ); - const daemons = - ((((response || {}).data || {}).data || {}).affected_items || [])[0] || - {}; + const daemons = response?.data?.data?.affected_items?.[0] || {}; const isCluster = - ((api || {}).cluster_info || {}).status === 'enabled' && + api?.cluster_info?.status === 'enabled' && daemons['wazuh-clusterd'] !== undefined; const wazuhdbExists = daemons['wazuh-db'] !== undefined; const execd = daemons['wazuh-execd'] === 'running'; @@ -564,7 +562,7 @@ export class WazuhApiCtrl { * @returns {Object} API response or ErrorResponse */ async makeRequest(context, method, path, data, id, response) { - const devTools = !!(data || {}).devTools; + const devTools = !!data?.devTools; try { let api; @@ -602,31 +600,22 @@ export class WazuhApiCtrl { }; // Set content type application/xml if needed - if ( - typeof (data || {}).body === 'string' && - (data || {}).origin === 'xmleditor' - ) { + if (typeof data?.body === 'string' && data?.origin === 'xmleditor') { data.headers['content-type'] = 'application/xml'; delete data.origin; } - if ( - typeof (data || {}).body === 'string' && - (data || {}).origin === 'json' - ) { + if (typeof data?.body === 'string' && data?.origin === 'json') { data.headers['content-type'] = 'application/json'; delete data.origin; } - if ( - typeof (data || {}).body === 'string' && - (data || {}).origin === 'raw' - ) { + if (typeof data?.body === 'string' && data?.origin === 'raw') { data.headers['content-type'] = 'application/octet-stream'; delete data.origin; } - const delay = (data || {}).delay || 0; + const delay = data?.delay || 0; if (delay) { // Remove the delay parameter that is used to add the sever API request to the queue job. @@ -665,7 +654,7 @@ export class WazuhApiCtrl { return check; } catch (error) { - const isDown = (error || {}).code === 'ECONNREFUSED'; + const isDown = error?.code === 'ECONNREFUSED'; if (!isDown) { context.wazuh.logger.error( @@ -702,7 +691,7 @@ export class WazuhApiCtrl { ); } - let responseBody = (responseToken || {}).data || {}; + let responseBody = responseToken?.data || {}; if (!responseBody) { responseBody = @@ -746,7 +735,7 @@ export class WazuhApiCtrl { ); } - const errorMsg = (error.response || {}).data || error.message; + const errorMsg = error.response?.data || error.message; context.wazuh.logger.error(errorMsg || error); @@ -755,7 +744,7 @@ export class WazuhApiCtrl { body: { error: '3013', message: errorMsg || error }, }); } else { - if ((error || {}).code && ApiErrorEquivalence[error.code]) { + if (error?.code && ApiErrorEquivalence[error.code]) { error.message = ApiErrorEquivalence[error.code]; } @@ -860,7 +849,7 @@ export class WazuhApiCtrl { throw new Error('Field id is required'); } - const filters = Array.isArray(((request || {}).body || {}).filters) + const filters = Array.isArray(request?.body?.filters) ? request.body.filters : []; let tmpPath = request.body.path; @@ -900,8 +889,7 @@ export class WazuhApiCtrl { request.body.filters && request.body.filters.length > 0 && request.body.filters.find(filter => filter._isCDBList); - const totalItems = (((output || {}).data || {}).data || {}) - .total_affected_items; + const totalItems = output?.data?.data?.total_affected_items; if (totalItems && !isList) { params.offset = 0; @@ -1109,7 +1097,7 @@ export class WazuhApiCtrl { { apiHostID }, ), ]); - const result = data.map(item => (item.data || {}).data || []); + const result = data.map(item => item.data?.data || []); const [hardwareResponse, osResponse] = result; // Fill syscollector object const syscollector = { From 9525a4b24be70d4c9d1f1bb008121440ec08a4ec Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:29:27 -0300 Subject: [PATCH 28/77] fix(lint): simplify state update with optional chaining and improve clarity in search bar component code structure --- .../public/components/search-bar/index.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/index.tsx b/plugins/wazuh-core/public/components/search-bar/index.tsx index d10d2ef2a1..1681103b89 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.tsx @@ -126,13 +126,11 @@ export const SearchBar = ({ closeSuggestionPopover: () => setIsOpenSuggestionPopover(false), openSuggestionPopover: () => setIsOpenSuggestionPopover(true), setQueryLanguageConfiguration: (configuration: any) => - setQueryLanguage(state => { - return { - ...state, - configuration: - configuration?.(state.configuration) || configuration, - }; - }), + setQueryLanguage(state => ({ + ...state, + configuration: + configuration?.(state.configuration) || configuration, + })), setQueryLanguageOutput: setQueryLanguageOutputRun, inputRef, queryLanguage: { @@ -221,12 +219,10 @@ export const SearchBar = ({ { - return { - value: id, - text: searchBarQueryLanguages[id].label, - }; - })} + options={modes.map(({ id }) => ({ + value: id, + text: searchBarQueryLanguages[id].label, + }))} value={queryLanguage.id} onChange={( event: React.ChangeEvent, From 824b184873dc4cf3145459b0eeef280a4a551b72 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:29:55 -0300 Subject: [PATCH 29/77] fix(lint): optimize settings update and clean up settings grouping logic for improved readability and performance in configuration.ts --- .../common/services/configuration.ts | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 5251403fe8..37599095af 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -153,8 +153,8 @@ export interface TConfigurationSettingCategory { documentationLink?: string; renderOrder?: number; } - type TConfigurationSettings = Record; + export interface IConfigurationStore { setup: () => Promise; start: () => Promise; @@ -417,13 +417,12 @@ export class Configuration implements IConfiguration { if (settings.length > 0) { this.logger.debug(`Reset settings: ${settings.join(', ')}`); - // eslint-disable-next-line unicorn/no-array-reduce - const updatedSettings = settings.reduce((accum, settingKey: string) => { - return { - ...accum, - [settingKey]: this.getSettingValue(settingKey), - }; - }, {}); + const updatedSettings = Object.fromEntries( + settings.map((settingKey: string) => [ + settingKey, + this.getSettingValue(settingKey), + ]), + ); const responseStore = await this.store.set(updatedSettings); this.logger.info('Settings were reset'); @@ -536,48 +535,44 @@ export class Configuration implements IConfiguration { } groupSettingsByCategory( - // eslint-disable-next-line @typescript-eslint/naming-convention - _settings: string[] | null = null, + settings: string[] | null = null, filterFunction: | ((setting: TConfigurationSettingWithKey) => boolean) | null = null, ) { - const settings = ( - _settings && Array.isArray(_settings) + const settingsMapped = ( + settings && Array.isArray(settings) ? [...this._settings.entries()].filter(([key]) => - _settings.includes(key), + settings.includes(key), ) : [...this._settings.entries()] - ).map(([key, value]) => { - return { - ...value, - key, - }; - }); + ).map(([key, value]) => ({ + ...value, + key, + })); const settingsSortedByCategories = ( filterFunction - ? settings.filter(element => filterFunction(element)) - : settings + ? settingsMapped.filter(element => filterFunction(element)) + : settingsMapped ) .sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)) // eslint-disable-next-line unicorn/no-array-reduce - .reduce((accum, pluginSettingConfiguration) => { - return { + .reduce( + (accum, pluginSettingConfiguration) => ({ ...accum, [pluginSettingConfiguration.category]: [ ...(accum[pluginSettingConfiguration.category] || []), { ...pluginSettingConfiguration }, ], - }; - }, {}); + }), + {}, + ); return Object.entries(settingsSortedByCategories) - .map(([category, settings]) => { - return { - category: this._categories.get(String(category)), - settings, - }; - }) + .map(([category, settings]) => ({ + category: this._categories.get(String(category)), + settings, + })) .filter(categoryEntry => categoryEntry.settings.length); } } From 642d7498b77431c97827006e45def8cdc54a9491 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:30:13 -0300 Subject: [PATCH 30/77] fix(lint): enhance rule for optional chaining to include empty ObjectExpression for better code quality and consistency --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index a9e09787ea..95c8c3aa19 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,7 +55,7 @@ module.exports = { }, { selector: - 'MemberExpression > LogicalExpression[operator="||"]:has(Identifier)', + 'MemberExpression > LogicalExpression[operator="||"]:has(Identifier):has(ObjectExpression[properties.length=0])', message: 'Use optional chaining operator instead (?.).', }, ], From e0895263e7a63af9ca923a0694a881b1b28e6fee Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:32:48 -0300 Subject: [PATCH 31/77] fix(lint): simplify array mapping in AQL tests for improved readability and consistency in test case logic --- .../search-bar/query-language/aql.test.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx index a2a726cd7f..2a931489aa 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx @@ -100,13 +100,11 @@ describe('Query language - AQL', () => { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => { - return { - type: 'field', - label, - description, - }; - }); + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); }, // eslint-disable-next-line default-param-last value(currentValue = '', { previousField }) { @@ -114,17 +112,13 @@ describe('Query language - AQL', () => { case 'field': { return ['value', 'value2', 'value3', 'value4'] .filter(value => value.startsWith(currentValue)) - .map(value => { - return { type: 'value', label: value }; - }); + .map(value => ({ type: 'value', label: value })); } case 'field2': { return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] .filter(value => value.startsWith(currentValue)) - .map(value => { - return { type: 'value', label: value }; - }); + .map(value => ({ type: 'value', label: value })); } default: { From f489338e3dff8d5e2923aec84edc964320ee32f2 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:33:15 -0300 Subject: [PATCH 32/77] fix(lint): refactor object mapping in AQL component for improved readability and consistency across query suggestion logic --- .../search-bar/query-language/aql.tsx | 93 ++++++++----------- 1 file changed, 38 insertions(+), 55 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx index 66f41586cd..65c286c6bb 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -140,12 +140,10 @@ export function tokenizer(input: string): ITokens { ); return [...input.matchAll(re)].flatMap(({ groups }) => - Object.entries(groups).map(([key, value]) => { - return { - type: key.startsWith('operator_group') ? 'operator_group' : key, - value, - }; - }), + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') ? 'operator_group' : key, + value, + })), ); } @@ -268,14 +266,11 @@ export async function getSuggestions( ({ label }) => label === lastToken.value, ) ? Object.keys(language.tokens.operator_compare.literal).map( - operator => { - return { - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }; - }, + operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + }), ) : []), ]; @@ -289,13 +284,11 @@ export async function getSuggestions( operator.startsWith(lastToken.value) && operator !== lastToken.value, ) - .map(operator => { - return { - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - }; - }), + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), ...(Object.keys(language.tokens.operator_compare.literal).includes( lastToken.value, ) @@ -338,13 +331,11 @@ export async function getSuggestions( }) ).map(element => mapSuggestionCreatorValue(element)), ...Object.entries(language.tokens.conjunction.literal).map( - ([conjunction, description]) => { - return { - type: 'conjunction', - label: conjunction, - description, - }; - }, + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), ), { type: 'operator_group', @@ -362,13 +353,11 @@ export async function getSuggestions( conjunction.startsWith(lastToken.value) && conjunction !== lastToken.value, ) - .map(conjunction => { - return { - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }; - }), + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), // fields if the input field is exact ...(Object.keys(language.tokens.conjunction.literal).includes( lastToken.value, @@ -396,13 +385,11 @@ export async function getSuggestions( } else if (lastToken.value === ')') { return ( // conjunction - Object.keys(language.tokens.conjunction.literal).map(conjunction => { - return { - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }; - }) + Object.keys(language.tokens.conjunction.literal).map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })) ); } @@ -532,13 +519,11 @@ export const AQL = { button={ - params.setQueryLanguageConfiguration(state => { - return { - ...state, - isOpenPopoverImplicitFilter: - !state.isOpenPopoverImplicitFilter, - }; - }) + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: + !state.isOpenPopoverImplicitFilter, + })) } iconType='filter' > @@ -551,12 +536,10 @@ export const AQL = { params.queryLanguage.configuration.isOpenPopoverImplicitFilter } closePopover={() => - params.setQueryLanguageConfiguration(state => { - return { - ...state, - isOpenPopoverImplicitFilter: false, - }; - }) + params.setQueryLanguageConfiguration(state => ({ + ...state, + isOpenPopoverImplicitFilter: false, + })) } > From 65c98003fbe9b761537c9c4d4c1df979eacef5d5 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:48:16 -0300 Subject: [PATCH 33/77] fix(lint): streamline object mapping in WQL for enhanced readability and consistency across query language component logic --- .../search-bar/query-language/wql.test.tsx | 28 ++-- .../search-bar/query-language/wql.tsx | 123 ++++++++---------- 2 files changed, 62 insertions(+), 89 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index c827524f24..2242faa1b9 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -155,13 +155,11 @@ describe('Query language - WQL', () => { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, - ].map(({ label, description }) => { - return { - type: 'field', - label, - description, - }; - }); + ].map(({ label, description }) => ({ + type: 'field', + label, + description, + })); }, // eslint-disable-next-line default-param-last value(currentValue = '', { field }) { @@ -169,17 +167,13 @@ describe('Query language - WQL', () => { case 'field': { return ['value', 'value2', 'value3', 'value4'] .filter(value => value.startsWith(currentValue)) - .map(value => { - return { type: 'value', label: value }; - }); + .map(value => ({ type: 'value', label: value })); } case 'field2': { return ['127.0.0.1', '127.0.0.2', '190.0.0.1', '190.0.0.2'] .filter(value => value.startsWith(currentValue)) - .map(value => { - return { type: 'value', label: value }; - }); + .map(value => ({ type: 'value', label: value })); } default: { @@ -455,11 +449,9 @@ describe('Query language - WQL', () => { options: {}, suggestions: { field: () => - ['field1', 'field2', 'field_not_number'].map(label => { - return { - label, - }; - }), + ['field1', 'field2', 'field_not_number'].map(label => ({ + label, + })), value: () => [], }, validate: { diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 1c763e6d9d..920ce6a009 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -182,20 +182,18 @@ export function tokenizer(input: string): ITokens { ); return [...input.matchAll(re)].flatMap(({ groups }) => - Object.entries(groups).map(([key, value]) => { - return { - type: key.startsWith('operator_group') // Transform operator_group group match - ? 'operator_group' - : key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' - : key, - value, - ...(key === 'value' && - (value && /^"([\S\s]+)"$/.test(value) - ? { formattedValue: value.match(/^"([\S\s]+)"$/)[1] } - : { formattedValue: value })), - }; - }), + Object.entries(groups).map(([key, value]) => ({ + type: key.startsWith('operator_group') // Transform operator_group group match + ? 'operator_group' + : key.startsWith('whitespace') // Transform whitespace group match + ? 'whitespace' + : key, + value, + ...(key === 'value' && + (value && /^"([\S\s]+)"$/.test(value) + ? { formattedValue: value.match(/^"([\S\s]+)"$/)[1] } + : { formattedValue: value })), + })), ); } @@ -414,14 +412,11 @@ export async function getSuggestions( ({ label }) => label === lastToken.value, ) ? Object.keys(language.tokens.operator_compare.literal).map( - operator => { - return { - type: 'operator_compare', - label: operator, - description: - language.tokens.operator_compare.literal[operator], - }; - }, + operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + }), ) : []), ]; @@ -447,13 +442,11 @@ export async function getSuggestions( operator.startsWith(lastToken.value) && operator !== lastToken.value, ) - .map(operator => { - return { - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - }; - }), + .map(operator => ({ + type: 'operator_compare', + label: operator, + description: language.tokens.operator_compare.literal[operator], + })), ...(Object.keys(language.tokens.operator_compare.literal).includes( lastToken.value, ) @@ -515,13 +508,11 @@ export async function getSuggestions( mapSuggestionCreatorValue(element, index, array), ), ...Object.entries(language.tokens.conjunction.literal).map( - ([conjunction, description]) => { - return { - type: 'conjunction', - label: conjunction, - description, - }; - }, + ([conjunction, description]) => ({ + type: 'conjunction', + label: conjunction, + description, + }), ), { type: 'operator_group', @@ -539,13 +530,11 @@ export async function getSuggestions( conjunction.startsWith(lastToken.value) && conjunction !== lastToken.value, ) - .map(conjunction => { - return { - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }; - }), + .map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })), // fields if the input field is exact ...(Object.keys(language.tokens.conjunction.literal).includes( lastToken.value, @@ -573,13 +562,11 @@ export async function getSuggestions( } else if (lastToken.value === ')') { return ( // conjunction - Object.keys(language.tokens.conjunction.literal).map(conjunction => { - return { - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - }; - }) + Object.keys(language.tokens.conjunction.literal).map(conjunction => ({ + type: 'conjunction', + label: conjunction, + description: language.tokens.conjunction.literal[conjunction], + })) ); } @@ -1050,24 +1037,20 @@ export const WQL = { const onSearch = output => { if (output?.error) { - params.setQueryLanguageOutput(state => { - return { - ...state, - searchBarProps: { - ...state.searchBarProps, - suggestions: transformSuggestionsToEuiSuggestItem( - output.error.map(error => { - return { - type: 'validation_error', - label: 'Invalid', - description: error, - }; - }), - ), - isInvalid: true, - }, - }; - }); + params.setQueryLanguageOutput(state => ({ + ...state, + searchBarProps: { + ...state.searchBarProps, + suggestions: transformSuggestionsToEuiSuggestItem( + output.error.map(error => ({ + type: 'validation_error', + label: 'Invalid', + description: error, + })), + ), + isInvalid: true, + }, + })); } else { params.onSearch(output); } @@ -1080,9 +1063,7 @@ export const WQL = { name='textAlign' buttonSize='m' options={params.queryLanguage.parameters?.options?.filterButtons.map( - ({ id, label }) => { - return { id, label }; - }, + ({ id, label }) => ({ id, label }), )} idToSelectedMap={{}} type='multi' From f5706d85f23b1036962c0d24a86d1b08cc17e31c Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:48:36 -0300 Subject: [PATCH 34/77] fix(lint): simplify object mapping in TableData for improved readability and consistency across data table component logic --- .../wazuh-core/public/components/table-data/table-data.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx index 474b6aebc8..b5eb6cd5de 100644 --- a/plugins/wazuh-core/public/components/table-data/table-data.tsx +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -358,9 +358,7 @@ export function TableData({ ref={tableRef} columns={tableColumns.map( // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ searchable, show, composeField, ...rest }) => { - return { ...rest }; - }, + ({ searchable, show, composeField, ...rest }) => ({ ...rest }), )} items={items} loading={isLoading} From ad97a400e3924b5e9a84fe5c73a94e3cbae44104 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:49:16 -0300 Subject: [PATCH 35/77] fix(lint): refactor object mapping in ExportTableCsv for improved readability and consistency across export functionality logic --- .../services/http/ui/components/export-table-csv.tsx | 10 ++++------ .../services/http/ui/components/server-table-data.tsx | 9 +++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx index 7089ce166c..32750f000c 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/export-table-csv.tsx @@ -38,12 +38,10 @@ export function ExportTableCsv({ try { const { endpoint, filters } = fetchContext; const formattedFilters = Object.entries(filters || []).map( - ([name, value]) => { - return { - name, - value, - }; - }, + ([name, value]) => ({ + name, + value, + }), ); showToast({ diff --git a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx index 8e0014ab03..0556fe3322 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx @@ -30,8 +30,8 @@ export function ServerTableData({ props.showSearchBar && (({ tableColumns, ...rest }) => { /* Render search bar*/ - const searchBarWQLOptions = useMemo(() => { - return { + const searchBarWQLOptions = useMemo( + () => ({ searchTermFields: tableColumns .filter( ({ field, searchable }) => @@ -41,8 +41,9 @@ export function ServerTableData({ [composeField || field].flat(), ), ...rest?.searchBarWQL?.options, - }; - }, [rest?.searchBarWQL?.options, rest?.selectedFields]); + }), + [rest?.searchBarWQL?.options, rest?.selectedFields], + ); return ( <> From 13e44e1a10c3c678986ffd3d1ff2447b92f29ac9 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:50:48 -0300 Subject: [PATCH 36/77] fix(lint): refactor error handling in GenericRequest for improved readability and consistency in HTTP client logic --- plugins/wazuh-core/public/services/http/generic-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts index a72c0d2904..e4b3b165f6 100644 --- a/plugins/wazuh-core/public/services/http/generic-client.ts +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -131,7 +131,7 @@ export class GenericRequest implements HTTPClientGeneric { throw error; } - return (((error || {}).response || {}).data || {}).message || false + return error?.response?.data?.message || false ? Promise.reject(new Error(error.response.data.message)) : Promise.reject(error || new Error('Server did not respond')); } From 51cbf6ffc786487f829bcf7a8df904615452945f Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:55:47 -0300 Subject: [PATCH 37/77] fix(lint): streamline error message extraction in WzRequest for improved readability and consistency in HTTP response handling --- plugins/wazuh-core/public/services/http/server-client.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index 62c9c3823f..a43cb33b59 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -202,7 +202,7 @@ export class WzRequest implements HTTPClientServer { if (hasFailed) { const error = response?.data?.data?.failed_items?.[0]?.error || {}; const failedIds = response?.data?.data?.failed_items?.[0]?.id || {}; - const message = (response.data || {}).message || 'Unexpected error'; + const message = response.data?.message || 'Unexpected error'; const errorMessage = `${message} (${error.code}) - ${error.message} ${ failedIds && failedIds.length > 1 ? ` Affected ids: ${failedIds} ` @@ -285,7 +285,7 @@ export class WzRequest implements HTTPClientServer { idHost, force, }); - const token = ((response || {}).data || {}).token; + const token = response?.data?.token; return token as string; } catch (error) { @@ -461,8 +461,7 @@ export class WzRequest implements HTTPClientServer { // TODO: implement // const wzMisc = new WzMisc(); // wzMisc.setApiIsDown(true); - const response: string = - (error.response.data || {}).message || error.message; + const response: string = error.response.data?.message || error.message; throw this.returnErrorInstance(response); } else { @@ -501,7 +500,7 @@ export class WzRequest implements HTTPClientServer { return response; } catch (error) { if (error.response) { - const response = (error.response.data || {}).message || error.message; + const response = error.response.data?.message || error.message; throw this.returnErrorInstance(response); } else { From 02e31a6661aec88545e1950fdeaafe9bf1ddbc26 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:56:02 -0300 Subject: [PATCH 38/77] fix(lint): add rule for optional chaining with empty array expressions for improved code quality in logical expressions --- .eslintrc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 95c8c3aa19..b4419929f7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -58,6 +58,11 @@ module.exports = { 'MemberExpression > LogicalExpression[operator="||"]:has(Identifier):has(ObjectExpression[properties.length=0])', message: 'Use optional chaining operator instead (?.).', }, + { + selector: + 'MemberExpression > LogicalExpression[operator="||"]:has(Identifier):has(ArrayExpression[elements.length=0])', + message: 'Use optional chaining operator instead (?.).', + }, ], 'prefer-arrow-callback': 'error', 'func-style': ['error', 'declaration', { allowArrowFunctions: true }], From 8f0ac563724b93dc401b815ce4baed97dae4992c Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 09:57:06 -0300 Subject: [PATCH 39/77] fix(lint): refactor token extraction using optional chaining for improved readability in ServerAPIClient response handling --- plugins/wazuh-core/server/services/server-api-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/server/services/server-api-client.ts b/plugins/wazuh-core/server/services/server-api-client.ts index e279582d68..910d01e39e 100644 --- a/plugins/wazuh-core/server/services/server-api-client.ts +++ b/plugins/wazuh-core/server/services/server-api-client.ts @@ -185,7 +185,7 @@ export class ServerAPIClient { ...(options?.authContext ? { data: options?.authContext } : {}), }; const response: AxiosResponse = await this.client(optionsRequest); - const token: string = (((response || {}).data || {}).data || {}).token; + const token: string = response?.data?.data?.token; return token; } From b6705444aa9b82f4537020cfff2dd527ad16486d Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 10:36:29 -0300 Subject: [PATCH 40/77] fix(enum): remove redundant enum definition for plugin settings, improving code clarity in configuration service file --- .../common/services/configuration.ts | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 37599095af..aa39e36a5e 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -1,4 +1,5 @@ import { cloneDeep } from 'lodash'; +import { EpluginSettingType } from '../constants'; import { formatLabelValuePair } from './settings'; import { formatBytes } from './file-size'; @@ -71,26 +72,6 @@ interface TConfigurationSettingOptionsSwitch { }; } -// eslint-disable-next-line @typescript-eslint/naming-convention -export enum EpluginSettingType { - // eslint-disable-next-line @typescript-eslint/naming-convention - text = 'text', - // eslint-disable-next-line @typescript-eslint/naming-convention - password = 'password', - // eslint-disable-next-line @typescript-eslint/naming-convention - textarea = 'textarea', - // eslint-disable-next-line @typescript-eslint/naming-convention - switch = 'switch', - // eslint-disable-next-line @typescript-eslint/naming-convention - number = 'number', - // eslint-disable-next-line @typescript-eslint/naming-convention - editor = 'editor', - // eslint-disable-next-line @typescript-eslint/naming-convention - select = 'select', - // eslint-disable-next-line @typescript-eslint/naming-convention - filepicker = 'filepicker', -} - export interface TConfigurationSetting { // Define the text displayed in the UI. title: string; From 8deb71f049fc4a9380e3b592cf93a512b12ab5e6 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 10:53:10 -0300 Subject: [PATCH 41/77] fix(lint): add rule for empty functions in test files to enhance code quality in TypeScript testing configuration --- .eslintrc.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index b4419929f7..9974491e1a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -356,5 +356,14 @@ module.exports = { ], }, }, + { + files: ['plugins/**/*.test.{js,jsx,ts,tsx}'], + rules: { + '@typescript-eslint/no-empty-function': [ + 'error', + { allow: ['arrowFunctions'] }, + ], + }, + }, ], }; From f33d35181c924070cc8e9f14bbfb310147d132f0 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 10:53:21 -0300 Subject: [PATCH 42/77] fix(refactor): optimize settings categorization logic for clarity and maintainability in configuration service file --- .../common/services/configuration.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index aa39e36a5e..4afeaeac40 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -535,19 +535,18 @@ export class Configuration implements IConfiguration { filterFunction ? settingsMapped.filter(element => filterFunction(element)) : settingsMapped - ) - .sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)) - // eslint-disable-next-line unicorn/no-array-reduce - .reduce( - (accum, pluginSettingConfiguration) => ({ - ...accum, - [pluginSettingConfiguration.category]: [ - ...(accum[pluginSettingConfiguration.category] || []), - { ...pluginSettingConfiguration }, - ], - }), - {}, - ); + ).sort((settingA, settingB) => settingA.key?.localeCompare?.(settingB.key)); + const result: any = {}; + + for (const pluginSettingConfiguration of settingsSortedByCategories) { + const category = pluginSettingConfiguration.category; + + if (!result[category]) { + result[category] = []; + } + + result[category].push({ ...pluginSettingConfiguration }); + } return Object.entries(settingsSortedByCategories) .map(([category, settings]) => ({ From 38c2bb6a06d3808228e3cb6ecd49fcc4b9fbd485 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 10:53:46 -0300 Subject: [PATCH 43/77] fix(lint): remove unused variable warnings and improve code readability in suggest input and AQL test files --- .../components/eui-suggest/suggest-input.js | 2 -- .../search-bar/query-language/aql.test.tsx | 16 +++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js index 36b4c45eaf..963caecd09 100644 --- a/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js +++ b/plugins/wazuh-core/public/components/search-bar/components/eui-suggest/suggest-input.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -47,7 +46,6 @@ export class EuiSuggestInput extends Component { append, tooltipContent, suggestions, - sendValue, onPopoverFocus, isPopoverOpen, onClosePopover, diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx index 2a931489aa..978bb55fe9 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SearchBar } from '../index'; @@ -13,21 +12,17 @@ describe('SearchBar component', () => { id: AQL.id, implicitQuery: 'id!=000;', suggestions: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - field(currentValue) { + field(_currentValue) { return []; }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - value(currentValue, { previousField }) { + value(_currentValue, { previousField: _previousField }) { return []; }, }, }, ], - /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, onSearch: () => {}, - /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { @@ -95,8 +90,7 @@ describe('Query language - AQL', () => { await getSuggestions(tokenizer(input), { id: 'aql', suggestions: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - field(currentValue) { + field(_currentValue) { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, @@ -212,8 +206,8 @@ describe('Query language - AQL', () => { ${'(field=value,field2>value2)'} | ${'(field=value,field2>value2)'} `( 'Transform the external input UQL to QL - UQL $UQL => $AQL', - async ({ UQL, AQL: changedInput }) => { - expect(AQL.transformUQLToQL(UQL)).toEqual(changedInput); + async ({ UQL: uql, AQL: changedInput }) => { + expect(AQL.transformUQLToQL(uql)).toEqual(changedInput); }, ); }); From 86efe85ba00014fe5ace191af11191532cee2c89 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 10:58:47 -0300 Subject: [PATCH 44/77] fix(lint): eliminate unnecessary eslint-disable comments and enhance type safety in query language AQL component --- .../search-bar/query-language/aql.tsx | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx index 65c286c6bb..3ad1bb10b2 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -34,7 +34,6 @@ Implemented schema: export const language = { // Tokens tokens: { - // eslint-disable-next-line camelcase operator_compare: { literal: { '=': 'equality', @@ -50,7 +49,6 @@ export const language = { ',': 'or', }, }, - // eslint-disable-next-line camelcase operator_group: { literal: { '(': 'open group', @@ -63,13 +61,10 @@ export const language = { // Suggestion mapper by language token type const suggestionMappingLanguageTokenType = { field: { iconType: 'kqlField', color: 'tint4' }, - // eslint-disable-next-line camelcase operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, value: { iconType: 'kqlValue', color: 'tint0' }, conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - // eslint-disable-next-line camelcase operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - // eslint-disable-next-line camelcase function_search: { iconType: 'search', color: 'tint5' }, }; @@ -294,14 +289,15 @@ export async function getSuggestions( ) ? ( await options.suggestions.value(undefined, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - previousField: getLastTokenWithValueByType(tokens, 'field')! - .value, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, + previousField: ( + getLastTokenWithValueByType(tokens, 'field') as IToken + ).value, + previousOperatorCompare: ( + getLastTokenWithValueByType( + tokens, + 'operator_compare', + ) as IToken + ).value, }) ).map(element => mapSuggestionCreatorValue(element)) : []), @@ -321,13 +317,12 @@ export async function getSuggestions( : []), ...( await options.suggestions.value(lastToken.value, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - previousField: getLastTokenWithValueByType(tokens, 'field')!.value, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - previousOperatorCompare: getLastTokenWithValueByType( - tokens, - 'operator_compare', - )!.value, + previousField: ( + getLastTokenWithValueByType(tokens, 'field') as IToken + ).value, + previousOperatorCompare: ( + getLastTokenWithValueByType(tokens, 'operator_compare') as IToken + ).value, }) ).map(element => mapSuggestionCreatorValue(element)), ...Object.entries(language.tokens.conjunction.literal).map( From 04bcf983a99fd93deca8025395e072ce2695c1ec Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 11:16:06 -0300 Subject: [PATCH 45/77] fix(refactor): enhance search bar query language structure and type safety, ensuring consistent initialization and prop typing --- .../search-bar/query-language/index.ts | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts index 95dbaf9f73..ff29f3b232 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/index.ts +++ b/plugins/wazuh-core/public/components/search-bar/query-language/index.ts @@ -1,41 +1,64 @@ +import React from 'react'; import { AQL } from './aql'; import { WQL } from './wql'; +export interface SuggestItem { + type: { iconType: string; color: string }; + label: string; + description?: string; +} + +interface SearchBarProps { + suggestions: SuggestItem[]; + onItemClick: (currentInput: string) => (item: SuggestItem) => void; + prepend?: React.ReactNode; + disableFocusTrap?: boolean; + isInvalid?: boolean; + onKeyPress?: (event: React.KeyboardEvent) => void; +} + interface SearchBarQueryLanguage { - description: string; - documentationLink?: string; id: string; label: string; + description: string; + documentationLink?: string; getConfiguration?: () => any; run: ( input: string | undefined, params: any, ) => Promise<{ - searchBarProps: any; + filterButtons?: React.ReactElement | null; + searchBarProps: SearchBarProps; output: { language: string; - unifiedQuery: string; + unifiedQuery?: string; + apiQuery?: { + q: string; + }; query: string; }; }>; - transformInput: ( + transformInput?: ( unifiedQuery: string, options: { configuration: any; parameters: any }, ) => string; + transformUQLToQL?: (unifiedQuery: string) => string; } // Register the query languages -export const searchBarQueryLanguages: Record = [ - AQL, - WQL, - // eslint-disable-next-line unicorn/no-array-reduce -].reduce((accum, item) => { - if (accum[item.id]) { - throw new Error(`Query language with id: ${item.id} already registered.`); +function initializeSearchBarQueryLanguages() { + const languages = [AQL, WQL]; + const result: Record = {}; + + for (const item of languages) { + if (result[item.id]) { + throw new Error(`Query language with id: ${item.id} already registered.`); + } + + result[item.id] = item; } - return { - ...accum, - [item.id]: item, - }; -}, {}); + return result; +} + +export const searchBarQueryLanguages = initializeSearchBarQueryLanguages(); From d1e9e59e0bf5be1fcc3fd9ea8f9e779704561788 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 11:18:16 -0300 Subject: [PATCH 46/77] fix(lint): turn off 'default-param-last' rule and clean up related eslint-disable comments in query language test files --- .eslintrc.js | 2 +- .../public/components/search-bar/query-language/aql.test.tsx | 1 - .../public/components/search-bar/query-language/wql.test.tsx | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9974491e1a..7ce8acf7a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -88,7 +88,7 @@ module.exports = { 'block-scoped-var': 'error', 'default-case': 'error', 'default-case-last': 'error', - 'default-param-last': 'error', + 'default-param-last': 'off', eqeqeq: ['error', 'always'], 'no-var': 'error', /* -------------------------------------------------------------------------- */ diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx index 978bb55fe9..92fb705926 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.test.tsx @@ -100,7 +100,6 @@ describe('Query language - AQL', () => { description, })); }, - // eslint-disable-next-line default-param-last value(currentValue = '', { previousField }) { switch (previousField) { case 'field': { diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index 2242faa1b9..5354a1282e 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -161,7 +161,6 @@ describe('Query language - WQL', () => { description, })); }, - // eslint-disable-next-line default-param-last value(currentValue = '', { field }) { switch (field) { case 'field': { From e99506ebb5d1688af9a565a4b73fbb50f4013e20 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 11:29:54 -0300 Subject: [PATCH 47/77] fix(lint): remove unused variables and clean up eslint-disable comments in WQL query language tests for better readability --- .../search-bar/query-language/wql.test.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index 5354a1282e..1a43032ddf 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -23,21 +23,17 @@ describe('SearchBar component', () => { }, }, suggestions: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - field(currentValue) { + field(_currentValue) { return []; }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - value(currentValue, { field }) { + value(_currentValue, { field: _field }) { return []; }, }, }, ], - /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, onSearch: () => {}, - /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly to match the snapshot of query language', async () => { @@ -54,7 +50,6 @@ function tokenCreator({ type, value, formattedValue }) { return { type, value, ...(formattedValue ? { formattedValue } : {}) }; } -/* eslint-disable max-len */ describe('Query language - WQL', () => { const t = { opGroup: (value?) => tokenCreator({ type: 'operator_group', value }), @@ -150,8 +145,7 @@ describe('Query language - WQL', () => { await getSuggestions(tokenizer(input), { id: 'aql', suggestions: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - field(currentValue) { + field(_currentValue) { return [ { label: 'field', description: 'Field' }, { label: 'field2', description: 'Field2' }, From 521178817d7b48ace82dad46f1ba631df0934158 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 12:16:30 -0300 Subject: [PATCH 48/77] fix(lint): update enum formatting rules in eslint config for improved consistency and clarity in variable naming conventions --- .eslintrc.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7ce8acf7a7..b9a2b79874 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -266,13 +266,16 @@ module.exports = { format: ['PascalCase'], }, { - selector: ['enum', 'enumMember'], + selector: ['enum'], + format: ['PascalCase'], + }, + { + selector: ['enumMember'], format: ['UPPER_CASE'], }, { selector: ['variable'], modifiers: ['global'], - types: ['number', 'string'], format: ['UPPER_CASE'], }, { From 3c0cd7dfe027e502a12835b9788e8562e1304029 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 13:50:46 -0300 Subject: [PATCH 49/77] fix(aql): refactor token types to use enums and constants for improved clarity and maintainability in AQL query language code --- .../search-bar/query-language/aql.tsx | 382 ++++++++++-------- 1 file changed, 208 insertions(+), 174 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx index 3ad1bb10b2..25c47c18fa 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -1,19 +1,52 @@ -/* eslint-disable unicorn/no-await-expression-member */ import React from 'react'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; -type ITokenType = - | 'field' - | 'operator_compare' - | 'operator_group' - | 'value' - | 'conjunction'; -interface IToken { - type: ITokenType; - value: string; +const AQL_ID = 'aql'; +const QUERY_TOKEN_KEYS = { + FIELD: 'field', + OPERATOR_COMPARE: 'operator_compare', + OPERATOR_GROUP: 'operator_group', + VALUE: 'value', + CONJUNCTION: 'conjunction', + FUNCTION_SEARCH: 'function_search', +} as const; + +type TokenTypeEnum = (typeof QUERY_TOKEN_KEYS)[keyof typeof QUERY_TOKEN_KEYS]; + +const GROUP_OPERATOR_BOUNDARY = { + OPEN: 'operator_group_open', + CLOSE: 'operator_group_close', +}; +const OPERATOR_COMPARE = { + EQUALITY: '=', + NOT_EQUALITY: '!=', + BIGGER: '>', + SMALLER: '<', + LIKE_AS: '~', +} as const; + +type OperatorCompare = (typeof OPERATOR_COMPARE)[keyof typeof OPERATOR_COMPARE]; + +const OPERATOR_GROUP = { + OPEN: '(', + CLOSE: ')', +} as const; + +type OperatorGroup = (typeof OPERATOR_GROUP)[keyof typeof OPERATOR_GROUP]; + +const CONJUNCTION = { + AND: ';', + OR: ',', +} as const; + +type Conjunction = (typeof CONJUNCTION)[keyof typeof CONJUNCTION]; + +interface TokenDescriptor { + type: TokenTypeEnum; + value: OperatorCompare | OperatorGroup | Conjunction; } -type ITokens = IToken[]; +type TokenList = TokenDescriptor[]; /* API Query Language Define the API Query Language to use in the search bar. @@ -31,49 +64,61 @@ Implemented schema: */ // Language definition -export const language = { +export const LANGUAGE = { // Tokens tokens: { - operator_compare: { + [QUERY_TOKEN_KEYS.OPERATOR_COMPARE]: { literal: { - '=': 'equality', - '!=': 'not equality', - '>': 'bigger', - '<': 'smaller', - '~': 'like as', + [OPERATOR_COMPARE.EQUALITY]: 'equality', + [OPERATOR_COMPARE.NOT_EQUALITY]: 'not equality', + [OPERATOR_COMPARE.BIGGER]: 'bigger', + [OPERATOR_COMPARE.SMALLER]: 'smaller', + [OPERATOR_COMPARE.LIKE_AS]: 'like as', }, }, - conjunction: { + [QUERY_TOKEN_KEYS.CONJUNCTION]: { literal: { - ';': 'and', - ',': 'or', + [CONJUNCTION.AND]: 'and', + [CONJUNCTION.OR]: 'or', }, }, - operator_group: { + [QUERY_TOKEN_KEYS.OPERATOR_GROUP]: { literal: { - '(': 'open group', - ')': 'close group', + [OPERATOR_GROUP.OPEN]: 'open group', + [OPERATOR_GROUP.CLOSE]: 'close group', }, }, }, -}; - +} as const; + +const OPERATORS = Object.keys( + LANGUAGE.tokens.operator_compare.literal, +) as OperatorCompare[]; +const CONJUNCTIONS = Object.keys( + LANGUAGE.tokens.conjunction.literal, +) as Conjunction[]; // Suggestion mapper by language token type -const suggestionMappingLanguageTokenType = { - field: { iconType: 'kqlField', color: 'tint4' }, - operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, - value: { iconType: 'kqlValue', color: 'tint0' }, - conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - function_search: { iconType: 'search', color: 'tint5' }, -}; +const SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE = { + [QUERY_TOKEN_KEYS.FIELD]: { iconType: 'kqlField', color: 'tint4' }, + [QUERY_TOKEN_KEYS.OPERATOR_COMPARE]: { + iconType: 'kqlOperand', + color: 'tint1', + }, + [QUERY_TOKEN_KEYS.VALUE]: { iconType: 'kqlValue', color: 'tint0' }, + [QUERY_TOKEN_KEYS.CONJUNCTION]: { iconType: 'kqlSelector', color: 'tint3' }, + [QUERY_TOKEN_KEYS.OPERATOR_GROUP]: { + iconType: 'tokenDenseVector', + color: 'tint3', + }, + [QUERY_TOKEN_KEYS.FUNCTION_SEARCH]: { iconType: 'search', color: 'tint5' }, +} as const; /** * Creator of intermediate interface of EuiSuggestItem * @param type * @returns */ -function mapSuggestionCreator(type: ITokenType) { +function mapSuggestionCreator(type: TokenTypeEnum) { return function ({ ...params }) { return { type, @@ -82,15 +127,15 @@ function mapSuggestionCreator(type: ITokenType) { }; } -const mapSuggestionCreatorField = mapSuggestionCreator('field'); -const mapSuggestionCreatorValue = mapSuggestionCreator('value'); +const mapSuggestionCreatorField = mapSuggestionCreator(QUERY_TOKEN_KEYS.FIELD); +const mapSuggestionCreatorValue = mapSuggestionCreator(QUERY_TOKEN_KEYS.VALUE); /** * Tokenize the input string. Returns an array with the tokens. * @param input * @returns */ -export function tokenizer(input: string): ITokens { +export function tokenizer(input: string): TokenList { // API regular expression // https://github.com/wazuh/wazuh/blob/v4.4.0-rc1/framework/wazuh/core/utils.py#L1242-L1257 // self.query_regex = re.compile( @@ -115,45 +160,42 @@ export function tokenizer(input: string): ITokens { // and added the optional operator to allow matching the entities when the query is not // completed. This helps to tokenize the query and manage when the input is not completed. // A ( character. - String.raw`(?\()?` + + String.raw`(?<${GROUP_OPERATOR_BOUNDARY.OPEN}>\()?` + // Field name: name of the field to look on DB. - String.raw`(?[\w.]+)?` + // Added an optional find + String.raw`(?<${QUERY_TOKEN_KEYS.FIELD}>[\w.]+)?` + // Added an optional find // Operator: looks for '=', '!=', '<', '>' or '~'. // This seems to be a bug because is not searching the literal valid operators. // I guess the operator is validated after the regular expression matches - `(?[${Object.keys( - language.tokens.operator_compare.literal, + `(?<${QUERY_TOKEN_KEYS.OPERATOR_COMPARE}>[${Object.keys( + LANGUAGE.tokens.operator_compare.literal, )}]{1,2})?` + // Added an optional find // Value: A string. - String.raw`(?(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + + String.raw`(?<${QUERY_TOKEN_KEYS.VALUE}>(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + String.raw`(?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]+)` + String.raw`(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]*)\))*)+)?` + // Added an optional find // A ) character. - String.raw`(?\))?` + - `(?[${Object.keys(language.tokens.conjunction.literal)}])?`, + String.raw`(?<${GROUP_OPERATOR_BOUNDARY.CLOSE}>\))?` + + `(?<${QUERY_TOKEN_KEYS.CONJUNCTION}>[${CONJUNCTIONS}])?`, 'g', ); return [...input.matchAll(re)].flatMap(({ groups }) => - Object.entries(groups).map(([key, value]) => ({ - type: key.startsWith('operator_group') ? 'operator_group' : key, + Object.entries(groups || {}).map(([key, value]) => ({ + type: key.startsWith(QUERY_TOKEN_KEYS.OPERATOR_GROUP) + ? QUERY_TOKEN_KEYS.OPERATOR_GROUP + : key, value, })), - ); + ) as TokenList; } interface QLOptionSuggestionEntityItem { description?: string; - label: string; + label?: string; } type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { - type: - | 'operator_group' - | 'field' - | 'operator_compare' - | 'value' - | 'conjunction'; + type: TokenTypeEnum; }; type SuggestItem = QLOptionSuggestionEntityItem & { @@ -161,11 +203,8 @@ type SuggestItem = QLOptionSuggestionEntityItem & { }; type QLOptionSuggestionHandler = ( - currentValue: string | undefined, - { - previousField, - previousOperatorCompare, - }: { previousField: string; previousOperatorCompare: string }, + currentValue?: string | undefined, + options?: { previousField: string; previousOperatorCompare: string }, ) => Promise; interface OptionsQL { @@ -181,7 +220,7 @@ interface OptionsQL { * @param tokenType token type to search * @returns */ -function getLastTokenWithValue(tokens: ITokens): IToken | undefined { +function getLastTokenWithValue(tokens: TokenList): TokenDescriptor | undefined { // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); @@ -197,9 +236,9 @@ function getLastTokenWithValue(tokens: ITokens): IToken | undefined { * @returns */ function getLastTokenWithValueByType( - tokens: ITokens, - tokenType: ITokenType, -): IToken | undefined { + tokens: TokenList, + tokenType: TokenTypeEnum, +): TokenDescriptor | undefined { // Find the last token by type // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = [...tokens]; @@ -211,6 +250,31 @@ function getLastTokenWithValueByType( return tokenFound; } +const getValueSuggestions = async ( + tokens: TokenDescriptor[], + options: OptionsQL, + suggestionValue?: string, +) => { + const previousField = ( + getLastTokenWithValueByType( + tokens, + QUERY_TOKEN_KEYS.FIELD, + ) as TokenDescriptor + ).value; + const previousOperatorCompare = ( + getLastTokenWithValueByType( + tokens, + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + ) as TokenDescriptor + ).value; + const suggestions = await options.suggestions.value(suggestionValue, { + previousField, + previousOperatorCompare, + }); + + return suggestions.map(element => mapSuggestionCreatorValue(element)); +}; + /** * Get the suggestions from the tokens * @param tokens @@ -219,13 +283,14 @@ function getLastTokenWithValueByType( * @returns */ export async function getSuggestions( - tokens: ITokens, + tokens: TokenList, options: OptionsQL, ): Promise { if (tokens.length === 0) { return []; } + const suggestions = await options.suggestions.field(); // Get last token const lastToken = getLastTokenWithValue(tokens); @@ -233,157 +298,124 @@ export async function getSuggestions( if (!lastToken?.type) { return [ // fields - ...(await options.suggestions.field()).map((element, index, array) => - mapSuggestionCreatorField(element, index, array), - ), + ...suggestions.map(element => mapSuggestionCreatorField(element)), { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, + label: OPERATOR_GROUP.OPEN, + description: + LANGUAGE.tokens.operator_group.literal[OPERATOR_GROUP.OPEN], }, ]; } switch (lastToken.type) { - case 'field': { + case QUERY_TOKEN_KEYS.FIELD: { return [ // fields that starts with the input but is not equals - ...(await options.suggestions.field()) + ...suggestions .filter( ({ label }) => - label.startsWith(lastToken.value) && label !== lastToken.value, + label?.startsWith(lastToken.value) && label !== lastToken.value, ) - .map((element, index, array) => - mapSuggestionCreatorField(element, index, array), - ), + .map(element => mapSuggestionCreatorField(element)), // operators if the input field is exact - ...((await options.suggestions.field()).some( - ({ label }) => label === lastToken.value, - ) - ? Object.keys(language.tokens.operator_compare.literal).map( - operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - }), - ) + ...(suggestions.some(({ label }) => label === lastToken.value) + ? OPERATORS.map(operator => ({ + type: QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + label: operator, + description: LANGUAGE.tokens.operator_compare.literal[operator], + })) : []), ]; } - case 'operator_compare': { + case QUERY_TOKEN_KEYS.OPERATOR_COMPARE: { + const getOperatorSuggestions = async (lastToken: TokenDescriptor) => { + const compareOperatorSuggestions = OPERATORS.filter( + operator => + operator.startsWith(lastToken.value) && + operator !== lastToken.value, + ).map(operator => ({ + type: QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + label: operator, + description: LANGUAGE.tokens.operator_compare.literal[operator], + })); + + return compareOperatorSuggestions; + }; + return [ - ...Object.keys(language.tokens.operator_compare.literal) - .filter( - operator => - operator.startsWith(lastToken.value) && - operator !== lastToken.value, - ) - .map(operator => ({ - type: 'operator_compare', - label: operator, - description: language.tokens.operator_compare.literal[operator], - })), - ...(Object.keys(language.tokens.operator_compare.literal).includes( - lastToken.value, - ) - ? ( - await options.suggestions.value(undefined, { - previousField: ( - getLastTokenWithValueByType(tokens, 'field') as IToken - ).value, - previousOperatorCompare: ( - getLastTokenWithValueByType( - tokens, - 'operator_compare', - ) as IToken - ).value, - }) - ).map(element => mapSuggestionCreatorValue(element)) + ...(await getOperatorSuggestions(lastToken)), + ...(OPERATORS.includes(lastToken.value as OperatorCompare) + ? await getValueSuggestions(tokens, options) : []), ]; } - case 'value': { + case QUERY_TOKEN_KEYS.VALUE: { return [ ...(lastToken.value ? [ { - type: 'function_search', + type: QUERY_TOKEN_KEYS.FUNCTION_SEARCH, label: 'Search', description: 'run the search query', }, ] : []), - ...( - await options.suggestions.value(lastToken.value, { - previousField: ( - getLastTokenWithValueByType(tokens, 'field') as IToken - ).value, - previousOperatorCompare: ( - getLastTokenWithValueByType(tokens, 'operator_compare') as IToken - ).value, - }) - ).map(element => mapSuggestionCreatorValue(element)), - ...Object.entries(language.tokens.conjunction.literal).map( + ...(await getValueSuggestions(tokens, options, lastToken.value)), + ...Object.entries(LANGUAGE.tokens.conjunction.literal).map( ([conjunction, description]) => ({ - type: 'conjunction', + type: QUERY_TOKEN_KEYS.CONJUNCTION, label: conjunction, description, }), ), { - type: 'operator_group', + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, label: ')', - description: language.tokens.operator_group.literal[')'], + description: LANGUAGE.tokens.operator_group.literal[')'], }, ]; } - case 'conjunction': { + case QUERY_TOKEN_KEYS.CONJUNCTION: { return [ - ...Object.keys(language.tokens.conjunction.literal) - .filter( - conjunction => - conjunction.startsWith(lastToken.value) && - conjunction !== lastToken.value, - ) - .map(conjunction => ({ - type: 'conjunction', - label: conjunction, - description: language.tokens.conjunction.literal[conjunction], - })), + ...CONJUNCTIONS.filter( + conjunction => + conjunction.startsWith(lastToken.value) && + conjunction !== lastToken.value, + ).map(conjunction => ({ + type: QUERY_TOKEN_KEYS.CONJUNCTION, + label: conjunction, + description: LANGUAGE.tokens.conjunction.literal[conjunction], + })), // fields if the input field is exact - ...(Object.keys(language.tokens.conjunction.literal).includes( - lastToken.value, - ) - ? (await options.suggestions.field()).map(element => - mapSuggestionCreatorField(element), - ) + ...(CONJUNCTIONS.includes(lastToken.value as Conjunction) + ? suggestions.map(element => mapSuggestionCreatorField(element)) : []), { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, + label: OPERATOR_GROUP.OPEN, + description: + LANGUAGE.tokens.operator_group.literal[OPERATOR_GROUP.OPEN], }, ]; } - case 'operator_group': { - if (lastToken.value === '(') { + case QUERY_TOKEN_KEYS.OPERATOR_GROUP: { + if (lastToken.value === OPERATOR_GROUP.OPEN) { return ( // fields - (await options.suggestions.field()).map(element => - mapSuggestionCreatorField(element), - ) + suggestions.map(element => mapSuggestionCreatorField(element)) ); } else if (lastToken.value === ')') { return ( // conjunction - Object.keys(language.tokens.conjunction.literal).map(conjunction => ({ - type: 'conjunction', + CONJUNCTIONS.map(conjunction => ({ + type: QUERY_TOKEN_KEYS.CONJUNCTION, label: conjunction, - description: language.tokens.conjunction.literal[conjunction], + description: LANGUAGE.tokens.conjunction.literal[conjunction], })) ); } @@ -410,7 +442,7 @@ export function transformSuggestionToEuiSuggestItem( const { type, ...rest } = suggestion; return { - type: { ...suggestionMappingLanguageTokenType[type] }, + type: { ...SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE[type] }, ...rest, }; } @@ -439,16 +471,14 @@ function getOutput(input: string, options: { implicitQuery?: string } = {}) { }`; return { - // eslint-disable-next-line no-use-before-define - language: AQL.id, + language: AQL_ID, query: unifiedQuery, unifiedQuery, }; } -// eslint-disable-next-line @typescript-eslint/naming-convention export const AQL = { - id: 'aql', + id: AQL_ID, label: 'AQL', description: 'API Query Language (AQL) allows to do queries.', documentationLink: webDocumentationLink('user-manual/api/queries.html'), @@ -457,9 +487,9 @@ export const AQL = { isOpenPopoverImplicitFilter: false, }; }, - async run(input, params) { + async run(input: string, params) { // Get the tokens from the input - const tokens: ITokens = tokenizer(input); + const tokens: TokenList = tokenizer(input); return { searchBarProps: { @@ -469,7 +499,7 @@ export const AQL = { await getSuggestions(tokens, params.queryLanguage.parameters), ), // Handler to manage when clicking in a suggestion item - onItemClick: currentInput => item => { + onItemClick: (currentInput: string) => item => { // When the clicked item has the `search` iconType, run the `onSearch` function if (item.type.iconType === 'search') { // Execute the search action @@ -478,22 +508,26 @@ export const AQL = { ); } else { // When the clicked item has another iconType - const lastToken: IToken = getLastTokenWithValue(tokens); + const lastToken = getLastTokenWithValue(tokens); // if the clicked suggestion is of same type of last token if ( lastToken && - suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE[lastToken.type] + .iconType === item.type.iconType ) { // replace the value of last token lastToken.value = item.label; } else { // add a new token of the selected type and value + const type = Object.entries( + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE, + ).find( + ([, { iconType }]) => iconType === item.type.iconType, + )?.[0] as TokenTypeEnum; + tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( - ([, { iconType }]) => iconType === item.type.iconType, - )[0], + type, value: item.label, }); } From 1b4dc2bd443337c8b5ff4fd3cbb7612ca7f91cd7 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:07:56 -0300 Subject: [PATCH 50/77] fix(lint): expand ESLint variable rule to include boolean, number, and string types for enhanced linting versatility --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index b9a2b79874..ecb532349d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -276,6 +276,7 @@ module.exports = { { selector: ['variable'], modifiers: ['global'], + types: ['boolean', 'number', 'string'], format: ['UPPER_CASE'], }, { From 51b5842747134850cd4435e6b2b0c1df48d4801e Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:18:55 -0300 Subject: [PATCH 51/77] fix(types): update UseStateStorageHook to default type 'any' for improved flexibility in hook implementation --- plugins/wazuh-core/public/hooks/use-state-storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/public/hooks/use-state-storage.ts b/plugins/wazuh-core/public/hooks/use-state-storage.ts index 845e36b726..ba41095327 100644 --- a/plugins/wazuh-core/public/hooks/use-state-storage.ts +++ b/plugins/wazuh-core/public/hooks/use-state-storage.ts @@ -16,7 +16,7 @@ function transformValueFromStorage(value: any) { return typeof value === 'string' ? JSON.parse(value) : value; } -export type UseStateStorageHook = ( +export type UseStateStorageHook = ( initialValue: T, storageSystem?: UseStateStorageSystem, storageKey?: string, From 7c5baf0d72986b29741cb5892bff3df3c9245a6c Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:19:15 -0300 Subject: [PATCH 52/77] fix(lint): add camelCase and PascalCase formats for object literal methods to ESLint rules for improved consistency in naming conventions --- .eslintrc.js | 4 ++++ plugins/wazuh-core/public/types.ts | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ecb532349d..c157393d4f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -261,6 +261,10 @@ module.exports = { selector: ['objectLiteralProperty', 'typeProperty'], format: null, }, + { + selector: ['objectLiteralMethod', 'typeMethod'], + format: ['camelCase', 'PascalCase'], + }, { selector: ['class', 'interface', 'typeLike'], format: ['PascalCase'], diff --git a/plugins/wazuh-core/public/types.ts b/plugins/wazuh-core/public/types.ts index c91ce07595..692922ac63 100644 --- a/plugins/wazuh-core/public/types.ts +++ b/plugins/wazuh-core/public/types.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { API_USER_STATUS_RUN_AS } from '../common/api-user-status-run-as'; import { Configuration } from '../common/services/configuration'; import { TableDataProps } from './components'; @@ -14,13 +15,10 @@ export interface WazuhCorePluginSetup { dashboardSecurity: DashboardSecurity; http: HTTPClient; ui: { - // eslint-disable-next-line @typescript-eslint/naming-convention TableData: ( prop: TableDataProps, ) => React.ComponentType>; - // eslint-disable-next-line @typescript-eslint/naming-convention SearchBar: (prop: any) => React.ComponentType; - // eslint-disable-next-line @typescript-eslint/naming-convention ServerTable: ( prop: ServerDataProps, ) => React.ComponentType>; @@ -38,13 +36,10 @@ export interface WazuhCorePluginStart { dashboardSecurity: DashboardSecurity; http: HTTPClient; ui: { - // eslint-disable-next-line @typescript-eslint/naming-convention TableData: ( prop: TableDataProps, ) => React.ComponentType>; - // eslint-disable-next-line @typescript-eslint/naming-convention SearchBar: (prop: any) => React.ComponentType; - // eslint-disable-next-line @typescript-eslint/naming-convention ServerTable: ( prop: ServerDataProps, ) => React.ComponentType>; From 3a156f6865519ddc85d3096b98f696edb4655a25 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:24:17 -0300 Subject: [PATCH 53/77] fix(lint): include camelCase and PascalCase formats for variable names in ESLint rules for improved naming consistency --- .eslintrc.js | 4 ++++ .../wazuh-core/public/components/table-data/table-data.tsx | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index c157393d4f..0503b6a896 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -248,6 +248,10 @@ module.exports = { leadingUnderscore: 'allow', }, { selector: 'import', format: ['camelCase', 'PascalCase'] }, + { + selector: 'variable', + format: ['camelCase', 'PascalCase'], + }, { selector: 'variable', types: ['function'], diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx index b5eb6cd5de..8127c0b41f 100644 --- a/plugins/wazuh-core/public/components/table-data/table-data.tsx +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -235,7 +235,6 @@ export function TableData({ totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions, }; - // eslint-disable-next-line @typescript-eslint/naming-convention const ReloadButton = ( triggerReload()}> From db7cde73f9011beac3541f90300eadc9b3776703 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:33:09 -0300 Subject: [PATCH 54/77] fix(lint): update unused variable handling in table-data component to adhere to ESLint rules for cleaner code consistency --- .eslintrc.js | 22 +++---------------- .../components/table-data/table-data.tsx | 8 +++++-- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0503b6a896..2fd2cb9cfc 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -210,6 +210,7 @@ module.exports = { /* -------------------------------------------------------------------------- */ /* @typescript-eslint */ /* -------------------------------------------------------------------------- */ + '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { @@ -244,31 +245,13 @@ module.exports = { 'error', { selector: 'default', - format: ['camelCase'], - leadingUnderscore: 'allow', - }, - { selector: 'import', format: ['camelCase', 'PascalCase'] }, - { - selector: 'variable', - format: ['camelCase', 'PascalCase'], - }, - { - selector: 'variable', - types: ['function'], - format: ['camelCase', 'PascalCase'], - }, - { - selector: 'function', format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', }, { selector: ['objectLiteralProperty', 'typeProperty'], format: null, }, - { - selector: ['objectLiteralMethod', 'typeMethod'], - format: ['camelCase', 'PascalCase'], - }, { selector: ['class', 'interface', 'typeLike'], format: ['PascalCase'], @@ -375,6 +358,7 @@ module.exports = { 'error', { allow: ['arrowFunctions'] }, ], + '@typescript-eslint/no-unused-vars': 'off', }, }, ], diff --git a/plugins/wazuh-core/public/components/table-data/table-data.tsx b/plugins/wazuh-core/public/components/table-data/table-data.tsx index 8127c0b41f..919dcc76cb 100644 --- a/plugins/wazuh-core/public/components/table-data/table-data.tsx +++ b/plugins/wazuh-core/public/components/table-data/table-data.tsx @@ -356,8 +356,12 @@ export function TableData({ ({ ...rest }), + ({ + searchable: _searchable, + show: _show, + composeField: _composeField, + ...rest + }) => ({ ...rest }), )} items={items} loading={isLoading} From b8315121a28ce4b8166c9deadd5f6e4543fb1f64 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:46:09 -0300 Subject: [PATCH 55/77] fix(lint): enhance withServices HOC to set displayName for better debugging and component identification consistency --- .../public/services/http/ui/with-services.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/ui/with-services.tsx b/plugins/wazuh-core/public/services/http/ui/with-services.tsx index 4e5d665568..f726472aaf 100644 --- a/plugins/wazuh-core/public/services/http/ui/with-services.tsx +++ b/plugins/wazuh-core/public/services/http/ui/with-services.tsx @@ -1,6 +1,12 @@ import React from 'react'; -// eslint-disable-next-line @typescript-eslint/naming-convention, react/display-name -export const withServices = services => WrappedComponent => props => ( - -); +export const withServices = + services => (WrappedComponent: React.ElementType) => { + const ComponentWithServices = (props: any) => ( + + ); + + ComponentWithServices.displayName = `WithServices(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; + + return ComponentWithServices; + }; From 19f57c4ab9a148c5b10c08af435d5e647d6929d9 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:46:35 -0300 Subject: [PATCH 56/77] fix(lint): remove unused eslint disables and update callback parameter naming for better adherence to coding standards --- plugins/wazuh-core/public/components/search-bar/index.test.tsx | 3 --- .../public/components/search-bar/query-language/wql.test.tsx | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/index.test.tsx b/plugins/wazuh-core/public/components/search-bar/index.test.tsx index 9500613ee8..4708cb28e5 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import React from 'react'; import { render } from '@testing-library/react'; import { SearchBar } from './index'; @@ -36,10 +35,8 @@ describe('SearchBar component', () => { }, }, ], - /* eslint-disable @typescript-eslint/no-empty-function */ onChange: () => {}, onSearch: () => {}, - /* eslint-enable @typescript-eslint/no-empty-function */ }; it('Renders correctly the initial render', async () => { diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index 1a43032ddf..cc9ee19501 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SearchBar } from '../index'; @@ -449,7 +448,7 @@ describe('Query language - WQL', () => { }, validate: { // eslint-disable-next-line @typescript-eslint/no-unused-vars - value: (token, { field, operator_compare }) => { + value: (token, { field, operator_compare: operatorCompare }) => { if (field === 'field_not_number') { const value = token.formattedValue || token.value; From 4d2fc7086173e577c2f6786f19b33119ebfaded2 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:47:00 -0300 Subject: [PATCH 57/77] fix(lint): remove unnecessary eslint-disable comment in server-table-data component for improved code clarity and standards compliance --- .../public/services/http/ui/components/server-table-data.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx index 0556fe3322..79f6fbf928 100644 --- a/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx +++ b/plugins/wazuh-core/public/services/http/ui/components/server-table-data.tsx @@ -6,7 +6,6 @@ import { ServerDataProps } from './types'; export function ServerTableData({ showActionExportFormatted, postActionButtons, - // eslint-disable-next-line @typescript-eslint/naming-convention ActionExportFormatted, ...props }: ServerDataProps) { From d34108d453827f53277e5c6cb5068e6c6e0de742 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:47:42 -0300 Subject: [PATCH 58/77] fix(lint): clean up request-interceptor by removing unused eslint-disable comments and improving parameter naming for clarity --- .../wazuh-core/public/services/http/request-interceptor.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/request-interceptor.ts b/plugins/wazuh-core/public/services/http/request-interceptor.ts index a6f1e4fdea..29311451a7 100644 --- a/plugins/wazuh-core/public/services/http/request-interceptor.ts +++ b/plugins/wazuh-core/public/services/http/request-interceptor.ts @@ -23,16 +23,14 @@ export class RequestInterceptorClient implements HTTPClientRequestInterceptor { private registerInterceptor() { this.logger.debug('Registering interceptor in core http'); this.unregisterInterceptor = this.http.intercept({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - responseError: (httpErrorResponse, controller) => { + responseError: (httpErrorResponse, _controller) => { if ( httpErrorResponse.response?.status === HTTP_STATUS_CODES.UNAUTHORIZED ) { this.cancel(); } }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - request: (current, controller) => { + request: (_current, _controller) => { if (!this.allow) { throw new Error('Disable request'); } From cd483182240ded99b7fabf64abb19904ddac9d6b Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:50:13 -0300 Subject: [PATCH 59/77] fix(lint): remove unnecessary eslint-disable comment from configuration-store for enhanced code clarity and standards compliance --- plugins/wazuh-core/public/utils/configuration-store.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/utils/configuration-store.ts b/plugins/wazuh-core/public/utils/configuration-store.ts index 51ba05f557..993ed745d1 100644 --- a/plugins/wazuh-core/public/utils/configuration-store.ts +++ b/plugins/wazuh-core/public/utils/configuration-store.ts @@ -90,7 +90,6 @@ export class ConfigurationStore implements IConfigurationStore { }; for (const setting of settings) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete updatedSettings[setting]; } From a4c5a12f3c7e37afb348e48e31b48df325dd3552 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:55:00 -0300 Subject: [PATCH 60/77] fix(lint): remove unnecessary eslint-disable comments from configuration and search-bar components for improved code clarity and standards compliance --- plugins/wazuh-core/common/services/configuration.ts | 2 -- plugins/wazuh-core/public/components/search-bar/index.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/plugins/wazuh-core/common/services/configuration.ts b/plugins/wazuh-core/common/services/configuration.ts index 4afeaeac40..f1d305918c 100644 --- a/plugins/wazuh-core/common/services/configuration.ts +++ b/plugins/wazuh-core/common/services/configuration.ts @@ -164,9 +164,7 @@ export interface IConfiguration { export class Configuration implements IConfiguration { store: IConfigurationStore | null = null; - // eslint-disable-next-line @typescript-eslint/naming-convention _settings: Map>; - // eslint-disable-next-line @typescript-eslint/naming-convention _categories: Map>; constructor( diff --git a/plugins/wazuh-core/public/components/search-bar/index.tsx b/plugins/wazuh-core/public/components/search-bar/index.tsx index 1681103b89..5725ecbe82 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.tsx @@ -68,7 +68,6 @@ export const SearchBar = ({ const debounceUpdateSearchBarTimer = useRef(); // Handler when searching - // eslint-disable-next-line @typescript-eslint/naming-convention const _onSearch = (output: any) => { // TODO: fix when searching onSearch(output); From 4a9e48c803f36f63e7302852c66d71809b7cd7d6 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:55:18 -0300 Subject: [PATCH 61/77] fix(lint): remove unused eslint-disable comment from search-bar component to enhance code clarity and maintain coding standards --- plugins/wazuh-core/public/components/search-bar/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/components/search-bar/index.tsx b/plugins/wazuh-core/public/components/search-bar/index.tsx index 5725ecbe82..c07a711b0c 100644 --- a/plugins/wazuh-core/public/components/search-bar/index.tsx +++ b/plugins/wazuh-core/public/components/search-bar/index.tsx @@ -168,7 +168,6 @@ export const SearchBar = ({ value={input} onChange={onChangeInput} onKeyPress={onKeyPressHandler} - // eslint-disable-next-line @typescript-eslint/no-empty-function onInputChange={() => {}} /* This method is run by EuiSuggest when there is a change in a div wrapper of the input and should be defined. Defining this property prevents an error. */ From 16ceef78cdac6aceb27995d2df07115e6bafa7a9 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:55:40 -0300 Subject: [PATCH 62/77] fix(lint): remove unused eslint-disable comment from wql.test.tsx to improve code clarity and maintain coding standards --- .../public/components/search-bar/query-language/wql.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx index cc9ee19501..c53e437aad 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.test.tsx @@ -447,7 +447,6 @@ describe('Query language - WQL', () => { value: () => [], }, validate: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars value: (token, { field, operator_compare: operatorCompare }) => { if (field === 'field_not_number') { const value = token.formattedValue || token.value; From 846200ec528e9766d4d65002b5716e8d87dfe9b1 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 15:58:37 -0300 Subject: [PATCH 63/77] fix(code): replace WQL.id with WQL_ID in query language file for better consistency and clarity in code structure --- .../public/components/search-bar/query-language/wql.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 920ce6a009..94944be558 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -11,6 +11,8 @@ https://documentation.wazuh.com/current/user-manual/api/queries.html // Example of another query language definition */ +const WQL_ID = 'wql'; + type ITokenType = | 'field' | 'operator_compare' @@ -725,8 +727,7 @@ function getOutput(input: string, options: OptionsQL) { ); return { - // eslint-disable-next-line no-use-before-define - language: WQL.id, + language: WQL_ID, apiQuery: { q: [ implicitQueryAsUQL, From ca83f017b367172ad16dc9c8f754e6d710e8692b Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:11:42 -0300 Subject: [PATCH 64/77] fix(lint): remove unused eslint-disable comment from server-client.ts to enhance code clarity and maintain coding standards --- .../public/components/search-bar/query-language/wql.tsx | 1 - plugins/wazuh-core/public/services/http/server-client.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 94944be558..d2d1269098 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -46,7 +46,6 @@ Implemented schema: const language = { // Tokens tokens: { - // eslint-disable-next-line camelcase operator_compare: { literal: { '=': 'equality', diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index a43cb33b59..a9f5f79ff2 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -65,7 +65,6 @@ export class WzRequest implements HTTPClientServer { private async requestInternal( method: HTTPVerb, path: string, - // eslint-disable-next-line default-param-last payload: any = null, options: RequestInternalOptions, ): Promise { From 649a2e74941db220ec65f5a7d47094657e6ae04a Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:13:21 -0300 Subject: [PATCH 65/77] fix(lint): eliminate unused eslint-disable comments in wql.tsx to enhance code readability and maintain coding standards --- .../public/components/search-bar/query-language/wql.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index d2d1269098..3fb4ce6a82 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -61,7 +61,6 @@ const language = { or: 'or', }, }, - // eslint-disable-next-line camelcase operator_group: { literal: { '(': 'open group', @@ -81,15 +80,11 @@ const language = { // Suggestion mapper by language token type const suggestionMappingLanguageTokenType = { field: { iconType: 'kqlField', color: 'tint4' }, - // eslint-disable-next-line camelcase operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, value: { iconType: 'kqlValue', color: 'tint0' }, conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - // eslint-disable-next-line camelcase operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - // eslint-disable-next-line camelcase function_search: { iconType: 'search', color: 'tint5' }, - // eslint-disable-next-line camelcase validation_error: { iconType: 'alert', color: 'tint2' }, }; From 1df9aa7480072ea374b65628b98deecc35611682 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:13:50 -0300 Subject: [PATCH 66/77] fix(lint): remove unnecessary eslint-disable comment in wql.tsx to improve code readability and maintain standards --- .../public/components/search-bar/query-language/wql.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 3fb4ce6a82..9a468a9ae9 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -980,7 +980,6 @@ function validate( return undefined; } -// eslint-disable-next-line @typescript-eslint/naming-convention export const WQL = { id: 'wql', label: 'WQL', From 8159bd61e8bc5185fa0a5f321fbc25170dfb9f9e Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:14:17 -0300 Subject: [PATCH 67/77] fix(types): replace hardcoded id with WQL_ID in ISearchBarModeWQL interface for improved type safety and consistency --- .../public/components/search-bar/query-language/wql.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 9a468a9ae9..28bc863dd5 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -243,7 +243,7 @@ interface OptionsQL { } export interface ISearchBarModeWQL extends OptionsQL { - id: 'wql'; + id: typeof WQL_ID; } /** From 1c30b77fd4c30c23f161d9c273c19561968ccd10 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:14:46 -0300 Subject: [PATCH 68/77] fix(lint): refactor conditional logic in wql.tsx to improve code clarity and maintain consistent formatting standards --- .../public/components/search-bar/query-language/wql.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 28bc863dd5..8326646994 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -1137,19 +1137,21 @@ export const WQL = { } else { // add a whitespace for conjunction // add a whitespace for grouping operator ) - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - !/\s$/.test(input) && + if ( + !/\s$/.test(input) && (item.type.iconType === suggestionMappingLanguageTokenType.conjunction.iconType || lastToken?.type === 'conjunction' || (item.type.iconType === suggestionMappingLanguageTokenType.operator_group .iconType && - item.label === ')')) && + item.label === ')')) + ) { tokens.push({ type: 'whitespace', value: ' ', }); + } // add a new token of the selected type and value tokens.push({ From a93ff8572e5cdfd60e435873b30388f09d31dd32 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 16:39:01 -0300 Subject: [PATCH 69/77] fix(api): use WAZUH_ERROR_DAEMONS_NOT_READY constant for error messaging consistency in WazuhApiCtrl error handling --- plugins/main/server/controllers/wazuh-api.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index ce9bf34c13..f0016b62b1 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -32,6 +32,7 @@ import { version as pluginVersion, revision as pluginRevision, } from '../../package.json'; +import { WAZUH_ERROR_DAEMONS_NOT_READY } from '../../../wazuh-core/common/constants'; export class WazuhApiCtrl { async getToken( @@ -156,7 +157,7 @@ export class WazuhApiCtrl { // Look for socket-related errors if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `ERROR3099 - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ responseManagerInfo.data.detail || 'Server not ready yet' }`, 3099, @@ -289,7 +290,7 @@ export class WazuhApiCtrl { if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `ERROR3099 - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ response.data.detail || 'Server not ready yet' }`, 3099, @@ -412,7 +413,7 @@ export class WazuhApiCtrl { ); } catch (error) { return ErrorResponse( - `ERROR3099 - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ error.response?.data?.detail || 'Server not ready yet' }`, 3099, @@ -662,7 +663,7 @@ export class WazuhApiCtrl { ); return ErrorResponse( - `ERROR3099 - ${error.message || 'Server not ready yet'}`, + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${error.message || 'Server not ready yet'}`, 3099, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, @@ -684,7 +685,7 @@ export class WazuhApiCtrl { if (responseIsDown) { return ErrorResponse( - `ERROR3099 - ${response.body.message || 'Server not ready yet'}`, + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${response.body.message || 'Server not ready yet'}`, 3099, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, From bd2417834815754c441b7d0a8dc9cd8cd05da66c Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 17:30:56 -0300 Subject: [PATCH 70/77] fix(api): update WazuhApiCtrl to use WAZUH_ERROR_CODES for consistent error messaging across server responses --- plugins/main/server/controllers/wazuh-api.ts | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index f0016b62b1..1ecfa5a2fe 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -32,7 +32,7 @@ import { version as pluginVersion, revision as pluginRevision, } from '../../package.json'; -import { WAZUH_ERROR_DAEMONS_NOT_READY } from '../../../wazuh-core/common/constants'; +import { WAZUH_ERROR_CODES } from '../../../wazuh-core/common/constants'; export class WazuhApiCtrl { async getToken( @@ -157,10 +157,10 @@ export class WazuhApiCtrl { // Look for socket-related errors if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ + `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ responseManagerInfo.data.detail || 'Server not ready yet' }`, - 3099, + WAZUH_ERROR_CODES.DAEMONS_NOT_READY, HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -290,10 +290,10 @@ export class WazuhApiCtrl { if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ + `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ response.data.detail || 'Server not ready yet' }`, - 3099, + WAZUH_ERROR_CODES.DAEMONS_NOT_READY, HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -413,10 +413,10 @@ export class WazuhApiCtrl { ); } catch (error) { return ErrorResponse( - `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ + `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ error.response?.data?.detail || 'Server not ready yet' }`, - 3099, + WAZUH_ERROR_CODES.DAEMONS_NOT_READY, error?.response?.status || HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -663,8 +663,8 @@ export class WazuhApiCtrl { ); return ErrorResponse( - `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${error.message || 'Server not ready yet'}`, - 3099, + `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${error.message || 'Server not ready yet'}`, + WAZUH_ERROR_CODES.DAEMONS_NOT_READY, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, ); @@ -685,8 +685,8 @@ export class WazuhApiCtrl { if (responseIsDown) { return ErrorResponse( - `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${response.body.message || 'Server not ready yet'}`, - 3099, + `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${response.body.message || 'Server not ready yet'}`, + WAZUH_ERROR_CODES.DAEMONS_NOT_READY, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, ); From d1c758158199528266f936e6342c4f24de50c9dd Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 17:31:59 -0300 Subject: [PATCH 71/77] fix(lint): update eslint config to enforce UPPER_CASE for enum and add naming convention for constants files --- .eslintrc.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 2fd2cb9cfc..e9dd5a364f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -257,11 +257,7 @@ module.exports = { format: ['PascalCase'], }, { - selector: ['enum'], - format: ['PascalCase'], - }, - { - selector: ['enumMember'], + selector: ['enum', 'enumMember'], format: ['UPPER_CASE'], }, { @@ -351,6 +347,20 @@ module.exports = { ], }, }, + { + // constants files + files: ['plugins/**/constants.{ts,js}'], + rules: { + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'variable', + modifiers: ['global'], + format: ['UPPER_CASE'], + }, + ], + }, + }, { files: ['plugins/**/*.test.{js,jsx,ts,tsx}'], rules: { From e8fb5877e4d30fa9ca6778d6b8da38f8eec2be2f Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 17:49:02 -0300 Subject: [PATCH 72/77] fix(settings-validator): refactor class to object, update methods for consistent argument handling and improve readability --- .../common/services/settings-validator.ts | 153 +++++++++--------- 1 file changed, 72 insertions(+), 81 deletions(-) diff --git a/plugins/wazuh-core/common/services/settings-validator.ts b/plugins/wazuh-core/common/services/settings-validator.ts index f0392e314a..15503d6330 100644 --- a/plugins/wazuh-core/common/services/settings-validator.ts +++ b/plugins/wazuh-core/common/services/settings-validator.ts @@ -1,47 +1,48 @@ -import path from 'path'; +import path from 'node:path'; import { formatBytes } from './file-size'; -export class SettingsValidator { +export const SettingsValidator = { /** * Create a function that is a composition of the input validations * @param functions SettingsValidator functions to compose * @returns composed validation */ - static compose(...functions) { - return function composedValidation(value) { - for (const fn of functions) { - const result = fn(value); + compose(...functions: ((value: any) => string | undefined)[]) { + return function composedValidation(value: any) { + for (const callback of functions) { + const result = callback(value); + if (typeof result === 'string' && result.length > 0) { return result; } } }; - } + }, /** * Check the value is a string * @param value * @returns */ - static isString(value: unknown): string | undefined { + isString(value: unknown): string | undefined { return typeof value === 'string' ? undefined : 'Value is not a string.'; - } + }, /** * Check the string has no spaces * @param value * @returns */ - static hasNoSpaces(value: string): string | undefined { + hasNoSpaces(value: string): string | undefined { return /^\S*$/.test(value) ? undefined : 'No whitespaces allowed.'; - } + }, /** * Check the string has no empty * @param value * @returns */ - static isNotEmptyString(value: string): string | undefined { + isNotEmptyString(value: string): string | undefined { if (typeof value === 'string') { if (value.length === 0) { return 'Value can not be empty.'; @@ -49,39 +50,43 @@ export class SettingsValidator { return undefined; } } - } + }, /** * Check the number of string lines is limited * @param options * @returns */ - static multipleLinesString( + multipleLinesString( options: { minRows?: number; maxRows?: number; maxLength?: number } = {}, ) { return function (value: string) { const lines = value.split(/\r\n|\r|\n/).length; + const maxLength = options.maxLength; + if ( - typeof options.maxLength !== 'undefined' && - value.split('\n').some(line => line.length > options.maxLength) + maxLength !== undefined && + value.split('\n').some(line => line.length > maxLength) ) { - return `The maximum length of a line is ${options.maxLength} characters.`; + return `The maximum length of a line is ${maxLength} characters.`; } - if (typeof options.minRows !== 'undefined' && lines < options.minRows) { + + if (options.minRows !== undefined && lines < options.minRows) { return `The string should have more or ${options.minRows} line/s.`; } - if (typeof options.maxRows !== 'undefined' && lines > options.maxRows) { + + if (options.maxRows !== undefined && lines > options.maxRows) { return `The string should have less or equal to ${options.maxRows} line/s.`; } }; - } + }, /** * Creates a function that checks the string does not contain some characters * @param invalidCharacters * @returns */ - static hasNotInvalidCharacters(...invalidCharacters: string[]) { + hasNotInvalidCharacters(...invalidCharacters: string[]) { return function (value: string): string | undefined { return invalidCharacters.some(invalidCharacter => value.includes(invalidCharacter), @@ -91,14 +96,14 @@ export class SettingsValidator { )}.` : undefined; }; - } + }, /** * Creates a function that checks the string does not start with a substring * @param invalidStartingCharacters * @returns */ - static noStartsWithString(...invalidStartingCharacters: string[]) { + noStartsWithString(...invalidStartingCharacters: string[]) { return function (value: string): string | undefined { return invalidStartingCharacters.some(invalidStartingCharacter => value.startsWith(invalidStartingCharacter), @@ -106,49 +111,47 @@ export class SettingsValidator { ? `It can't start with: ${invalidStartingCharacters.join(', ')}.` : undefined; }; - } + }, /** * Creates a function that checks the string is not equals to some values * @param invalidLiterals * @returns */ - static noLiteralString(...invalidLiterals: string[]) { + noLiteralString(...invalidLiterals: string[]) { return function (value: string): string | undefined { - return invalidLiterals.some(invalidLiteral => value === invalidLiteral) + return invalidLiterals.includes(value) ? `It can't be: ${invalidLiterals.join(', ')}.` : undefined; }; - } + }, /** * Check the value is a boolean * @param value * @returns */ - static isBoolean(value: string): string | undefined { + isBoolean(value: string): string | undefined { return typeof value === 'boolean' ? undefined : 'It should be a boolean. Allowed values: true or false.'; - } + }, /** * Check the value is a number * @param value * @returns */ - static isNumber(value: string): string | undefined { + isNumber(value: string): string | undefined { return typeof value === 'number' ? undefined : 'Value is not a number.'; - } + }, /** * Check the value is a number between some optional limits * @param options * @returns */ - static number( - options: { min?: number; max?: number; integer?: boolean } = {}, - ) { + number(options: { min?: number; max?: number; integer?: boolean } = {}) { return function (value: number) { if (typeof value !== 'number') { return 'Value is not a number.'; @@ -158,130 +161,119 @@ export class SettingsValidator { return 'Number should be an integer.'; } - if (typeof options.min !== 'undefined' && value < options.min) { + if (options.min !== undefined && value < options.min) { return `Value should be greater or equal than ${options.min}.`; } - if (typeof options.max !== 'undefined' && value > options.max) { + + if (options.max !== undefined && value > options.max) { return `Value should be lower or equal than ${options.max}.`; } }; - } + }, /** * Creates a function that checks if the value is a json * @param validateParsed Optional parameter to validate the parsed object * @returns */ - static json(validateParsed: (object: any) => string | undefined) { + json(validateParsed: (object: any) => string | undefined) { return function (value: string) { let jsonObject; + // Try to parse the string as JSON try { jsonObject = JSON.parse(value); - } catch (error) { + } catch { return "Value can't be parsed. There is some error."; } return validateParsed ? validateParsed(jsonObject) : undefined; }; - } + }, /** * Creates a function that checks is the value is an array and optionally validates each element * @param validationElement Optional function to validate each element of the array * @returns */ - static array(validationElement: (json: any) => string | undefined) { + array(validationElement: (json: any) => string | undefined) { return function (value: unknown[]) { // Check the JSON is an array if (!Array.isArray(value)) { return 'Value is not a valid list.'; } + let index: number; + return validationElement - ? value.reduce((accum, elementValue) => { - if (accum) { - return accum; - } - - const resultValidationElement = validationElement(elementValue); - if (resultValidationElement) { - return resultValidationElement; - } - - return accum; - }, undefined) + ? (index = value.findIndex(element => validationElement(element))) >= 0 + ? validationElement(value[index]) + : undefined : undefined; }; - } + }, /** * Creates a function that checks if the value is equal to list of values * @param literals Array of values to compare * @returns */ - static literal(literals: unknown[]) { + literal(literals: unknown[]) { return function (value: any): string | undefined { return literals.includes(value) ? undefined : `Invalid value. Allowed values: ${literals.map(String).join(', ')}.`; }; - } + }, // FilePicker - static filePickerSupportedExtensions = + filePickerSupportedExtensions: (extensions: string[]) => (options: { name: string }) => { - if ( - typeof options === 'undefined' || - typeof options.name === 'undefined' - ) { + if (options === undefined || options.name === undefined) { return; } + if (!extensions.includes(path.extname(options.name))) { return `File extension is invalid. Allowed file extensions: ${extensions.join( ', ', )}.`; } - }; + }, /** * filePickerFileSize * @param options */ - static filePickerFileSize = + filePickerFileSize: (options: { maxBytes?: number; minBytes?: number; meaningfulUnit?: boolean; }) => (value: { size: number }) => { - if (typeof value === 'undefined' || typeof value.size === 'undefined') { + if (value === undefined || value.size === undefined) { return; } - if ( - typeof options.minBytes !== 'undefined' && - value.size <= options.minBytes - ) { + + if (options.minBytes !== undefined && value.size <= options.minBytes) { return `File size should be greater or equal than ${ options.meaningfulUnit ? formatBytes(options.minBytes) : `${options.minBytes} bytes` }.`; } - if ( - typeof options.maxBytes !== 'undefined' && - value.size >= options.maxBytes - ) { + + if (options.maxBytes !== undefined && value.size >= options.maxBytes) { return `File size should be lower or equal than ${ options.meaningfulUnit ? formatBytes(options.maxBytes) : `${options.maxBytes} bytes` }.`; } - }; + }, - //IPv4: 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 - //IPv6: This is a set or eight hexadecimal expressions, each from 0000 to FFFF. 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + // IPv4: 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 + // IPv6: This is a set or eight hexadecimal expressions, each from 0000 to FFFF. 2001:0db8:85a3:0000:0000:8a2e:0370:7334 // FQDN: Maximum of 63 characters per label. // Can only contain numbers, letters and hyphens (-) @@ -292,10 +284,10 @@ export class SettingsValidator { // Hostname: Maximum of 63 characters per label. Same rules as FQDN apply. - static serverAddressHostnameFQDNIPv4IPv6(value: string) { + serverAddressHostnameFQDNIPv4IPv6(value: string) { const isFQDNOrHostname = - /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ](?:\.[a-zA-Z0-9áéíóúüñ-]{0,62}[a-zA-Z0-9áéíóúüñ]){0,}$/; - const isIPv6 = /^(?:[0-9a-fA-F]{4}:){7}[0-9a-fA-F]{4}$/; + /^(?!-)(?!.*--)[\dA-Za-záéíñóúü-]{0,62}[\dA-Za-záéíñóúü](?:\.[\dA-Za-záéíñóúü-]{0,62}[\dA-Za-záéíñóúü])*$/; + const isIPv6 = /^(?:[\dA-Fa-f]{4}:){7}[\dA-Fa-f]{4}$/; if ( value.length > 255 || @@ -303,6 +295,5 @@ export class SettingsValidator { ) { return 'It should be a valid hostname, FQDN, IPv4 or uncompressed IPv6'; } - return undefined; - } -} + }, +}; From 675ee006e93806ed423c84f639a77d0c6db34b79 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 17:58:51 -0300 Subject: [PATCH 73/77] fix(lint): enhance eslint config to allow UPPER_CASE, camelCase, and PascalCase for global variables in naming conventions --- .eslintrc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index e9dd5a364f..16133261ff 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -260,6 +260,11 @@ module.exports = { selector: ['enum', 'enumMember'], format: ['UPPER_CASE'], }, + { + selector: ['variable'], + modifiers: ['global'], + format: ['UPPER_CASE', 'camelCase', 'PascalCase'], + }, { selector: ['variable'], modifiers: ['global'], From 3f3f9e60bba272259008555bf0240287d1d86a55 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Fri, 6 Dec 2024 18:00:07 -0300 Subject: [PATCH 74/77] fix(constants): update import path and refactor configuration constants for improved readability and consistency with naming conventions --- plugins/wazuh-core/common/constants.ts | 584 +++++++++++-------------- 1 file changed, 256 insertions(+), 328 deletions(-) diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index e4ad9aa1ab..726ac2c535 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -9,13 +9,13 @@ * * Find more information about this on the LICENSE file. */ -import path from 'path'; +import path from 'node:path'; import { version } from '../package.json'; // import { validate as validateNodeCronInterval } from 'node-cron'; import { SettingsValidator } from '../common/services/settings-validator'; // Plugin -export const PLUGIN_VERSION = version; + export const PLUGIN_VERSION_SHORT = version.split('.').splice(0, 2).join('.'); // Index patterns - Wazuh alerts @@ -101,7 +101,7 @@ export const WAZUH_SECURITY_PLUGINS = [ ]; // App configuration -export const WAZUH_CONFIGURATION_CACHE_TIME = 10000; // time in ms; +export const WAZUH_CONFIGURATION_CACHE_TIME = 10_000; // time in ms; // Reserved ids for Users/Role mapping export const WAZUH_API_RESERVED_ID_LOWER_THAN = 100; @@ -109,6 +109,7 @@ export const WAZUH_API_RESERVED_WUI_SECURITY_RULES = [1, 2]; // Wazuh data path const WAZUH_DATA_PLUGIN_PLATFORM_BASE_PATH = 'data'; + export const WAZUH_DATA_PLUGIN_PLATFORM_BASE_ABSOLUTE_PATH = path.join( __dirname, '../../../', @@ -148,7 +149,9 @@ export const WAZUH_DATA_DOWNLOADS_REPORTS_DIRECTORY_PATH = path.join( export const WAZUH_QUEUE_CRON_FREQ = '*/15 * * * * *'; // Every 15 seconds // Wazuh errors -export const WAZUH_ERROR_DAEMONS_NOT_READY = 'ERROR3099'; +export enum WAZUH_ERROR_CODES { + DAEMONS_NOT_READY = 3099, +} // Agents export enum WAZUH_AGENTS_OS_TYPE { @@ -234,7 +237,7 @@ export const WAZUH_LINK_SLACK = 'https://wazuh.com/community/join-us-on-slack'; export const HEALTH_CHECK = 'health-check'; // Health check -export const HEALTH_CHECK_REDIRECTION_TIME = 300; //ms +export const HEALTH_CHECK_REDIRECTION_TIME = 300; // ms // Plugin platform settings // Default timeFilter set by the app @@ -246,7 +249,7 @@ export const PLUGIN_PLATFORM_SETTING_NAME_TIME_FILTER = 'timepicker:timeDefaults'; // Default maxBuckets set by the app -export const WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS = 200000; +export const WAZUH_PLUGIN_PLATFORM_SETTING_MAX_BUCKETS = 200_000; export const PLUGIN_PLATFORM_SETTING_NAME_MAX_BUCKETS = 'timeline:max_buckets'; // Default metaFields set by the app @@ -386,7 +389,7 @@ export const NOT_TIME_FIELD_NAME_INDEX_PATTERN = 'not_time_field_name_index_pattern'; // Customization -export const CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES = 1048576; +export const CUSTOMIZATION_ENDPOINT_PAYLOAD_UPLOAD_CUSTOM_FILE_MAXIMUM_BYTES = 1_048_576; // Plugin settings export enum SettingCategory { @@ -400,23 +403,27 @@ export enum SettingCategory { API_CONNECTION, } -type TPluginSettingOptionsTextArea = { +interface TPluginSettingOptionsArrayOf { + arrayOf: Record; +} + +interface TPluginSettingOptionsTextArea { maxRows?: number; minRows?: number; maxLength?: number; -}; +} -type TPluginSettingOptionsSelect = { +interface TPluginSettingOptionsSelect { select: { text: string; value: any }[]; -}; +} -type TPluginSettingOptionsEditor = { +interface TPluginSettingOptionsEditor { editor: { language: string; }; -}; +} -type TPluginSettingOptionsFile = { +interface TPluginSettingOptionsFile { file: { type: 'image'; extensions?: string[]; @@ -437,24 +444,24 @@ type TPluginSettingOptionsFile = { resolveStaticURL: (filename: string) => string; }; }; -}; +} -type TPluginSettingOptionsNumber = { +interface TPluginSettingOptionsNumber { number: { min?: number; max?: number; integer?: boolean; }; -}; +} -type TPluginSettingOptionsSwitch = { +interface TPluginSettingOptionsSwitch { switch: { values: { disabled: { label?: string; value: any }; enabled: { label?: string; value: any }; }; }; -}; +} export enum EpluginSettingType { text = 'text', @@ -469,7 +476,7 @@ export enum EpluginSettingType { custom = 'custom', } -export type TPluginSetting = { +export interface TPluginSetting { // Define the text displayed in the UI. title: string; // Description. @@ -509,7 +516,8 @@ export type TPluginSetting = { | TPluginSettingOptionsNumber | TPluginSettingOptionsSelect | TPluginSettingOptionsSwitch - | TPluginSettingOptionsTextArea; + | TPluginSettingOptionsTextArea + | TPluginSettingOptionsArrayOf; // Transform the input value. The result is saved in the form global state of Settings/Configuration uiFormTransformChangedInputValue?: (value: any) => any; // Transform the configuration value or default as initial value for the input in Settings/Configuration @@ -520,68 +528,8 @@ export type TPluginSetting = { validateUIForm?: (value: any) => string | undefined; // Validate function creator to validate the setting in the backend. validate?: (value: unknown) => string | undefined; -}; - -export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; -export type TPluginSettingCategory = { - title: string; - description?: string; - documentationLink?: string; - renderOrder?: number; -}; - -export const PLUGIN_SETTINGS_CATEGORIES: { - [category: number]: TPluginSettingCategory; -} = { - [SettingCategory.HEALTH_CHECK]: { - title: 'Health check', - description: "Checks will be executed by the app's Healthcheck.", - renderOrder: SettingCategory.HEALTH_CHECK, - }, - [SettingCategory.GENERAL]: { - title: 'General', - description: - 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', - renderOrder: SettingCategory.GENERAL, - }, - [SettingCategory.SECURITY]: { - title: 'Security', - description: 'Application security options such as unauthorized roles.', - renderOrder: SettingCategory.SECURITY, - }, - [SettingCategory.MONITORING]: { - title: 'Task:Monitoring', - description: - 'Options related to the agent status monitoring job and its storage in indexes.', - renderOrder: SettingCategory.MONITORING, - }, - [SettingCategory.STATISTICS]: { - title: 'Task:Statistics', - description: - 'Options related to the daemons manager monitoring job and their storage in indexes.', - renderOrder: SettingCategory.STATISTICS, - }, - [SettingCategory.VULNERABILITIES]: { - title: 'Vulnerabilities', - description: - 'Options related to the agent vulnerabilities monitoring job and its storage in indexes.', - renderOrder: SettingCategory.VULNERABILITIES, - }, - [SettingCategory.CUSTOMIZATION]: { - title: 'Custom branding', - description: - 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', - documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', - renderOrder: SettingCategory.CUSTOMIZATION, - }, - [SettingCategory.API_CONNECTION]: { - title: 'API connections', - description: 'Options related to the API connections.', - renderOrder: SettingCategory.API_CONNECTION, - }, -}; - -export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { +} +export const PLUGIN_SETTINGS: Record = { 'alerts.sample.prefix': { title: 'Sample alerts prefix', description: @@ -597,7 +545,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, requiresRunningHealthCheck: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validate: SettingsValidator.compose( @@ -639,15 +587,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.fields': { title: 'Known fields', @@ -670,15 +616,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.maxBuckets': { title: 'Set max buckets to 200000', @@ -701,15 +645,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.metaFields': { title: 'Remove meta fields', @@ -732,15 +674,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.pattern': { title: 'Index pattern', @@ -763,15 +703,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.setup': { title: 'API version', @@ -794,15 +732,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.template': { title: 'Index template', @@ -825,15 +761,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'checks.timeFilter': { title: 'Set time filter to 24h', @@ -856,15 +790,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'configuration.ui_api_editable': { title: 'Configuration UI editable', @@ -888,15 +820,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'cron.prefix': { title: 'Cron prefix', @@ -911,7 +841,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: WAZUH_STATISTICS_DEFAULT_PREFIX, isConfigurableFromSettings: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validate: SettingsValidator.compose( @@ -959,12 +889,14 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { ): any { try { return JSON.parse(value); - } catch (error) { + } catch { return value; } }, validateUIForm: function (value) { - return SettingsValidator.json(this.validate)(value); + return SettingsValidator.json( + this.validate as (value: unknown) => string | undefined, + )(value); }, validate: SettingsValidator.compose( SettingsValidator.array( @@ -1010,11 +942,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, requiresRunningHealthCheck: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: function (value) { return SettingsValidator.literal( - this.options.select.map(({ value }) => value), + this.options?.select.map(({ value }) => value), )(value); }, }, @@ -1075,24 +1007,16 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function ( - value: number, - ): string { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'cron.statistics.index.shards': { title: 'Index shards', @@ -1114,22 +1038,16 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'cron.statistics.interval': { title: 'Interval', @@ -1171,15 +1089,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'customization.enabled': { title: 'Status', @@ -1202,15 +1118,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'customization.logo.app': { title: 'App main logo', @@ -1251,11 +1165,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ - ...this.options.file.size, + ...(this.options as TPluginSettingOptionsFile)?.file.size, meaningfulUnit: true, }), SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, + (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], ), )(value); }, @@ -1299,11 +1213,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ - ...this.options.file.size, + ...(this.options as TPluginSettingOptionsFile)?.file.size, meaningfulUnit: true, }), SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, + (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], ), )(value); }, @@ -1346,11 +1260,11 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { validateUIForm: function (value) { return SettingsValidator.compose( SettingsValidator.filePickerFileSize({ - ...this.options.file.size, + ...(this.options as TPluginSettingOptionsFile)?.file.size, meaningfulUnit: true, }), SettingsValidator.filePickerSupportedExtensions( - this.options.file.extensions, + (this.options as TPluginSettingOptionsFile)?.file.extensions ?? [], ), )(value); }, @@ -1370,14 +1284,14 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, options: { maxRows: 2, maxLength: 50 }, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: function (value) { return SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, + maxRows: (this.options as TPluginSettingOptionsTextArea)?.maxRows, + maxLength: (this.options as TPluginSettingOptionsTextArea)?.maxLength, }), )(value); }, @@ -1397,14 +1311,14 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { isConfigurableFromSettings: true, options: { maxRows: 3, maxLength: 40 }, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: function (value) { return SettingsValidator.compose( SettingsValidator.isString, SettingsValidator.multipleLinesString({ - maxRows: this.options?.maxRows, - maxLength: this.options?.maxLength, + maxRows: (this.options as TPluginSettingOptionsTextArea)?.maxRows, + maxLength: (this.options as TPluginSettingOptionsTextArea)?.maxLength, }), )(value); }, @@ -1423,7 +1337,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: '', isConfigurableFromSettings: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: SettingsValidator.compose( SettingsValidator.isString, @@ -1444,7 +1358,7 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { defaultValue: '', isConfigurableFromSettings: false, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: SettingsValidator.compose( SettingsValidator.isString, @@ -1472,15 +1386,13 @@ export const PLUGIN_SETTINGS: { [key: string]: TPluginSetting } = { }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, hosts: { title: 'Server hosts', @@ -1520,12 +1432,12 @@ hosts: username: wazuh-wui password: wazuh-wui run_as: false`, - transformFrom: value => { - return value.map(hostData => { + transformFrom: value => + value.map((hostData: Record) => { const key = Object.keys(hostData)?.[0]; + return { ...hostData[key], id: key }; - }); - }, + }), }, }, options: { @@ -1536,7 +1448,7 @@ hosts: type: EpluginSettingType.text, defaultValue: 'default', isConfigurableFromSettings: true, - validateUIForm: function (value) { + validateUIForm: function (value: string) { return this.validate(value); }, validate: SettingsValidator.compose( @@ -1550,7 +1462,7 @@ hosts: type: EpluginSettingType.text, defaultValue: 'https://localhost', isConfigurableFromSettings: true, - validateUIForm: function (value) { + validateUIForm: function (value: string) { return this.validate(value); }, validate: SettingsValidator.compose( @@ -1562,31 +1474,23 @@ hosts: title: 'Port', description: 'Port', type: EpluginSettingType.number, - defaultValue: 55000, + defaultValue: 55_000, isConfigurableFromSettings: true, options: { number: { min: 0, - max: 65535, + max: 65_535, integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function ( - value: number, - ) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, - validateUIForm: function (value) { + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, + validateUIForm: function (value: number) { return this.validate( this.uiFormTransformInputValueToConfigurationValue(value), ); }, - validate: function (value) { + validate: function (value: number) { return SettingsValidator.number(this.options.number)(value); }, }, @@ -1596,7 +1500,7 @@ hosts: type: EpluginSettingType.text, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, - validateUIForm: function (value) { + validateUIForm: function (value: string) { return this.validate(value); }, validate: SettingsValidator.compose( @@ -1610,7 +1514,7 @@ hosts: type: EpluginSettingType.password, defaultValue: 'wazuh-wui', isConfigurableFromSettings: true, - validateUIForm: function (value) { + validateUIForm: function (value: string) { return this.validate(value); }, validate: SettingsValidator.compose( @@ -1632,12 +1536,8 @@ hosts: }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validateUIForm: function (value) { + uiFormTransformChangedInputValue: Boolean, + validateUIForm: function (value: string) { return this.validate(value); }, validate: SettingsValidator.isBoolean, @@ -1645,11 +1545,7 @@ hosts: }, }, isConfigurableFromSettings: false, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, // TODO: add validation // validate: SettingsValidator.isBoolean, // validate: function (schema) { @@ -1682,13 +1578,15 @@ hosts: ): any { try { return JSON.parse(value); - } catch (error) { + } catch { return value; } }, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validateUIForm: function (value) { - return SettingsValidator.json(this.validate)(value); + return SettingsValidator.json( + this.validate as (value: unknown) => string | undefined, + )(value); }, validate: SettingsValidator.compose( SettingsValidator.array( @@ -1734,15 +1632,13 @@ hosts: }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'wazuh.updates.disabled': { title: 'Check updates', @@ -1764,12 +1660,10 @@ hosts: }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, - validate: SettingsValidator.isBoolean, + uiFormTransformChangedInputValue: Boolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, pattern: { title: 'Index pattern', @@ -1787,7 +1681,7 @@ hosts: requiresRunningHealthCheck: true, // Validation: https://github.com/elastic/elasticsearch/blob/v7.10.2/docs/reference/indices/create-index.asciidoc validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: SettingsValidator.compose( SettingsValidator.isString, @@ -1819,7 +1713,7 @@ hosts: 'Maximum time, in milliseconds, the app will wait for an API response when making requests to it. It will be ignored if the value is set under 1500 milliseconds.', category: SettingCategory.GENERAL, type: EpluginSettingType.number, - defaultValue: 20000, + defaultValue: 20_000, isConfigurableFromSettings: true, options: { number: { @@ -1827,22 +1721,16 @@ hosts: integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'wazuh.monitoring.creation': { title: 'Index creation', @@ -1879,11 +1767,11 @@ hosts: isConfigurableFromSettings: true, requiresRunningHealthCheck: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: function (value) { return SettingsValidator.literal( - this.options.select.map(({ value }) => value), + this.options?.select.map(({ value }) => value), )(value); }, }, @@ -1909,15 +1797,13 @@ hosts: }, }, }, - uiFormTransformChangedInputValue: function ( - value: boolean | string, - ): boolean { - return Boolean(value); - }, + uiFormTransformChangedInputValue: Boolean, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, - validate: SettingsValidator.isBoolean, + validate: SettingsValidator.isBoolean as ( + value: unknown, + ) => string | undefined, }, 'wazuh.monitoring.frequency': { title: 'Frequency', @@ -1939,22 +1825,16 @@ hosts: integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'wazuh.monitoring.pattern': { title: 'Index pattern', @@ -1970,7 +1850,7 @@ hosts: isConfigurableFromSettings: true, requiresRunningHealthCheck: true, validateUIForm: function (value) { - return this.validate(value); + return this.validate?.(value); }, validate: SettingsValidator.compose( SettingsValidator.isString, @@ -2011,22 +1891,16 @@ hosts: integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'wazuh.monitoring.shards': { title: 'Index shards', @@ -2048,22 +1922,16 @@ hosts: integer: true, }, }, - uiFormTransformConfigurationValueToInputValue: function (value: number) { - return String(value); - }, - uiFormTransformInputValueToConfigurationValue: function ( - value: string, - ): number { - return Number(value); - }, + uiFormTransformConfigurationValueToInputValue: String, + uiFormTransformInputValueToConfigurationValue: Number, validateUIForm: function (value) { - return this.validate( - this.uiFormTransformInputValueToConfigurationValue(value), + return this.validate?.( + this.uiFormTransformInputValueToConfigurationValue?.(value), ); }, - validate: function (value) { - return SettingsValidator.number(this.options.number)(value); - }, + validate: function (value: number) { + return SettingsValidator.number(this.options?.number)(value); + } as (value: unknown) => string | undefined, }, 'vulnerabilities.pattern': { title: 'Index pattern', @@ -2101,8 +1969,66 @@ hosts: ), }, }; - export type TPluginSettingKey = keyof typeof PLUGIN_SETTINGS; +export type TPluginSettingWithKey = TPluginSetting & { key: TPluginSettingKey }; +export interface TPluginSettingCategory { + title: string; + description?: string; + documentationLink?: string; + renderOrder?: number; +} + +export const PLUGIN_SETTINGS_CATEGORIES: Record< + number, + TPluginSettingCategory +> = { + [SettingCategory.HEALTH_CHECK]: { + title: 'Health check', + description: "Checks will be executed by the app's Healthcheck.", + renderOrder: SettingCategory.HEALTH_CHECK, + }, + [SettingCategory.GENERAL]: { + title: 'General', + description: + 'Basic app settings related to alerts index pattern, hide the manager alerts in the dashboards, logs level and more.', + renderOrder: SettingCategory.GENERAL, + }, + [SettingCategory.SECURITY]: { + title: 'Security', + description: 'Application security options such as unauthorized roles.', + renderOrder: SettingCategory.SECURITY, + }, + [SettingCategory.MONITORING]: { + title: 'Task:Monitoring', + description: + 'Options related to the agent status monitoring job and its storage in indexes.', + renderOrder: SettingCategory.MONITORING, + }, + [SettingCategory.STATISTICS]: { + title: 'Task:Statistics', + description: + 'Options related to the daemons manager monitoring job and their storage in indexes.', + renderOrder: SettingCategory.STATISTICS, + }, + [SettingCategory.VULNERABILITIES]: { + title: 'Vulnerabilities', + description: + 'Options related to the agent vulnerabilities monitoring job and its storage in indexes.', + renderOrder: SettingCategory.VULNERABILITIES, + }, + [SettingCategory.CUSTOMIZATION]: { + title: 'Custom branding', + description: + 'If you want to use custom branding elements such as logos, you can do so by editing the settings below.', + documentationLink: 'user-manual/wazuh-dashboard/white-labeling.html', + renderOrder: SettingCategory.CUSTOMIZATION, + }, + [SettingCategory.API_CONNECTION]: { + title: 'API connections', + description: 'Options related to the API connections.', + renderOrder: SettingCategory.API_CONNECTION, + }, +}; export enum HTTP_STATUS_CODES { CONTINUE = 100, @@ -2192,3 +2118,5 @@ export const WAZUH_ROLE_ADMINISTRATOR_ID = 1; // ID used to refer the createOsdUrlStateStorage state export const OSD_URL_STATE_STORAGE_ID = 'state:storeInSessionStorage'; + +export { version as PLUGIN_VERSION } from '../package.json'; From 4db6b3908614e7f99761456ca3bf3e4049ecc1ea Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Mon, 9 Dec 2024 10:01:53 -0300 Subject: [PATCH 75/77] fix(wazuh-api): replace WAZUH_ERROR_CODES with WAZUH_ERROR_DAEMONS_NOT_READY for improved clarity and error handling consistency --- plugins/main/server/controllers/wazuh-api.ts | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/main/server/controllers/wazuh-api.ts b/plugins/main/server/controllers/wazuh-api.ts index 1ecfa5a2fe..f0016b62b1 100644 --- a/plugins/main/server/controllers/wazuh-api.ts +++ b/plugins/main/server/controllers/wazuh-api.ts @@ -32,7 +32,7 @@ import { version as pluginVersion, revision as pluginRevision, } from '../../package.json'; -import { WAZUH_ERROR_CODES } from '../../../wazuh-core/common/constants'; +import { WAZUH_ERROR_DAEMONS_NOT_READY } from '../../../wazuh-core/common/constants'; export class WazuhApiCtrl { async getToken( @@ -157,10 +157,10 @@ export class WazuhApiCtrl { // Look for socket-related errors if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ responseManagerInfo.data.detail || 'Server not ready yet' }`, - WAZUH_ERROR_CODES.DAEMONS_NOT_READY, + 3099, HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -290,10 +290,10 @@ export class WazuhApiCtrl { if (this.checkResponseIsDown(context, responseManagerInfo)) { return ErrorResponse( - `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ response.data.detail || 'Server not ready yet' }`, - WAZUH_ERROR_CODES.DAEMONS_NOT_READY, + 3099, HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -413,10 +413,10 @@ export class WazuhApiCtrl { ); } catch (error) { return ErrorResponse( - `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${ + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${ error.response?.data?.detail || 'Server not ready yet' }`, - WAZUH_ERROR_CODES.DAEMONS_NOT_READY, + 3099, error?.response?.status || HTTP_STATUS_CODES.SERVICE_UNAVAILABLE, response, ); @@ -663,8 +663,8 @@ export class WazuhApiCtrl { ); return ErrorResponse( - `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${error.message || 'Server not ready yet'}`, - WAZUH_ERROR_CODES.DAEMONS_NOT_READY, + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${error.message || 'Server not ready yet'}`, + 3099, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, ); @@ -685,8 +685,8 @@ export class WazuhApiCtrl { if (responseIsDown) { return ErrorResponse( - `ERROR${WAZUH_ERROR_CODES.DAEMONS_NOT_READY} - ${response.body.message || 'Server not ready yet'}`, - WAZUH_ERROR_CODES.DAEMONS_NOT_READY, + `${WAZUH_ERROR_DAEMONS_NOT_READY} - ${response.body.message || 'Server not ready yet'}`, + 3099, HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, response, ); From 808d3bef0982e7527bb294106161601514520538 Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Mon, 9 Dec 2024 10:10:36 -0300 Subject: [PATCH 76/77] fix(constants): rename pluginPlatformRequestHeaders to PLUGIN_PLATFORM_REQUEST_HEADERS for improved consistency in naming conventions --- plugins/wazuh-core/public/services/http/constants.ts | 2 +- plugins/wazuh-core/public/services/http/generic-client.ts | 4 ++-- plugins/wazuh-core/public/services/http/server-client.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/wazuh-core/public/services/http/constants.ts b/plugins/wazuh-core/public/services/http/constants.ts index 93a7e74f16..ad812bbb3d 100644 --- a/plugins/wazuh-core/public/services/http/constants.ts +++ b/plugins/wazuh-core/public/services/http/constants.ts @@ -1,4 +1,4 @@ -export const pluginPlatformRequestHeaders = { +export const PLUGIN_PLATFORM_REQUEST_HEADERS = { 'osd-xsrf': 'kibana', }; diff --git a/plugins/wazuh-core/public/services/http/generic-client.ts b/plugins/wazuh-core/public/services/http/generic-client.ts index e4b3b165f6..967ffa0911 100644 --- a/plugins/wazuh-core/public/services/http/generic-client.ts +++ b/plugins/wazuh-core/public/services/http/generic-client.ts @@ -1,5 +1,5 @@ import { Logger } from '../../../common/services/configuration'; -import { pluginPlatformRequestHeaders } from './constants'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; import { HTTPClientGeneric, HTTPClientRequestInterceptor, @@ -36,7 +36,7 @@ export class GenericRequest implements HTTPClientGeneric { const timeout = await this.services.getTimeout(); const requestHeaders = { - ...pluginPlatformRequestHeaders, + ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', }; const url = this.services.getURL(path); diff --git a/plugins/wazuh-core/public/services/http/server-client.ts b/plugins/wazuh-core/public/services/http/server-client.ts index a9f5f79ff2..69faac8c77 100644 --- a/plugins/wazuh-core/public/services/http/server-client.ts +++ b/plugins/wazuh-core/public/services/http/server-client.ts @@ -19,7 +19,7 @@ import { WzRequestServices, ServerAPIResponseItemsDataHTTPClient, } from './types'; -import { pluginPlatformRequestHeaders } from './constants'; +import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants'; interface RequestInternalOptions { shouldRetry?: boolean; @@ -85,7 +85,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: method, headers: { - ...pluginPlatformRequestHeaders, + ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', ...overwriteHeaders, }, @@ -441,7 +441,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: 'POST', headers: { - ...pluginPlatformRequestHeaders, + ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', }, url: url, @@ -483,7 +483,7 @@ export class WzRequest implements HTTPClientServer { const options = { method: 'POST', headers: { - ...pluginPlatformRequestHeaders, + ...PLUGIN_PLATFORM_REQUEST_HEADERS, 'content-type': 'application/json', }, url: url, From 342f4ead7de1768e2cc91f1a9d9a1df267e6516b Mon Sep 17 00:00:00 2001 From: Guido Modarelli Date: Mon, 9 Dec 2024 11:49:32 -0300 Subject: [PATCH 77/77] fix(constants): refactor query language constants and improve code consistency by utilizing centralized definitions and types --- plugins/wazuh-core/common/types.ts | 18 +- .../search-bar/query-language/aql.tsx | 73 ++-- .../search-bar/query-language/constants.ts | 39 ++ .../search-bar/query-language/wql.tsx | 384 ++++++++++-------- 4 files changed, 301 insertions(+), 213 deletions(-) create mode 100644 plugins/wazuh-core/public/components/search-bar/query-language/constants.ts diff --git a/plugins/wazuh-core/common/types.ts b/plugins/wazuh-core/common/types.ts index f22c311bac..e818c55850 100644 --- a/plugins/wazuh-core/common/types.ts +++ b/plugins/wazuh-core/common/types.ts @@ -1,11 +1,3 @@ -export interface AvailableUpdates { - apiId: string; - last_check?: Date | string | undefined; - mayor: Update[]; - minor: Update[]; - patch: Update[]; -} - export interface Update { description: string; published_date: string; @@ -17,3 +9,13 @@ export interface Update { tag: string; title: string; } + +export interface AvailableUpdates { + apiId: string; + last_check?: Date | string | undefined; + mayor: Update[]; + minor: Update[]; + patch: Update[]; +} + +export type OmitStrict = Pick>; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx index 25c47c18fa..bf45485016 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/aql.tsx @@ -1,6 +1,17 @@ import React from 'react'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiCode } from '@elastic/eui'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; +import { OmitStrict } from '../../../../common/types'; +import { + CONJUNCTION, + Conjunction, + GROUP_OPERATOR_BOUNDARY, + ICON_TYPE, + OPERATOR_COMPARE, + OPERATOR_GROUP, + OperatorCompare, + OperatorGroup, +} from './constants'; const AQL_ID = 'aql'; const QUERY_TOKEN_KEYS = { @@ -12,35 +23,10 @@ const QUERY_TOKEN_KEYS = { FUNCTION_SEARCH: 'function_search', } as const; -type TokenTypeEnum = (typeof QUERY_TOKEN_KEYS)[keyof typeof QUERY_TOKEN_KEYS]; - -const GROUP_OPERATOR_BOUNDARY = { - OPEN: 'operator_group_open', - CLOSE: 'operator_group_close', -}; -const OPERATOR_COMPARE = { - EQUALITY: '=', - NOT_EQUALITY: '!=', - BIGGER: '>', - SMALLER: '<', - LIKE_AS: '~', -} as const; - -type OperatorCompare = (typeof OPERATOR_COMPARE)[keyof typeof OPERATOR_COMPARE]; - -const OPERATOR_GROUP = { - OPEN: '(', - CLOSE: ')', -} as const; - -type OperatorGroup = (typeof OPERATOR_GROUP)[keyof typeof OPERATOR_GROUP]; - -const CONJUNCTION = { - AND: ';', - OR: ',', -} as const; - -type Conjunction = (typeof CONJUNCTION)[keyof typeof CONJUNCTION]; +type TokenTypeEnum = (typeof QUERY_TOKEN_KEYS)[keyof OmitStrict< + typeof QUERY_TOKEN_KEYS, + 'FUNCTION_SEARCH' +>]; interface TokenDescriptor { type: TokenTypeEnum; @@ -99,18 +85,24 @@ const CONJUNCTIONS = Object.keys( ) as Conjunction[]; // Suggestion mapper by language token type const SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE = { - [QUERY_TOKEN_KEYS.FIELD]: { iconType: 'kqlField', color: 'tint4' }, + [QUERY_TOKEN_KEYS.FIELD]: { iconType: ICON_TYPE.KQL_FIELD, color: 'tint4' }, [QUERY_TOKEN_KEYS.OPERATOR_COMPARE]: { - iconType: 'kqlOperand', + iconType: ICON_TYPE.KQL_OPERAND, color: 'tint1', }, - [QUERY_TOKEN_KEYS.VALUE]: { iconType: 'kqlValue', color: 'tint0' }, - [QUERY_TOKEN_KEYS.CONJUNCTION]: { iconType: 'kqlSelector', color: 'tint3' }, + [QUERY_TOKEN_KEYS.VALUE]: { iconType: ICON_TYPE.KQL_VALUE, color: 'tint0' }, + [QUERY_TOKEN_KEYS.CONJUNCTION]: { + iconType: ICON_TYPE.KQL_SELECTOR, + color: 'tint3', + }, [QUERY_TOKEN_KEYS.OPERATOR_GROUP]: { - iconType: 'tokenDenseVector', + iconType: ICON_TYPE.TOKEN_DENSE_VECTOR, color: 'tint3', }, - [QUERY_TOKEN_KEYS.FUNCTION_SEARCH]: { iconType: 'search', color: 'tint5' }, + [QUERY_TOKEN_KEYS.FUNCTION_SEARCH]: { + iconType: ICON_TYPE.SEARCH, + color: 'tint5', + }, } as const; /** @@ -373,8 +365,9 @@ export async function getSuggestions( ), { type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, - label: ')', - description: LANGUAGE.tokens.operator_group.literal[')'], + label: OPERATOR_GROUP.CLOSE, + description: + LANGUAGE.tokens.operator_group.literal[OPERATOR_GROUP.CLOSE], }, ]; } @@ -409,7 +402,7 @@ export async function getSuggestions( // fields suggestions.map(element => mapSuggestionCreatorField(element)) ); - } else if (lastToken.value === ')') { + } else if (lastToken.value === OPERATOR_GROUP.CLOSE) { return ( // conjunction CONJUNCTIONS.map(conjunction => ({ @@ -487,7 +480,7 @@ export const AQL = { isOpenPopoverImplicitFilter: false, }; }, - async run(input: string, params) { + async run(input: string, params: any) { // Get the tokens from the input const tokens: TokenList = tokenizer(input); @@ -501,7 +494,7 @@ export const AQL = { // Handler to manage when clicking in a suggestion item onItemClick: (currentInput: string) => item => { // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { + if (item.type.iconType === ICON_TYPE.SEARCH) { // Execute the search action params.onSearch( getOutput(currentInput, params.queryLanguage.parameters), diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/constants.ts b/plugins/wazuh-core/public/components/search-bar/query-language/constants.ts new file mode 100644 index 0000000000..ae0f805b24 --- /dev/null +++ b/plugins/wazuh-core/public/components/search-bar/query-language/constants.ts @@ -0,0 +1,39 @@ +export const OPERATOR_COMPARE = { + EQUALITY: '=', + NOT_EQUALITY: '!=', + BIGGER: '>', + SMALLER: '<', + LIKE_AS: '~', +} as const; +export type OperatorCompare = + (typeof OPERATOR_COMPARE)[keyof typeof OPERATOR_COMPARE]; + +export const OPERATOR_GROUP = { + OPEN: '(', + CLOSE: ')', +} as const; + +export type OperatorGroup = + (typeof OPERATOR_GROUP)[keyof typeof OPERATOR_GROUP]; + +export const CONJUNCTION = { + AND: ';', + OR: ',', +} as const; + +export type Conjunction = (typeof CONJUNCTION)[keyof typeof CONJUNCTION]; + +export const GROUP_OPERATOR_BOUNDARY = { + OPEN: 'operator_group_open', + CLOSE: 'operator_group_close', +}; + +export const ICON_TYPE = { + KQL_FIELD: 'kqlField', + KQL_OPERAND: 'kqlOperand', + KQL_VALUE: 'kqlValue', + KQL_SELECTOR: 'kqlSelector', + TOKEN_DENSE_VECTOR: 'tokenDenseVector', + SEARCH: 'search', + ALERT: 'alert', +} as const; diff --git a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx index 8326646994..6ad016092e 100644 --- a/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx +++ b/plugins/wazuh-core/public/components/search-bar/query-language/wql.tsx @@ -3,7 +3,15 @@ import React from 'react'; import { EuiButtonGroup } from '@elastic/eui'; import { SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT } from '../../../../common/constants'; import { webDocumentationLink } from '../../../../common/services/web_documentation'; +import { OmitStrict } from '../../../../common/types'; import { tokenizer as tokenizerUQL } from './aql'; +import { + CONJUNCTION as CONJUNCTION_UQL, + GROUP_OPERATOR_BOUNDARY, + ICON_TYPE, + OPERATOR_COMPARE, + OPERATOR_GROUP, +} from './constants'; /* UI Query language https://documentation.wazuh.com/current/user-manual/api/queries.html @@ -12,20 +20,40 @@ https://documentation.wazuh.com/current/user-manual/api/queries.html */ const WQL_ID = 'wql'; - -type ITokenType = - | 'field' - | 'operator_compare' - | 'operator_group' - | 'value' - | 'conjunction' - | 'whitespace'; -interface IToken { - type: ITokenType; +const QUERY_TOKEN_KEYS = { + FIELD: 'field', + OPERATOR_COMPARE: 'operator_compare', + OPERATOR_GROUP: 'operator_group', + VALUE: 'value', + CONJUNCTION: 'conjunction', + FUNCTION_SEARCH: 'function_search', + WHITESPACE: 'whitespace', + VALIDATION_ERROR: 'validation_error', +} as const; + +type TokenTypeEnum = (typeof QUERY_TOKEN_KEYS)[keyof OmitStrict< + typeof QUERY_TOKEN_KEYS, + 'FUNCTION_SEARCH' | 'VALIDATION_ERROR' +>]; +export const CONJUNCTION_WQL = { + AND: 'and', + OR: 'or', +} as const; + +export type Conjunction = + (typeof CONJUNCTION_WQL)[keyof typeof CONJUNCTION_WQL]; + +interface TokenDescriptor { + type: TokenTypeEnum; value: string; formattedValue?: string; } -type ITokens = IToken[]; +type TokenList = TokenDescriptor[]; + +enum MODE { + PREVIOUS = 'previous', + NEXT = 'next', +} /* API Query Language Define the API Query Language to use in the search bar. @@ -46,46 +74,61 @@ Implemented schema: const language = { // Tokens tokens: { - operator_compare: { + [QUERY_TOKEN_KEYS.OPERATOR_COMPARE]: { literal: { - '=': 'equality', - '!=': 'not equality', - '>': 'bigger', - '<': 'smaller', - '~': 'like as', + [OPERATOR_COMPARE.EQUALITY]: 'equality', + [OPERATOR_COMPARE.NOT_EQUALITY]: 'not equality', + [OPERATOR_COMPARE.BIGGER]: 'bigger', + [OPERATOR_COMPARE.SMALLER]: 'smaller', + [OPERATOR_COMPARE.LIKE_AS]: 'like as', }, }, conjunction: { literal: { - and: 'and', - or: 'or', + [CONJUNCTION_WQL.AND]: 'and', + [CONJUNCTION_WQL.OR]: 'or', }, }, - operator_group: { + [QUERY_TOKEN_KEYS.OPERATOR_GROUP]: { literal: { - '(': 'open group', - ')': 'close group', + [OPERATOR_GROUP.OPEN]: 'open group', + [OPERATOR_GROUP.CLOSE]: 'close group', }, }, }, equivalencesToUQL: { conjunction: { literal: { - and: ';', - or: ',', + and: CONJUNCTION_UQL.AND, + or: CONJUNCTION_UQL.OR, }, }, }, }; // Suggestion mapper by language token type -const suggestionMappingLanguageTokenType = { - field: { iconType: 'kqlField', color: 'tint4' }, - operator_compare: { iconType: 'kqlOperand', color: 'tint1' }, - value: { iconType: 'kqlValue', color: 'tint0' }, - conjunction: { iconType: 'kqlSelector', color: 'tint3' }, - operator_group: { iconType: 'tokenDenseVector', color: 'tint3' }, - function_search: { iconType: 'search', color: 'tint5' }, - validation_error: { iconType: 'alert', color: 'tint2' }, +const SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE = { + [QUERY_TOKEN_KEYS.FIELD]: { iconType: ICON_TYPE.KQL_FIELD, color: 'tint4' }, + [QUERY_TOKEN_KEYS.OPERATOR_COMPARE]: { + iconType: ICON_TYPE.KQL_OPERAND, + color: 'tint1', + }, + [QUERY_TOKEN_KEYS.VALUE]: { iconType: ICON_TYPE.KQL_VALUE, color: 'tint0' }, + [QUERY_TOKEN_KEYS.CONJUNCTION]: { + iconType: ICON_TYPE.KQL_SELECTOR, + color: 'tint3', + }, + [QUERY_TOKEN_KEYS.OPERATOR_GROUP]: { + iconType: ICON_TYPE.TOKEN_DENSE_VECTOR, + color: 'tint3', + }, + [QUERY_TOKEN_KEYS.FUNCTION_SEARCH]: { + iconType: ICON_TYPE.SEARCH, + color: 'tint5', + }, + [QUERY_TOKEN_KEYS.VALIDATION_ERROR]: { + iconType: ICON_TYPE.ALERT, + color: 'tint2', + }, }; /** @@ -93,7 +136,7 @@ const suggestionMappingLanguageTokenType = { * @param type * @returns */ -function mapSuggestionCreator(type: ITokenType) { +function mapSuggestionCreator(type: TokenTypeEnum) { return function ({ label, ...params }) { return { type, @@ -106,8 +149,8 @@ function mapSuggestionCreator(type: ITokenType) { }; } -const mapSuggestionCreatorField = mapSuggestionCreator('field'); -const mapSuggestionCreatorValue = mapSuggestionCreator('value'); +const mapSuggestionCreatorField = mapSuggestionCreator(QUERY_TOKEN_KEYS.FIELD); +const mapSuggestionCreatorValue = mapSuggestionCreator(QUERY_TOKEN_KEYS.VALUE); /** * Transform the conjunction to the query language syntax @@ -117,8 +160,8 @@ const mapSuggestionCreatorValue = mapSuggestionCreator('value'); function transformQLConjunction(conjunction: string): string { // If the value has a whitespace or comma, then return conjunction === language.equivalencesToUQL.conjunction.literal['and'] - ? ` ${language.tokens.conjunction.literal['and']} ` - : ` ${language.tokens.conjunction.literal['or']} `; + ? ` ${language.tokens.conjunction.literal[CONJUNCTION_WQL.AND]} ` + : ` ${language.tokens.conjunction.literal[CONJUNCTION_WQL.OR]} `; } /** @@ -140,52 +183,52 @@ function transformQLValue(value: string): string { * @param input * @returns */ -export function tokenizer(input: string): ITokens { +export function tokenizer(input: string): TokenList { const re = new RegExp( // A ( character. - String.raw`(?\()?` + + String.raw`(?<${GROUP_OPERATOR_BOUNDARY.OPEN}>\()?` + // Whitespace - String.raw`(?\s+)?` + + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_1>\s+)?` + // Field name: name of the field to look on DB. - String.raw`(?[\w.]+)?` + // Added an optional find + String.raw`(?<${QUERY_TOKEN_KEYS.FIELD}>[\w.]+)?` + // Added an optional find // Whitespace - String.raw`(?\s+)?` + + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_2>\s+)?` + // Operator: looks for '=', '!=', '<', '>' or '~'. // This seems to be a bug because is not searching the literal valid operators. // I guess the operator is validated after the regular expression matches - `(?[${Object.keys( + `(?<${QUERY_TOKEN_KEYS.OPERATOR_COMPARE}>[${Object.keys( language.tokens.operator_compare.literal, )}]{1,2})?` + // Added an optional find // Whitespace - String.raw`(?\s+)?` + + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_3>\s+)?` + // Value: A string. // Simple value // Quoted ", "value, "value", "escaped \"quote" // Escape quoted string with escaping quotes: https://stackoverflow.com/questions/249791/regex-for-quoted-string-with-escaping-quotes - String.raw`(?(?:(?:[^"\s]+|(?:"(?:[^"\\]|\\")*")|(?:"(?:[^"\\]|\\")*)|")))?` + + String.raw`(?<${QUERY_TOKEN_KEYS.VALUE}>(?:(?:[^"\s]+|(?:"(?:[^"\\]|\\")*")|(?:"(?:[^"\\]|\\")*)|")))?` + // Whitespace - String.raw`(?\s+)?` + + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_4>\s+)?` + // A ) character. - String.raw`(?\))?` + + String.raw`(?<${GROUP_OPERATOR_BOUNDARY.CLOSE}>\))?` + // Whitespace - String.raw`(?\s+)?` + - `(?${Object.keys(language.tokens.conjunction.literal).join( - '|', - )})?` + + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_5>\s+)?` + + `(?<${QUERY_TOKEN_KEYS.CONJUNCTION}>${Object.keys( + language.tokens.conjunction.literal, + ).join('|')})?` + // Whitespace - String.raw`(?\s+)?`, + String.raw`(?<${QUERY_TOKEN_KEYS.WHITESPACE}_6>\s+)?`, 'g', ); return [...input.matchAll(re)].flatMap(({ groups }) => Object.entries(groups).map(([key, value]) => ({ - type: key.startsWith('operator_group') // Transform operator_group group match - ? 'operator_group' - : key.startsWith('whitespace') // Transform whitespace group match - ? 'whitespace' + type: key.startsWith(QUERY_TOKEN_KEYS.OPERATOR_GROUP) // Transform operator_group group match + ? QUERY_TOKEN_KEYS.OPERATOR_GROUP + : key.startsWith(QUERY_TOKEN_KEYS.WHITESPACE) // Transform whitespace group match + ? QUERY_TOKEN_KEYS.WHITESPACE : key, value, - ...(key === 'value' && + ...(key === QUERY_TOKEN_KEYS.VALUE && (value && /^"([\S\s]+)"$/.test(value) ? { formattedValue: value.match(/^"([\S\s]+)"$/)[1] } : { formattedValue: value })), @@ -199,13 +242,10 @@ interface QLOptionSuggestionEntityItem { } type QLOptionSuggestionEntityItemTyped = QLOptionSuggestionEntityItem & { - type: - | 'operator_group' - | 'field' - | 'operator_compare' - | 'value' - | 'conjunction' - | 'function_search'; + type: (typeof QUERY_TOKEN_KEYS)[keyof OmitStrict< + typeof QUERY_TOKEN_KEYS, + 'WHITESPACE' | 'VALIDATION_ERROR' + >]; }; type SuggestItem = QLOptionSuggestionEntityItem & { @@ -235,7 +275,7 @@ interface OptionsQL { value?: Record< string, ( - token: IToken, + token: TokenDescriptor, nearTokens: { field: string; operator: string }, ) => string | undefined >; @@ -252,12 +292,12 @@ export interface ISearchBarModeWQL extends OptionsQL { * @param tokenType token type to search * @returns */ -function getLastTokenDefined(tokens: ITokens): IToken | undefined { +function getLastTokenDefined(tokens: TokenList): TokenDescriptor | undefined { // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = [...tokens]; const shallowCopyArrayReversed = shallowCopyArray.reverse(); const tokenFound = shallowCopyArrayReversed.find( - ({ type, value }) => type !== 'whitespace' && value, + ({ type, value }) => type !== QUERY_TOKEN_KEYS.WHITESPACE && value, ); return tokenFound; @@ -270,9 +310,9 @@ function getLastTokenDefined(tokens: ITokens): IToken | undefined { * @returns */ function getLastTokenDefinedByType( - tokens: ITokens, - tokenType: ITokenType, -): IToken | undefined { + tokens: TokenList, + tokenType: TokenTypeEnum, +): TokenDescriptor | undefined { // Find the last token by type // Reverse the tokens array and use the Array.protorype.find method const shallowCopyArray = [...tokens]; @@ -293,17 +333,17 @@ function getLastTokenDefinedByType( * @returns */ function getTokenNearTo( - tokens: ITokens, - tokenType: ITokenType, - mode: 'previous' | 'next' = 'previous', + tokens: TokenList, + tokenType: TokenTypeEnum, + mode: MODE = MODE.PREVIOUS, options: { tokenReferencePosition?: number; tokenFoundShouldHaveValue?: boolean; } = {}, -): IToken | undefined { +): TokenDescriptor | undefined { const shallowCopyTokens = [...tokens]; const computedShallowCopyTokens = - mode === 'previous' + mode === MODE.PREVIOUS ? shallowCopyTokens .slice( 0, @@ -325,7 +365,7 @@ function getTokenNearTo( function getTokenValueRegularExpression() { return new RegExp( // Value: A string. - String.raw`^(?(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + + String.raw`^(?<${QUERY_TOKEN_KEYS.VALUE}>(?:(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\/'"=@%<>{}]*)\))*` + String.raw`(?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|^[\[\]\w _\-.:?\\/'"=@%<>{}]+)` + String.raw`(?:\((?:\[[\[\]\w _\-.,:?\\/'"=@%<>{}]*]|[\[\]\w _\-.:?\\/'"=@%<>{}]*)\))*)+)$`, ); @@ -360,7 +400,7 @@ function filterTokenValueSuggestion( * @returns */ export async function getSuggestions( - tokens: ITokens, + tokens: TokenList, options: OptionsQL, ): Promise { if (tokens.length === 0) { @@ -375,7 +415,7 @@ export async function getSuggestions( return [ // Search function { - type: 'function_search', + type: QUERY_TOKEN_KEYS.FUNCTION_SEARCH, label: 'Search', description: 'run the search query', }, @@ -384,15 +424,16 @@ export async function getSuggestions( mapSuggestionCreatorField(element, index, array), ), { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, + label: OPERATOR_GROUP.OPEN, + description: + language.tokens.operator_group.literal[OPERATOR_GROUP.OPEN], }, ]; } switch (lastToken.type) { - case 'field': { + case QUERY_TOKEN_KEYS.FIELD: { return [ // fields that starts with the input but is not equals ...(await options.suggestions.field()) @@ -409,7 +450,7 @@ export async function getSuggestions( ) ? Object.keys(language.tokens.operator_compare.literal).map( operator => ({ - type: 'operator_compare', + type: QUERY_TOKEN_KEYS.OPERATOR_COMPARE, label: operator, description: language.tokens.operator_compare.literal[operator], }), @@ -418,11 +459,14 @@ export async function getSuggestions( ]; } - case 'operator_compare': { - const field = getLastTokenDefinedByType(tokens, 'field')?.value; + case QUERY_TOKEN_KEYS.OPERATOR_COMPARE: { + const field = getLastTokenDefinedByType( + tokens, + QUERY_TOKEN_KEYS.FIELD, + )?.value; const operatorCompare = getLastTokenDefinedByType( tokens, - 'operator_compare', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, )?.value; // If there is no a previous field, then no return suggestions because it would be an syntax @@ -439,7 +483,7 @@ export async function getSuggestions( operator !== lastToken.value, ) .map(operator => ({ - type: 'operator_compare', + type: QUERY_TOKEN_KEYS.OPERATOR_COMPARE, label: operator, description: language.tokens.operator_compare.literal[operator], })), @@ -465,11 +509,14 @@ export async function getSuggestions( ]; } - case 'value': { - const field = getLastTokenDefinedByType(tokens, 'field')?.value; + case QUERY_TOKEN_KEYS.VALUE: { + const field = getLastTokenDefinedByType( + tokens, + QUERY_TOKEN_KEYS.FIELD, + )?.value; const operatorCompare = getLastTokenDefinedByType( tokens, - 'operator_compare', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, )?.value; /* If there is no a previous field or operator_compare, then no return suggestions because @@ -482,7 +529,7 @@ export async function getSuggestions( ...(lastToken.formattedValue ? [ { - type: 'function_search', + type: QUERY_TOKEN_KEYS.FUNCTION_SEARCH, label: 'Search', description: 'run the search query', }, @@ -505,20 +552,21 @@ export async function getSuggestions( ), ...Object.entries(language.tokens.conjunction.literal).map( ([conjunction, description]) => ({ - type: 'conjunction', + type: QUERY_TOKEN_KEYS.CONJUNCTION, label: conjunction, description, }), ), { - type: 'operator_group', - label: ')', - description: language.tokens.operator_group.literal[')'], + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, + label: OPERATOR_GROUP.CLOSE, + description: + language.tokens.operator_group.literal[OPERATOR_GROUP.CLOSE], }, ]; } - case 'conjunction': { + case QUERY_TOKEN_KEYS.CONJUNCTION: { return [ ...Object.keys(language.tokens.conjunction.literal) .filter( @@ -527,7 +575,7 @@ export async function getSuggestions( conjunction !== lastToken.value, ) .map(conjunction => ({ - type: 'conjunction', + type: QUERY_TOKEN_KEYS.CONJUNCTION, label: conjunction, description: language.tokens.conjunction.literal[conjunction], })), @@ -540,26 +588,27 @@ export async function getSuggestions( ) : []), { - type: 'operator_group', - label: '(', - description: language.tokens.operator_group.literal['('], + type: QUERY_TOKEN_KEYS.OPERATOR_GROUP, + label: OPERATOR_GROUP.OPEN, + description: + language.tokens.operator_group.literal[OPERATOR_GROUP.OPEN], }, ]; } - case 'operator_group': { - if (lastToken.value === '(') { + case QUERY_TOKEN_KEYS.OPERATOR_GROUP: { + if (lastToken.value === OPERATOR_GROUP.OPEN) { return ( // fields (await options.suggestions.field()).map(element => mapSuggestionCreatorField(element), ) ); - } else if (lastToken.value === ')') { + } else if (lastToken.value === OPERATOR_GROUP.CLOSE) { return ( // conjunction Object.keys(language.tokens.conjunction.literal).map(conjunction => ({ - type: 'conjunction', + type: QUERY_TOKEN_KEYS.CONJUNCTION, label: conjunction, description: language.tokens.conjunction.literal[conjunction], })) @@ -588,7 +637,7 @@ export function transformSuggestionToEuiSuggestItem( const { type, ...rest } = suggestion; return { - type: { ...suggestionMappingLanguageTokenType[type] }, + type: { ...SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE[type] }, ...rest, }; } @@ -618,11 +667,11 @@ export function transformUQLToQL(input: string) { .filter(({ value }) => value) .map(({ type, value }) => { switch (type) { - case 'conjunction': { + case QUERY_TOKEN_KEYS.CONJUNCTION: { return transformQLConjunction(value); } - case 'value': { + case QUERY_TOKEN_KEYS.VALUE: { return transformQLValue(value); } @@ -634,10 +683,12 @@ export function transformUQLToQL(input: string) { .join(''); } -export function shouldUseSearchTerm(tokens: ITokens): boolean { +export function shouldUseSearchTerm(tokens: TokenList): boolean { return !( - tokens.some(({ type, value }) => type === 'operator_compare' && value) && - tokens.some(({ type, value }) => type === 'field' && value) + tokens.some( + ({ type, value }) => type === QUERY_TOKEN_KEYS.OPERATOR_COMPARE && value, + ) && + tokens.some(({ type, value }) => type === QUERY_TOKEN_KEYS.FIELD && value) ); } @@ -668,24 +719,24 @@ export function transformSpecificQLToUnifiedQL( return tokens .filter( ({ type, value, formattedValue }) => - type !== 'whitespace' && (formattedValue ?? value), + type !== QUERY_TOKEN_KEYS.WHITESPACE && (formattedValue ?? value), ) .map(({ type, value, formattedValue }) => { switch (type) { - case 'value': { + case QUERY_TOKEN_KEYS.VALUE: { // If the value is wrapped with ", then replace the escaped double quotation mark (\") // by double quotation marks (") // WARN: This could cause a problem with value that contains this sequence \" const extractedValue = formattedValue === value ? formattedValue - : formattedValue.replaceAll(String.raw`\"`, '"'); + : formattedValue?.replaceAll(String.raw`\"`, '"'); return extractedValue || value; } - case 'conjunction': { - return value === 'and' + case QUERY_TOKEN_KEYS.CONJUNCTION: { + return value === CONJUNCTION_WQL.AND ? language.equivalencesToUQL.conjunction.literal['and'] : language.equivalencesToUQL.conjunction.literal['or']; } @@ -748,7 +799,7 @@ function getOutput(input: string, options: OptionsQL) { * @param token * @returns */ -function validateTokenValue(token: IToken): string | undefined { +function validateTokenValue(token: TokenDescriptor): string | undefined { const re = getTokenValueRegularExpression(); const value = token.formattedValue ?? token.value; const match = value.match(re); @@ -775,7 +826,7 @@ function validateTokenValue(token: IToken): string | undefined { } type ITokenValidator = ( - tokenValue: IToken, + tokenValue: TokenDescriptor, proximityTokens: any, ) => string | undefined; @@ -786,22 +837,22 @@ type ITokenValidator = ( * @returns */ function validatePartial( - tokens: ITokens, + tokens: TokenList, validate: { field: ITokenValidator; value: ITokenValidator }, ): undefined | string { // Ensure is not in search term mode if (!shouldUseSearchTerm(tokens)) { return ( tokens - .map((token: IToken, index) => { + .map((token: TokenDescriptor, index) => { if (token.value) { - if (token.type === 'field') { + if (token.type === QUERY_TOKEN_KEYS.FIELD) { // Ensure there is a operator next to field to check if the fields is valid or not. // This allows the user can type the field token and get the suggestions for the field. const tokenOperatorNearToField = getTokenNearTo( tokens, - 'operator_compare', - 'next', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + MODE.NEXT, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -814,11 +865,11 @@ function validatePartial( } // Check if the value is allowed - if (token.type === 'value') { + if (token.type === QUERY_TOKEN_KEYS.VALUE) { const tokenFieldNearToValue = getTokenNearTo( tokens, - 'field', - 'previous', + QUERY_TOKEN_KEYS.FIELD, + MODE.PREVIOUS, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -826,8 +877,8 @@ function validatePartial( ); const tokenOperatorCompareNearToValue = getTokenNearTo( tokens, - 'operator_compare', - 'previous', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + MODE.PREVIOUS, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -861,20 +912,20 @@ function validatePartial( * @returns */ function validate( - tokens: ITokens, + tokens: TokenList, validate: { field: ITokenValidator; value: ITokenValidator }, ): undefined | string[] { if (!shouldUseSearchTerm(tokens)) { const errors = tokens - .map((token: IToken, index) => { + .map((token: TokenDescriptor, index) => { const errors = []; if (token.value) { - if (token.type === 'field') { + if (token.type === QUERY_TOKEN_KEYS.FIELD) { const tokenOperatorNearToField = getTokenNearTo( tokens, - 'operator_compare', - 'next', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + MODE.NEXT, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -882,8 +933,8 @@ function validate( ); const tokenValueNearToField = getTokenNearTo( tokens, - 'value', - 'next', + QUERY_TOKEN_KEYS.VALUE, + MODE.NEXT, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -902,11 +953,11 @@ function validate( } // Check if the value is allowed - if (token.type === 'value') { + if (token.type === QUERY_TOKEN_KEYS.VALUE) { const tokenFieldNearToValue = getTokenNearTo( tokens, - 'field', - 'previous', + QUERY_TOKEN_KEYS.FIELD, + MODE.PREVIOUS, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -914,8 +965,8 @@ function validate( ); const tokenOperatorCompareNearToValue = getTokenNearTo( tokens, - 'operator_compare', - 'previous', + QUERY_TOKEN_KEYS.OPERATOR_COMPARE, + MODE.PREVIOUS, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -938,17 +989,17 @@ function validate( } // Check if the value is allowed - if (token.type === 'conjunction') { + if (token.type === QUERY_TOKEN_KEYS.CONJUNCTION) { const tokenWhitespaceNearToFieldNext = getTokenNearTo( tokens, - 'whitespace', - 'next', + QUERY_TOKEN_KEYS.WHITESPACE, + MODE.NEXT, { tokenReferencePosition: index }, ); const tokenFieldNearToFieldNext = getTokenNearTo( tokens, - 'field', - 'next', + QUERY_TOKEN_KEYS.FIELD, + MODE.NEXT, { tokenReferencePosition: index, tokenFoundShouldHaveValue: true, @@ -981,7 +1032,7 @@ function validate( } export const WQL = { - id: 'wql', + id: WQL_ID, label: 'WQL', description: 'WQL (Wazuh Query Language) provides a human query syntax based on the Wazuh API query language.', @@ -995,7 +1046,7 @@ export const WQL = { }, async run(input, params) { // Get the tokens from the input - const tokens: ITokens = tokenizer(input); + const tokens: TokenList = tokenizer(input); // Get the implicit query as query language syntax // eslint-disable-next-line @typescript-eslint/no-unused-vars const implicitQueryAsQL = params.queryLanguage.parameters?.options @@ -1037,7 +1088,7 @@ export const WQL = { ...state.searchBarProps, suggestions: transformSuggestionsToEuiSuggestItem( output.error.map(error => ({ - type: 'validation_error', + type: QUERY_TOKEN_KEYS.VALIDATION_ERROR, label: 'Invalid', description: error, })), @@ -1090,7 +1141,7 @@ export const WQL = { validationPartial ? [ { - type: 'validation_error', + type: QUERY_TOKEN_KEYS.VALIDATION_ERROR, label: 'Invalid', description: validationPartial, }, @@ -1100,15 +1151,15 @@ export const WQL = { // Handler to manage when clicking in a suggestion item onItemClick: currentInput => item => { // There is an error, clicking on the item does nothing - if (item.type.iconType === 'alert') { + if (item.type.iconType === ICON_TYPE.ALERT) { return; } // When the clicked item has the `search` iconType, run the `onSearch` function - if (item.type.iconType === 'search') { + if (item.type.iconType === ICON_TYPE.SEARCH) { // Execute the search action // Get the tokens from the input - const tokens: ITokens = tokenizer(currentInput); + const tokens: TokenList = tokenizer(currentInput); const validationStrict = validate(tokens, validators); // Get the output of query language const output = { @@ -1119,19 +1170,20 @@ export const WQL = { onSearch(output); } else { // When the clicked item has another iconType - const lastToken: IToken | undefined = getLastTokenDefined(tokens); + const lastToken: TokenDescriptor | undefined = + getLastTokenDefined(tokens); // if the clicked suggestion is of same type of last token if ( lastToken && - suggestionMappingLanguageTokenType[lastToken.type].iconType === - item.type.iconType + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE[lastToken.type] + .iconType === item.type.iconType ) { // replace the value of last token with the current one. // if the current token is a value, then transform it lastToken.value = item.type.iconType === - suggestionMappingLanguageTokenType.value.iconType + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE.value.iconType ? transformQLValue(item.label) : item.label; } else { @@ -1140,27 +1192,29 @@ export const WQL = { if ( !/\s$/.test(input) && (item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType || - lastToken?.type === 'conjunction' || + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE.conjunction.iconType || + lastToken?.type === QUERY_TOKEN_KEYS.CONJUNCTION || (item.type.iconType === - suggestionMappingLanguageTokenType.operator_group + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE.operator_group .iconType && - item.label === ')')) + item.label === OPERATOR_GROUP.CLOSE)) ) { tokens.push({ - type: 'whitespace', + type: QUERY_TOKEN_KEYS.WHITESPACE, value: ' ', }); } // add a new token of the selected type and value tokens.push({ - type: Object.entries(suggestionMappingLanguageTokenType).find( + type: Object.entries( + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE, + ).find( ([, { iconType }]) => iconType === item.type.iconType, )[0], value: item.type.iconType === - suggestionMappingLanguageTokenType.value.iconType + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE.value.iconType ? transformQLValue(item.label) : item.label, }); @@ -1168,10 +1222,10 @@ export const WQL = { // add a whitespace for conjunction if ( item.type.iconType === - suggestionMappingLanguageTokenType.conjunction.iconType + SUGGESTION_MAPPING_LANGUAGE_TOKEN_TYPE.conjunction.iconType ) { tokens.push({ - type: 'whitespace', + type: QUERY_TOKEN_KEYS.WHITESPACE, value: ' ', }); } @@ -1200,7 +1254,7 @@ export const WQL = { if (event.key === 'Enter') { // Get the tokens from the input const input = event.currentTarget.value; - const tokens: ITokens = tokenizer(input); + const tokens: TokenList = tokenizer(input); const validationStrict = validate(tokens, validators); // Get the output of query language const output = {