From c6e4ea0dbd71ed93655664ce42962c42857371f9 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Thu, 27 Jun 2024 13:40:06 +0200 Subject: [PATCH 01/34] Added engine plugin with a first nav menu and a simple react router system --- docker/imposter/api-info/api_info.json | 4 +- docker/osd-dev/dev.sh | 3 +- docker/osd-dev/dev.yml | 1 + plugins/main/opensearch_dashboards.json | 9 +- plugins/main/public/app-router.tsx | 13 +- plugins/main/public/kibana-services.ts | 3 + plugins/main/public/plugin.ts | 2 + .../react-services/navigation-service.tsx | 8 +- plugins/main/public/types.ts | 2 + plugins/main/public/utils/applications.ts | 20 +++ plugins/wazuh-engine/.i18nrc.json | 7 + plugins/wazuh-engine/README.md | 76 ++++++++ plugins/wazuh-engine/common/constants.ts | 0 plugins/wazuh-engine/common/types.ts | 0 .../wazuh-engine/opensearch_dashboards.json | 8 + plugins/wazuh-engine/package.json | 25 +++ .../public/components/decoders/decoders.tsx | 5 + .../public/components/decoders/index.ts | 1 + .../wazuh-engine/public/components/engine.tsx | 88 ++++++++++ .../public/components/filters/filters.tsx | 5 + .../public/components/filters/index.ts | 1 + .../public/components/integrations/index.ts | 1 + .../components/integrations/integrations.tsx | 5 + .../public/components/kvdbs/index.ts | 1 + .../public/components/kvdbs/kvdbs.tsx | 5 + .../public/components/outputs/index.ts | 1 + .../public/components/outputs/outputs.tsx | 5 + .../public/components/policies/index.ts | 1 + .../public/components/policies/policies.tsx | 5 + .../public/components/rules/index.ts | 1 + .../public/components/rules/rules.tsx | 5 + plugins/wazuh-engine/public/hooks/index.ts | 0 plugins/wazuh-engine/public/index.ts | 8 + .../wazuh-engine/public/plugin-services.ts | 7 + plugins/wazuh-engine/public/plugin.ts | 28 +++ plugins/wazuh-engine/public/services/index.ts | 0 plugins/wazuh-engine/public/types.ts | 9 + plugins/wazuh-engine/public/utils/index.ts | 0 plugins/wazuh-engine/scripts/jest.js | 19 ++ plugins/wazuh-engine/scripts/manifest.js | 17 ++ plugins/wazuh-engine/scripts/runner.js | 148 ++++++++++++++++ plugins/wazuh-engine/server/index.ts | 11 ++ .../wazuh-engine/server/plugin-services.ts | 7 + plugins/wazuh-engine/server/plugin.ts | 67 ++++++++ plugins/wazuh-engine/server/routes/index.ts | 3 + .../saved-object/get-saved-object.test.ts | 64 +++++++ .../services/saved-object/get-saved-object.ts | 34 ++++ .../server/services/saved-object/index.ts | 2 + .../saved-object/set-saved-object.test.ts | 62 +++++++ .../services/saved-object/set-saved-object.ts | 35 ++++ .../saved-object/types/available-updates.ts | 81 +++++++++ .../services/saved-object/types/index.ts | 2 + .../saved-object/types/user-preferences.ts | 33 ++++ .../services/updates/get-updates.test.ts | 162 ++++++++++++++++++ .../server/services/updates/get-updates.ts | 120 +++++++++++++ .../server/services/updates/index.ts | 1 + .../get-user-preferences.test.ts | 70 ++++++++ .../user-preferences/get-user-preferences.ts | 32 ++++ .../server/services/user-preferences/index.ts | 2 + .../update-user-preferences.test.ts | 91 ++++++++++ .../update-user-preferences.ts | 39 +++++ plugins/wazuh-engine/server/types.ts | 19 ++ plugins/wazuh-engine/test/jest/config.js | 41 +++++ plugins/wazuh-engine/translations/en-US.json | 79 +++++++++ plugins/wazuh-engine/tsconfig.json | 17 ++ plugins/wazuh-engine/yarn.lock | 12 ++ 66 files changed, 1620 insertions(+), 13 deletions(-) create mode 100644 plugins/wazuh-engine/.i18nrc.json create mode 100755 plugins/wazuh-engine/README.md create mode 100644 plugins/wazuh-engine/common/constants.ts create mode 100644 plugins/wazuh-engine/common/types.ts create mode 100644 plugins/wazuh-engine/opensearch_dashboards.json create mode 100644 plugins/wazuh-engine/package.json create mode 100644 plugins/wazuh-engine/public/components/decoders/decoders.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/index.ts create mode 100644 plugins/wazuh-engine/public/components/engine.tsx create mode 100644 plugins/wazuh-engine/public/components/filters/filters.tsx create mode 100644 plugins/wazuh-engine/public/components/filters/index.ts create mode 100644 plugins/wazuh-engine/public/components/integrations/index.ts create mode 100644 plugins/wazuh-engine/public/components/integrations/integrations.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/index.ts create mode 100644 plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/index.ts create mode 100644 plugins/wazuh-engine/public/components/outputs/outputs.tsx create mode 100644 plugins/wazuh-engine/public/components/policies/index.ts create mode 100644 plugins/wazuh-engine/public/components/policies/policies.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/index.ts create mode 100644 plugins/wazuh-engine/public/components/rules/rules.tsx create mode 100644 plugins/wazuh-engine/public/hooks/index.ts create mode 100644 plugins/wazuh-engine/public/index.ts create mode 100644 plugins/wazuh-engine/public/plugin-services.ts create mode 100644 plugins/wazuh-engine/public/plugin.ts create mode 100644 plugins/wazuh-engine/public/services/index.ts create mode 100644 plugins/wazuh-engine/public/types.ts create mode 100644 plugins/wazuh-engine/public/utils/index.ts create mode 100644 plugins/wazuh-engine/scripts/jest.js create mode 100644 plugins/wazuh-engine/scripts/manifest.js create mode 100755 plugins/wazuh-engine/scripts/runner.js create mode 100644 plugins/wazuh-engine/server/index.ts create mode 100644 plugins/wazuh-engine/server/plugin-services.ts create mode 100644 plugins/wazuh-engine/server/plugin.ts create mode 100644 plugins/wazuh-engine/server/routes/index.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/get-saved-object.test.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/get-saved-object.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/index.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/set-saved-object.test.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/set-saved-object.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/types/index.ts create mode 100644 plugins/wazuh-engine/server/services/saved-object/types/user-preferences.ts create mode 100644 plugins/wazuh-engine/server/services/updates/get-updates.test.ts create mode 100644 plugins/wazuh-engine/server/services/updates/get-updates.ts create mode 100644 plugins/wazuh-engine/server/services/updates/index.ts create mode 100644 plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.test.ts create mode 100644 plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.ts create mode 100644 plugins/wazuh-engine/server/services/user-preferences/index.ts create mode 100644 plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.test.ts create mode 100644 plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.ts create mode 100644 plugins/wazuh-engine/server/types.ts create mode 100644 plugins/wazuh-engine/test/jest/config.js create mode 100644 plugins/wazuh-engine/translations/en-US.json create mode 100644 plugins/wazuh-engine/tsconfig.json create mode 100644 plugins/wazuh-engine/yarn.lock diff --git a/docker/imposter/api-info/api_info.json b/docker/imposter/api-info/api_info.json index 390d836403..fbd8634f9e 100644 --- a/docker/imposter/api-info/api_info.json +++ b/docker/imposter/api-info/api_info.json @@ -1,7 +1,7 @@ { "data": { "title": "Wazuh API REST", - "api_version": "4.9.0", + "api_version": "5.0.0", "revision": 1, "license_name": "GPL 2.0", "license_url": "https://github.com/wazuh/wazuh/blob/4.5/LICENSE", @@ -9,4 +9,4 @@ "timestamp": "2022-06-13T17:20:03Z" }, "error": 0 -} \ No newline at end of file +} diff --git a/docker/osd-dev/dev.sh b/docker/osd-dev/dev.sh index 584c467c77..6c468b472e 100755 --- a/docker/osd-dev/dev.sh +++ b/docker/osd-dev/dev.sh @@ -16,7 +16,7 @@ os_versions=( '2.11.1' '2.12.0' '2.13.0' - + '2.14.0' ) osd_versions=( @@ -35,6 +35,7 @@ osd_versions=( '2.11.1' '2.12.0' '2.13.0' + '2.14.0' ) wzs_version=( diff --git a/docker/osd-dev/dev.yml b/docker/osd-dev/dev.yml index 92c29e45af..892a336007 100755 --- a/docker/osd-dev/dev.yml +++ b/docker/osd-dev/dev.yml @@ -245,6 +245,7 @@ services: - '${SRC}/main:/home/node/kbn/plugins/wazuh' - '${SRC}/wazuh-core:/home/node/kbn/plugins/wazuh-core' - '${SRC}/wazuh-check-updates:/home/node/kbn/plugins/wazuh-check-updates' + - '${SRC}/wazuh-engine:/home/node/kbn/plugins/wazuh-engine' - wd_certs:/home/node/kbn/certs/ - ${WAZUH_DASHBOARD_CONF}:/home/node/kbn/config/opensearch_dashboards.yml - ./config/${OSD_MAJOR}/osd/wazuh.yml:/home/node/kbn/data/wazuh/config/wazuh.yml diff --git a/plugins/main/opensearch_dashboards.json b/plugins/main/opensearch_dashboards.json index 1aa9b714ce..ea53df3589 100644 --- a/plugins/main/opensearch_dashboards.json +++ b/plugins/main/opensearch_dashboards.json @@ -2,9 +2,7 @@ "id": "wazuh", "version": "5.0.0-00", "opensearchDashboardsVersion": "opensearchDashboards", - "configPath": [ - "wazuh" - ], + "configPath": ["wazuh"], "requiredPlugins": [ "navigation", "data", @@ -20,7 +18,8 @@ "opensearchDashboardsUtils", "opensearchDashboardsLegacy", "wazuhCheckUpdates", - "wazuhCore" + "wazuhCore", + "wazuhEngine" ], "optionalPlugins": [ "security", @@ -30,4 +29,4 @@ ], "server": true, "ui": true -} \ No newline at end of file +} diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 9bb66dd7a2..48ffc31f0d 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -1,7 +1,11 @@ import React, { useEffect } from 'react'; import { Router, Route, Switch, Redirect } from 'react-router-dom'; import { ToolsRouter } from './components/tools/tools-router'; -import { getWazuhCorePlugin, getWzMainParams } from './kibana-services'; +import { + getWazuhCorePlugin, + getWazuhEnginePlugin, + getWzMainParams, +} from './kibana-services'; import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { useDispatch } from 'react-redux'; import { checkPluginVersion } from './utils'; @@ -96,6 +100,13 @@ export function Application(props) { exact render={props => } > + { + const { Engine } = getWazuhEnginePlugin(); + return ; + }} + > diff --git a/plugins/main/public/kibana-services.ts b/plugins/main/public/kibana-services.ts index 1c536dc5e1..9bb2ec11ef 100644 --- a/plugins/main/public/kibana-services.ts +++ b/plugins/main/public/kibana-services.ts @@ -15,6 +15,7 @@ import { VisualizationsStart } from '../../../src/plugins/visualizations/public' import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; import { AppPluginStartDependencies } from './types'; import { WazuhCheckUpdatesPluginStart } from '../../wazuh-check-updates/public'; +import { WazuhEnginePluginStart } from '../../wazuh-engine/public'; let angularModule: any = null; let discoverModule: any = null; @@ -47,6 +48,8 @@ export const [getWazuhCheckUpdatesPlugin, setWazuhCheckUpdatesPlugin] = createGetterSetter('WazuhCheckUpdatesPlugin'); export const [getWazuhCorePlugin, setWazuhCorePlugin] = createGetterSetter('WazuhCorePlugin'); +export const [getWazuhEnginePlugin, setWazuhEnginePlugin] = + createGetterSetter('WazuhEnginePlugin'); export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter( 'headerActionMenuMounter', diff --git a/plugins/main/public/plugin.ts b/plugins/main/public/plugin.ts index 872c54eab7..d849739ac2 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -24,6 +24,7 @@ import { setWazuhCheckUpdatesPlugin, setHeaderActionMenuMounter, setWazuhCorePlugin, + setWazuhEnginePlugin, } from './kibana-services'; import { validate as validateNodeCronInterval } from 'node-cron'; import { @@ -210,6 +211,7 @@ export class WazuhPlugin setErrorOrchestrator(ErrorOrchestratorService); setWazuhCheckUpdatesPlugin(plugins.wazuhCheckUpdates); setWazuhCorePlugin(plugins.wazuhCore); + setWazuhEnginePlugin(plugins.wazuhEngine); return {}; } } diff --git a/plugins/main/public/react-services/navigation-service.tsx b/plugins/main/public/react-services/navigation-service.tsx index 195dd4a1d1..9360969880 100644 --- a/plugins/main/public/react-services/navigation-service.tsx +++ b/plugins/main/public/react-services/navigation-service.tsx @@ -62,23 +62,23 @@ class NavigationService { } public navigate(path: string, state?: any): void { - if(!state){ + if (!state) { this.history.push(path); } else { this.history.push({ pathname: path, - state + state, }); } } public replace(path: string, state?: any): void { - if(!state){ + if (!state) { this.history.replace(path); } else { this.history.replace({ pathname: path, - state + state, }); } } diff --git a/plugins/main/public/types.ts b/plugins/main/public/types.ts index 9d3c0e7915..973e732303 100644 --- a/plugins/main/public/types.ts +++ b/plugins/main/public/types.ts @@ -19,6 +19,7 @@ import { } from '../../../src/plugins/telemetry/public'; import { WazuhCheckUpdatesPluginStart } from '../../wazuh-check-updates/public'; import { WazuhCorePluginStart } from '../../wazuh-core/public'; +import { WazuhEnginePluginStart } from '../../wazuh-engine/public'; import { DashboardStart } from '../../../src/plugins/dashboard/public'; export interface AppPluginStartDependencies { @@ -32,6 +33,7 @@ export interface AppPluginStartDependencies { telemetry: TelemetryPluginStart; wazuhCheckUpdates: WazuhCheckUpdatesPluginStart; wazuhCore: WazuhCorePluginStart; + wazuhEngine: WazuhEnginePluginStart; dashboard: DashboardStart; } export interface AppDependencies { diff --git a/plugins/main/public/utils/applications.ts b/plugins/main/public/utils/applications.ts index 46331558ea..04f28a85c5 100644 --- a/plugins/main/public/utils/applications.ts +++ b/plugins/main/public/utils/applications.ts @@ -833,6 +833,25 @@ const about = { redirectTo: () => '/settings?tab=about', }; +export const engine = { + category: 'wz-category-server-management', + id: 'wz-engine', + title: i18n.translate('wz-app-engine-title', { + defaultMessage: 'Engine', + }), + breadcrumbLabel: i18n.translate('wz-app-engine-breadcrumbLabel', { + defaultMessage: 'Engine', + }), + description: i18n.translate('wz-app-engine-description', { + defaultMessage: 'Change', + }), + euiIconType: 'lensApp', + order: 602, + showInOverviewApp: false, + showInAgentMenu: false, + redirectTo: () => `/engine`, +}; + export const Applications = [ fileIntegrityMonitoring, overview, @@ -857,6 +876,7 @@ export const Applications = [ docker, endpointSummary, rules, + engine, decoders, cdbLists, endpointGroups, diff --git a/plugins/wazuh-engine/.i18nrc.json b/plugins/wazuh-engine/.i18nrc.json new file mode 100644 index 0000000000..d3fbe36ca8 --- /dev/null +++ b/plugins/wazuh-engine/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "wazuhEngine", + "paths": { + "wazuhEngine": "." + }, + "translations": ["translations/en-US.json"] +} diff --git a/plugins/wazuh-engine/README.md b/plugins/wazuh-engine/README.md new file mode 100755 index 0000000000..78b6e6a52d --- /dev/null +++ b/plugins/wazuh-engine/README.md @@ -0,0 +1,76 @@ +# Wazuh Check Updates Plugin + +**Wazuh Check Updates Plugin** is an extension for Wazuh that allows users to stay informed about new updates available. This plugin has been designed to work in conjunction with the main Wazuh plugin and has the following features. + +## Features + +### 1. Notification of New Updates + +The main functionality of the plugin is to notify users about the availability of new updates. For this purpose, it exposes a component that is displayed in a bottom bar. In addition to notifying the user about new updates, it provides a link to redirect to the API Configuration page and gives the user the option to opt out of receiving further notifications of this kind. + +Every time a page is loaded, the UpdatesNotification component is rendered. The component is responsible for querying the following: + +1. **User Preferences:** It retrieves information from the saved object containing user preferences to determine if the user has chosen to display more notifications about new updates and to obtain the latest updates that the user dismissed in a notification. + +2. **Available Updates:** It retrieves the available updates for each available API. To determine where to retrieve the information, it first checks the session storage for the key `checkUpdates`. If the value is `executed`, it then searches for available updates in a saved object; otherwise, it queries the Wazuh API and makes a request for each available API, finally saving the information in the saved object and setting the session storage `checkUpdates` to `executed`. + The endpoint has two parameters: + `query_api`: Determines whether the Check Updates plugin retrieves data from the Wazuh API or from a saved object. + `force_query`: When `query_api` is set to true, it determines whether the Wazuh API internally obtains the data or fetches it from the CTI Service. + +If the user had not chosen not to receive notifications of new updates and if the new updates are different from the last ones dismissed, then the component renders a bottom bar to notify that there are new updates. + +### 2. Get available updates function + +The plugin provides a function for fetching the available updates for each API. This function is utilized by the main plugin on the API Configuration page. This page presents a table listing the APIs, along with their respective versions and update statuses, both of which are obtained through the mentioned function. + +## Use cases + +### User logs in + +1. The user logs in. +2. The main Wazuh plugin is loaded and renders the UpdatesNotification component from the Check Updates plugin. +3. The `UpdatesNotification` component checks the user's preferences (stored in a saved object) to determine if the user has dismissed notifications about new updates. If the user has dismissed them, the component returns nothing; otherwise, it proceeds to the next steps. +4. The UpdatesNotification component checks the `checkUpdates` value in the browser's session storage to determine if a query about available updates from the Wazuh Server API has already been executed. Since the user has just logged in, this value will not exist in the session storage. +5. The component makes a request to the Check Updates plugin API with the `query_api` parameter set to true and `force_query` set to false. The `checkUpdates` value in the session storage is updated to `true`. +6. The updates are stored in a saved object for future reference. +7. It's possible that the user has dismissed specific updates. In such cases, the dismissed updates are compared with the updates retrieved from the API. If they match, the component returns nothing; otherwise, it proceeds to the next steps. +8. The component displays a bottom bar to notify the user of the availability of new updates. +9. The user can access the details of the updates by clicking a link in the bottom bar, which takes them to the API Configuration page. +10. The user can also dismiss the updates and choose whether they no longer wish to receive this type of notification. + +### User goes to API Configuration page + +1. The user goes to the API Configuration page. +2. The APIs table is rendered, which retrieves the available updates stored in the saved object. The table includes, among other things, the `Version` and `Updates status` columns: + +- **Version column:** Indicates the current version of the server. If the endpoint's response to query available updates returns an error, the version is not displayed. +- **Updates status column:** Indicates the server's status regarding available updates, which can be in one of four states: + - **Up to date:** The server is up to date with the latest available version. + - **Available updates:** There are updates available. In this case, you can view the details of the available updates by clicking an icon, and a Flyout will open with the details. + - **Checking updates disabled:** The server has the service for checking updates disabled. + - **Error checking updates:** An error occurred when trying to query the Wazuh Server API. In this case, a tooltip will display the error message. + +3. The user has the option to force a direct request for available updates to the Wazuh Server API instead of querying them in the saved object. To do this, they can click the "Check updates" button. If there are any changes when retrieving the information, the results will be reflected in the table. +4. The user can also modify their preferences (stored in a saved object) regarding whether they want to continue receiving notifications about new updates. + +## Data Storage + +The data managed by the plugin is stored and queried in saved objects. There are two types of saved objects. + +### 1. Available Updates + +The saved object of type "wazuh-check-updates-available-updates" stores the available updates for each API and the last date when a request was made to the Wazuh API to fetch the data. There is a single object of this type for the entire application, shared among all users. + +### 2. User Preferences + +The saved objects of type "wazuh-check-updates-user-preferences" store user preferences related to available updates. These objects store whether the user prefers not to receive notifications for new updates and the latest updates that the user dismissed when closing the notification. There can be one object of this type for each user. + +## Software and libraries used + +- [OpenSearch](https://opensearch.org/) + +- [Elastic UI Framework](https://eui.elastic.co/) + +- [Node.js](https://nodejs.org) + +- [React](https://reactjs.org) diff --git a/plugins/wazuh-engine/common/constants.ts b/plugins/wazuh-engine/common/constants.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/common/types.ts b/plugins/wazuh-engine/common/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/opensearch_dashboards.json b/plugins/wazuh-engine/opensearch_dashboards.json new file mode 100644 index 0000000000..4f4c184fc5 --- /dev/null +++ b/plugins/wazuh-engine/opensearch_dashboards.json @@ -0,0 +1,8 @@ +{ + "id": "wazuhEngine", + "version": "5.0.0-00", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": ["opensearchDashboardsUtils", "wazuhCore"] +} diff --git a/plugins/wazuh-engine/package.json b/plugins/wazuh-engine/package.json new file mode 100644 index 0000000000..ee7c858e98 --- /dev/null +++ b/plugins/wazuh-engine/package.json @@ -0,0 +1,25 @@ +{ + "name": "wazuh-engine", + "version": "5.0.0", + "revision": "00", + "pluginPlatform": { + "version": "2.13.0" + }, + "description": "Wazuh Engine", + "private": true, + "scripts": { + "build": "yarn plugin-helpers build --opensearch-dashboards-version=$OPENSEARCH_DASHBOARDS_VERSION", + "plugin-helpers": "node ../../scripts/plugin_helpers", + "osd": "node ../../scripts/osd", + "test:ui:runner": "node ../../scripts/functional_test_runner.js", + "test:server": "plugin-helpers test:server", + "test:browser": "plugin-helpers test:browser", + "test:jest": "node scripts/jest --runInBand", + "test:jest:runner": "node scripts/runner test" + }, + "dependencies": {}, + "devDependencies": { + "@testing-library/user-event": "^14.5.0", + "@types/": "testing-library/user-event" + } +} diff --git a/plugins/wazuh-engine/public/components/decoders/decoders.tsx b/plugins/wazuh-engine/public/components/decoders/decoders.tsx new file mode 100644 index 0000000000..fc40d7f7bd --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/decoders.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Decoders = () => { + return
Decoders
; +}; diff --git a/plugins/wazuh-engine/public/components/decoders/index.ts b/plugins/wazuh-engine/public/components/decoders/index.ts new file mode 100644 index 0000000000..3ba7021444 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/index.ts @@ -0,0 +1 @@ +export { Decoders } from './decoders'; diff --git a/plugins/wazuh-engine/public/components/engine.tsx b/plugins/wazuh-engine/public/components/engine.tsx new file mode 100644 index 0000000000..575663a87e --- /dev/null +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { EuiSideNav, EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; +import { Route, Switch, Redirect } from 'react-router-dom'; +import { Decoders } from './decoders'; +import { Filters } from './filters'; +import { Outputs } from './outputs'; +import { Rules } from './rules'; +import { Integrations } from './integrations'; +import { KVDBs } from './kvdbs'; +import { Policies } from './policies'; + +export const Engine = props => { + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + const sideNav = [ + { + name: 'Engine', + id: 'engine', + items: [ + { + name: 'Decoders', + id: 'decoders', + }, + { + name: 'Rules', + id: 'rules', + }, + { + name: 'Outputs', + id: 'outputs', + }, + { + name: 'Filters', + id: 'filters', + }, + { + name: 'Integrations', + id: 'integrations', + }, + { + name: 'Policies', + id: 'policies', + }, + { + name: 'KVDBs', + id: 'kvdbs', + }, + ].map(item => ({ + ...item, + onClick: () => { + props.navigationService.getInstance().navigate(`/engine/${item.id}`); + }, + isSelected: props.location.pathname === `/engine/${item.id}`, + })), + }, + ]; + + return ( + + + toggleOpenOnMobile()} + //isOpenOnMobile={isSideNavOpenOnMobile} + //TODO: Width mustn't be hardcoded + style={{ width: 192 }} + items={sideNav} + /> + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/filters/filters.tsx b/plugins/wazuh-engine/public/components/filters/filters.tsx new file mode 100644 index 0000000000..9f33a383c9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/filters/filters.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Filters = () => { + return
Filters
; +}; diff --git a/plugins/wazuh-engine/public/components/filters/index.ts b/plugins/wazuh-engine/public/components/filters/index.ts new file mode 100644 index 0000000000..830ef9d8f8 --- /dev/null +++ b/plugins/wazuh-engine/public/components/filters/index.ts @@ -0,0 +1 @@ +export { Filters } from './filters'; diff --git a/plugins/wazuh-engine/public/components/integrations/index.ts b/plugins/wazuh-engine/public/components/integrations/index.ts new file mode 100644 index 0000000000..1e9cfbc4f1 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/index.ts @@ -0,0 +1 @@ +export { Integrations } from './integrations'; diff --git a/plugins/wazuh-engine/public/components/integrations/integrations.tsx b/plugins/wazuh-engine/public/components/integrations/integrations.tsx new file mode 100644 index 0000000000..6d79af46e9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/integrations.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Integrations = () => { + return
Integrations
; +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/index.ts b/plugins/wazuh-engine/public/components/kvdbs/index.ts new file mode 100644 index 0000000000..58adcc2291 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/index.ts @@ -0,0 +1 @@ +export { KVDBs } from './kvdbs'; diff --git a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx new file mode 100644 index 0000000000..0eb897740f --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const KVDBs = () => { + return
KVDBs
; +}; diff --git a/plugins/wazuh-engine/public/components/outputs/index.ts b/plugins/wazuh-engine/public/components/outputs/index.ts new file mode 100644 index 0000000000..e41beb42b1 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/index.ts @@ -0,0 +1 @@ +export { Outputs } from './outputs'; diff --git a/plugins/wazuh-engine/public/components/outputs/outputs.tsx b/plugins/wazuh-engine/public/components/outputs/outputs.tsx new file mode 100644 index 0000000000..3fc9102f8a --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/outputs.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Outputs = () => { + return
Outputs
; +}; diff --git a/plugins/wazuh-engine/public/components/policies/index.ts b/plugins/wazuh-engine/public/components/policies/index.ts new file mode 100644 index 0000000000..76a4276901 --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/index.ts @@ -0,0 +1 @@ +export { Policies } from './policies'; diff --git a/plugins/wazuh-engine/public/components/policies/policies.tsx b/plugins/wazuh-engine/public/components/policies/policies.tsx new file mode 100644 index 0000000000..180789b230 --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/policies.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Policies = () => { + return
Policies
; +}; diff --git a/plugins/wazuh-engine/public/components/rules/index.ts b/plugins/wazuh-engine/public/components/rules/index.ts new file mode 100644 index 0000000000..933b848dc0 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/index.ts @@ -0,0 +1 @@ +export { Rules } from './rules'; diff --git a/plugins/wazuh-engine/public/components/rules/rules.tsx b/plugins/wazuh-engine/public/components/rules/rules.tsx new file mode 100644 index 0000000000..c7b6a09484 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/rules.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Rules = () => { + return
Rules
; +}; diff --git a/plugins/wazuh-engine/public/hooks/index.ts b/plugins/wazuh-engine/public/hooks/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/public/index.ts b/plugins/wazuh-engine/public/index.ts new file mode 100644 index 0000000000..218035687c --- /dev/null +++ b/plugins/wazuh-engine/public/index.ts @@ -0,0 +1,8 @@ +import { WazuhEnginePlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export function plugin() { + return new WazuhEnginePlugin(); +} +export type { WazuhEnginePluginSetup, WazuhEnginePluginStart } from './types'; diff --git a/plugins/wazuh-engine/public/plugin-services.ts b/plugins/wazuh-engine/public/plugin-services.ts new file mode 100644 index 0000000000..afd603a713 --- /dev/null +++ b/plugins/wazuh-engine/public/plugin-services.ts @@ -0,0 +1,7 @@ +import { CoreStart } from 'opensearch-dashboards/public'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; +import { WazuhCorePluginStart } from '../../wazuh-core/public'; + +export const [getCore, setCore] = createGetterSetter('Core'); +export const [getWazuhCore, setWazuhCore] = + createGetterSetter('WazuhCore'); diff --git a/plugins/wazuh-engine/public/plugin.ts b/plugins/wazuh-engine/public/plugin.ts new file mode 100644 index 0000000000..fbd9b2867a --- /dev/null +++ b/plugins/wazuh-engine/public/plugin.ts @@ -0,0 +1,28 @@ +import { CoreSetup, CoreStart, Plugin } from 'opensearch-dashboards/public'; +import { + AppPluginStartDependencies, + WazuhEnginePluginSetup, + WazuhEnginePluginStart, +} from './types'; +import { setCore, setWazuhCore } from './plugin-services'; +import { Engine } from './components/engine'; + +export class WazuhEnginePlugin + implements Plugin +{ + public setup(core: CoreSetup): WazuhEnginePluginSetup { + return {}; + } + + public start( + core: CoreStart, + plugins: AppPluginStartDependencies, + ): WazuhEnginePluginStart { + setCore(core); + setWazuhCore(plugins.wazuhCore); + + return { Engine }; + } + + public stop() {} +} diff --git a/plugins/wazuh-engine/public/services/index.ts b/plugins/wazuh-engine/public/services/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/public/types.ts b/plugins/wazuh-engine/public/types.ts new file mode 100644 index 0000000000..4f8ec512e8 --- /dev/null +++ b/plugins/wazuh-engine/public/types.ts @@ -0,0 +1,9 @@ +import { WazuhCorePluginStart } from '../../wazuh-core/public'; + +export interface WazuhEnginePluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhEnginePluginStart {} + +export interface AppPluginStartDependencies { + wazuhCore: WazuhCorePluginStart; +} diff --git a/plugins/wazuh-engine/public/utils/index.ts b/plugins/wazuh-engine/public/utils/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/scripts/jest.js b/plugins/wazuh-engine/scripts/jest.js new file mode 100644 index 0000000000..cb58c54ec0 --- /dev/null +++ b/plugins/wazuh-engine/scripts/jest.js @@ -0,0 +1,19 @@ +// # Run Jest tests +// +// All args will be forwarded directly to Jest, e.g. to watch tests run: +// +// node scripts/jest --watch +// +// or to build code coverage: +// +// node scripts/jest --coverage +// +// See all cli options in https://facebook.github.io/jest/docs/cli.html + +const path = require('path'); +process.argv.push('--config', path.resolve(__dirname, '../test/jest/config.js')); + +require('../../../src/setup_node_env'); +const jest = require('../../../node_modules/jest'); + +jest.run(process.argv.slice(2)); diff --git a/plugins/wazuh-engine/scripts/manifest.js b/plugins/wazuh-engine/scripts/manifest.js new file mode 100644 index 0000000000..711059d3ac --- /dev/null +++ b/plugins/wazuh-engine/scripts/manifest.js @@ -0,0 +1,17 @@ + +/* eslint-disable @typescript-eslint/no-var-requires */ + +const fs = require('fs'); + +/** + * Reads the package.json file. + * @returns {Object} JSON object. + */ +function loadPackageJson() { + const packageJson = fs.readFileSync('./package.json'); + return JSON.parse(packageJson); +} + +module.exports = { + loadPackageJson +}; \ No newline at end of file diff --git a/plugins/wazuh-engine/scripts/runner.js b/plugins/wazuh-engine/scripts/runner.js new file mode 100755 index 0000000000..5ba9b132ab --- /dev/null +++ b/plugins/wazuh-engine/scripts/runner.js @@ -0,0 +1,148 @@ +/* eslint-disable array-element-newline */ +/* eslint-disable @typescript-eslint/no-var-requires */ + +/** +Runs yarn commands using a Docker container. + +Intended to test and build locally. + +Uses development images. Must be executed from the root folder of the project. + +See /docker/runner/docker-compose.yml for available environment variables. + +# Usage: +# ------------- +# - node scripts/runner [] +# - yarn test:jest:runner [] +# - yarn build:runner +*/ + +const childProcess = require('child_process'); +const { loadPackageJson } = require('./manifest'); + +const COMPOSE_DIR = '../../docker/runner'; + +function getProjectInfo() { + const manifest = loadPackageJson(); + + return { + app: 'osd', + version: manifest['pluginPlatform']['version'], + repo: process.cwd(), + }; +} + +function getBuildArgs({ app, version }) { + return `--opensearch-dashboards-version=${version}`; +} + +/** + * Transforms the Jest CLI options from process.argv back to a string. + * If no options are provided, default ones are generated. + * @returns {String} Space separated string with all Jest CLI options provided. + */ +function getJestArgs() { + // Take args only after `test` word + const index = process.argv.indexOf('test'); + const args = process.argv.slice(index + 1); + // Remove duplicates using set + return Array.from(new Set([...args, '--runInBand'])).join(' '); +} + +/** + * Generates the execution parameters if they are not set. + * @returns {Object} Default environment variables. + */ +const buildEnvVars = ({ app, version, repo, cmd, args }) => { + return { + APP: app, + VERSION: version, + REPO: repo, + CMD: cmd, + ARGS: args, + }; +}; + +/** + * Captures the SIGINT signal (Ctrl + C) to stop the container and exit. + */ +function setupAbortController() { + process.on('SIGINT', () => { + childProcess.spawnSync('docker', [ + 'compose', + '--project-directory', + COMPOSE_DIR, + 'stop', + ]); + process.exit(); + }); +} + +/** + * Start the container. + */ +function startRunner() { + const runner = childProcess.spawn('docker', [ + 'compose', + '--project-directory', + COMPOSE_DIR, + 'up', + ]); + + runner.stdout.on('data', data => { + console.log(`${data}`); + }); + + runner.stderr.on('data', data => { + console.error(`${data}`); + }); +} + +/** + * Main function + */ +function main() { + if (process.argv.length < 2) { + process.stderr.write('Required parameters not provided'); + process.exit(-1); + } + + const projectInfo = getProjectInfo(); + let envVars = {}; + + switch (process.argv[2]) { + case 'build': + envVars = buildEnvVars({ + ...projectInfo, + cmd: 'plugin-helpers build', + args: getBuildArgs({ ...projectInfo }), + }); + break; + + case 'test': + envVars = buildEnvVars({ + ...projectInfo, + cmd: 'test:jest', + args: getJestArgs(), + }); + break; + + default: + // usage(); + console.error('Unsupported or invalid yarn command.'); + process.exit(-1); + } + + // Check the required environment variables are set + for (const [key, value] of Object.entries(envVars)) { + if (!process.env[key]) { + process.env[key] = value; + } + console.log(`${key}: ${process.env[key]}`); + } + + setupAbortController(); + startRunner(); +} + +main(); diff --git a/plugins/wazuh-engine/server/index.ts b/plugins/wazuh-engine/server/index.ts new file mode 100644 index 0000000000..90b22740f3 --- /dev/null +++ b/plugins/wazuh-engine/server/index.ts @@ -0,0 +1,11 @@ +import { PluginInitializerContext } from '../../../src/core/server'; +import { WazuhEnginePlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WazuhEnginePlugin(initializerContext); +} + +export type { WazuhEnginePluginSetup, WazuhEnginePluginStart } from './types'; diff --git a/plugins/wazuh-engine/server/plugin-services.ts b/plugins/wazuh-engine/server/plugin-services.ts new file mode 100644 index 0000000000..23d5cfe8b1 --- /dev/null +++ b/plugins/wazuh-engine/server/plugin-services.ts @@ -0,0 +1,7 @@ +import { CoreStart } from 'opensearch-dashboards/server'; +import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/common'; +import { WazuhCorePluginStart } from '../../wazuh-core/server'; + +export const [getCore, setCore] = createGetterSetter('Core'); +export const [getWazuhCore, setWazuhCore] = + createGetterSetter('WazuhCore'); diff --git a/plugins/wazuh-engine/server/plugin.ts b/plugins/wazuh-engine/server/plugin.ts new file mode 100644 index 0000000000..efcf1eb4e6 --- /dev/null +++ b/plugins/wazuh-engine/server/plugin.ts @@ -0,0 +1,67 @@ +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from 'opensearch-dashboards/server'; + +import { + PluginSetup, + WazuhEnginePluginSetup, + WazuhEnginePluginStart, + AppPluginStartDependencies, +} from './types'; +import { defineRoutes } from './routes'; +import { setCore, setWazuhCore } from './plugin-services'; +import { ISecurityFactory } from '../../wazuh-core/server/services/security-factory'; + +declare module 'opensearch-dashboards/server' { + interface RequestHandlerContext { + wazuh_check_updates: { + logger: Logger; + security: ISecurityFactory; + }; + } +} + +export class WazuhEnginePlugin + implements Plugin +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: PluginSetup) { + this.logger.debug('Setup'); + + setWazuhCore(plugins.wazuhCore); + + core.http.registerRouteHandlerContext('wazuh_engine', () => { + return { + logger: this.logger, + }; + }); + + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start( + core: CoreStart, + plugins: AppPluginStartDependencies, + ): WazuhEnginePluginStart { + this.logger.debug('Started'); + setCore(core); + + return {}; + } + + public stop() {} +} diff --git a/plugins/wazuh-engine/server/routes/index.ts b/plugins/wazuh-engine/server/routes/index.ts new file mode 100644 index 0000000000..c42f1068fd --- /dev/null +++ b/plugins/wazuh-engine/server/routes/index.ts @@ -0,0 +1,3 @@ +import { IRouter } from 'opensearch-dashboards/server'; + +export function defineRoutes(router: IRouter) {} diff --git a/plugins/wazuh-engine/server/services/saved-object/get-saved-object.test.ts b/plugins/wazuh-engine/server/services/saved-object/get-saved-object.test.ts new file mode 100644 index 0000000000..58d771e7f8 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/get-saved-object.test.ts @@ -0,0 +1,64 @@ +import { + getInternalSavedObjectsClient, + getWazuhCore, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; +import { getSavedObject } from './get-saved-object'; + +const mockedGetInternalObjectsClient = + getInternalSavedObjectsClient as jest.Mock; +const mockedGetWazuhCheckUpdatesServices = + getWazuhCheckUpdatesServices as jest.Mock; +jest.mock('../../plugin-services'); + +describe('getSavedObject function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return saved object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: () => ({ attributes: 'value' }), + })); + + const response = await getSavedObject('type'); + + expect(response).toEqual('value'); + }); + + test('should return an empty object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: jest.fn().mockRejectedValue({ output: { statusCode: 404 } }), + })); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const response = await getSavedObject('type'); + + expect(response).toEqual({}); + }); + + test('should return an error', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + get: jest.fn().mockRejectedValue(new Error('getSavedObject error')), + })); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const promise = getSavedObject('type'); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-engine/server/services/saved-object/get-saved-object.ts b/plugins/wazuh-engine/server/services/saved-object/get-saved-object.ts new file mode 100644 index 0000000000..fec5c3a548 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/get-saved-object.ts @@ -0,0 +1,34 @@ +import { + getInternalSavedObjectsClient, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; +import { savedObjectType } from '../../../common/types'; + +export const getSavedObject = async ( + type: string, + id?: string, +): Promise => { + try { + const client = getInternalSavedObjectsClient(); + + const responseGet = await client.get(type, id || type); + + const result = (responseGet?.attributes || {}) as savedObjectType; + return result; + } catch (error: any) { + if (error?.output?.statusCode === 404) { + return {}; + } + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get saved object'; + + const { logger } = getWazuhCheckUpdatesServices(); + + logger.error(message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-engine/server/services/saved-object/index.ts b/plugins/wazuh-engine/server/services/saved-object/index.ts new file mode 100644 index 0000000000..cca5f45685 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/index.ts @@ -0,0 +1,2 @@ +export { getSavedObject } from './get-saved-object'; +export { setSavedObject } from './set-saved-object'; diff --git a/plugins/wazuh-engine/server/services/saved-object/set-saved-object.test.ts b/plugins/wazuh-engine/server/services/saved-object/set-saved-object.test.ts new file mode 100644 index 0000000000..1d484739b6 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/set-saved-object.test.ts @@ -0,0 +1,62 @@ +import { + getInternalSavedObjectsClient, + getWazuhCore, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; +import { setSavedObject } from './set-saved-object'; + +const mockedGetInternalObjectsClient = + getInternalSavedObjectsClient as jest.Mock; +const mockedGetWazuhCheckUpdatesServices = + getWazuhCheckUpdatesServices as jest.Mock; +jest.mock('../../plugin-services'); + +describe('setSavedObject function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return saved object', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + create: () => ({ attributes: { hide_update_notifications: true } }), + })); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const response = await setSavedObject( + 'wazuh-check-updates-user-preferences', + { hide_update_notifications: true }, + 'admin', + ); + + expect(response).toEqual({ hide_update_notifications: true }); + }); + + test('should return an error', async () => { + mockedGetInternalObjectsClient.mockImplementation(() => ({ + create: jest.fn().mockRejectedValue(new Error('setSavedObject error')), + })); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const promise = setSavedObject( + 'wazuh-check-updates-user-preferences', + { hide_update_notifications: true }, + 'admin', + ); + + await expect(promise).rejects.toThrow('setSavedObject error'); + }); +}); diff --git a/plugins/wazuh-engine/server/services/saved-object/set-saved-object.ts b/plugins/wazuh-engine/server/services/saved-object/set-saved-object.ts new file mode 100644 index 0000000000..5e45bda413 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/set-saved-object.ts @@ -0,0 +1,35 @@ +import { savedObjectType } from '../../../common/types'; +import { + getInternalSavedObjectsClient, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; + +export const setSavedObject = async ( + type: string, + value: savedObjectType, + id?: string, +): Promise => { + try { + const client = getInternalSavedObjectsClient(); + + const responseCreate = await client.create(type, value, { + id: id || type, + overwrite: true, + refresh: true, + }); + + return responseCreate?.attributes; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to update saved object'; + + const { logger } = getWazuhCheckUpdatesServices(); + + logger.error(message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts b/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts new file mode 100644 index 0000000000..40820e4f87 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts @@ -0,0 +1,81 @@ +import { SavedObjectsFieldMapping, SavedObjectsType } from 'opensearch-dashboards/server'; +import { SAVED_OBJECT_UPDATES } from '../../../../common/constants'; + +const updateObjectType: SavedObjectsFieldMapping = { + properties: { + description: { + type: 'text', + }, + published_date: { + type: 'date', + }, + semver: { + type: 'nested', + properties: { + major: { + type: 'integer', + }, + minor: { + type: 'integer', + }, + patch: { + type: 'integer', + }, + }, + }, + tag: { + type: 'text', + }, + title: { + type: 'text', + }, + }, +}; + +export const availableUpdatesObject: SavedObjectsType = { + name: SAVED_OBJECT_UPDATES, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + last_check_date: { + type: 'date', + }, + apis_available_updates: { + type: 'nested', + properties: { + api_id: { + type: 'text', + }, + current_version: { + type: 'text', + }, + update_check: { + type: 'boolean', + }, + status: { + type: 'text', + }, + last_check_date: { + type: 'date', + }, + last_available_major: updateObjectType, + last_available_minor: updateObjectType, + last_available_patch: updateObjectType, + error: { + type: 'nested', + properties: { + title: { + type: 'text', + }, + detail: { + type: 'text', + }, + }, + }, + }, + }, + }, + }, + migrations: {}, +}; diff --git a/plugins/wazuh-engine/server/services/saved-object/types/index.ts b/plugins/wazuh-engine/server/services/saved-object/types/index.ts new file mode 100644 index 0000000000..fdd8c1463e --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/types/index.ts @@ -0,0 +1,2 @@ +export { availableUpdatesObject } from './available-updates'; +export { userPreferencesObject } from './user-preferences'; diff --git a/plugins/wazuh-engine/server/services/saved-object/types/user-preferences.ts b/plugins/wazuh-engine/server/services/saved-object/types/user-preferences.ts new file mode 100644 index 0000000000..8e41f5d82d --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/types/user-preferences.ts @@ -0,0 +1,33 @@ +import { SavedObjectsType } from 'opensearch-dashboards/server'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../../common/constants'; + +export const userPreferencesObject: SavedObjectsType = { + name: SAVED_OBJECT_USER_PREFERENCES, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + last_dismissed_updates: { + type: 'nested', + properties: { + api_id: { + type: 'text', + }, + last_major: { + type: 'text', + }, + last_minor: { + type: 'text', + }, + last_patch: { + type: 'text', + }, + }, + }, + hide_update_notifications: { + type: 'boolean', + }, + }, + }, + migrations: {}, +}; diff --git a/plugins/wazuh-engine/server/services/updates/get-updates.test.ts b/plugins/wazuh-engine/server/services/updates/get-updates.test.ts new file mode 100644 index 0000000000..3277b9dd63 --- /dev/null +++ b/plugins/wazuh-engine/server/services/updates/get-updates.test.ts @@ -0,0 +1,162 @@ +import { getSavedObject } from '../saved-object/get-saved-object'; +import { setSavedObject } from '../saved-object/set-saved-object'; +import { + getWazuhCheckUpdatesServices, + getWazuhCore, +} from '../../plugin-services'; +import { API_UPDATES_STATUS } from '../../../common/types'; +import { getUpdates } from './get-updates'; +import { SAVED_OBJECT_UPDATES } from '../../../common/constants'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedSetSavedObject = setSavedObject as jest.Mock; +jest.mock('../saved-object/set-saved-object'); + +const mockedGetWazuhCore = getWazuhCore as jest.Mock; +const mockedGetWazuhCheckUpdatesServices = + getWazuhCheckUpdatesServices as jest.Mock; +jest.mock('../../plugin-services'); + +describe('getUpdates function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return available updates from saved object', async () => { + mockedGetSavedObject.mockImplementation(() => ({ + last_check_date: '2023-09-30T14:00:00.000Z', + apis_available_updates: [ + { + api_id: 'api id', + current_version: '4.3.1', + status: API_UPDATES_STATUS.UP_TO_DATE, + last_available_patch: { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + major: 4, + minor: 3, + patch: 8, + }, + tag: 'v4.3.8', + title: 'Wazuh v4.3.8', + }, + }, + ], + })); + + mockedGetWazuhCore.mockImplementation(() => ({ + serverAPIHostEntries: { + getHostsEntries: jest.fn(() => []), + }, + })); + + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const updates = await getUpdates(); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith(SAVED_OBJECT_UPDATES); + + expect(updates).toEqual({ + last_check_date: '2023-09-30T14:00:00.000Z', + apis_available_updates: [ + { + api_id: 'api id', + current_version: '4.3.1', + status: API_UPDATES_STATUS.UP_TO_DATE, + last_available_patch: { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + major: 4, + minor: 3, + patch: 8, + }, + tag: 'v4.3.8', + title: 'Wazuh v4.3.8', + }, + }, + ], + }); + }); + + test('should return available updates from api', async () => { + mockedSetSavedObject.mockImplementation(() => ({})); + mockedGetWazuhCore.mockImplementation(() => ({ + api: { + client: { + asInternalUser: { + request: jest.fn().mockImplementation(() => ({ + data: { + data: { + uuid: '7f828fd6-ef68-4656-b363-247b5861b84c', + current_version: '4.3.1', + last_available_patch: { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + major: 4, + minor: 3, + patch: 8, + }, + tag: 'v4.3.8', + title: 'Wazuh v4.3.8', + }, + }, + }, + })), + }, + }, + }, + manageHosts: { + get: jest.fn(() => [{ id: 'api id' }]), + }, + })); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const updates = await getUpdates(true); + + expect(updates).toEqual({ + last_check_date: expect.any(Date), + apis_available_updates: [ + { + api_id: 'api id', + current_version: '4.3.1', + status: API_UPDATES_STATUS.AVAILABLE_UPDATES, + last_available_patch: { + description: + '## Manager\r\n\r\n### Fixed\r\n\r\n- Fixed a crash when overwrite rules are triggered...', + published_date: '2022-05-18T10:12:43Z', + semver: { + major: 4, + minor: 3, + patch: 8, + }, + tag: 'v4.3.8', + title: 'Wazuh v4.3.8', + }, + }, + ], + }); + }); +}); diff --git a/plugins/wazuh-engine/server/services/updates/get-updates.ts b/plugins/wazuh-engine/server/services/updates/get-updates.ts new file mode 100644 index 0000000000..2b8d50df05 --- /dev/null +++ b/plugins/wazuh-engine/server/services/updates/get-updates.ts @@ -0,0 +1,120 @@ +import { + API_UPDATES_STATUS, + AvailableUpdates, + ResponseApiAvailableUpdates, +} from '../../../common/types'; +import { SAVED_OBJECT_UPDATES } from '../../../common/constants'; +import { getSavedObject, setSavedObject } from '../saved-object'; +import { + getWazuhCheckUpdatesServices, + getWazuhCore, +} from '../../plugin-services'; + +export const getUpdates = async ( + queryApi = false, + forceQuery = false, +): Promise => { + try { + if (!queryApi) { + const availableUpdates = (await getSavedObject( + SAVED_OBJECT_UPDATES, + )) as AvailableUpdates; + + return availableUpdates; + } + + const { manageHosts, api: wazuhApiClient } = getWazuhCore(); + + const hosts: { id: string }[] = await manageHosts.get(); + + const apisAvailableUpdates = await Promise.all( + hosts?.map(async api => { + const data = {}; + const method = 'GET'; + const path = `/manager/version/check?force_query=${forceQuery}`; + const options = { + apiHostID: api.id, + forceRefresh: true, + }; + try { + const response = await wazuhApiClient.client.asInternalUser.request( + method, + path, + data, + options, + ); + + const update = response.data.data as ResponseApiAvailableUpdates; + + const { + current_version, + update_check, + last_available_major, + last_available_minor, + last_available_patch, + last_check_date, + } = update; + + const getStatus = () => { + if (update_check === false) { + return API_UPDATES_STATUS.DISABLED; + } + + if ( + last_available_major?.tag || + last_available_minor?.tag || + last_available_patch?.tag + ) { + return API_UPDATES_STATUS.AVAILABLE_UPDATES; + } + + return API_UPDATES_STATUS.UP_TO_DATE; + }; + + return { + current_version, + update_check, + last_available_major, + last_available_minor, + last_available_patch, + last_check_date: last_check_date || undefined, + api_id: api.id, + status: getStatus(), + }; + } catch (e: any) { + const error = { + title: e.response?.data?.title, + detail: e.response?.data?.detail ?? e.message, + }; + + return { + api_id: api.id, + status: API_UPDATES_STATUS.ERROR, + error, + }; + } + }), + ); + + const savedObject = { + apis_available_updates: apisAvailableUpdates, + last_check_date: new Date(), + }; + + await setSavedObject(SAVED_OBJECT_UPDATES, savedObject); + + return savedObject; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get available updates'; + + const { logger } = getWazuhCheckUpdatesServices(); + + logger.error(message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-engine/server/services/updates/index.ts b/plugins/wazuh-engine/server/services/updates/index.ts new file mode 100644 index 0000000000..cecc732c86 --- /dev/null +++ b/plugins/wazuh-engine/server/services/updates/index.ts @@ -0,0 +1 @@ +export { getUpdates } from './get-updates'; diff --git a/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.test.ts b/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.test.ts new file mode 100644 index 0000000000..16b31ad72b --- /dev/null +++ b/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.test.ts @@ -0,0 +1,70 @@ +import { getSavedObject } from '../saved-object/get-saved-object'; +import { getUserPreferences } from './get-user-preferences'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { + getWazuhCore, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedGetWazuhCore = getWazuhCore as jest.Mock; +const mockedGetWazuhCheckUpdatesServices = + getWazuhCheckUpdatesServices as jest.Mock; +jest.mock('../../plugin-services'); + +describe('getUserPreferences function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return user preferences', async () => { + mockedGetSavedObject.mockImplementation(() => ({ + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + })); + + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const response = await getUserPreferences('admin'); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith( + SAVED_OBJECT_USER_PREFERENCES, + 'admin', + ); + + expect(response).toEqual({ + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + }); + }); + + test('should return an error', async () => { + mockedGetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = getUserPreferences('admin'); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.ts b/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.ts new file mode 100644 index 0000000000..07562b675c --- /dev/null +++ b/plugins/wazuh-engine/server/services/user-preferences/get-user-preferences.ts @@ -0,0 +1,32 @@ +import _ from 'lodash'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { UserPreferences } from '../../../common/types'; +import { getSavedObject } from '../saved-object'; +import { getWazuhCheckUpdatesServices } from '../../plugin-services'; + +export const getUserPreferences = async ( + username: string, +): Promise => { + try { + const userPreferences = (await getSavedObject( + SAVED_OBJECT_USER_PREFERENCES, + username, + )) as UserPreferences; + + const userPreferencesWithoutUsername = _.omit(userPreferences, 'username'); + + return userPreferencesWithoutUsername; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to get user preferences'; + + const { logger } = getWazuhCheckUpdatesServices(); + + logger.error(message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-engine/server/services/user-preferences/index.ts b/plugins/wazuh-engine/server/services/user-preferences/index.ts new file mode 100644 index 0000000000..b0011fdc48 --- /dev/null +++ b/plugins/wazuh-engine/server/services/user-preferences/index.ts @@ -0,0 +1,2 @@ +export { updateUserPreferences } from './update-user-preferences'; +export { getUserPreferences } from './get-user-preferences'; diff --git a/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.test.ts b/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.test.ts new file mode 100644 index 0000000000..3797f4d6b9 --- /dev/null +++ b/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.test.ts @@ -0,0 +1,91 @@ +import { updateUserPreferences } from '.'; +import { getSavedObject } from '../saved-object/get-saved-object'; +import { setSavedObject } from '../saved-object/set-saved-object'; +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { + getWazuhCore, + getWazuhCheckUpdatesServices, +} from '../../plugin-services'; + +const mockedGetSavedObject = getSavedObject as jest.Mock; +jest.mock('../saved-object/get-saved-object'); + +const mockedSetSavedObject = setSavedObject as jest.Mock; +jest.mock('../saved-object/set-saved-object'); + +const mockedGetWazuhCore = getWazuhCore as jest.Mock; +const mockedGetWazuhCheckUpdatesServices = + getWazuhCheckUpdatesServices as jest.Mock; +jest.mock('../../plugin-services'); + +describe('updateUserPreferences function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return user preferences', async () => { + mockedGetSavedObject.mockImplementation(() => ({ + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + })); + + mockedSetSavedObject.mockImplementation(() => {}); + mockedGetWazuhCheckUpdatesServices.mockImplementation(() => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + + const response = await updateUserPreferences('admin', { + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + expect(getSavedObject).toHaveBeenCalledWith( + SAVED_OBJECT_USER_PREFERENCES, + 'admin', + ); + + expect(response).toEqual({ + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + }); + }); + + test('should return an error', async () => { + mockedSetSavedObject.mockRejectedValue(new Error('getSavedObject error')); + + const promise = updateUserPreferences('admin', { + last_dismissed_updates: [ + { + api_id: 'api id', + last_patch: '4.3.1', + }, + ], + hide_update_notifications: false, + }); + + expect(getSavedObject).toHaveBeenCalledTimes(1); + + await expect(promise).rejects.toThrow('getSavedObject error'); + }); +}); diff --git a/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.ts b/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.ts new file mode 100644 index 0000000000..a4c50b9992 --- /dev/null +++ b/plugins/wazuh-engine/server/services/user-preferences/update-user-preferences.ts @@ -0,0 +1,39 @@ +import { SAVED_OBJECT_USER_PREFERENCES } from '../../../common/constants'; +import { UserPreferences } from '../../../common/types'; +import { getWazuhCheckUpdatesServices } from '../../plugin-services'; +import { getSavedObject, setSavedObject } from '../saved-object'; + +export const updateUserPreferences = async ( + username: string, + preferences: UserPreferences, +): Promise => { + try { + const userPreferences = + ((await getSavedObject( + SAVED_OBJECT_USER_PREFERENCES, + username, + )) as UserPreferences) || {}; + + const newUserPreferences = { ...userPreferences, ...preferences }; + + await setSavedObject( + SAVED_OBJECT_USER_PREFERENCES, + newUserPreferences, + username, + ); + + return newUserPreferences; + } catch (error) { + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : 'Error trying to update user preferences'; + + const { logger } = getWazuhCheckUpdatesServices(); + + logger.error(message); + return Promise.reject(error); + } +}; diff --git a/plugins/wazuh-engine/server/types.ts b/plugins/wazuh-engine/server/types.ts new file mode 100644 index 0000000000..cae04b2ef8 --- /dev/null +++ b/plugins/wazuh-engine/server/types.ts @@ -0,0 +1,19 @@ +import { + WazuhCorePluginStart, + WazuhCorePluginSetup, +} from '../../wazuh-core/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface AppPluginStartDependencies {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhEnginePluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WazuhEnginePluginStart {} + +export type PluginSetup = { + wazuhCore: WazuhCorePluginSetup; +}; + +export interface AppPluginStartDependencies { + wazuhCore: WazuhCorePluginStart; +} diff --git a/plugins/wazuh-engine/test/jest/config.js b/plugins/wazuh-engine/test/jest/config.js new file mode 100644 index 0000000000..c49cd92aa0 --- /dev/null +++ b/plugins/wazuh-engine/test/jest/config.js @@ -0,0 +1,41 @@ +import path from 'path'; + +const kbnDir = path.resolve(__dirname, '../../../../'); + +export default { + rootDir: path.resolve(__dirname, '../..'), + roots: ['/public', '/server', '/common'], + modulePaths: [`${kbnDir}/node_modules`], + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', './!**/node_modules/**'], + moduleNameMapper: { + '^ui/(.*)': `${kbnDir}/src/ui/public/$1`, + // eslint-disable-next-line max-len + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kbnDir}/src/dev/jest/mocks/file_mock.js`, + '\\.(css|less|scss)$': `${kbnDir}/src/dev/jest/mocks/style_mock.js`, + axios: 'axios/dist/node/axios.cjs', + }, + setupFiles: [ + `${kbnDir}/src/dev/jest/setup/babel_polyfill.js`, + `${kbnDir}/src/dev/jest/setup/enzyme.js`, + ], + collectCoverage: true, + coverageDirectory: './target/test-coverage', + coverageReporters: ['html', 'text-summary', 'json-summary'], + globals: { + 'ts-jest': { + skipBabel: true, + }, + }, + moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'html'], + modulePathIgnorePatterns: ['__fixtures__/', 'target/'], + testMatch: ['**/*.test.{js,ts,tsx}'], + transform: { + '^.+\\.js$': `${kbnDir}/src/dev/jest/babel_transform.js`, + '^.+\\.tsx?$': `${kbnDir}/src/dev/jest/babel_transform.js`, + '^.+\\.html?$': `${kbnDir}/src/dev/jest/babel_transform.js`, + }, + transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.js$'], + snapshotSerializers: [`${kbnDir}/node_modules/enzyme-to-json/serializer`], + testEnvironment: 'jest-environment-jsdom', + reporters: ['default', `${kbnDir}/src/dev/jest/junit_reporter.js`], +}; diff --git a/plugins/wazuh-engine/translations/en-US.json b/plugins/wazuh-engine/translations/en-US.json new file mode 100644 index 0000000000..9022cc65e3 --- /dev/null +++ b/plugins/wazuh-engine/translations/en-US.json @@ -0,0 +1,79 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + }, + "relative": { + "years": { + "units": "year" + }, + "months": { + "units": "month" + }, + "days": { + "units": "day" + }, + "hours": { + "units": "hour" + }, + "minutes": { + "units": "minute" + }, + "seconds": { + "units": "second" + } + } + }, + "messages": {} +} diff --git a/plugins/wazuh-engine/tsconfig.json b/plugins/wazuh-engine/tsconfig.json new file mode 100644 index 0000000000..d3b63f9aee --- /dev/null +++ b/plugins/wazuh-engine/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "common/**/*.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../typings/**/*", + "public/hooks" + ], + "exclude": [] +} \ No newline at end of file diff --git a/plugins/wazuh-engine/yarn.lock b/plugins/wazuh-engine/yarn.lock new file mode 100644 index 0000000000..8b01b7a7bc --- /dev/null +++ b/plugins/wazuh-engine/yarn.lock @@ -0,0 +1,12 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@testing-library/user-event@^14.5.0": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== + +"@types/@testing-library/user-event": + version "0.0.0-semantically-released" + resolved "https://codeload.github.com/testing-library/user-event/tar.gz/d0362796a33c2d39713998f82ae309020c37b385" From 9afcc49ca2256c1e78650b0ffd34ff7d72ea4396 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Fri, 28 Jun 2024 14:07:13 +0200 Subject: [PATCH 02/34] Added simple styles --- .../public/components/decoders/decoders.tsx | 2 +- .../public/components/engine-layout.tsx | 13 +++ .../wazuh-engine/public/components/engine.tsx | 106 +++++++++++------- .../public/components/filters/filters.tsx | 2 +- .../components/integrations/integrations.tsx | 2 +- .../public/components/kvdbs/kvdbs.tsx | 2 +- .../public/components/outputs/outputs.tsx | 2 +- .../public/components/policies/policies.tsx | 2 +- .../public/components/rules/rules.tsx | 2 +- 9 files changed, 85 insertions(+), 48 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/engine-layout.tsx diff --git a/plugins/wazuh-engine/public/components/decoders/decoders.tsx b/plugins/wazuh-engine/public/components/decoders/decoders.tsx index fc40d7f7bd..f9976c4a94 100644 --- a/plugins/wazuh-engine/public/components/decoders/decoders.tsx +++ b/plugins/wazuh-engine/public/components/decoders/decoders.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Decoders = () => { - return
Decoders
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/engine-layout.tsx b/plugins/wazuh-engine/public/components/engine-layout.tsx new file mode 100644 index 0000000000..5b9eacac4d --- /dev/null +++ b/plugins/wazuh-engine/public/components/engine-layout.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { EuiTitle } from '@elastic/eui'; + +export const EngineLayout = ({ children, title }) => { + return ( + <> + +

{title}

+
+
{children}
+ + ); +}; diff --git a/plugins/wazuh-engine/public/components/engine.tsx b/plugins/wazuh-engine/public/components/engine.tsx index 575663a87e..2beed1aa9a 100644 --- a/plugins/wazuh-engine/public/components/engine.tsx +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -1,5 +1,11 @@ import React, { useState } from 'react'; -import { EuiSideNav, EuiPage, EuiPageSideBar, EuiPageBody } from '@elastic/eui'; +import { + EuiSideNav, + EuiPage, + EuiPageSideBar, + EuiPageBody, + EuiPanel, +} from '@elastic/eui'; import { Route, Switch, Redirect } from 'react-router-dom'; import { Decoders } from './decoders'; import { Filters } from './filters'; @@ -8,6 +14,45 @@ import { Rules } from './rules'; import { Integrations } from './integrations'; import { KVDBs } from './kvdbs'; import { Policies } from './policies'; +import { EngineLayout } from './engine-layout'; + +const views = [ + { + name: 'Decoders', + id: 'decoders', + render: Decoders, + }, + { + name: 'Rules', + id: 'rules', + render: Rules, + }, + { + name: 'Outputs', + id: 'outputs', + render: Outputs, + }, + { + name: 'Filters', + id: 'filters', + render: Filters, + }, + { + name: 'Integrations', + id: 'integrations', + render: Integrations, + }, + { + name: 'Policies', + id: 'policies', + render: Policies, + }, + { + name: 'KVDBs', + id: 'kvdbs', + render: KVDBs, + }, +]; export const Engine = props => { const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); @@ -19,36 +64,7 @@ export const Engine = props => { { name: 'Engine', id: 'engine', - items: [ - { - name: 'Decoders', - id: 'decoders', - }, - { - name: 'Rules', - id: 'rules', - }, - { - name: 'Outputs', - id: 'outputs', - }, - { - name: 'Filters', - id: 'filters', - }, - { - name: 'Integrations', - id: 'integrations', - }, - { - name: 'Policies', - id: 'policies', - }, - { - name: 'KVDBs', - id: 'kvdbs', - }, - ].map(item => ({ + items: views.map(({ render, ...item }) => ({ ...item, onClick: () => { props.navigationService.getInstance().navigate(`/engine/${item.id}`); @@ -72,16 +88,24 @@ export const Engine = props => { /> - - - - - - - - - - + + + {views.map(item => ( + { + return ( + + {item.render()} + + ); + }} + /> + ))} + + + ); diff --git a/plugins/wazuh-engine/public/components/filters/filters.tsx b/plugins/wazuh-engine/public/components/filters/filters.tsx index 9f33a383c9..1249251462 100644 --- a/plugins/wazuh-engine/public/components/filters/filters.tsx +++ b/plugins/wazuh-engine/public/components/filters/filters.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Filters = () => { - return
Filters
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/integrations/integrations.tsx b/plugins/wazuh-engine/public/components/integrations/integrations.tsx index 6d79af46e9..3fc80d507f 100644 --- a/plugins/wazuh-engine/public/components/integrations/integrations.tsx +++ b/plugins/wazuh-engine/public/components/integrations/integrations.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Integrations = () => { - return
Integrations
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx index 0eb897740f..4f0b279895 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const KVDBs = () => { - return
KVDBs
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/outputs/outputs.tsx b/plugins/wazuh-engine/public/components/outputs/outputs.tsx index 3fc9102f8a..b1db559340 100644 --- a/plugins/wazuh-engine/public/components/outputs/outputs.tsx +++ b/plugins/wazuh-engine/public/components/outputs/outputs.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Outputs = () => { - return
Outputs
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/policies/policies.tsx b/plugins/wazuh-engine/public/components/policies/policies.tsx index 180789b230..f9e529144a 100644 --- a/plugins/wazuh-engine/public/components/policies/policies.tsx +++ b/plugins/wazuh-engine/public/components/policies/policies.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Policies = () => { - return
Policies
; + return <>Hi; }; diff --git a/plugins/wazuh-engine/public/components/rules/rules.tsx b/plugins/wazuh-engine/public/components/rules/rules.tsx index c7b6a09484..57ab2a94d2 100644 --- a/plugins/wazuh-engine/public/components/rules/rules.tsx +++ b/plugins/wazuh-engine/public/components/rules/rules.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Rules = () => { - return
Rules
; + return <>Hi; }; From a2835922cf4bb5e7fdbc470ce7b8b7114a334293 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Wed, 3 Jul 2024 09:39:01 +0200 Subject: [PATCH 03/34] Mocked imposter --- docker/imposter/lists/get_lists.json | 25 +++++++++++++++++++++++++ docker/imposter/wazuh-config.yml | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 docker/imposter/lists/get_lists.json diff --git a/docker/imposter/lists/get_lists.json b/docker/imposter/lists/get_lists.json new file mode 100644 index 0000000000..1b6f7e0c0a --- /dev/null +++ b/docker/imposter/lists/get_lists.json @@ -0,0 +1,25 @@ +{ + "data": { + "affected_items": [ + { + "filename": "test1", + "relative_dirname": "test/1", + "elements": "8", + "date": "2024-06-03", + "description": "test mock" + }, + { + "filename": "test2", + "relative_dirname": "test/2", + "elements": "76", + "date": "2024-06-02", + "description": "test 2 mock" + } + ], + "total_affected_items": 2, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified lists were returned", + "error": 0 +} diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index 67a56da9ce..a29d132aae 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -406,6 +406,9 @@ resources: # Get CDB lists info - method: GET path: /lists + response: + statusCode: 200 + staticFile: lists/get_lists.json # Get CDB list file content - method: GET From 82082a830f41b369531ac3a71f957b7e9a826286 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Mon, 8 Jul 2024 08:42:58 +0200 Subject: [PATCH 04/34] added kvdb --- plugins/main/public/app-router.tsx | 27 +++- .../wazuh-engine/public/components/engine.tsx | 9 +- .../kvdbs/components/keys/key-info.tsx | 35 +++++ .../kvdbs/components/keys/keys-columns.tsx | 14 ++ .../kvdbs/components/kvdb-columns.tsx | 32 +++++ .../kvdbs/components/kvdb-overview.tsx | 120 +++++++++++++++++ .../public/components/kvdbs/kvdbs.tsx | 13 +- .../public/controllers/resources-handler.ts | 125 ++++++++++++++++++ plugins/wazuh-engine/public/services/index.ts | 3 + 9 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/keys/keys-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx create mode 100644 plugins/wazuh-engine/public/controllers/resources-handler.ts diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 48ffc31f0d..9b98033801 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -9,7 +9,7 @@ import { import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { useDispatch } from 'react-redux'; import { checkPluginVersion } from './utils'; -import { WzAuthentication, loadAppConfig } from './react-services'; +import { WzAuthentication, WzRequest, loadAppConfig } from './react-services'; import { WzMenuWrapper } from './components/wz-menu/wz-menu-wrapper'; import { WzAgentSelectorWrapper } from './components/wz-agent-selector/wz-agent-selector-wrapper'; import { ToastNotificationsModal } from './components/notifications/modal'; @@ -25,7 +25,14 @@ import { Settings } from './components/settings'; import { WzSecurity } from './components/security'; import $ from 'jquery'; import NavigationService from './react-services/navigation-service'; - +import { TableWzAPI } from './components/common/tables'; +import { + AddNewCdbListButton, + AddNewFileButton, + ManageFiles, + UploadFilesButton, +} from './controllers/management/components/management/common/actions-buttons'; +import WzListEditor from './controllers/management/components/management/cdblists/views/list-editor.tsx'; export function Application(props) { const dispatch = useDispatch(); const navigationService = NavigationService.getInstance(); @@ -104,7 +111,21 @@ export function Application(props) { path={'/engine'} render={props => { const { Engine } = getWazuhEnginePlugin(); - return ; + return ( + + ); }} > diff --git a/plugins/wazuh-engine/public/components/engine.tsx b/plugins/wazuh-engine/public/components/engine.tsx index 2beed1aa9a..651d38b8dc 100644 --- a/plugins/wazuh-engine/public/components/engine.tsx +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -15,6 +15,7 @@ import { Integrations } from './integrations'; import { KVDBs } from './kvdbs'; import { Policies } from './policies'; import { EngineLayout } from './engine-layout'; +import { getServices, setServices } from '../services'; const views = [ { @@ -60,6 +61,12 @@ export const Engine = props => { setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); }; + try { + !getServices(); + } catch (error) { + setServices(props); + } + const sideNav = [ { name: 'Engine', @@ -97,7 +104,7 @@ export const Engine = props => { render={() => { return ( - {item.render()} + {item.render(props)} ); }} diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx new file mode 100644 index 0000000000..76a74a0622 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutHeader, + EuiFlyoutBody, +} from '@elastic/eui'; +import { getServices } from '../../../../services'; + +export const KeyInfo = ({ keys, setKeysRequest }) => { + const WzListEditor = getServices().WzListEditor; + + return ( + <> + + + View keys of this Database + + + + + { + setKeysRequest(false); + }} + updateListContent={keys => { + setKeysRequest(keys); + }} + > + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/keys/keys-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/keys/keys-columns.tsx new file mode 100644 index 0000000000..236a583e75 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/keys/keys-columns.tsx @@ -0,0 +1,14 @@ +export const columns = [ + { + field: 'key', + name: 'Key', + align: 'left', + sortable: true, + }, + { + field: 'value', + name: 'Value', + align: 'left', + sortable: true, + }, +]; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx new file mode 100644 index 0000000000..f53a91b30c --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx @@ -0,0 +1,32 @@ +export const columns = [ + { + field: 'date', + name: 'Date', + align: 'left', + sortable: true, + }, + { + field: 'filename', + name: 'Name', + align: 'left', + sortable: true, + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + field: 'elements', + name: 'Elements', + align: 'left', + sortable: true, + }, +]; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx new file mode 100644 index 0000000000..164919c7ea --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { columns } from './kvdb-columns'; +import { ResourcesHandler } from '../../../controllers/resources-handler'; +import { EuiFlyout } from '@elastic/eui'; +import { KeyInfo } from './keys/key-info'; +import { getServices } from '../../../services'; + +export const KVDBTable = ({ TableWzAPI }) => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [getKeysRequest, setKeysRequest] = useState(false); + const resourcesHandler = new ResourcesHandler('lists'); + const WzRequest = getServices().WzRequest; + const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], + }; + + /** + * Columns and Rows properties + */ + const getRowProps = item => { + const { id, name } = item; + + return { + 'data-test-subj': `row-${id || name}`, + className: 'customRowClass', + onClick: async ev => { + const result = await resourcesHandler.getFileContent( + item.filename, + item.relative_dirname, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + setKeysRequest(file); + setIsFlyoutVisible(true); + }, + }; + }; + + const closeFlyout = () => setIsFlyoutVisible(false); + + const ManageFiles = getServices().actionButtons.manageFiles; + const AddNewFileButton = getServices().actionButtons.addNewFileButton; + const AddNewCdbListButton = getServices().actionButtons.addNewCdbListButton; + const UploadFilesButton = getServices().actionButtons.uploadFilesButton; + const actionButtons = [ + , + , + , + , + ]; + + return ( + <> + { + try { + const response = await WzRequest.apiReq('GET', '/lists', { + params: { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + searchTable + endpoint={'/lists'} + isExpandable={true} + downloadCsv + showReload + rowProps={getRowProps} + tablePageSizeOptions={[10, 25, 50, 100]} + /> + {isFlyoutVisible && ( + + + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx index 4f0b279895..90dfb3a804 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx @@ -1,5 +1,16 @@ import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPage } from '@elastic/eui'; +import { KVDBTable } from './components/kvdb-overview'; +import { getServices } from '../../services'; export const KVDBs = () => { - return <>Hi; + return ( + + + + + + + + ); }; diff --git a/plugins/wazuh-engine/public/controllers/resources-handler.ts b/plugins/wazuh-engine/public/controllers/resources-handler.ts new file mode 100644 index 0000000000..4cb322c95f --- /dev/null +++ b/plugins/wazuh-engine/public/controllers/resources-handler.ts @@ -0,0 +1,125 @@ +import { getServices } from '../services'; + +type LISTS = 'lists'; +export type Resource = LISTS; +export const ResourcesConstants = { + LISTS: 'lists', +}; + +export const resourceDictionary = { + [ResourcesConstants.LISTS]: { + resourcePath: '/lists', + permissionResource: value => `list:file:${value}`, + }, +}; + +export class ResourcesHandler { + resource: Resource; + WzRequest: any; + constructor(_resource: Resource) { + this.resource = _resource; + this.WzRequest = getServices().WzRequest; + } + + private getResourcePath = () => { + return `${resourceDictionary[this.resource].resourcePath}`; + }; + + private getResourceFilesPath = (fileName?: string) => { + const basePath = `${this.getResourcePath()}/files`; + return `${basePath}${fileName ? `/${fileName}` : ''}`; + }; + + /** + * Get info of any type of resource KVDB lists... + */ + async getResource(filters = {}) { + try { + const result: any = await this.WzRequest.apiReq( + 'GET', + this.getResourcePath(), + filters, + ); + return (result || {}).data || false; + } catch (error) { + throw error; + } + } + + /** + * Get the content of any type of file KVDB lists... + * @param {String} fileName + */ + async getFileContent(fileName, relativeDirname) { + try { + const result: any = await this.WzRequest.apiReq( + 'GET', + this.getResourceFilesPath(fileName), + { + params: { + raw: true, + relative_dirname: relativeDirname, + }, + }, + ); + return (result || {}).data || ''; + } catch (error) { + throw error; + } + } + + /** + * Update the content of any type of file KVDB lists... + * @param {String} fileName + * @param {String} content + * @param {Boolean} overwrite + */ + async updateFile( + fileName: string, + content: string, + overwrite: boolean, + relativeDirname?: string, + ) { + try { + const result = await this.WzRequest.apiReq( + 'PUT', + this.getResourceFilesPath(fileName), + { + params: { + overwrite: overwrite, + ...(this.resource !== 'lists' + ? { relative_dirname: relativeDirname } + : {}), + }, + body: content.toString(), + origin: 'raw', + }, + ); + return result; + } catch (error) { + throw error; + } + } + + /** + * Delete any type of file KVDB lists... + * @param {Resource} resource + * @param {String} fileName + */ + async deleteFile(fileName: string, relativeDirname?: string) { + try { + const result = await this.WzRequest.apiReq( + 'DELETE', + this.getResourceFilesPath(fileName), + { + params: { + relative_dirname: relativeDirname, + }, + }, + ); + return result; + } catch (error) { + throw error; + } + } +} diff --git a/plugins/wazuh-engine/public/services/index.ts b/plugins/wazuh-engine/public/services/index.ts index e69de29bb2..5cd86c2c2b 100644 --- a/plugins/wazuh-engine/public/services/index.ts +++ b/plugins/wazuh-engine/public/services/index.ts @@ -0,0 +1,3 @@ +import { createGetterSetter } from '../../../../src/plugins/opensearch_dashboards_utils/common'; + +export const [getServices, setServices] = createGetterSetter(); From 8e756f2eb3a451f0c0df87f9c2c72bc75234ec99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 9 Jul 2024 10:31:04 +0200 Subject: [PATCH 05/34] feat(rules): create Rules section on Engine app - List rules - Rule detail on flyout with tabs: - Table - JSON - Relationship (visualization) - Events - Import file - Create new rule - Visual editor (form) - File editor - Adapt components from main to support the required use cases - Add plugin dependencies to engine plugin - Enhance DocViewer component - Create component to fetch and display data from indexer --- plugins/main/public/app-router.tsx | 50 ++- .../common/data-grid/data-grid-service.ts | 60 ++-- .../pattern/rules/data-source-repository.ts | 45 +++ .../data-source/pattern/rules/data-source.ts | 33 ++ .../common/data-source/pattern/rules/index.ts | 2 + .../common/doc-viewer/doc-viewer.scss | 25 ++ .../common/doc-viewer/doc-viewer.tsx | 39 ++- .../doc-viewer/table_row_btn_filter_add.tsx | 75 +++++ .../table_row_btn_filter_exists.tsx | 84 +++++ .../table_row_btn_filter_remove.tsx | 75 +++++ .../table_row_btn_toggle_column.tsx | 89 ++++++ .../common/search-bar/search-bar-service.ts | 1 + .../common/tables/table-data-basic.tsx | 236 ++++++++++++++ .../components/common/tables/table-data.tsx | 292 ++++++++++++++++++ .../common/tables/table-indexer.tsx | 244 +++++++++++++++ .../document-view-table-and-json.tsx | 35 ++- .../wazuh-engine/opensearch_dashboards.json | 8 +- .../public/components/engine-layout.tsx | 17 +- .../wazuh-engine/public/components/engine.tsx | 6 +- .../rules/components/file-editor.tsx | 49 +++ .../components/rules/components/form.tsx | 24 ++ .../components/rules/components/index.ts | 1 + .../components/rules/components/layout.tsx | 48 +++ .../public/components/rules/index.ts | 2 +- .../public/components/rules/pages/list.tsx | 258 ++++++++++++++++ .../public/components/rules/pages/new.tsx | 61 ++++ .../public/components/rules/router.tsx | 18 ++ .../public/components/rules/rules.tsx | 5 - .../public/components/rules/spec.json | 74 +++++ .../public/components/rules/visualization.ts | 54 ++++ 30 files changed, 1976 insertions(+), 34 deletions(-) create mode 100644 plugins/main/public/components/common/data-source/pattern/rules/data-source-repository.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/rules/data-source.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/rules/index.ts create mode 100644 plugins/main/public/components/common/doc-viewer/doc-viewer.scss create mode 100644 plugins/main/public/components/common/doc-viewer/table_row_btn_filter_add.tsx create mode 100644 plugins/main/public/components/common/doc-viewer/table_row_btn_filter_exists.tsx create mode 100644 plugins/main/public/components/common/doc-viewer/table_row_btn_filter_remove.tsx create mode 100644 plugins/main/public/components/common/doc-viewer/table_row_btn_toggle_column.tsx create mode 100644 plugins/main/public/components/common/tables/table-data-basic.tsx create mode 100644 plugins/main/public/components/common/tables/table-data.tsx create mode 100644 plugins/main/public/components/common/tables/table-indexer.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/components/file-editor.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/components/form.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/components/index.ts create mode 100644 plugins/wazuh-engine/public/components/rules/components/layout.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/pages/list.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/pages/new.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/router.tsx delete mode 100644 plugins/wazuh-engine/public/components/rules/rules.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/spec.json create mode 100644 plugins/wazuh-engine/public/components/rules/visualization.ts diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 48ffc31f0d..77f7d1de0d 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import { Router, Route, Switch, Redirect } from 'react-router-dom'; import { ToolsRouter } from './components/tools/tools-router'; import { + getPlugins, getWazuhCorePlugin, getWazuhEnginePlugin, getWzMainParams, @@ -9,7 +10,12 @@ import { import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { useDispatch } from 'react-redux'; import { checkPluginVersion } from './utils'; -import { WzAuthentication, loadAppConfig } from './react-services'; +import { + AppState, + GenericRequest, + WzAuthentication, + loadAppConfig, +} from './react-services'; import { WzMenuWrapper } from './components/wz-menu/wz-menu-wrapper'; import { WzAgentSelectorWrapper } from './components/wz-agent-selector/wz-agent-selector-wrapper'; import { ToastNotificationsModal } from './components/notifications/modal'; @@ -25,6 +31,22 @@ import { Settings } from './components/settings'; import { WzSecurity } from './components/security'; import $ from 'jquery'; import NavigationService from './react-services/navigation-service'; +import { TableIndexer } from './components/common/tables/table-indexer'; +import { + RulesDataSource, + RulesDataSourceRepository, +} from './components/common/data-source/pattern/rules'; +import { useDocViewer } from './components/common/doc-viewer'; +import DocViewer from './components/common/doc-viewer/doc-viewer'; +import { WazuhFlyoutDiscover } from './components/common/wazuh-discover/wz-flyout-discover'; +import { + FILTER_OPERATOR, + PatternDataSource, + PatternDataSourceFilterManager, +} from './components/common/data-source'; +import { DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER } from '../common/constants'; +import { useForm } from './components/common/form/hooks'; +import { InputForm } from './components/common/form'; export function Application(props) { const dispatch = useDispatch(); @@ -104,7 +126,31 @@ export function Application(props) { path={'/engine'} render={props => { const { Engine } = getWazuhEnginePlugin(); - return ; + return ( + + ); }} > diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index 71a2f494a5..b50b69bafa 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -7,8 +7,10 @@ export const MAX_ENTRIES_PER_QUERY = 10000; import { EuiDataGridColumn } from '@elastic/eui'; import { tDataGridColumn } from './use-data-grid'; -export const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => { - const data = resultsHits.map((hit) => { +export const parseData = ( + resultsHits: SearchResponse['hits']['hits'], +): any[] => { + const data = resultsHits.map(hit => { if (!hit) { return {}; } @@ -25,15 +27,20 @@ export const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => return data; }; -export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) => { - const field = indexPattern.fields.find((field) => field.name === columnId); +export const getFieldFormatted = ( + rowIndex, + columnId, + indexPattern, + rowsParsed, +) => { + const field = indexPattern.fields.find(field => field.name === columnId); let fieldValue = null; if (columnId.includes('.')) { // when the column is a nested field. The column could have 2 to n levels // get dinamically the value of the nested field const nestedFields = columnId.split('.'); fieldValue = rowsParsed[rowIndex]; - nestedFields.forEach((field) => { + nestedFields.forEach(field => { if (fieldValue) { fieldValue = fieldValue[field]; } @@ -66,14 +73,26 @@ export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) }; // receive search params -export const exportSearchToCSV = async (params: SearchParams): Promise => { +export const exportSearchToCSV = async ( + params: SearchParams, +): Promise => { const DEFAULT_MAX_SIZE_PER_CALL = 1000; - const { indexPattern, filters = [], query, sorting, fields, pagination } = params; + const { + indexPattern, + filters = [], + query, + sorting, + fields, + pagination, + filePrefix = 'events', + } = params; // when the pageSize is greater than the default max size per call (10000) // then we need to paginate the search const mustPaginateSearch = pagination?.pageSize && pagination?.pageSize > DEFAULT_MAX_SIZE_PER_CALL; - const pageSize = mustPaginateSearch ? DEFAULT_MAX_SIZE_PER_CALL : pagination?.pageSize; + const pageSize = mustPaginateSearch + ? DEFAULT_MAX_SIZE_PER_CALL + : pagination?.pageSize; const totalHits = pagination?.pageSize || DEFAULT_MAX_SIZE_PER_CALL; let pageIndex = params.pagination?.pageIndex || 0; let hitsCount = 0; @@ -104,13 +123,13 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => } const resultsFields = fields; - const data = allHits.map((hit) => { + const data = allHits.map(hit => { // check if the field type is a date const dateFields = indexPattern.fields.getByType('date'); - const dateFieldsNames = dateFields.map((field) => field.name); + const dateFieldsNames = dateFields.map(field => field.name); const flattenHit = indexPattern.flattenHit(hit); // replace the date fields with the formatted date - dateFieldsNames.forEach((field) => { + dateFieldsNames.forEach(field => { if (flattenHit[field]) { flattenHit[field] = beautifyDate(flattenHit[field]); } @@ -125,8 +144,8 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => if (!data || data.length === 0) return; const parsedData = data - .map((row) => { - const parsedRow = resultsFields?.map((field) => { + .map(row => { + const parsedRow = resultsFields?.map(field => { const value = row[field]; if (value === undefined || value === null) { return ''; @@ -147,20 +166,25 @@ export const exportSearchToCSV = async (params: SearchParams): Promise => if (blobData) { // @ts-ignore - FileSaver?.saveAs(blobData, `events-${new Date().toISOString()}.csv`); + FileSaver?.saveAs( + blobData, + `${filePrefix}-${new Date().toISOString()}.csv`, + ); } }; export const parseColumns = ( fields: IFieldType[], - defaultColumns: tDataGridColumn[] = [] + defaultColumns: tDataGridColumn[] = [], ): EuiDataGridColumn[] => { // remove _source field becuase is a object field and is not supported - fields = fields.filter((field) => field.name !== '_source'); + fields = fields.filter(field => field.name !== '_source'); // merge the properties of the field with the default columns const columns = - fields.map((field) => { - const defaultColumn = defaultColumns.find((column) => column.id === field.name); + fields.map(field => { + const defaultColumn = defaultColumns.find( + column => column.id === field.name, + ); return { ...field, id: field.name, diff --git a/plugins/main/public/components/common/data-source/pattern/rules/data-source-repository.ts b/plugins/main/public/components/common/data-source/pattern/rules/data-source-repository.ts new file mode 100644 index 0000000000..d87c41e7f1 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/rules/data-source-repository.ts @@ -0,0 +1,45 @@ +import { PatternDataSourceRepository } from '../pattern-data-source-repository'; +import { tParsedIndexPattern } from '../../index'; + +export class RulesDataSourceRepository extends PatternDataSourceRepository { + constructor() { + super(); + } + + async get(id: string) { + const dataSource = await super.get(id); + if (this.validate(dataSource)) { + return dataSource; + } else { + throw new Error('Rules index pattern not found'); + } + } + + async getAll() { + const indexs = await super.getAll(); + return indexs.filter(this.validate); + } + + validate(dataSource): boolean { + // check if the dataSource has the id or the title have the vulnerabilities word + const fieldsToCheck = ['id', 'attributes.title']; + // must check in the object and the attributes + for (const field of fieldsToCheck) { + if ( + dataSource[field] && + dataSource[field].toLowerCase().includes('rules') + ) { + return true; + } + } + return false; + } + + getDefault() { + return Promise.resolve(null); + } + + setDefault(dataSource: tParsedIndexPattern) { + return; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/rules/data-source.ts b/plugins/main/public/components/common/data-source/pattern/rules/data-source.ts new file mode 100644 index 0000000000..150df4c88d --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/rules/data-source.ts @@ -0,0 +1,33 @@ +import { + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, +} from '../../../../../../common/constants'; +import { tFilter, PatternDataSourceFilterManager } from '../../index'; +import { PatternDataSource } from '../pattern-data-source'; + +class CustomPatternDataSource extends PatternDataSource { + constructor(id: string, title: string) { + super(id, title); + } + getFetchFilters(): Filter[] { + return []; + } +} + +export class RulesDataSource extends CustomPatternDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getFixedFilters(): tFilter[] { + return [...this.getClusterManagerFilters()]; + } + + getClusterManagerFilters() { + return PatternDataSourceFilterManager.getClusterManagerFilters( + this.id, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, + ); + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/rules/index.ts b/plugins/main/public/components/common/data-source/pattern/rules/index.ts new file mode 100644 index 0000000000..cae336db28 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/rules/index.ts @@ -0,0 +1,2 @@ +export * from './data-source'; +export * from './data-source-repository'; diff --git a/plugins/main/public/components/common/doc-viewer/doc-viewer.scss b/plugins/main/public/components/common/doc-viewer/doc-viewer.scss new file mode 100644 index 0000000000..603a7964ef --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.scss @@ -0,0 +1,25 @@ +.osdDocViewerTable tr:hover { + .osdDocViewer__actionButton { + opacity: 1; + } +} + +.osdDocViewer__buttons { + // width: 60px; + + // Show all icons if one is focused, + // IE doesn't support, but the fallback is just the focused button becomes visible + &:focus-within { + .osdDocViewer__actionButton { + opacity: 1; + } + } +} + +.osdDocViewer__actionButton { + opacity: 0; + + &:focus { + opacity: 1; + } +} diff --git a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx index 6cd7cb1384..b18583a4d2 100644 --- a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx @@ -4,6 +4,11 @@ import { escapeRegExp } from 'lodash'; import { i18n } from '@osd/i18n'; import { FieldIcon } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { DocViewTableRowBtnFilterAdd } from './table_row_btn_filter_add'; +import { DocViewTableRowBtnFilterRemove } from './table_row_btn_filter_remove'; +import { DocViewTableRowBtnToggleColumn } from './table_row_btn_toggle_column'; +import { DocViewTableRowBtnFilterExists } from './table_row_btn_filter_exists'; +import './doc-viewer.scss'; const COLLAPSE_LINE_LENGTH = 350; const DOT_PREFIX_RE = /(.).+?\./g; @@ -80,7 +85,15 @@ const DocViewer = (props: tDocViewerProps) => { const [fieldRowOpen, setFieldRowOpen] = useState( {} as Record, ); - const { flattened, formatted, mapping, indexPattern } = props; + const { + flattened, + formatted, + mapping, + indexPattern, + onFilter, + onToggleColumn, + isColumnActive, + } = props; return ( <> @@ -90,6 +103,7 @@ const DocViewer = (props: tDocViewerProps) => { {Object.keys(flattened) .sort() .map((field, index) => { + const valueRaw = flattened[field]; const value = String(formatted[field]); const fieldMapping = mapping(field); const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; @@ -120,6 +134,29 @@ const DocViewer = (props: tDocViewerProps) => { return ( + {typeof onFilter === 'function' && ( + + onFilter(fieldMapping, valueRaw, '+')} + /> + onFilter(fieldMapping, valueRaw, '-')} + /> + {typeof onToggleColumn === 'function' && ( + + )} + onFilter('_exists_', field, '+')} + scripted={fieldMapping && fieldMapping.scripted} + /> + + )} void; + disabled: boolean; +} + +export function DocViewTableRowBtnFilterAdd({ + onClick, + disabled = false, +}: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_exists.tsx b/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_exists.tsx new file mode 100644 index 0000000000..6cb8ba6fd3 --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_exists.tsx @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; + scripted?: boolean; +} + +export function DocViewTableRowBtnFilterExists({ + onClick, + disabled = false, + scripted = false, +}: Props) { + const tooltipContent = disabled ? ( + scripted ? ( + + ) : ( + + ) + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_remove.tsx b/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_remove.tsx new file mode 100644 index 0000000000..ba1fa141c8 --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table_row_btn_filter_remove.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; +} + +export function DocViewTableRowBtnFilterRemove({ + onClick, + disabled = false, +}: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/doc-viewer/table_row_btn_toggle_column.tsx b/plugins/main/public/components/common/doc-viewer/table_row_btn_toggle_column.tsx new file mode 100644 index 0000000000..eac734dd35 --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table_row_btn_toggle_column.tsx @@ -0,0 +1,89 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + active: boolean; + disabled?: boolean; + onClick: () => void; +} + +export function DocViewTableRowBtnToggleColumn({ + onClick, + active, + disabled = false, +}: Props) { + if (disabled) { + return ( + + ); + } + return ( + + } + > + + + ); +} diff --git a/plugins/main/public/components/common/search-bar/search-bar-service.ts b/plugins/main/public/components/common/search-bar/search-bar-service.ts index 40227d533b..667d278560 100644 --- a/plugins/main/public/components/common/search-bar/search-bar-service.ts +++ b/plugins/main/public/components/common/search-bar/search-bar-service.ts @@ -9,6 +9,7 @@ import dateMath from '@elastic/datemath'; export type SearchParams = { indexPattern: IndexPattern; + filePrefix: string; } & tSearchParams; import { parse } from 'query-string'; diff --git a/plugins/main/public/components/common/tables/table-data-basic.tsx b/plugins/main/public/components/common/tables/table-data-basic.tsx new file mode 100644 index 0000000000..c28d08149f --- /dev/null +++ b/plugins/main/public/components/common/tables/table-data-basic.tsx @@ -0,0 +1,236 @@ +/* + * 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, { useState, useEffect, useRef, useMemo } from 'react'; +import { EuiBasicTable, EuiBasicTableProps, EuiSpacer } from '@elastic/eui'; +import _ from 'lodash'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; +import { UI_LOGGER_LEVELS } from '../../../../common/constants'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { SearchBar, SearchBarProps } from '../../search-bar'; + +export interface ITableWithSearcHBarProps { + /** + * Function to fetch the data + */ + onSearch: ( + { + // pagination: { pageIndex: number; pageSize: number }, + // sorting: { sort: { field: string; direction: string } }, + }, + ) => Promise<{ items: any[]; totalItems: number }>; + /** + * Properties for the search bar + */ + searchBarProps?: Omit< + SearchBarProps, + 'defaultMode' | 'modes' | 'onSearch' | 'input' + >; + /** + * 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; + /** + * API endpoint + */ + endpoint: string; + /** + * Search bar properties for WQL + */ + searchBarWQL?: any; + /** + * Visible fields + */ + selectedFields: string[]; + /** + * API request searchParams + */ + searchParams?: any; +} + +export function TableDataBasic({ + onSearch, + tableColumns, + rowProps, + tablePageSizeOptions = [15, 25, 50, 100], + tableInitialSortingDirection = 'asc', + tableInitialSortingField = '', + tableProps = {}, + reload, + ...rest +}: ITableWithSearcHBarProps) { + const [loading, setLoading] = useState(false); + const [items, setItems] = useState([]); + const [totalItems, setTotalItems] = useState(0); + const [searchParams, setParams] = useState(rest.searchParams || {}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: tablePageSizeOptions[0], + }); + const [sorting, setSorting] = useState({ + sort: { + field: tableInitialSortingField, + direction: tableInitialSortingDirection, + }, + }); + const [refresh, setRefresh] = useState(Date.now()); + + const isMounted = useRef(false); + const tableRef = useRef(); + + 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 onSearch function). + if (isMounted.current) { + // Reset the page index when the reload changes. + // This will cause that onSearch function is triggered because to changes in pagination in the another effect. + updateRefresh(); + } + }, [reload]); + + useEffect( + function () { + (async () => { + try { + setLoading(true); + + //Reset the table selection in case is enabled + tableRef.current.setSelection([]); + + const { items, totalItems } = await onSearch({ + searchParams, + pagination, + sorting, + }); + setItems(items); + setTotalItems(totalItems); + } catch (error) { + setItems([]); + setTotalItems(0); + const options = { + context: `${TableDataBasic.name}.useEffect`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `${error.name}: Error fetching items`, + }, + }; + getErrorOrchestrator().handleError(options); + } + setLoading(false); + })(); + }, + [searchParams, 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 onSearch function). + if (isMounted.current && !_.isEqual(rest.searchParams, searchParams)) { + setParams(rest.searchParams || {}); + updateRefresh(); + } + }, [rest.searchParams]); + + // 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, + }; + return ( + <> + ({ ...rest }), + )} + items={items} + loading={loading} + pagination={tablePagination} + sorting={sorting} + onChange={tableOnChange} + rowProps={rowProps} + {...tableProps} + /> + + ); +} diff --git a/plugins/main/public/components/common/tables/table-data.tsx b/plugins/main/public/components/common/tables/table-data.tsx new file mode 100644 index 0000000000..2f7663d682 --- /dev/null +++ b/plugins/main/public/components/common/tables/table-data.tsx @@ -0,0 +1,292 @@ +/* + * 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, { ReactNode, useEffect, useState } from 'react'; +import { + EuiTitle, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButtonEmpty, + EuiToolTip, + EuiIcon, + EuiCheckboxGroup, +} from '@elastic/eui'; +import { useStateStorage } from '../hooks'; +import { TableDataBasic } from './table-data-basic'; + +/** + * Search input custom filter button + */ +interface CustomFilterButton { + label: string; + field: string; + value: string; +} + +const getFilters = searchParams => { + return searchParams; + // API could needs to extract the current filters from the default + const { default: defaultFilters, ...restFilters } = filters; + return Object.keys(restFilters).length ? restFilters : defaultFilters; +}; + +const getColumMetaField = item => item.field || item.name; + +export function TableData({ + actionButtons, + postActionButtons, + addOnTitle, + setReload, + fetchData, + ...rest +}: { + actionButtons?: + | ReactNode + | ReactNode[] + | (({ filters }: { filters }) => ReactNode); + postActionButtons?: + | ReactNode + | ReactNode[] + | (({ filters }: { filters }) => ReactNode); + + title?: string; + addOnTitle?: ReactNode; + description?: string; + preTable?: ReactNode; + postTable?: ReactNode; + downloadCsv?: boolean | string; + searchTable?: boolean; + endpoint: string; + buttonOptions?: CustomFilterButton[]; + onFiltersChange?: Function; + showReload?: boolean; + searchBarProps?: any; + reload?: boolean; + onDataChange?: Function; + fetchData: ({ + pagination, + sorting, + searchParams, + }) => Promise<{ items: any[]; totalItems: number }>; + setReload?: (newValue: number) => void; +}) { + const [totalItems, setTotalItems] = useState(0); + const [searchContext, setSearchParams] = useState({}); + const [isLoading, setIsLoading] = useState(false); + + const onFiltersChange = searchContext => + typeof rest.onFiltersChange === 'function' + ? rest.onFiltersChange(searchContext) + : null; + + const onDataChange = data => + typeof rest.onDataChange === 'function' ? rest.onDataChange(data) : null; + + /** + * Changing the reloadFootprint timestamp will trigger reloading the table + */ + const [reloadFootprint, setReloadFootprint] = useState(rest.reload || 0); + + 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 onSearch = async ({ pagination, sorting, searchParams }) => { + try { + const searchContext = { pagination, sorting, searchParams }; + setIsLoading(true); + setSearchParams(searchContext); + onFiltersChange(searchContext); + + const { items, totalItems } = await fetchData(searchContext); + + setIsLoading(false); + setTotalItems(totalItems); + + const result = { + items: rest.mapResponseItem ? items.map(rest.mapResponseItem) : items, + totalItems, + }; + + onDataChange(result); + + return 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({ ...searchContext, totalItems, tableColumns }); + } + }; + + /** + * Generate a new reload footprint and set reload to propagate refresh + */ + const triggerReload = () => { + setReloadFootprint(Date.now()); + if (setReload) { + setReload(Date.now()); + } + }; + + useEffect(() => { + if (rest.reload) triggerReload(); + }, [rest.reload]); + + const ReloadButton = ( + + triggerReload()}> + Refresh + + + ); + + const header = ( + <> + + + + + {rest.title && ( + +

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

+
+ )} +
+ {addOnTitle ? ( + + {addOnTitle} + + ) : null} +
+
+ + + {/* Render optional custom action button */} + {renderActionButtons(actionButtons)} + {/* Render optional reload button */} + {rest.showReload && 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={{}} + /> + + + )} + + ); + + return ( + + {header} + {rest.description && ( + + {rest.description} + + )} + {rest.preTable && {rest.preTable}} + + + + {rest.postTable && {rest.postTable}} + + ); +} diff --git a/plugins/main/public/components/common/tables/table-indexer.tsx b/plugins/main/public/components/common/tables/table-indexer.tsx new file mode 100644 index 0000000000..9a8144a5f4 --- /dev/null +++ b/plugins/main/public/components/common/tables/table-indexer.tsx @@ -0,0 +1,244 @@ +import React, { useState } from 'react'; +import { IntlProvider } from 'react-intl'; +import { + EuiFlexGroup, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiButtonEmpty, + EuiFlexItem, +} from '@elastic/eui'; +import { getWazuhCorePlugin } from '../../../kibana-services'; +import { + ErrorHandler, + ErrorFactory, + HttpError, +} from '../../../react-services/error-management'; +import { LoadingSpinner } from '../loading-spinner/loading-spinner'; +// common components/hooks +import useSearchBar from '../../common/search-bar/use-search-bar'; +import { exportSearchToCSV } from '../../common/data-grid/data-grid-service'; + +import { + tParsedIndexPattern, + PatternDataSource, +} from '../../common/data-source'; +import { useDataSource } from '../../common/data-source/hooks'; +import { IndexPattern } from '../../../../../src/plugins/data/public'; +import { DocumentViewTableAndJson } from '../wazuh-discover/components/document-view-table-and-json'; +import { WzSearchBar } from '../../common/search-bar'; +import { TableData } from './table-data'; + +type TDocumentDetailsTab = { + id: string; + name: string; + content: any; +}; + +export const TableIndexer = ({ + DataSource, + DataSourceRepository, + documentDetailsExtraTabs, + exportCSVPrefixFilename = '', + tableProps = {}, +}: { + DataSource: any; + DataSourceRepository; + documentDetailsExtraTabs?: { + pre?: + | TDocumentDetailsTab[] + | (({ document: any, indexPattern: any }) => TDocumentDetailsTab[]); + post?: + | TDocumentDetailsTab[] + | (({ document: any, indexPattern: any }) => TDocumentDetailsTab[]); + }; + exportCSVPrefixFilename: string; + tableProps: any; // TODO: use props of TableData? +}) => { + const { + dataSource, + filters, + fetchFilters, + isLoading: isDataSourceLoading, + fetchData, + setFilters, + } = useDataSource({ + DataSource: DataSource, + repository: new DataSourceRepository(), + }); + + const { searchBarProps } = useSearchBar({ + indexPattern: dataSource?.indexPattern as IndexPattern, + filters, + setFilters, + }); + const { query } = searchBarProps; + + const [indexPattern, setIndexPattern] = useState( + undefined, + ); + const [inspectedHit, setInspectedHit] = useState(null); + const [isExporting, setIsExporting] = useState(false); + + const sideNavDocked = getWazuhCorePlugin().hooks.useDockedSideNav(); + + const onClickExportResults = async ({ + dataSource, + tableColumns, + sorting, + totalItems, + searchParams, + filePrefix, + }) => { + const params = { + indexPattern: dataSource.indexPattern as IndexPattern, + filters: searchParams.fetchFilters, + query: searchParams.query, + fields: tableColumns.map(({ field }) => field).filter(value => value), + pagination: { + pageIndex: 0, + pageSize: totalItems, + }, + sorting, + filePrefix, + }; + try { + setIsExporting(true); + await exportSearchToCSV(params); + } catch (error) { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error downloading csv report', + }); + ErrorHandler.handleError(searchError); + } finally { + setIsExporting(false); + } + }; + + const { postActionButtons, ...restTableProps } = tableProps; + + const enhancedPostActionButtons = function (props) { + return ( + <> + {postActionButtons && ( + {postActionButtons(props)} + )} + + + onClickExportResults({ + ...props, + dataSource, + filePrefix: exportCSVPrefixFilename, + }) + } + > + Export formatted + + + + ); + }; + + const getRowProps = item => { + return { + onClick: () => { + setInspectedHit(item._document); + }, + }; + }; + + return ( + + <> + {isDataSourceLoading ? ( + + ) : ( + + } + saveStateStorage={{ + system: 'localStorage', + key: 'wz-engine:rules-main', + }} + showFieldSelector + // rowProps={getRowProps} + fetchData={({ pagination, sorting, searchParams: { query } }) => { + setIndexPattern(dataSource?.indexPattern); + + return fetchData({ + query, + pagination, + sorting: { + columns: [ + { + id: sorting.field, + direction: sorting.direction, + }, + ], + }, + }) + .then(results => { + return { + items: results.hits.hits.map(document => ({ + _document: document, + ...document._source, + })), + totalItems: results.hits.total, + }; + }) + .catch(error => { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error fetching data', + }); + ErrorHandler.handleError(searchError); + }); + }} + fetchParams={{ query, fetchFilters }} + /> + )} + {inspectedHit && ( + setInspectedHit(null)} size='m'> + + +

Details

+
+
+ + + + + +
+ )} + +
+ ); +}; diff --git a/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx b/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx index 7e2f34d1e1..7368854d3f 100644 --- a/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx +++ b/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx @@ -1,23 +1,51 @@ import React from 'react'; import { EuiFlexItem, EuiCodeBlock, EuiTabbedContent } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; -import DocViewer from '../../doc-viewer/doc-viewer' +import DocViewer from '../../doc-viewer/doc-viewer'; import { useDocViewer } from '../../doc-viewer'; -export const DocumentViewTableAndJson = ({ document, indexPattern }) => { +export const DocumentViewTableAndJson = ({ + document, + indexPattern, + extraTabs = [], + tableProps = {}, +}) => { const docViewerProps = useDocViewer({ doc: document, indexPattern: indexPattern as IndexPattern, }); + const renderExtraTabs = tabs => { + if (!tabs) { + return []; + } + return [ + ...(typeof tabs === 'function' + ? tabs({ document, indexPattern }) + : tabs + ).map(({ id, name, content: Content }) => ({ + id, + name, + content: ( + + ), + })), + ]; + }; + return ( , + content: , }, { id: 'json', @@ -33,6 +61,7 @@ export const DocumentViewTableAndJson = ({ document, indexPattern }) => { ), }, + ...renderExtraTabs(extraTabs.post), ]} /> diff --git a/plugins/wazuh-engine/opensearch_dashboards.json b/plugins/wazuh-engine/opensearch_dashboards.json index 4f4c184fc5..3ad2b9256c 100644 --- a/plugins/wazuh-engine/opensearch_dashboards.json +++ b/plugins/wazuh-engine/opensearch_dashboards.json @@ -4,5 +4,11 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["opensearchDashboardsUtils", "wazuhCore"] + "requiredPlugins": [ + "opensearchDashboardsUtils", + "wazuhCore", + "dashboard", + "embeddable", + "data" + ] } diff --git a/plugins/wazuh-engine/public/components/engine-layout.tsx b/plugins/wazuh-engine/public/components/engine-layout.tsx index 5b9eacac4d..f9394ace2a 100644 --- a/plugins/wazuh-engine/public/components/engine-layout.tsx +++ b/plugins/wazuh-engine/public/components/engine-layout.tsx @@ -1,12 +1,25 @@ import React from 'react'; import { EuiTitle } from '@elastic/eui'; +import { getCore } from '../plugin-services'; export const EngineLayout = ({ children, title }) => { + React.useEffect(() => { + getCore().chrome.setBreadcrumbs([ + { + className: 'osdBreadcrumbs', + text: 'Engine', + }, + { + className: 'osdBreadcrumbs', + text: title, + }, + ]); + }, []); return ( <> - + {/*

{title}

-
+
*/}
{children}
); diff --git a/plugins/wazuh-engine/public/components/engine.tsx b/plugins/wazuh-engine/public/components/engine.tsx index 2beed1aa9a..c25c32bb42 100644 --- a/plugins/wazuh-engine/public/components/engine.tsx +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -97,7 +97,11 @@ export const Engine = props => { render={() => { return ( - {item.render()} + {item.render({ + ...props, + title: item.name, + basePath: `/engine/${item.id}`, + })} ); }} diff --git a/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx b/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx new file mode 100644 index 0000000000..29b1aa6149 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { + EuiCodeEditor, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; + +export const RuleFileEditor = ({ + initialContent = '', + isEditable = false, + ...props +}) => { + const { useForm, InputForm } = props; + const { fields } = useForm({ + filename: { type: 'text', initialValue: '' }, + content: { type: 'text', initialValue: initialContent || '' }, + }); + + return ( + <> + + + + + + {isEditable && ( + {}}> + Save + + )} + + + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx new file mode 100644 index 0000000000..7f80525bf2 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import spec from '../spec.json'; + +const transformSpecToForm = specification => { + return Object.fromEntries( + Object.entries(specification).map(([key, value]) => [ + key, + { type: value.field.type, initialValue: value.field.initialValue }, + ]), + ); +}; + +export const RuleForm = props => { + const { useForm, InputForm } = props; + const { fields } = useForm(transformSpecToForm(spec)); + + return ( + <> + {Object.entries(fields).map(([name, formField]) => ( + + ))} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/components/index.ts b/plugins/wazuh-engine/public/components/rules/components/index.ts new file mode 100644 index 0000000000..5d15fe1b3c --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/index.ts @@ -0,0 +1 @@ +export * from './layout'; diff --git a/plugins/wazuh-engine/public/components/rules/components/layout.tsx b/plugins/wazuh-engine/public/components/rules/components/layout.tsx new file mode 100644 index 0000000000..da302cbb75 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/layout.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, +} from '@elastic/eui'; + +export const Layout = ({ + title, + children, + actions, +}: { + title: React.ReactNode; + children: React.ReactNode; + actions?: any; +}) => { + return ( + <> + + + +

{title}

+
+
+ {actions && ( + + + + )} +
+ +
{children}
+ + ); +}; + +const ViewActions = ({ actions }) => { + return Array.isArray(actions) ? ( + + {actions.map(action => ( + {action} + ))} + + ) : ( + actions() + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/index.ts b/plugins/wazuh-engine/public/components/rules/index.ts index 933b848dc0..7280e01ba5 100644 --- a/plugins/wazuh-engine/public/components/rules/index.ts +++ b/plugins/wazuh-engine/public/components/rules/index.ts @@ -1 +1 @@ -export { Rules } from './rules'; +export { Rules } from './router'; diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx new file mode 100644 index 0000000000..1488d3c2b8 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import { getDashboard } from '../visualization'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public/'; +import { getCore } from '../../../plugin-services'; +import { EuiButton } from '@elastic/eui'; +import { Layout } from '../components'; + +export const defaultColumns: EuiDataGridColumn[] = [ + { + field: 'name', + name: 'Name', + sortable: true, + show: true, + }, + { + field: 'metadata.title', + name: 'Title', + sortable: true, + show: true, + }, + { + field: 'metadata.description', + name: 'Description', + sortable: true, + show: true, + }, + { + field: 'metadata.author.name', + name: 'Author name', + sortable: true, + show: true, + }, + { + field: 'metadata.author.date', + name: 'Author date', + sortable: true, + show: true, + }, + { + field: 'metadata.author.email', + name: 'Author email', + sortable: true, + show: true, + }, + { + field: 'metadata.author.url', + name: 'Author URL', + sortable: true, + show: true, + }, + { + field: 'metadata.compatibility', + name: 'Compatibility', + sortable: true, + show: true, + }, + { + field: 'metadata.integration', + name: 'Integration', + sortable: true, + show: true, + }, + { + field: 'metadata.versions', + name: 'Versions', + sortable: true, + show: true, + }, + { + field: 'metadata.references', + name: 'References', + sortable: true, + show: true, + }, + { + field: 'parents', + name: 'Parent', + sortable: true, + }, + { + name: 'Actions', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Export', + isPrimary: true, + description: 'Export file', + icon: 'exportAction', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-export', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete file', + icon: 'trash', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-delete', + }, + ], + }, +]; + +export const RulesList = props => { + const { + TableIndexer, + RulesDataSource, + RulesDataSourceRepository, + DashboardContainerByValueRenderer: DashboardByRenderer, + WazuhFlyoutDiscover, + PatternDataSource, + AppState, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + PatternDataSourceFilterManager, + FILTER_OPERATOR, + title, + } = props; + + const actions = [ + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + { + props.navigationService.getInstance().navigate('/engine/rules/new'); + }} + > + Create Rule + , + ]; + + return ( + + {}, + }, + isSelectable: true, + }, + }} + exportCSVPrefixFilename='rules' + documentDetailsExtraTabs={{ + post: params => [ + { + id: 'relationship', + name: 'Relationship', + content: () => ( + + ), + }, + { + id: 'events', + name: 'Events', + content: () => { + const filterManager = React.useMemo( + () => new FilterManager(getCore().uiSettings), + [], + ); + return ( + + // this.renderDiscoverExpandedRow(...args) + // } + /> + ); + }, + }, + ], + }} + /> + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/pages/new.tsx b/plugins/wazuh-engine/public/components/rules/pages/new.tsx new file mode 100644 index 0000000000..0253bfcc52 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/new.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { RuleFileEditor } from '../components/file-editor'; + +export const CreateRule = props => { + const [view, setView] = useState('visual-editor'); + + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ...(view === 'visual-editor' + ? [ + { + setView('file-editor'); + }} + iconType='apmTrace' + > + Switch to file editor + , + ] + : [ + { + setView('visual-editor'); + }} + iconType='apmTrace' + > + Switch to visual editor + , + ]), + ]; + + return ( + + {view === 'visual-editor' && } + {view === 'file-editor' && ( + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/router.tsx b/plugins/wazuh-engine/public/components/rules/router.tsx new file mode 100644 index 0000000000..5faf80c15f --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/router.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { RulesList } from './pages/list'; +import { CreateRule } from './pages/new'; + +export const Rules = props => { + return ( + + + + + } + > + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/rules.tsx b/plugins/wazuh-engine/public/components/rules/rules.tsx deleted file mode 100644 index 57ab2a94d2..0000000000 --- a/plugins/wazuh-engine/public/components/rules/rules.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const Rules = () => { - return <>Hi; -}; diff --git a/plugins/wazuh-engine/public/components/rules/spec.json b/plugins/wazuh-engine/public/components/rules/spec.json new file mode 100644 index 0000000000..01141abf01 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/spec.json @@ -0,0 +1,74 @@ +{ + "name": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.title": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.description": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.author.name": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.author.date": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.author.email": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.author.url": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.compatibility": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.integration": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.versions": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "metadata.references": { + "field": { + "type": "text", + "initialValue": "" + } + }, + "parents": { + "field": { + "type": "text", + "initialValue": "" + } + } +} diff --git a/plugins/wazuh-engine/public/components/rules/visualization.ts b/plugins/wazuh-engine/public/components/rules/visualization.ts new file mode 100644 index 0000000000..73e9f20506 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/visualization.ts @@ -0,0 +1,54 @@ +const getVisualization = (indexPatternId: string, ruleID: string) => { + return { + id: 'Wazuh-rules-vega', + title: `Child rules of ${ruleID}`, + type: 'vega', + params: { + spec: `{\n $schema: https://vega.github.io/schema/vega/v5.json\n description: An example of Cartesian layouts for a node-link diagram of hierarchical data.\n padding: 5\n signals: [\n {\n name: labels\n value: true\n bind: {\n input: checkbox\n }\n }\n {\n name: layout\n value: tidy\n bind: {\n input: radio\n options: [\n tidy\n cluster\n ]\n }\n }\n {\n name: links\n value: diagonal\n bind: {\n input: select\n options: [\n line\n curve\n diagonal\n orthogonal\n ]\n }\n }\n {\n name: separation\n value: false\n bind: {\n input: checkbox\n }\n }\n ]\n data: [\n {\n name: tree\n url: {\n /*\n An object instead of a string for the "url" param is treated as an OpenSearch query. Anything inside this object is not part of the Vega language, but only understood by OpenSearch Dashboards and OpenSearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\n\n OpenSearch Dashboards has a special handling for the fields surrounded by "%". They are processed before the the query is sent to OpenSearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\n */\n\n // Apply dashboard context filters when set\n // %context%: true\n // Filter the time picker (upper right corner) with this field\n // %timefield%: @timestamp\n\n /*\n See .search() documentation for : https://opensearch.org/docs/latest/clients/javascript/\n */\n\n // Which index to search\n index: wazuh-rules\n\n\n // If "data_source.enabled: true", optionally set the data source name to query from (omit field if querying from local cluster)\n // data_source_name: Example US Cluster\n\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\n body: {\n query: {\n bool: {\n should: [\n {\n match_phrase: {\n name: ${ruleID}\n }\n }\n {\n match_phrase: {\n parents: ${ruleID}\n }\n }\n ]\n minimum_should_match: 1\n }\n }\n /* query: {\n match_all: {\n }\n } */\n size: 1000\n }\n }\n /*\n OpenSearch will return results in this format:\n\n aggregations: {\n time_buckets: {\n buckets: [\n {\n key_as_string: 2015-11-30T22:00:00.000Z\n key: 1448920800000\n doc_count: 0\n },\n {\n key_as_string: 2015-11-30T23:00:00.000Z\n key: 1448924400000\n doc_count: 0\n }\n ...\n ]\n }\n }\n\n For our graph, we only need the list of bucket values. Use the format.property to discard everything else.\n */\n format: {\n property: hits.hits\n }\n transform: [\n {\n type: stratify\n key: _source.id\n parentKey: _source.parents\n }\n {\n type: tree\n method: {\n signal: layout\n }\n size: [\n {\n signal: height\n }\n {\n signal: width - 100\n }\n ]\n separation: {\n signal: separation\n }\n as: [\n y\n x\n depth\n children\n ]\n }\n ]\n }\n {\n name: links\n source: tree\n transform: [\n {\n type: treelinks\n }\n {\n type: linkpath\n orient: horizontal\n shape: {\n signal: links\n }\n }\n ]\n }\n ]\n scales: [\n {\n name: color\n type: linear\n range: {\n scheme: magma\n }\n domain: {\n data: tree\n field: depth\n }\n zero: true\n }\n ]\n marks: [\n {\n type: path\n from: {\n data: links\n }\n encode: {\n update: {\n path: {\n field: path\n }\n stroke: {\n value: "#ccc"\n }\n }\n }\n }\n {\n type: symbol\n from: {\n data: tree\n }\n encode: {\n enter: {\n size: {\n value: 100\n }\n stroke: {\n value: "#fff"\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n fill: {\n scale: color\n field: depth\n }\n }\n }\n }\n {\n type: text\n from: {\n data: tree\n }\n encode: {\n enter: {\n text: {\n field: _source.id\n }\n fontSize: {\n value: 15\n }\n baseline: {\n value: middle\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n dx: {\n signal: datum.children ? -7 : 7\n }\n align: {\n signal: datum.children ? \'right\' : \'left\'\n }\n opacity: {\n signal: labels ? 1 : 0\n }\n }\n }\n }\n ]\n}`, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [], + }, + }; +}; + +export const getDashboard = ( + indexPatternId: string, + ruleID: string, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + ruleVis: { + gridData: { + w: 42, + h: 12, + x: 0, + y: 0, + i: 'ruleVis', + }, + type: 'visualization', + explicitInput: { + id: 'ruleVis', + savedVis: getVisualization(indexPatternId, ruleID), + }, + }, + }; +}; From f8891e735e61d5bc79de67d881c52c42664c2f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 9 Jul 2024 15:01:41 +0200 Subject: [PATCH 06/34] feat(script): sample data generator for rules dataset --- scripts/sample-data/dataset/rules/main.py | 113 ++++++++++++++++++ .../sample-data/dataset/rules/template.json | 97 +++++++++++++++ scripts/sample-data/requirements.txt | 1 + scripts/sample-data/rules/readme.md | 15 +++ scripts/sample-data/rules/rules.py | 96 +++++++++++++++ scripts/sample-data/rules/template.json | 47 ++++++++ scripts/sample-data/script.py | 75 ++++++++++++ 7 files changed, 444 insertions(+) create mode 100644 scripts/sample-data/dataset/rules/main.py create mode 100644 scripts/sample-data/dataset/rules/template.json create mode 100644 scripts/sample-data/requirements.txt create mode 100644 scripts/sample-data/rules/readme.md create mode 100644 scripts/sample-data/rules/rules.py create mode 100644 scripts/sample-data/rules/template.json create mode 100644 scripts/sample-data/script.py diff --git a/scripts/sample-data/dataset/rules/main.py b/scripts/sample-data/dataset/rules/main.py new file mode 100644 index 0000000000..43a6d9b64b --- /dev/null +++ b/scripts/sample-data/dataset/rules/main.py @@ -0,0 +1,113 @@ +import random +import sys +import os.path +import json +from opensearchpy import helpers +from pathlib import Path + +index_template_file='template.json' +default_count='10000' +default_index_name='wazuh-rules' + +def generate_document(params): + id_int = int(params["id"]) + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + + # https://github.com/wazuh/wazuh/blob/11334-dev-new-wazuh-engine/src/engine/ruleset/schemas/wazuh-asset.json + data = { + "ecs": {"version": "1.7.0"}, + "name": f'rule/{str(id_int)}/0', + "metadata": { + "title": f'Rule title {str(id_int)}', + "description": f'Rule description {str(id_int)}', + "author": { + "name": f'Rule author name {str(id_int)}', + "date": f'2024-07-04', + "email": f'email@sample.com', + "url": f'web.sample.com' + }, + "compatibility": f'compatibiliy_device', + "integration": f'integration {random.choice(["1","2","3"])}', + "versions": ['0.1', '0.2'], + "references": [f'Ref 01', f'Ref 02'] + }, + "wazuh": { + "cluster": { + "name": "wazuh" + } + }, + "parents": [], + # "check": {}, # enhance + # "allow": {}, # enhance + # "normalize": {}, # enhance + # "outputs": [], # enhance + # "definitions": {} # enhance + } + if(bool(random.getrandbits(1))): + top_limit = id_int - 1 if id_int - 1 > 0 else 0 + data["parents"] = [f'rule/{str(random.randint(0, top_limit))}/0'] + + return data + +def generate_documents(params): + for i in range(0, int(params["count"])): + yield generate_document({"id": i}) + +def get_params(ctx): + count = '' + while not count.isdigit(): + count = input_question(f'How many documents do you want to generate? [default={default_count}]', {"default_value": default_count}) + + index_name = input_question(f'Enter the index name [default={default_index_name}]', {"default_value": default_index_name}) + + return { + "count": count, + "index_name": index_name + } + +def input_question(message, options = {}): + response = input(message) + + if(options["default_value"] and response == ''): + response = options["default_value"] + + return response + + +def main(ctx): + client = ctx["client"] + logger = ctx["logger"] + logger.info('Getting configuration') + + config = get_params(ctx) + logger.info(f'Config {config}') + + resolved_index_template_file = os.path.join(Path(__file__).parent, index_template_file) + + logger.info(f'Checking existence of index [{config["index_name"]}]') + if client.indices.exists(config["index_name"]): + logger.info(f'Index found [{config["index_name"]}]') + should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) + if should_delete_index == 'Y': + client.indices.delete(config["index_name"]) + logger.info(f'Index [{config["index_name"]}] deleted') + else: + logger.error(f'Index found [{config["index_name"]}] should be removed before create and insert documents') + sys.exit(1) + + if not os.path.exists(resolved_index_template_file): + logger.error(f'Index template found [{resolved_index_template_file}]') + sys.exit(1) + + with open(resolved_index_template_file) as templateFile: + index_template = json.load(templateFile) + try: + client.indices.create(index=config["index_name"], body=index_template) + logger.info(f'Index [{config["index_name"]}] created') + except Exception as e: + logger.error(f'Error: {e}') + sys.exit(1) + + helpers.bulk(client, generate_documents(config), index=config['index_name']) + logger.info(f'Data was indexed into [{config["index_name"]}]') + diff --git a/scripts/sample-data/dataset/rules/template.json b/scripts/sample-data/dataset/rules/template.json new file mode 100644 index 0000000000..c906e9d5cb --- /dev/null +++ b/scripts/sample-data/dataset/rules/template.json @@ -0,0 +1,97 @@ +{ + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "name": { + "type": "keyword" + }, + "metadata": { + "properties": { + "title": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "author": { + "properties": { + "name": { + "type": "keyword" + }, + "date": { + "type": "keyword" + }, + "email": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "compatibility": { + "type": "keyword" + }, + "integration": { + "type": "keyword" + }, + "versions": { + "type": "keyword" + }, + "references": { + "type": "keyword" + } + } + }, + "parents": { + "type": "keyword" + }, + "allow": { + "type": "keyword" + }, + "normalize": { + "type": "keyword" + }, + "outputs": { + "type": "keyword" + }, + "definitions": { + "type": "keyword" + }, + "wazuh": { + "properties": { + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": 1000 + } + }, + "refresh_interval": "2s" + } + } +} diff --git a/scripts/sample-data/requirements.txt b/scripts/sample-data/requirements.txt new file mode 100644 index 0000000000..7e8ce75a5b --- /dev/null +++ b/scripts/sample-data/requirements.txt @@ -0,0 +1 @@ +opensearch_py==2.4.2 diff --git a/scripts/sample-data/rules/readme.md b/scripts/sample-data/rules/readme.md new file mode 100644 index 0000000000..a9431cb6db --- /dev/null +++ b/scripts/sample-data/rules/readme.md @@ -0,0 +1,15 @@ +# Description + +TODO + +# Files + +TODO + +# Usage + +1. Install the requirements with `pip install -r requirements.txt`. For some Operating Systems it will fail and suggest a different way to install it (`sudo pacman -S python-xyz`, `sudo apt install python-xyz`, etc.). + If the package is not found in this way, we can install it running `pip install -r requirements.txt --break-system-packages` (It is recommended to avoid this option if possible) + +2. Run the script with `python3 script.py` +3. Follow the instructions that it will show on the console. diff --git a/scripts/sample-data/rules/rules.py b/scripts/sample-data/rules/rules.py new file mode 100644 index 0000000000..070e24b8cd --- /dev/null +++ b/scripts/sample-data/rules/rules.py @@ -0,0 +1,96 @@ +import random +import sys +import os.path +import json +from opensearchpy import helpers + +index_template_file='template.json' + +def generate_document(params): + id_int = int(params["id"]) + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + data = { + "ecs": {"version": "1.7.0"}, + "id": str(id_int), + "wazuh": { + "cluster": { + "name": "wazuh" + } + }, + "parent_id": None + } + if(bool(random.getrandbits(1))): + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + data["parent_id"] = str(random.randint(0, top_limit)) + + return data + +def generate_documents(params): + for i in range(0, int(params["count"])): + yield generate_document({"id": i}) + +def get_params(ctx): + default_count='100' + default_index_name='wazuh-rules' + count = '' + while not count.isdigit(): + count = input_question(f'\nHow many events do you want to generate? [default={default_count}]\n', {"default_value": default_count}) + + index_name = input_question(f'\nEnter the index name [default={default_index_name}]: \n', {"default_value": default_index_name}) + + return { + "count": count, + "index_name": index_name + } + +def input_question(message, options = {}): + response = input(message) + + if(options["default_value"] and response == ''): + response = options["default_value"] + + return response + + + +def main(ctx): + ctx.logger.info('Getting configuration') + config = get_params(ctx) + print(f'Config {config}') + + client = ctx["client"] + + print(f'Checking existence of index [{config["index_name"]}]') + if client.indices.exists(config["index_name"]): + print(f'Index found [{config["index_name"]}]') + should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) + if should_delete_index == 'Y': + client.indices.delete(config["index_name"]) + print(f'Index [{config["index_name"]}] deleted') + else: + print(f'Index found [{config["index_name"]}] should be removed before create and insert documents') + sys.exit(1) + + print(f'Index not found [{config["index_name"]}]') + + if not os.path.exists(index_template_file): + print(f'Index template found [{index_template_file}]') + sys.exit(1) + + with open(index_template_file) as templateFile: + index_template = json.load(templateFile) + try: + client.indices.create(index=config["index_name"], body=index_template) + print(f'Index [{config["index_name"]}] created') + except Exception as e: + print('Error: {}'.format(e)) + sys.exit(1) + + generator = generate_documents(config) + helpers.bulk(client, generator, index=config['index_name']) + + +dataset = { + "name": "rules", + "run": main +} diff --git a/scripts/sample-data/rules/template.json b/scripts/sample-data/rules/template.json new file mode 100644 index 0000000000..1a24d704fd --- /dev/null +++ b/scripts/sample-data/rules/template.json @@ -0,0 +1,47 @@ +{ + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "id": { + "type": "keyword" + }, + "parent": { + "type": "keyword" + }, + "wazuh": { + "properties": { + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": 1000 + } + }, + "refresh_interval": "2s" + } + } +} diff --git a/scripts/sample-data/script.py b/scripts/sample-data/script.py new file mode 100644 index 0000000000..c3b00ab23b --- /dev/null +++ b/scripts/sample-data/script.py @@ -0,0 +1,75 @@ +from opensearchpy import OpenSearch, helpers +import json +import os.path +import warnings +from importlib import import_module +import logging +import sys + +warnings.filterwarnings("ignore") + +def get_opensearch_connection(): + verified = False + connection_config_file='connection.json' + if os.path.exists(connection_config_file): + with open(connection_config_file) as configFile: + config = json.load(configFile) + if 'ip' not in config or 'port' not in config or 'username' not in config or 'password' not in config: + print('\nConnection configuration file is not properly configured. Continuing without it.') + else: + verified = True + else: + print('\nConnection configuration file not found. Continuing without it.') + + if not verified: + ip = input("\nEnter the IP of your Indexer [default=0.0.0.0]: \n") + if ip == '': + ip = '0.0.0.0' + + port = input("\nEnter the port of your Indexer [default=9200]: \n") + if port == '': + port = '9200' + + username = input("\nUsername [default=admin]: \n") + if username == '': + username = 'admin' + + password = input("\nPassword [default=admin]: \n") + if password == '': + password = 'admin' + + config = {'ip':ip,'port':port,'username':username,'password':password} + + store = input("\nDo you want to store these settings for future use? (y/n) [default=n] \n") + if store == '': + store = 'n' + + while store != 'y' and store != 'n': + store = input("\nInvalid option.\n Do you want to store these settings for future use? (y/n) \n") + if store == 'y': + with open(connection_config_file, 'w') as configFile: + json.dump(config, configFile) + return config + + +def main(): + config = get_opensearch_connection() + client = OpenSearch([{'host':config['ip'],'port':config['port']}], http_auth=(config['username'], config['password']), use_ssl=True, verify_certs=False) + logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO) + + if not client.ping(): + logger.error('Could not connect to the indexer') + return + + module_name = sys.argv[1] + + if not module_name: + logger.error('No dataset selected') + + module = import_module(f'dataset.{module_name}.main') + logger.info(f'Running dataset [{module_name}]') + module.main({"client":client, "logger": logging.getLogger(module_name)}) + +if __name__=="__main__": + main() From 85717ded794c101e13fc25fba14d7169354112b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 9 Jul 2024 16:10:50 +0200 Subject: [PATCH 07/34] feat(engine): switch order menu and replace placeholders on views --- .../public/components/decoders/decoders.tsx | 2 +- plugins/wazuh-engine/public/components/engine.tsx | 10 +++++----- .../wazuh-engine/public/components/filters/filters.tsx | 2 +- .../public/components/integrations/integrations.tsx | 2 +- .../wazuh-engine/public/components/outputs/outputs.tsx | 2 +- .../public/components/policies/policies.tsx | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/wazuh-engine/public/components/decoders/decoders.tsx b/plugins/wazuh-engine/public/components/decoders/decoders.tsx index f9976c4a94..e734d6b98b 100644 --- a/plugins/wazuh-engine/public/components/decoders/decoders.tsx +++ b/plugins/wazuh-engine/public/components/decoders/decoders.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Decoders = () => { - return <>Hi; + return <>Decoders; }; diff --git a/plugins/wazuh-engine/public/components/engine.tsx b/plugins/wazuh-engine/public/components/engine.tsx index 324a6d3e75..d69e49c552 100644 --- a/plugins/wazuh-engine/public/components/engine.tsx +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -38,16 +38,16 @@ const views = [ id: 'filters', render: Filters, }, - { - name: 'Integrations', - id: 'integrations', - render: Integrations, - }, { name: 'Policies', id: 'policies', render: Policies, }, + { + name: 'Integrations', + id: 'integrations', + render: Integrations, + }, { name: 'KVDBs', id: 'kvdbs', diff --git a/plugins/wazuh-engine/public/components/filters/filters.tsx b/plugins/wazuh-engine/public/components/filters/filters.tsx index 1249251462..a8b14115f3 100644 --- a/plugins/wazuh-engine/public/components/filters/filters.tsx +++ b/plugins/wazuh-engine/public/components/filters/filters.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Filters = () => { - return <>Hi; + return <>Filters; }; diff --git a/plugins/wazuh-engine/public/components/integrations/integrations.tsx b/plugins/wazuh-engine/public/components/integrations/integrations.tsx index 3fc80d507f..21ad97b621 100644 --- a/plugins/wazuh-engine/public/components/integrations/integrations.tsx +++ b/plugins/wazuh-engine/public/components/integrations/integrations.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Integrations = () => { - return <>Hi; + return <>Integrations; }; diff --git a/plugins/wazuh-engine/public/components/outputs/outputs.tsx b/plugins/wazuh-engine/public/components/outputs/outputs.tsx index b1db559340..31bbcf9db9 100644 --- a/plugins/wazuh-engine/public/components/outputs/outputs.tsx +++ b/plugins/wazuh-engine/public/components/outputs/outputs.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Outputs = () => { - return <>Hi; + return <>Outputs; }; diff --git a/plugins/wazuh-engine/public/components/policies/policies.tsx b/plugins/wazuh-engine/public/components/policies/policies.tsx index f9e529144a..0e5ff09310 100644 --- a/plugins/wazuh-engine/public/components/policies/policies.tsx +++ b/plugins/wazuh-engine/public/components/policies/policies.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const Policies = () => { - return <>Hi; + return <>Policies; }; From 715587ffc6a89fe9636e9f3a0518e6fe10fa8a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 9 Jul 2024 17:12:39 +0200 Subject: [PATCH 08/34] feat(engine): use wazuh-asset specification to manage the creation or list of rules --- plugins/main/public/app-router.tsx | 2 + .../common/tables/table-indexer.tsx | 76 +-- .../components/rules/components/form.tsx | 15 +- .../public/components/rules/pages/list.tsx | 435 ++++++++++-------- .../public/components/rules/spec.json | 286 +++++++++--- .../rules/utils/transform-asset-spec.ts | 57 +++ 6 files changed, 533 insertions(+), 338 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 635f2c3776..b2d26ec242 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -57,6 +57,7 @@ import { UploadFilesButton, } from './controllers/management/components/management/common/actions-buttons'; import WzListEditor from './controllers/management/components/management/cdblists/views/list-editor.tsx'; +import { DocumentViewTableAndJson } from './components/common/wazuh-discover/components/document-view-table-and-json'; export function Application(props) { const dispatch = useDispatch(); const navigationService = NavigationService.getInstance(); @@ -154,6 +155,7 @@ export function Application(props) { RulesDataSourceRepository={RulesDataSourceRepository} useDocViewer={useDocViewer} DocViewer={DocViewer} + DocumentViewTableAndJson={DocumentViewTableAndJson} useForm={useForm} InputForm={InputForm} GenericRequest={GenericRequest} diff --git a/plugins/main/public/components/common/tables/table-indexer.tsx b/plugins/main/public/components/common/tables/table-indexer.tsx index 9a8144a5f4..04ab81652d 100644 --- a/plugins/main/public/components/common/tables/table-indexer.tsx +++ b/plugins/main/public/components/common/tables/table-indexer.tsx @@ -1,15 +1,6 @@ -import React, { useState } from 'react'; +import React from 'react'; import { IntlProvider } from 'react-intl'; -import { - EuiFlexGroup, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiButtonEmpty, - EuiFlexItem, -} from '@elastic/eui'; -import { getWazuhCorePlugin } from '../../../kibana-services'; +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; import { ErrorHandler, ErrorFactory, @@ -26,33 +17,19 @@ import { } from '../../common/data-source'; import { useDataSource } from '../../common/data-source/hooks'; import { IndexPattern } from '../../../../../src/plugins/data/public'; -import { DocumentViewTableAndJson } from '../wazuh-discover/components/document-view-table-and-json'; import { WzSearchBar } from '../../common/search-bar'; import { TableData } from './table-data'; -type TDocumentDetailsTab = { - id: string; - name: string; - content: any; -}; - export const TableIndexer = ({ DataSource, DataSourceRepository, - documentDetailsExtraTabs, exportCSVPrefixFilename = '', tableProps = {}, + onSetIndexPattern, }: { DataSource: any; DataSourceRepository; - documentDetailsExtraTabs?: { - pre?: - | TDocumentDetailsTab[] - | (({ document: any, indexPattern: any }) => TDocumentDetailsTab[]); - post?: - | TDocumentDetailsTab[] - | (({ document: any, indexPattern: any }) => TDocumentDetailsTab[]); - }; + onSetIndexPattern: () => void; exportCSVPrefixFilename: string; tableProps: any; // TODO: use props of TableData? }) => { @@ -75,14 +52,6 @@ export const TableIndexer = ({ }); const { query } = searchBarProps; - const [indexPattern, setIndexPattern] = useState( - undefined, - ); - const [inspectedHit, setInspectedHit] = useState(null); - const [isExporting, setIsExporting] = useState(false); - - const sideNavDocked = getWazuhCorePlugin().hooks.useDockedSideNav(); - const onClickExportResults = async ({ dataSource, tableColumns, @@ -104,7 +73,6 @@ export const TableIndexer = ({ filePrefix, }; try { - setIsExporting(true); await exportSearchToCSV(params); } catch (error) { const searchError = ErrorFactory.create(HttpError, { @@ -112,11 +80,15 @@ export const TableIndexer = ({ message: 'Error downloading csv report', }); ErrorHandler.handleError(searchError); - } finally { - setIsExporting(false); } }; + React.useEffect(() => { + if (dataSource?.indexPattern) { + onSetIndexPattern && onSetIndexPattern(dataSource?.indexPattern); + } + }, [dataSource?.indexPattern]); + const { postActionButtons, ...restTableProps } = tableProps; const enhancedPostActionButtons = function (props) { @@ -178,8 +150,6 @@ export const TableIndexer = ({ showFieldSelector // rowProps={getRowProps} fetchData={({ pagination, sorting, searchParams: { query } }) => { - setIndexPattern(dataSource?.indexPattern); - return fetchData({ query, pagination, @@ -212,32 +182,6 @@ export const TableIndexer = ({ fetchParams={{ query, fetchFilters }} /> )} - {inspectedHit && ( - setInspectedHit(null)} size='m'> - - -

Details

-
-
- - - - - -
- )} ); diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index 7f80525bf2..3a66d556e0 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -1,18 +1,11 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import spec from '../spec.json'; - -const transformSpecToForm = specification => { - return Object.fromEntries( - Object.entries(specification).map(([key, value]) => [ - key, - { type: value.field.type, initialValue: value.field.initialValue }, - ]), - ); -}; +import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; export const RuleForm = props => { const { useForm, InputForm } = props; - const { fields } = useForm(transformSpecToForm(spec)); + const specForm = useMemo(() => transfromAssetSpecToForm(spec), []); + const { fields } = useForm(specForm); return ( <> diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx index 1488d3c2b8..bf0fbe870c 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -1,123 +1,23 @@ -import React from 'react'; +import React, { useState } from 'react'; import { getDashboard } from '../visualization'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; import { FilterManager } from '../../../../../../src/plugins/data/public/'; import { getCore } from '../../../plugin-services'; -import { EuiButton } from '@elastic/eui'; +import { + EuiButton, + EuiContextMenu, + EuiPopover, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlexGroup, + EuiTitle, +} from '@elastic/eui'; import { Layout } from '../components'; +import specification from '../spec.json'; +import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; -export const defaultColumns: EuiDataGridColumn[] = [ - { - field: 'name', - name: 'Name', - sortable: true, - show: true, - }, - { - field: 'metadata.title', - name: 'Title', - sortable: true, - show: true, - }, - { - field: 'metadata.description', - name: 'Description', - sortable: true, - show: true, - }, - { - field: 'metadata.author.name', - name: 'Author name', - sortable: true, - show: true, - }, - { - field: 'metadata.author.date', - name: 'Author date', - sortable: true, - show: true, - }, - { - field: 'metadata.author.email', - name: 'Author email', - sortable: true, - show: true, - }, - { - field: 'metadata.author.url', - name: 'Author URL', - sortable: true, - show: true, - }, - { - field: 'metadata.compatibility', - name: 'Compatibility', - sortable: true, - show: true, - }, - { - field: 'metadata.integration', - name: 'Integration', - sortable: true, - show: true, - }, - { - field: 'metadata.versions', - name: 'Versions', - sortable: true, - show: true, - }, - { - field: 'metadata.references', - name: 'References', - sortable: true, - show: true, - }, - { - field: 'parents', - name: 'Parent', - sortable: true, - }, - { - name: 'Actions', - show: true, - actions: [ - { - name: 'View', - isPrimary: true, - description: 'View details', - icon: 'eye', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); - }, - 'data-test-subj': 'action-view', - }, - { - name: 'Export', - isPrimary: true, - description: 'Export file', - icon: 'exportAction', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); - }, - 'data-test-subj': 'action-export', - }, - { - name: 'Delete', - isPrimary: true, - description: 'Delete file', - icon: 'trash', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); - }, - 'data-test-subj': 'action-delete', - }, - ], - }, -]; +const specColumns = transformAssetSpecToListTableColumn(specification); export const RulesList = props => { const { @@ -132,6 +32,7 @@ export const RulesList = props => { PatternDataSourceFilterManager, FILTER_OPERATOR, title, + DocumentViewTableAndJson, } = props; const actions = [ @@ -153,106 +54,248 @@ export const RulesList = props => { , ]; + const [indexPattern, setIndexPattern] = React.useState(null); + const [inspectedHit, setInspectedHit] = React.useState(null); + const [selectedItems, setSelectedItems] = useState([]); + + const defaultColumns = [ + ...specColumns, + { + name: 'Actions', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: ({ _document }) => { + setInspectedHit(_document); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit', + icon: 'pencil', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-edit', + }, + { + name: 'Export', + isPrimary: true, + description: 'Export file', + icon: 'exportAction', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-export', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete file', + icon: 'trash', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-delete', + }, + ], + }, + ]; + return ( TableActions({ ...props, selectedItems }), + tableSortingInitialField: defaultColumns[0].field, tableSortingInitialDirection: 'asc', tableProps: { - itemId: 'id', + itemId: 'name', selection: { - onSelectionChange: () => {}, + onSelectionChange: item => { + setSelectedItems(item); + }, }, isSelectable: true, }, }} exportCSVPrefixFilename='rules' - documentDetailsExtraTabs={{ - post: params => [ - { - id: 'relationship', - name: 'Relationship', - content: () => ( - + {inspectedHit && ( + setInspectedHit(null)} size='m'> + + +

Details: {inspectedHit._source.name}

+
+
+ + + [ + { + id: 'relationship', + name: 'Relationship', + content: () => ( + + ), }, - hidePanelTitles: false, - }} - /> - ), - }, - { - id: 'events', - name: 'Events', - content: () => { - const filterManager = React.useMemo( - () => new FilterManager(getCore().uiSettings), - [], - ); - return ( - { + const filterManager = React.useMemo( + () => new FilterManager(getCore().uiSettings), + [], + ); + return ( + + // this.renderDiscoverExpandedRow(...args) + // } + /> + ); }, - { id: 'rule.description', displayAsText: 'Description' }, - { id: 'rule.level', displayAsText: 'Level' }, - ]} - filterManager={filterManager} - initialFetchFilters={[ - ...PatternDataSourceFilterManager.getClusterManagerFilters( - AppState.getCurrentPattern(), - DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, - ), - PatternDataSourceFilterManager.createFilter( - FILTER_OPERATOR.IS, - 'rule.id', - params.document._source.name.match( - /^[^/]+\/([^/]+)\/[^/]+$/, - )[1], // TODO: do the alerts use the name format of the rule? - AppState.getCurrentPattern(), - ), - ]} - // expandedRowComponent={(...args) => - // this.renderDiscoverExpandedRow(...args) - // } - /> - ); + }, + ], + }} + tableProps={{ + onFilter(...rest) { + // TODO: implement using the dataSource + }, + onToggleColumn() { + // TODO: reseach if make sense the ability to toggle columns + }, + }} + /> + + +
+ )} +
+ ); +}; + +const TableActions = ({ selectedItems }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(state => !state)} + > + Actions + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + { + /* TODO: implement */ + }, }, - }, - ], - }} + { isSeparator: true }, + { + name: 'Delete', + disabled: selectedItems.length === 0, + 'data-test-subj': 'deleteAction', + onClick: () => { + /* TODO: implement */ + }, + }, + ], + }, + ]} /> - + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/spec.json b/plugins/wazuh-engine/public/components/rules/spec.json index 01141abf01..297de41a22 100644 --- a/plugins/wazuh-engine/public/components/rules/spec.json +++ b/plugins/wazuh-engine/public/components/rules/spec.json @@ -1,74 +1,230 @@ { - "name": { - "field": { - "type": "text", - "initialValue": "" + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "wazuh-asset.json", + "name": "schema/wazuh-asset/0", + "title": "Schema for Wazuh assets", + "type": "object", + "description": "Schema for Wazuh assets", + "additionalProperties": false, + "required": ["name", "metadata"], + "anyOf": [ + { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["parse"] + }, + { + "required": ["normalize"] + } + ], + "not": { + "anyOf": [ + { + "required": ["allow"] + }, + { + "required": ["outputs"] + } + ] + } + }, + { + "required": ["outputs"], + "not": { + "anyOf": [ + { + "required": ["normalize"] + }, + { + "required": ["parse"] + } + ] + } + }, + { + "required": ["allow", "parents"], + "not": { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["normalize"] + } + ] + } } - }, - "metadata.title": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.description": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.author.name": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.author.date": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.author.email": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.author.url": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.compatibility": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.integration": { - "field": { - "type": "text", - "initialValue": "" - } - }, - "metadata.versions": { - "field": { - "type": "text", - "initialValue": "" + ], + "patternProperties": { + "parse\\|\\S+": { + "$ref": "#/definitions/_parse" } }, - "metadata.references": { - "field": { - "type": "text", - "initialValue": "" + "properties": { + "name": { + "type": "string", + "description": "Name of the asset, short and concise name to identify this asset", + "pattern": "^[^/]+/[^/]+/[^/]+$" + }, + "metadata": { + "type": "object", + "description": "Metadata of this asset", + "additionalProperties": false, + "required": [ + "integration", + "title", + "description", + "compatibility", + "versions", + "author", + "references" + ], + "properties": { + "integration": { + "type": "string", + "description": "The integration this asset belongs to" + }, + "title": { + "type": "string", + "description": "Short and concise description of this asset" + }, + "description": { + "type": "string", + "description": "Long description of this asset, explaining what it does and how it works" + }, + "compatibility": { + "type": "string", + "description": "Description of the supported services and versions of the logs processed by this asset" + }, + "versions": { + "type": "array", + "description": "A list of the service versions supported", + "items": { + "type": "string" + } + }, + "author": { + "type": "object", + "description": "Author", + "additionalProperties": false, + "required": ["name", "date"], + "properties": { + "name": { + "type": "string", + "description": "Name/Organization" + }, + "email": { + "type": "string", + "description": "Email" + }, + "url": { + "type": "string", + "description": "URL linking to the author's website" + }, + "date": { + "type": "string", + "description": "Date of the author" + } + } + }, + "references": { + "type": "array", + "description": "References to external resources" + } + } + }, + "parents": { + "type": "array", + "description": "This asset will process events coming only from the specified parents", + "items": { + "type": "string" + } + }, + "check": { + "$ref": "#/definitions/_check" + }, + "allow": { + "$ref": "#/definitions/_check" + }, + "normalize": { + "type": "array", + "description": "Modify the event. All operations are performed in declaration order and on best effort, this stage is a list composed of blocks, where each block can be a map [map] or a conditional map [check, map].", + "minItems": 1, + "items": { + "$ref": "#/definitions/_normalizeBlock" + } + }, + "outputs": { + "type": "array", + "description": "Outputs of the asset. All outputs are performed in declaration order and on best effort, this stage is a list composed of specific outputs types.", + "minItems": 1 + }, + "definitions": { + "type": "object", + "description": "Variable definitions, used to define variables that can be reused in other parts of the asset", + "minProperties": 1 } }, - "parents": { - "field": { - "type": "text", - "initialValue": "" + "definitions": { + "_check": { + "oneOf": [ + { + "type": "array", + "description": "Check list, all conditions must be met in order to further process events with this asset, conditions are expressed as `field`: `condition`, where `field` is the field to check and `condition` can be a value, a reference or a conditional helper function.", + "items": { + "allOf": [ + { + "$ref": "fields.json#" + }, + { + "maxProperties": 1 + } + ] + }, + "minItems": 1 + }, + { + "type": "string", + "description": "Check conditional expression, the expression must be valuated to true in order to further process events with this asset" + } + ] + }, + "_parse": { + "type": "array", + "description": "Parse the event using the specified parser engine. Suports `logpar` parser.", + "minItems": 1, + "items": { + "type": "string" + } + }, + "_normalizeBlock": { + "type": "object", + "description": "Never shown", + "minItems": 1, + "additionalProperties": true, + "properties": { + "map": { + "description": "Modify fields on the event, an array composed of tuples with syntax `- field`: `value`, where `field` is the field to modify and `value` is the new value. If `value` is a function helper, it will be executed and the result will be used as new value if executed correctly. If `value` is a reference it will be used as new value only if the reference exists.", + "type": "array", + "minItems": 1, + "items": { + "allOf": [ + { + "$ref": "fields.json#" + }, + { + "maxProperties": 1 + } + ] + } + }, + "check": { + "$ref": "#/definitions/_check" + } + } } } } diff --git a/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts new file mode 100644 index 0000000000..8b8e46afa3 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts @@ -0,0 +1,57 @@ +const mapSpecTypeToInput = { + string: 'text', + array: 'text', +}; + +function createIter(fnItem) { + function iter(spec, parent = '') { + if (!spec.properties) { + return {}; + } + return Object.fromEntries( + Object.entries(spec.properties).reduce((accum, [key, value]) => { + const keyPath = [parent, key].filter(v => v).join('.'); + if (value.type === 'object') { + Object.entries(iter(value, keyPath)).forEach(entry => + accum.push(entry), + ); + } else if (value.type) { + accum.push([keyPath, fnItem({ key, keyPath, spec: value })]); + } + return accum; + }, []), + ); + } + + return iter; +} + +export const transfromAssetSpecToForm = createIter(({ spec }) => ({ + type: mapSpecTypeToInput[spec.type] || spec.type, + initialValue: '', + ...(spec.pattern + ? { + validate: value => + new RegExp(spec.pattern).test(value) + ? undefined + : `Value does not match the pattern: ${spec.pattern}`, + } + : {}), +})); + +export const transformAssetSpecToListTableColumn = function ( + spec, + fieldNamesMap?: { [key: string]: string } = {}, +) { + const t = createIter(({ spec }) => ({ + type: mapSpecTypeToInput[spec.type] || spec.type, + initialValue: '', + })); + + return Object.entries(t(spec)).map(([key]) => ({ + field: key, + name: fieldNamesMap?.[key] || key, + sortable: true, + show: true, + })); +}; From 043371b62977d43decf613f459ff752b64b04232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 11 Jul 2024 11:18:45 +0200 Subject: [PATCH 09/34] fix: useForm for arrayOf type --- plugins/main/public/components/common/form/hooks.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index e270978295..84d13e1587 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -119,11 +119,11 @@ export function enhanceFormFields( state, [...pathField, 'fields', _state.length], Object.entries(field.fields).reduce( - (accum, [key, { defaultValue }]) => ({ + (accum, [key, { defaultValue, initialValue }]) => ({ ...accum, [key]: { - currentValue: cloneDeep(defaultValue), - initialValue: cloneDeep(defaultValue), + currentValue: cloneDeep(initialValue), + initialValue: cloneDeep(initialValue), defaultValue: cloneDeep(defaultValue), }, }), From 2f09e046bd8c016af3d10124558f50feb8816856 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Thu, 11 Jul 2024 11:19:31 +0200 Subject: [PATCH 10/34] Add routing, db creation, actions buttons are replaced and imposter response from pathname change --- docker/imposter/lists/get_lists.json | 4 +- plugins/main/public/app-router.tsx | 6 - .../management/cdblists/views/list-editor.tsx | 178 ++++++++++-------- .../kvdbs/components/forms/addDatabase.tsx | 97 ++++++++++ .../kvdbs/components/keys/key-info.tsx | 7 +- .../kvdbs/components/kvdb-columns.tsx | 156 +++++++++++---- .../kvdbs/components/kvdb-overview.tsx | 53 ++---- .../public/components/kvdbs/kvdbs.tsx | 21 ++- .../public/components/kvdbs/spec.json | 27 +++ 9 files changed, 376 insertions(+), 173 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/spec.json diff --git a/docker/imposter/lists/get_lists.json b/docker/imposter/lists/get_lists.json index 1b6f7e0c0a..5ce5829763 100644 --- a/docker/imposter/lists/get_lists.json +++ b/docker/imposter/lists/get_lists.json @@ -3,14 +3,14 @@ "affected_items": [ { "filename": "test1", - "relative_dirname": "test/1", + "relative_dirname": "mockPath/test/1", "elements": "8", "date": "2024-06-03", "description": "test mock" }, { "filename": "test2", - "relative_dirname": "test/2", + "relative_dirname": "mockPath/test/version2", "elements": "76", "date": "2024-06-02", "description": "test 2 mock" diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index b2d26ec242..3054796091 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -161,12 +161,6 @@ export function Application(props) { GenericRequest={GenericRequest} TableWzAPI={TableWzAPI} WzRequest={WzRequest} - actionButtons={{ - manageFiles: ManageFiles, - addNewFileButton: AddNewFileButton, - addNewCdbListButton: AddNewCdbListButton, - uploadFilesButton: UploadFilesButton, - }} WzListEditor={WzListEditor} {...props} /> diff --git a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx index 88ba63855d..e792558a9d 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.tsx @@ -27,7 +27,11 @@ import { import { connect } from 'react-redux'; -import { resourceDictionary, ResourcesHandler, ResourcesConstants } from '../../common/resources-handler'; +import { + resourceDictionary, + ResourcesHandler, + ResourcesConstants, +} from '../../common/resources-handler'; import { getToasts } from '../../../../../../kibana-services'; @@ -61,7 +65,9 @@ class WzListEditor extends Component { } componentDidMount() { - const { listContent: { content } } = this.props; + const { + listContent: { content }, + } = this.props; const obj = this.contentToObject(content); this.items = { ...obj }; const items = this.contentToArray(obj); @@ -88,7 +94,7 @@ class WzListEditor extends Component { contentToObject(content) { const items = {}; const lines = content.split('\n'); - lines.forEach((line) => { + lines.forEach(line => { const split = line.startsWith('"') ? line.split('":') : line.split(':'); const key = split[0]; const value = split[1] || ''; @@ -102,8 +108,10 @@ class WzListEditor extends Component { */ itemsToRaw() { let raw = ''; - Object.keys(this.items).forEach((key) => { - raw = raw ? `${raw}\n${key}:${this.items[key]}` : `${key}:${this.items[key]}`; + Object.keys(this.items).forEach(key => { + raw = raw + ? `${raw}\n${key}:${this.items[key]}` + : `${key}:${this.items[key]}`; }); return raw; } @@ -116,7 +124,12 @@ class WzListEditor extends Component { async saveList(name, path, addingNew = false) { try { if (!name) { - this.showToast('warning', 'Invalid name', 'CDB list name cannot be empty', 3000); + this.showToast( + 'warning', + 'Invalid name', + 'CDB list name cannot be empty', + 3000, + ); return; } name = name.endsWith('.cdb') ? name.replace('.cdb', '') : name; @@ -127,7 +140,7 @@ class WzListEditor extends Component { 'warning', 'Please insert at least one item', 'Please insert at least one item, a CDB list cannot be empty', - 3000 + 3000, ); return; } @@ -137,7 +150,12 @@ class WzListEditor extends Component { const file = { name: name, content: raw, path: path }; this.props.updateListContent(file); this.setState({ showWarningRestart: true }); - this.showToast('success', 'Success', 'CBD List successfully created', 3000); + this.showToast( + 'success', + 'Success', + 'CBD List successfully created', + 3000, + ); } else { this.setState({ showWarningRestart: true }); this.showToast('success', 'Success', 'CBD List updated', 3000); @@ -180,44 +198,46 @@ class WzListEditor extends Component { }); }; - onChangeKey = (e) => { + onChangeKey = e => { this.setState({ addingKey: e.target.value, }); }; - onChangeValue = (e) => { + onChangeValue = e => { this.setState({ addingValue: e.target.value, }); }; - onChangeEditingValue = (e) => { + onChangeEditingValue = e => { this.setState({ editingValue: e.target.value, }); }; - onNewListNameChange = (e) => { + onNewListNameChange = e => { this.setState({ newListName: e.target.value, }); }; - getUpdatePermissions = (name) => { + getUpdatePermissions = name => { return [ { action: `${ResourcesConstants.LISTS}:update`, - resource: resourceDictionary[ResourcesConstants.LISTS].permissionResource(name), + resource: + resourceDictionary[ResourcesConstants.LISTS].permissionResource(name), }, ]; }; - getDeletePermissions = (name) => { + getDeletePermissions = name => { return [ { action: `${ResourcesConstants.LISTS}:delete`, - resource: resourceDictionary[ResourcesConstants.LISTS].permissionResource(name), + resource: + resourceDictionary[ResourcesConstants.LISTS].permissionResource(name), }, ]; }; @@ -234,7 +254,7 @@ class WzListEditor extends Component { {addingKey} key already exists , - 3000 + 3000, ); return; } @@ -282,12 +302,12 @@ class WzListEditor extends Component {

- + this.props.clearContent()} /> @@ -299,10 +319,10 @@ class WzListEditor extends Component { @@ -320,7 +340,7 @@ class WzListEditor extends Component { permissions={this.getUpdatePermissions(name)} fill isDisabled={items.length === 0} - iconType="save" + iconType='save' isLoading={this.state.isSaving} onClick={async () => this.saveList(name, path, newList)} > @@ -334,7 +354,7 @@ class WzListEditor extends Component { this.openAddEntry()} > Add new entry @@ -354,32 +374,32 @@ class WzListEditor extends Component { {this.state.isPopoverOpen && (
- + this.addItem()} @@ -388,7 +408,9 @@ class WzListEditor extends Component { - this.closeAddEntry()}>Close + this.closeAddEntry()}> + Close + @@ -409,22 +431,11 @@ class WzListEditor extends Component { - - - this.props.clearContent()} - /> - - {name} - + {name} - + {path} @@ -449,10 +460,10 @@ class WzListEditor extends Component { if (this.state.editing === item.key) { return ( ); } else { @@ -463,26 +474,26 @@ class WzListEditor extends Component { { name: 'Actions', align: 'left', - render: (item) => { + render: item => { if (this.state.editing === item.key) { return ( - + { this.setEditedValue(); }} - color="primary" + color='primary' /> - + this.setState({ editing: false })} - color="danger" + color='danger' /> @@ -491,9 +502,9 @@ class WzListEditor extends Component { return ( { @@ -502,16 +513,16 @@ class WzListEditor extends Component { editingValue: item.value, }); }} - color="primary" + color='primary' /> this.deleteItem(item.key)} - color="danger" + color='danger' /> ); @@ -522,7 +533,10 @@ class WzListEditor extends Component { } render() { - const { listContent: { name, path }, isLoading } = this.props; + const { + listContent: { name, path }, + isLoading, + } = this.props; const message = isLoading ? false : 'No results...'; @@ -546,7 +560,7 @@ class WzListEditor extends Component { value: name, }, ], - name + name, ); this.setState({ generatingCsv: false }); } catch (error) { @@ -563,7 +577,7 @@ class WzListEditor extends Component { getErrorOrchestrator().handleError(options); this.setState({ generatingCsv: false }); } - } + }; return ( @@ -572,15 +586,14 @@ class WzListEditor extends Component { {/* File name and back button when watching or editing a CDB list */} - {(!addingNew && this.renderTitle(name, path)) || - this.renderInputNameForNewCdbList()} + {this.renderTitle(name, path)} {/* This flex item is for separating between title and save button */} {/* Pop over to add new key and value */} {!addingNew && ( exportToCsv()} @@ -590,14 +603,23 @@ class WzListEditor extends Component { )} {!this.state.editing && - this.renderAddAndSave(listName, path, !addingNew, this.state.items)} + this.renderAddAndSave( + listName, + path, + !addingNew, + this.state.items, + )} {this.state.showWarningRestart && ( - + this.setState({ showWarningRestart: false })} - onRestartedError={() => this.setState({ showWarningRestart: true })} + onRestarted={() => + this.setState({ showWarningRestart: false }) + } + onRestartedError={() => + this.setState({ showWarningRestart: true }) + } /> )} @@ -608,7 +630,7 @@ class WzListEditor extends Component { { +const mapDispatchToProps = dispatch => { return { - updateWazuhNotReadyYet: (wazuhNotReadyYet) => + updateWazuhNotReadyYet: wazuhNotReadyYet => dispatch(updateWazuhNotReadyYet(wazuhNotReadyYet)), }; }; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx new file mode 100644 index 0000000000..fb7b0dcbf9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx @@ -0,0 +1,97 @@ +import React, { useMemo, useState } from 'react'; +import spec from '../../spec.json'; +import { transfromAssetSpecToForm } from '../../../rules/utils/transform-asset-spec'; +import { getServices } from '../../../../services'; +import { + EuiButton, + EuiButtonIcon, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiConfirmModal, +} from '@elastic/eui'; + +export const AddDatabase = () => { + const [isGoBackModalVisible, setIsGoBackModalVisible] = useState(false); + const InputForm = getServices().InputForm; + const useForm = getServices().useForm; + const specForm = useMemo(() => transfromAssetSpecToForm(spec), []); + const { fields } = useForm(specForm); + const navigationService = getServices().navigationService; + + let modal; + + if (isGoBackModalVisible) { + modal = ( + { + setIsGoBackModalVisible(false); + }} + onConfirm={async () => { + setIsGoBackModalVisible(false); + navigationService.getInstance().navigate('/engine/kvdbs'); + }} + cancelButtonText="No, don't do it" + confirmButtonText='Yes, do it' + defaultFocusedButton='confirm' + > +

Are you sure you'll come back? All changes will be lost.

+
+ ); + } + + return ( + <> + + + setIsGoBackModalVisible(true)} + /> + + + +

Create new database

+
+
+ + + Documentation + + + + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + + + + {}}> + Save + + +
+ + {Object.entries(fields).map(([name, formField]) => ( + + ))} + {modal} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx index 76a74a0622..3e69dda61e 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiFlyoutHeader, + EuiButtonIcon, EuiFlyoutBody, } from '@elastic/eui'; import { getServices } from '../../../../services'; @@ -12,11 +12,6 @@ export const KeyInfo = ({ keys, setKeysRequest }) => { return ( <> - - - View keys of this Database - - { + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const [getActualDB, setActualDB] = useState(null); + let modal; + + if (isDeleteModalVisible) { + modal = ( + { + setIsDeleteModalVisible(false); + }} + onConfirm={async () => { + await resourcesHandler.deleteFile( + getActualDB.filename || getActualDB.name, + ); + setIsDeleteModalVisible(false); + }} + cancelButtonText="No, don't do it" + confirmButtonText='Yes, do it' + defaultFocusedButton='confirm' + > +

Are you sure?

+
+ ); + } + const resourcesHandler = new ResourcesHandler('lists'); + + return [ + { + name: 'Open DB', + align: 'left', + render: item => { + return ( + <> + { + const result = await resourcesHandler.getFileContent( + item.filename, + item.relative_dirname, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + setKeysRequest(file); + setIsFlyoutVisible(true); + }} + /> + + ); + }, + }, + { + field: 'date', + name: 'Date', + align: 'left', + sortable: true, + }, + { + field: 'filename', + name: 'Name', + align: 'left', + sortable: true, + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + field: 'elements', + name: 'Elements', + align: 'left', + sortable: true, + }, + { + name: 'Actions', + align: 'left', + render: item => { + return ( + <> + {}} + /> + { + setActualDB(item); + setIsDeleteModalVisible(true); + }} + color='danger' + /> + { + ev.stopPropagation(); + }} + /> + {modal} + + ); + }, + }, + ]; +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx index 164919c7ea..84c1649ebe 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; import { columns } from './kvdb-columns'; -import { ResourcesHandler } from '../../../controllers/resources-handler'; -import { EuiFlyout } from '@elastic/eui'; +import { EuiFlyout, EuiButtonEmpty } from '@elastic/eui'; import { KeyInfo } from './keys/key-info'; import { getServices } from '../../../services'; export const KVDBTable = ({ TableWzAPI }) => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [getKeysRequest, setKeysRequest] = useState(false); - const resourcesHandler = new ResourcesHandler('lists'); const WzRequest = getServices().WzRequest; + const navigationService = getServices().navigationService; + const closeFlyout = () => setIsFlyoutVisible(false); const searchBarWQLOptions = { searchTermFields: ['filename', 'relative_dirname'], filterButtons: [ @@ -21,42 +21,16 @@ export const KVDBTable = ({ TableWzAPI }) => { ], }; - /** - * Columns and Rows properties - */ - const getRowProps = item => { - const { id, name } = item; - - return { - 'data-test-subj': `row-${id || name}`, - className: 'customRowClass', - onClick: async ev => { - const result = await resourcesHandler.getFileContent( - item.filename, - item.relative_dirname, - ); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - setKeysRequest(file); - setIsFlyoutVisible(true); - }, - }; - }; - - const closeFlyout = () => setIsFlyoutVisible(false); - - const ManageFiles = getServices().actionButtons.manageFiles; - const AddNewFileButton = getServices().actionButtons.addNewFileButton; - const AddNewCdbListButton = getServices().actionButtons.addNewCdbListButton; - const UploadFilesButton = getServices().actionButtons.uploadFilesButton; const actionButtons = [ - , - , - , - , + { + navigationService.getInstance().navigate('/engine/kvdbs/new'); + }} + > + Add new database + , ]; return ( @@ -65,7 +39,7 @@ export const KVDBTable = ({ TableWzAPI }) => { title='Databases' description='From here you can manage your keys databases.' actionButtons={actionButtons} - tableColumns={columns} + tableColumns={columns(setIsFlyoutVisible, setKeysRequest)} tableInitialSortingField='filename' searchBarWQL={{ options: searchBarWQLOptions, @@ -104,7 +78,6 @@ export const KVDBTable = ({ TableWzAPI }) => { isExpandable={true} downloadCsv showReload - rowProps={getRowProps} tablePageSizeOptions={[10, 25, 50, 100]} /> {isFlyoutVisible && ( diff --git a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx index 90dfb3a804..f243d3a1f6 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx @@ -1,16 +1,19 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPage } from '@elastic/eui'; +import { Route, Switch } from 'react-router-dom'; import { KVDBTable } from './components/kvdb-overview'; import { getServices } from '../../services'; +import { AddDatabase } from './components/forms/addDatabase'; -export const KVDBs = () => { +export const KVDBs = props => { return ( - - - - - - - + + + + + } + > + ); }; diff --git a/plugins/wazuh-engine/public/components/kvdbs/spec.json b/plugins/wazuh-engine/public/components/kvdbs/spec.json new file mode 100644 index 0000000000..77457898ae --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/spec.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "wazuh-asset.json", + "name": "schema/wazuh-asset/0", + "title": "Schema for Wazuh assets", + "type": "object", + "description": "Schema for Wazuh assets", + "additionalProperties": false, + "required": ["filename"], + "properties": { + "filename": { + "type": "string", + "description": "Name of the asset, short and concise name to identify this asset", + "pattern": "^[^/]+$" + }, + "description": { + "type": "string", + "description": "Description of the asset", + "pattern": "^.*$" + }, + "relative_dirname": { + "type": "string", + "description": "Relative directory name where the asset is located", + "pattern": "^[^/]+(/[^/]+)*$" + } + } +} From 0cf956bf4deae4ec1f43fbfdc608a022d3594813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 11 Jul 2024 11:24:30 +0200 Subject: [PATCH 11/34] feat(engine): enhance and fix rule asset components - fix bugs on TableIndexer component - replace icon on export formatted button of TableIndexer component - rename rule creation endpoint path from new to create - add spec merge for rule asset - move rule detail flyout to another component - enhance the form to create rule asset - create withGuard, withGuardAsync and withDataSourceFetch HOCs - add component to edit the rule asset - enhance render of rule and parents on Rule asset list - enhance utils to transform the spec --- .../common/tables/table-indexer.tsx | 19 +- .../components/rules/components/detail.tsx | 140 +++++++++ .../components/rules/components/form.tsx | 89 +++++- .../public/components/rules/hocs/index.ts | 2 + .../rules/hocs/with-data-source-fetch.tsx | 20 ++ .../components/rules/hocs/with-guard.tsx | 76 +++++ .../rules/pages/{new.tsx => create.tsx} | 0 .../public/components/rules/pages/edit.tsx | 61 ++++ .../public/components/rules/pages/list.tsx | 271 ++++++------------ .../public/components/rules/router.tsx | 11 +- .../public/components/rules/spec-merge.json | 87 ++++++ .../rules/utils/transform-asset-spec.ts | 46 +-- 12 files changed, 598 insertions(+), 224 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/rules/components/detail.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/hocs/index.ts create mode 100644 plugins/wazuh-engine/public/components/rules/hocs/with-data-source-fetch.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/hocs/with-guard.tsx rename plugins/wazuh-engine/public/components/rules/pages/{new.tsx => create.tsx} (100%) create mode 100644 plugins/wazuh-engine/public/components/rules/pages/edit.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/spec-merge.json diff --git a/plugins/main/public/components/common/tables/table-indexer.tsx b/plugins/main/public/components/common/tables/table-indexer.tsx index 04ab81652d..1ec44f61f8 100644 --- a/plugins/main/public/components/common/tables/table-indexer.tsx +++ b/plugins/main/public/components/common/tables/table-indexer.tsx @@ -100,7 +100,7 @@ export const TableIndexer = ({ onClickExportResults({ ...props, @@ -116,14 +116,6 @@ export const TableIndexer = ({ ); }; - const getRowProps = item => { - return { - onClick: () => { - setInspectedHit(item._document); - }, - }; - }; - return ( <> @@ -143,12 +135,7 @@ export const TableIndexer = ({ showSaveQuery={true} /> } - saveStateStorage={{ - system: 'localStorage', - key: 'wz-engine:rules-main', - }} showFieldSelector - // rowProps={getRowProps} fetchData={({ pagination, sorting, searchParams: { query } }) => { return fetchData({ query, @@ -156,8 +143,8 @@ export const TableIndexer = ({ sorting: { columns: [ { - id: sorting.field, - direction: sorting.direction, + id: sorting.sort.field, + direction: sorting.sort.direction, }, ], }, diff --git a/plugins/wazuh-engine/public/components/rules/components/detail.tsx b/plugins/wazuh-engine/public/components/rules/components/detail.tsx new file mode 100644 index 0000000000..5c8507ab3b --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/detail.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { getDashboard } from '../visualization'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public/'; +import { getCore } from '../../../plugin-services'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlexGroup, + EuiTitle, +} from '@elastic/eui'; +import { withDataSourceFetch } from '../hocs/with-data-source-fetch'; + +export const Detail = withDataSourceFetch( + ({ + data, + indexPattern, + onClose, + DocumentViewTableAndJson, + WazuhFlyoutDiscover, + PatternDataSource, + AppState, + PatternDataSourceFilterManager, + FILTER_OPERATOR, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + DashboardContainerByValueRenderer: DashboardByRenderer, + }) => { + // To be able to display a non-loaded rule, the component should fetch it before + // to display it + return ( + + + +

Details: {data._source.name}

+
+
+ + + [ + { + id: 'relationship', + name: 'Relationship', + content: () => ( + + ), + }, + { + id: 'events', + name: 'Events', + content: () => { + const filterManager = React.useMemo( + () => new FilterManager(getCore().uiSettings), + [], + ); + return ( + + // this.renderDiscoverExpandedRow(...args) + // } + /> + ); + }, + }, + ], + }} + tableProps={{ + onFilter(...rest) { + // TODO: implement using the dataSource + }, + onToggleColumn() { + // TODO: reseach if make sense the ability to toggle columns + }, + }} + /> + + +
+ ); + }, +); diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index 3a66d556e0..fe72ca0069 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -1,17 +1,98 @@ import React, { useMemo } from 'react'; import spec from '../spec.json'; +import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiButton, +} from '@elastic/eui'; + +const addSpaceBetween = (accum, item) => ( + <> + {accum} + + {item} + +); export const RuleForm = props => { const { useForm, InputForm } = props; - const specForm = useMemo(() => transfromAssetSpecToForm(spec), []); + const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); const { fields } = useForm(specForm); + const fieldsSplitted = Object.entries(fields) + // .filter(([name]) => name === 'parents') + .reduce( + (accum, item) => { + const [name] = item; + if (name.startsWith('metadata') || ['name', 'parents'].includes(name)) { + accum.attributes.push(item); + } else { + accum.steps.push(item); + } + return accum; + }, + { + attributes: [], + steps: [], + }, + ); + + const renderInput = ({ name, ...rest }) => { + if (rest.type !== 'arrayOf') { + return ; + } + + return ( + + + + +

{name}

+
+
+
+ {rest.fields.map(item => ( + <> + {Object.entries(item).map(([name, field]) => + renderInput({ ...field, name }), + )} + + ))} + Add +
+ ); + }; + return ( <> - {Object.entries(fields).map(([name, formField]) => ( - - ))} + {[ + { title: 'Attributes', fields: fieldsSplitted.attributes }, + { title: 'Steps', fields: fieldsSplitted.steps }, + ] + .map(params => ) + .reduce(addSpaceBetween)} ); }; + +const InputGroup = ({ title, fields, renderInput }) => ( + + + + +

{title}

+
+
+
+ {fields + .map(([name, formField]) => + renderInput({ ...formField, name: formField?._meta?.label || name }), + ) + .reduce(addSpaceBetween)} +
+); diff --git a/plugins/wazuh-engine/public/components/rules/hocs/index.ts b/plugins/wazuh-engine/public/components/rules/hocs/index.ts new file mode 100644 index 0000000000..4ad6a3532d --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/hocs/index.ts @@ -0,0 +1,2 @@ +export * from './with-data-source-fetch'; +export * from './with-guard'; diff --git a/plugins/wazuh-engine/public/components/rules/hocs/with-data-source-fetch.tsx b/plugins/wazuh-engine/public/components/rules/hocs/with-data-source-fetch.tsx new file mode 100644 index 0000000000..2af0ac8453 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/hocs/with-data-source-fetch.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { withGuardAsync } from './with-guard'; + +export const withDataSourceFetch = withGuardAsync( + ({ data }) => { + if (typeof data === 'string') { + return { + // TODO: fetch data and return + ok: true, + data: {}, + }; + } + return { + ok: false, + data: { data }, + }; + }, + () => <>, + () => <>, +); diff --git a/plugins/wazuh-engine/public/components/rules/hocs/with-guard.tsx b/plugins/wazuh-engine/public/components/rules/hocs/with-guard.tsx new file mode 100644 index 0000000000..e29d42ba7a --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/hocs/with-guard.tsx @@ -0,0 +1,76 @@ +/* + * Wazuh app - React HOC to render a component depending of if it fulfills a condition or the wrapped component instead + * 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 } from 'react'; + +export const withGuard = + (condition: (props: any) => boolean, ComponentFulfillsCondition) => + WrappedComponent => + props => { + return condition(props) ? ( + + ) : ( + + ); + }; + +export const withGuardAsync = + ( + condition: (props: any) => { ok: boolean; data: any }, + ComponentFulfillsCondition: React.JSX.Element, + ComponentLoadingResolution: null | React.JSX.Element = null, + ) => + WrappedComponent => + props => { + const [loading, setLoading] = useState(true); + const [fulfillsCondition, setFulfillsCondition] = useState({ + ok: false, + data: {}, + }); + + const execCondition = async () => { + try { + setLoading(true); + setFulfillsCondition({ ok: false, data: {} }); + setFulfillsCondition( + await condition({ ...props, check: execCondition }), + ); + } catch (error) { + setFulfillsCondition({ ok: false, data: { error } }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + execCondition(); + }, []); + + if (loading) { + return ComponentLoadingResolution ? ( + + ) : null; + } + + return fulfillsCondition.ok ? ( + + ) : ( + + ); + }; diff --git a/plugins/wazuh-engine/public/components/rules/pages/new.tsx b/plugins/wazuh-engine/public/components/rules/pages/create.tsx similarity index 100% rename from plugins/wazuh-engine/public/components/rules/pages/new.tsx rename to plugins/wazuh-engine/public/components/rules/pages/create.tsx diff --git a/plugins/wazuh-engine/public/components/rules/pages/edit.tsx b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx new file mode 100644 index 0000000000..0643d3ec96 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { RuleFileEditor } from '../components/file-editor'; + +export const EditRule = props => { + const [view, setView] = useState('visual-editor'); + + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ...(view === 'visual-editor' + ? [ + { + setView('file-editor'); + }} + iconType='apmTrace' + > + Switch to file editor + , + ] + : [ + { + setView('visual-editor'); + }} + iconType='apmTrace' + > + Switch to visual editor + , + ]), + ]; + + return ( + + {view === 'visual-editor' && } + {view === 'file-editor' && ( + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx index bf0fbe870c..0701d10128 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -1,39 +1,18 @@ import React, { useState } from 'react'; -import { getDashboard } from '../visualization'; -import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { FilterManager } from '../../../../../../src/plugins/data/public/'; -import { getCore } from '../../../plugin-services'; import { EuiButton, EuiContextMenu, EuiPopover, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlexGroup, - EuiTitle, + EuiButtonEmpty, } from '@elastic/eui'; import { Layout } from '../components'; import specification from '../spec.json'; import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; - -const specColumns = transformAssetSpecToListTableColumn(specification); +import { Detail } from '../components/detail'; export const RulesList = props => { - const { - TableIndexer, - RulesDataSource, - RulesDataSourceRepository, - DashboardContainerByValueRenderer: DashboardByRenderer, - WazuhFlyoutDiscover, - PatternDataSource, - AppState, - DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, - PatternDataSourceFilterManager, - FILTER_OPERATOR, - title, - DocumentViewTableAndJson, - } = props; + const { TableIndexer, RulesDataSource, RulesDataSourceRepository, title } = + props; const actions = [ { { - props.navigationService.getInstance().navigate('/engine/rules/new'); + props.navigationService.getInstance().navigate('/engine/rules/create'); }} > Create Rule @@ -58,59 +37,84 @@ export const RulesList = props => { const [inspectedHit, setInspectedHit] = React.useState(null); const [selectedItems, setSelectedItems] = useState([]); - const defaultColumns = [ - ...specColumns, - { - name: 'Actions', - show: true, - actions: [ - { - name: 'View', - isPrimary: true, - description: 'View details', - icon: 'eye', - type: 'icon', - onClick: ({ _document }) => { - setInspectedHit(_document); - }, - 'data-test-subj': 'action-view', + const defaultColumns = React.useMemo( + () => [ + ...transformAssetSpecToListTableColumn(specification, { + name: { + render: (prop, item) => ( + setInspectedHit(item._document)}> + {prop} + + ), }, - { - name: 'Edit', - isPrimary: true, - description: 'Edit', - icon: 'pencil', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); - }, - 'data-test-subj': 'action-edit', + parents: { + render: (prop, item) => + prop.map(parent => ( + { + // TODO: implement + // setInspectedHit(parent); + }} + > + {prop} + + )), }, - { - name: 'Export', - isPrimary: true, - description: 'Export file', - icon: 'exportAction', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); + }), + { + name: 'Actions', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: ({ _document }) => { + setInspectedHit(_document); + }, + 'data-test-subj': 'action-view', }, - 'data-test-subj': 'action-export', - }, - { - name: 'Delete', - isPrimary: true, - description: 'Delete file', - icon: 'trash', - type: 'icon', - onClick: (...rest) => { - console.log({ rest }); + { + name: 'Edit', + isPrimary: true, + description: 'Edit', + icon: 'pencil', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-edit', }, - 'data-test-subj': 'action-delete', - }, - ], - }, - ]; + { + name: 'Export', + isPrimary: true, + description: 'Export file', + icon: 'exportAction', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-export', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete file', + icon: 'trash', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-delete', + }, + ], + }, + ], + [], + ); return ( @@ -119,6 +123,7 @@ export const RulesList = props => { DataSourceRepository={RulesDataSourceRepository} tableProps={{ title: 'Catalog', + description: 'Manage the engine rules', tableColumns: defaultColumns, actionButtons: props => TableActions({ ...props, selectedItems }), tableSortingInitialField: defaultColumns[0].field, @@ -132,117 +137,21 @@ export const RulesList = props => { }, isSelectable: true, }, + saveStateStorage: { + system: 'localStorage', + key: 'wz-engine:rules-main', + }, }} exportCSVPrefixFilename='rules' onSetIndexPattern={setIndexPattern} /> {inspectedHit && ( - setInspectedHit(null)} size='m'> - - -

Details: {inspectedHit._source.name}

-
-
- - - [ - { - id: 'relationship', - name: 'Relationship', - content: () => ( - - ), - }, - { - id: 'events', - name: 'Events', - content: () => { - const filterManager = React.useMemo( - () => new FilterManager(getCore().uiSettings), - [], - ); - return ( - - // this.renderDiscoverExpandedRow(...args) - // } - /> - ); - }, - }, - ], - }} - tableProps={{ - onFilter(...rest) { - // TODO: implement using the dataSource - }, - onToggleColumn() { - // TODO: reseach if make sense the ability to toggle columns - }, - }} - /> - - -
+ setInspectedHit(null)} + data={inspectedHit} + indexPattern={indexPattern} + /> )}
); diff --git a/plugins/wazuh-engine/public/components/rules/router.tsx b/plugins/wazuh-engine/public/components/rules/router.tsx index 5faf80c15f..74d90709a4 100644 --- a/plugins/wazuh-engine/public/components/rules/router.tsx +++ b/plugins/wazuh-engine/public/components/rules/router.tsx @@ -1,14 +1,21 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { RulesList } from './pages/list'; -import { CreateRule } from './pages/new'; +import { CreateRule } from './pages/create'; +import { EditRule } from './pages/edit'; export const Rules = props => { return ( - + + { + return ; + }} + > } diff --git a/plugins/wazuh-engine/public/components/rules/spec-merge.json b/plugins/wazuh-engine/public/components/rules/spec-merge.json new file mode 100644 index 0000000000..b52aa7bc0e --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/spec-merge.json @@ -0,0 +1,87 @@ +{ + "metadata.author.name": { + "_meta": { + "label": "Author name" + } + }, + "metadata.author.date": { + "_meta": { + "label": "Author date" + } + }, + "metadata.author.url": { + "_meta": { + "label": "Author URL" + } + }, + "metadata.author.email": { + "_meta": { + "label": "Author email" + } + }, + "metadata.references": { + "type": "arrayOf", + "initialValue": [{ "reference": "" }], + "fields": { + "reference": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "References" + } + }, + "metadata.integration": { + "_meta": { + "label": "Integration" + } + }, + "metadata.title": { + "_meta": { + "label": "Title" + } + }, + "metadata.description": { + "_meta": { + "label": "Description" + } + }, + "metadata.compatibility": { + "_meta": { + "label": "Comptability" + } + }, + "metadata.versions": { + "type": "arrayOf", + "initialValue": [{ "version": "" }], + "fields": { + "version": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Versions" + } + }, + "parents": { + "type": "arrayOf", + "initialValue": [{ "parent": "" }], + "fields": { + "parent": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Parents" + } + }, + "normalize": { + "type": "textarea", + "_meta": { + "label": "Normalize" + } + } +} diff --git a/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts index 8b8e46afa3..6da81cbf35 100644 --- a/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts +++ b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts @@ -1,6 +1,5 @@ const mapSpecTypeToInput = { string: 'text', - array: 'text', }; function createIter(fnItem) { @@ -26,32 +25,37 @@ function createIter(fnItem) { return iter; } -export const transfromAssetSpecToForm = createIter(({ spec }) => ({ - type: mapSpecTypeToInput[spec.type] || spec.type, - initialValue: '', - ...(spec.pattern - ? { - validate: value => - new RegExp(spec.pattern).test(value) - ? undefined - : `Value does not match the pattern: ${spec.pattern}`, - } - : {}), -})); - -export const transformAssetSpecToListTableColumn = function ( +export const transfromAssetSpecToForm = function ( spec, - fieldNamesMap?: { [key: string]: string } = {}, + mergeProps?: { [key: string]: string } = {}, ) { - const t = createIter(({ spec }) => ({ + return createIter(({ keyPath, spec }) => ({ type: mapSpecTypeToInput[spec.type] || spec.type, initialValue: '', + ...(spec.pattern + ? { + validate: value => + new RegExp(spec.pattern).test(value) + ? undefined + : `Value does not match the pattern: ${spec.pattern}`, + } + : {}), + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), + }))(spec); +}; + +export const transformAssetSpecToListTableColumn = function ( + spec, + mergeProps?: { [key: string]: string } = {}, +) { + const t = createIter(({ keyPath }) => ({ + field: keyPath, + name: keyPath, + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), })); - return Object.entries(t(spec)).map(([key]) => ({ + return Object.entries(t(spec)).map(([key, value]) => ({ field: key, - name: fieldNamesMap?.[key] || key, - sortable: true, - show: true, + ...value, })); }; From 5bf2ebf66baf5632f52ba9ea8567f8a31dde1b35 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Fri, 12 Jul 2024 13:03:51 +0200 Subject: [PATCH 12/34] Create database feat updated --- .../kvdbs/components/forms/addDatabase.tsx | 13 ++++-- .../kvdbs/components/kvdb-columns.tsx | 43 ++++++------------- .../public/components/kvdbs/spec-merge.json | 17 ++++++++ 3 files changed, 41 insertions(+), 32 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/kvdbs/spec-merge.json diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx index fb7b0dcbf9..13b7089668 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx @@ -1,5 +1,7 @@ import React, { useMemo, useState } from 'react'; import spec from '../../spec.json'; +import specMerge from '../../spec-merge.json'; + import { transfromAssetSpecToForm } from '../../../rules/utils/transform-asset-spec'; import { getServices } from '../../../../services'; import { @@ -17,7 +19,7 @@ export const AddDatabase = () => { const [isGoBackModalVisible, setIsGoBackModalVisible] = useState(false); const InputForm = getServices().InputForm; const useForm = getServices().useForm; - const specForm = useMemo(() => transfromAssetSpecToForm(spec), []); + const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); const { fields } = useForm(specForm); const navigationService = getServices().navigationService; @@ -82,14 +84,19 @@ export const AddDatabase = () => {
- {}}> + { + /*TODO=> Add funcionallity*/ + }} + > Save
{Object.entries(fields).map(([name, formField]) => ( - + ))} {modal} diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx index 15d49064e7..1d8f522303 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx @@ -31,33 +31,6 @@ export const columns = (setIsFlyoutVisible, setKeysRequest) => { const resourcesHandler = new ResourcesHandler('lists'); return [ - { - name: 'Open DB', - align: 'left', - render: item => { - return ( - <> - { - const result = await resourcesHandler.getFileContent( - item.filename, - item.relative_dirname, - ); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - setKeysRequest(file); - setIsFlyoutVisible(true); - }} - /> - - ); - }, - }, { field: 'date', name: 'Date', @@ -96,8 +69,20 @@ export const columns = (setIsFlyoutVisible, setKeysRequest) => { <> {}} + iconType='eye' + onClick={async () => { + const result = await resourcesHandler.getFileContent( + item.filename, + item.relative_dirname, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + setKeysRequest(file); + setIsFlyoutVisible(true); + }} /> Date: Fri, 12 Jul 2024 14:39:09 +0200 Subject: [PATCH 13/34] feat(engine): refactor form based on group of inputs --- .../public/components/common/form/hooks.tsx | 1 + .../public/components/common/form/index.tsx | 10 +- .../public/common/form/group-form.tsx | 150 ++++++++++++++++++ .../wazuh-engine/public/common/form/index.ts | 1 + .../components/rules/components/form.tsx | 98 ++---------- .../public/components/rules/pages/list.tsx | 7 + .../public/components/rules/spec-merge.json | 42 +++-- .../rules/utils/transform-asset-spec.ts | 1 + 8 files changed, 211 insertions(+), 99 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/form/group-form.tsx create mode 100644 plugins/wazuh-engine/public/common/form/index.ts diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index 84d13e1587..4c9176c429 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -100,6 +100,7 @@ export function enhanceFormFields( [fieldKey]: { ...(field.type === 'arrayOf' ? { + ...field, type: field.type, fields: (() => { return restFieldState.fields.map((fieldState, index) => diff --git a/plugins/main/public/components/common/form/index.tsx b/plugins/main/public/components/common/form/index.tsx index 08ef95f94d..7aad033579 100644 --- a/plugins/main/public/components/common/form/index.tsx +++ b/plugins/main/public/components/common/form/index.tsx @@ -16,6 +16,7 @@ export interface InputFormProps { onChange: (event: React.ChangeEvent) => void; error?: string; label?: string | React.ReactNode; + labelAppend?: string | React.ReactNode; header?: | React.ReactNode | ((props: { value: any; error?: string }) => React.ReactNode); @@ -40,6 +41,7 @@ export const InputForm = ({ onChange, error, label, + labelAppend, header, footer, preInput, @@ -66,7 +68,13 @@ export const InputForm = ({ ); return label ? ( - + <> {typeof header === 'function' ? header({ value, error }) : header} diff --git a/plugins/wazuh-engine/public/common/form/group-form.tsx b/plugins/wazuh-engine/public/common/form/group-form.tsx new file mode 100644 index 0000000000..d62be0bc28 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/group-form.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { get } from 'lodash'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiButton, + EuiIcon, + EuiToolTip, +} from '@elastic/eui'; + +const addSpaceBetween = (accum, item) => ( + <> + {accum} + + {item} + +); + +const groupFieldsFormByGroup = (fields, groupKey) => + Object.entries(fields) + .map(([name, item]) => ({ ...item, name })) + .reduce((accum, item) => { + const groupName = get(item, groupKey) || '__default'; + + if (!accum[groupName]) { + accum[groupName] = []; + } + + accum[groupName].push(item); + return accum; + }, {}); + +export const FormGroup = ({ + specForm, + groupInputsByKey, + renderGroups, + onSave = props => { + console.log(props); + }, + ...props +}: { + specForm: any; + groupInputsByKey: string; + renderGroups: { title: string; groupKey: string }[]; + onSave: ({ fields, errors, changed }) => void; +}) => { + const { useForm, InputForm } = props; + const { fields, errors, changed } = useForm(specForm); + + const fieldsSplitted = groupFieldsFormByGroup(fields, groupInputsByKey); + + const renderInput = ({ name, ...rest }) => { + if (rest.type !== 'arrayOf') { + return ; + } + + return ( + + + + +

{name}

+
+
+
+ {rest.fields.map(item => ( + <> + {Object.entries(item).map(([name, field]) => + renderInput({ ...field, name }), + )} + + ))} + Add +
+ ); + }; + + return ( + <> + {renderGroups + .map(({ title, groupKey }) => ( + + )) + .reduce(addSpaceBetween)} + + + + onSave({ fields, errors, changed })} + > + Save + + + + + ); +}; + +const InputGroup = ({ title, fields, renderInput }) => ( + + + + +

{title}

+
+
+
+ {fields + .map(formField => + renderInput({ + ...formField, + name: ( + + ), + }), + ) + .reduce(addSpaceBetween)} +
+); + +const InputLabel = ({ + label, + description, +}: { + label: string; + description: string; +}) => ( + <> + {label} + {description && ( + + + + + + )} + +); diff --git a/plugins/wazuh-engine/public/common/form/index.ts b/plugins/wazuh-engine/public/common/form/index.ts new file mode 100644 index 0000000000..90b4e2df9d --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/index.ts @@ -0,0 +1 @@ +export * from './group-form'; diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index fe72ca0069..44b3776fa3 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -2,97 +2,23 @@ import React, { useMemo } from 'react'; import spec from '../spec.json'; import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiSpacer, - EuiButton, -} from '@elastic/eui'; - -const addSpaceBetween = (accum, item) => ( - <> - {accum} - - {item} - -); +import { FormGroup } from '../../../common/form'; export const RuleForm = props => { const { useForm, InputForm } = props; const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); - const { fields } = useForm(specForm); - - const fieldsSplitted = Object.entries(fields) - // .filter(([name]) => name === 'parents') - .reduce( - (accum, item) => { - const [name] = item; - if (name.startsWith('metadata') || ['name', 'parents'].includes(name)) { - accum.attributes.push(item); - } else { - accum.steps.push(item); - } - return accum; - }, - { - attributes: [], - steps: [], - }, - ); - - const renderInput = ({ name, ...rest }) => { - if (rest.type !== 'arrayOf') { - return ; - } - - return ( - - - - -

{name}

-
-
-
- {rest.fields.map(item => ( - <> - {Object.entries(item).map(([name, field]) => - renderInput({ ...field, name }), - )} - - ))} - Add -
- ); - }; return ( - <> - {[ - { title: 'Attributes', fields: fieldsSplitted.attributes }, - { title: 'Steps', fields: fieldsSplitted.steps }, - ] - .map(params => ) - .reduce(addSpaceBetween)} - + ); }; - -const InputGroup = ({ title, fields, renderInput }) => ( - - - - -

{title}

-
-
-
- {fields - .map(([name, formField]) => - renderInput({ ...formField, name: formField?._meta?.label || name }), - ) - .reduce(addSpaceBetween)} -
-); diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx index 0701d10128..acb577087e 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -46,6 +46,7 @@ export const RulesList = props => { {prop} ), + show: true, }, parents: { render: (prop, item) => @@ -61,6 +62,12 @@ export const RulesList = props => { )), }, + 'metadata.title': { + show: true, + }, + 'metadata.description': { + show: true, + }, }), { name: 'Actions', diff --git a/plugins/wazuh-engine/public/components/rules/spec-merge.json b/plugins/wazuh-engine/public/components/rules/spec-merge.json index b52aa7bc0e..43ce9f61ed 100644 --- a/plugins/wazuh-engine/public/components/rules/spec-merge.json +++ b/plugins/wazuh-engine/public/components/rules/spec-merge.json @@ -1,22 +1,32 @@ { + "name": { + "_meta": { + "label": "Name", + "groupForm": "metadata" + } + }, "metadata.author.name": { "_meta": { - "label": "Author name" + "label": "Name", + "groupForm": "author" } }, "metadata.author.date": { "_meta": { - "label": "Author date" + "label": "Date", + "groupForm": "author" } }, "metadata.author.url": { "_meta": { - "label": "Author URL" + "label": "URL", + "groupForm": "author" } }, "metadata.author.email": { "_meta": { - "label": "Author email" + "label": "Email", + "groupForm": "author" } }, "metadata.references": { @@ -29,27 +39,32 @@ } }, "_meta": { - "label": "References" + "label": "References", + "groupForm": "metadata" } }, "metadata.integration": { "_meta": { - "label": "Integration" + "label": "Integration", + "groupForm": "metadata" } }, "metadata.title": { "_meta": { - "label": "Title" + "label": "Title", + "groupForm": "metadata" } }, "metadata.description": { "_meta": { - "label": "Description" + "label": "Description", + "groupForm": "metadata" } }, "metadata.compatibility": { "_meta": { - "label": "Comptability" + "label": "Comptability", + "groupForm": "metadata" } }, "metadata.versions": { @@ -62,7 +77,8 @@ } }, "_meta": { - "label": "Versions" + "label": "Versions", + "groupForm": "metadata" } }, "parents": { @@ -75,13 +91,15 @@ } }, "_meta": { - "label": "Parents" + "label": "Parents", + "groupForm": "metadata" } }, "normalize": { "type": "textarea", "_meta": { - "label": "Normalize" + "label": "Normalize", + "groupForm": "steps" } } } diff --git a/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts index 6da81cbf35..866dc2048c 100644 --- a/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts +++ b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts @@ -32,6 +32,7 @@ export const transfromAssetSpecToForm = function ( return createIter(({ keyPath, spec }) => ({ type: mapSpecTypeToInput[spec.type] || spec.type, initialValue: '', + _spec: spec, ...(spec.pattern ? { validate: value => From f5804a27320b5f2b32c42e8eb66f500d40913de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 Jul 2024 14:43:21 +0200 Subject: [PATCH 14/34] remove(engine): unused script files related to rules sample data --- scripts/sample-data/rules/readme.md | 15 ---- scripts/sample-data/rules/rules.py | 96 ------------------------- scripts/sample-data/rules/template.json | 47 ------------ 3 files changed, 158 deletions(-) delete mode 100644 scripts/sample-data/rules/readme.md delete mode 100644 scripts/sample-data/rules/rules.py delete mode 100644 scripts/sample-data/rules/template.json diff --git a/scripts/sample-data/rules/readme.md b/scripts/sample-data/rules/readme.md deleted file mode 100644 index a9431cb6db..0000000000 --- a/scripts/sample-data/rules/readme.md +++ /dev/null @@ -1,15 +0,0 @@ -# Description - -TODO - -# Files - -TODO - -# Usage - -1. Install the requirements with `pip install -r requirements.txt`. For some Operating Systems it will fail and suggest a different way to install it (`sudo pacman -S python-xyz`, `sudo apt install python-xyz`, etc.). - If the package is not found in this way, we can install it running `pip install -r requirements.txt --break-system-packages` (It is recommended to avoid this option if possible) - -2. Run the script with `python3 script.py` -3. Follow the instructions that it will show on the console. diff --git a/scripts/sample-data/rules/rules.py b/scripts/sample-data/rules/rules.py deleted file mode 100644 index 070e24b8cd..0000000000 --- a/scripts/sample-data/rules/rules.py +++ /dev/null @@ -1,96 +0,0 @@ -import random -import sys -import os.path -import json -from opensearchpy import helpers - -index_template_file='template.json' - -def generate_document(params): - id_int = int(params["id"]) - top_limit = id_int - 1 if id_int - 1 > -1 else 0 - data = { - "ecs": {"version": "1.7.0"}, - "id": str(id_int), - "wazuh": { - "cluster": { - "name": "wazuh" - } - }, - "parent_id": None - } - if(bool(random.getrandbits(1))): - top_limit = id_int - 1 if id_int - 1 > -1 else 0 - data["parent_id"] = str(random.randint(0, top_limit)) - - return data - -def generate_documents(params): - for i in range(0, int(params["count"])): - yield generate_document({"id": i}) - -def get_params(ctx): - default_count='100' - default_index_name='wazuh-rules' - count = '' - while not count.isdigit(): - count = input_question(f'\nHow many events do you want to generate? [default={default_count}]\n', {"default_value": default_count}) - - index_name = input_question(f'\nEnter the index name [default={default_index_name}]: \n', {"default_value": default_index_name}) - - return { - "count": count, - "index_name": index_name - } - -def input_question(message, options = {}): - response = input(message) - - if(options["default_value"] and response == ''): - response = options["default_value"] - - return response - - - -def main(ctx): - ctx.logger.info('Getting configuration') - config = get_params(ctx) - print(f'Config {config}') - - client = ctx["client"] - - print(f'Checking existence of index [{config["index_name"]}]') - if client.indices.exists(config["index_name"]): - print(f'Index found [{config["index_name"]}]') - should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) - if should_delete_index == 'Y': - client.indices.delete(config["index_name"]) - print(f'Index [{config["index_name"]}] deleted') - else: - print(f'Index found [{config["index_name"]}] should be removed before create and insert documents') - sys.exit(1) - - print(f'Index not found [{config["index_name"]}]') - - if not os.path.exists(index_template_file): - print(f'Index template found [{index_template_file}]') - sys.exit(1) - - with open(index_template_file) as templateFile: - index_template = json.load(templateFile) - try: - client.indices.create(index=config["index_name"], body=index_template) - print(f'Index [{config["index_name"]}] created') - except Exception as e: - print('Error: {}'.format(e)) - sys.exit(1) - - generator = generate_documents(config) - helpers.bulk(client, generator, index=config['index_name']) - - -dataset = { - "name": "rules", - "run": main -} diff --git a/scripts/sample-data/rules/template.json b/scripts/sample-data/rules/template.json deleted file mode 100644 index 1a24d704fd..0000000000 --- a/scripts/sample-data/rules/template.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "mappings": { - "date_detection": false, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "id": { - "type": "keyword" - }, - "parent": { - "type": "keyword" - }, - "wazuh": { - "properties": { - "cluster": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": 1000 - } - }, - "refresh_interval": "2s" - } - } -} From 4f128c2e185fd44da8410bb571a6fca596cd6845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 Jul 2024 15:37:55 +0200 Subject: [PATCH 15/34] feat(engine): enhance sample data rule dataset --- scripts/sample-data/dataset/rules/main.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/sample-data/dataset/rules/main.py b/scripts/sample-data/dataset/rules/main.py index 43a6d9b64b..3d7c6b5ddd 100644 --- a/scripts/sample-data/dataset/rules/main.py +++ b/scripts/sample-data/dataset/rules/main.py @@ -8,6 +8,7 @@ index_template_file='template.json' default_count='10000' default_index_name='wazuh-rules' +asset_identifier='rule' def generate_document(params): id_int = int(params["id"]) @@ -16,12 +17,12 @@ def generate_document(params): # https://github.com/wazuh/wazuh/blob/11334-dev-new-wazuh-engine/src/engine/ruleset/schemas/wazuh-asset.json data = { "ecs": {"version": "1.7.0"}, - "name": f'rule/{str(id_int)}/0', + "name": f'{asset_identifier}/{str(id_int)}/0', "metadata": { - "title": f'Rule title {str(id_int)}', - "description": f'Rule description {str(id_int)}', + "title": f'Asset title {str(id_int)}', + "description": f'Asset description {str(id_int)}', "author": { - "name": f'Rule author name {str(id_int)}', + "name": f'Asset author name {str(id_int)}', "date": f'2024-07-04', "email": f'email@sample.com', "url": f'web.sample.com' @@ -45,7 +46,7 @@ def generate_document(params): } if(bool(random.getrandbits(1))): top_limit = id_int - 1 if id_int - 1 > 0 else 0 - data["parents"] = [f'rule/{str(random.randint(0, top_limit))}/0'] + data["parents"] = [f'{asset_identifier}/{str(random.randint(0, top_limit))}/0'] return data From cb18fec70132a2b336af126d82f574cfc55c082d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 Jul 2024 15:38:36 +0200 Subject: [PATCH 16/34] feat(engine): add filters and outputs sample data datasets --- scripts/sample-data/dataset/filters/main.py | 114 ++++++++++++++++++ .../sample-data/dataset/filters/template.json | 97 +++++++++++++++ scripts/sample-data/dataset/outputs/main.py | 114 ++++++++++++++++++ .../sample-data/dataset/outputs/template.json | 97 +++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 scripts/sample-data/dataset/filters/main.py create mode 100644 scripts/sample-data/dataset/filters/template.json create mode 100644 scripts/sample-data/dataset/outputs/main.py create mode 100644 scripts/sample-data/dataset/outputs/template.json diff --git a/scripts/sample-data/dataset/filters/main.py b/scripts/sample-data/dataset/filters/main.py new file mode 100644 index 0000000000..e8fc1fc6a7 --- /dev/null +++ b/scripts/sample-data/dataset/filters/main.py @@ -0,0 +1,114 @@ +import random +import sys +import os.path +import json +from opensearchpy import helpers +from pathlib import Path + +index_template_file='template.json' +default_count='10000' +default_index_name='wazuh-filters' +asset_identifier='filter' + +def generate_document(params): + id_int = int(params["id"]) + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + + # https://github.com/wazuh/wazuh/blob/11334-dev-new-wazuh-engine/src/engine/ruleset/schemas/wazuh-asset.json + data = { + "ecs": {"version": "1.7.0"}, + "name": f'{asset_identifier}/{str(id_int)}/0', + "metadata": { + "title": f'Asset title {str(id_int)}', + "description": f'Asset description {str(id_int)}', + "author": { + "name": f'Asset author name {str(id_int)}', + "date": f'2024-07-04', + "email": f'email@sample.com', + "url": f'web.sample.com' + }, + "compatibility": f'compatibiliy_device', + "integration": f'integration {random.choice(["1","2","3"])}', + "versions": ['0.1', '0.2'], + "references": [f'Ref 01', f'Ref 02'] + }, + "wazuh": { + "cluster": { + "name": "wazuh" + } + }, + "parents": [], + # "check": {}, # enhance + # "allow": {}, # enhance + # "normalize": {}, # enhance + # "outputs": [], # enhance + # "definitions": {} # enhance + } + if(bool(random.getrandbits(1))): + top_limit = id_int - 1 if id_int - 1 > 0 else 0 + data["parents"] = [f'{asset_identifier}/{str(random.randint(0, top_limit))}/0'] + + return data + +def generate_documents(params): + for i in range(0, int(params["count"])): + yield generate_document({"id": i}) + +def get_params(ctx): + count = '' + while not count.isdigit(): + count = input_question(f'How many documents do you want to generate? [default={default_count}]', {"default_value": default_count}) + + index_name = input_question(f'Enter the index name [default={default_index_name}]', {"default_value": default_index_name}) + + return { + "count": count, + "index_name": index_name + } + +def input_question(message, options = {}): + response = input(message) + + if(options["default_value"] and response == ''): + response = options["default_value"] + + return response + + +def main(ctx): + client = ctx["client"] + logger = ctx["logger"] + logger.info('Getting configuration') + + config = get_params(ctx) + logger.info(f'Config {config}') + + resolved_index_template_file = os.path.join(Path(__file__).parent, index_template_file) + + logger.info(f'Checking existence of index [{config["index_name"]}]') + if client.indices.exists(config["index_name"]): + logger.info(f'Index found [{config["index_name"]}]') + should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) + if should_delete_index == 'Y': + client.indices.delete(config["index_name"]) + logger.info(f'Index [{config["index_name"]}] deleted') + else: + logger.error(f'Index found [{config["index_name"]}] should be removed before create and insert documents') + sys.exit(1) + + if not os.path.exists(resolved_index_template_file): + logger.error(f'Index template found [{resolved_index_template_file}]') + sys.exit(1) + + with open(resolved_index_template_file) as templateFile: + index_template = json.load(templateFile) + try: + client.indices.create(index=config["index_name"], body=index_template) + logger.info(f'Index [{config["index_name"]}] created') + except Exception as e: + logger.error(f'Error: {e}') + sys.exit(1) + + helpers.bulk(client, generate_documents(config), index=config['index_name']) + logger.info(f'Data was indexed into [{config["index_name"]}]') + diff --git a/scripts/sample-data/dataset/filters/template.json b/scripts/sample-data/dataset/filters/template.json new file mode 100644 index 0000000000..c906e9d5cb --- /dev/null +++ b/scripts/sample-data/dataset/filters/template.json @@ -0,0 +1,97 @@ +{ + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "name": { + "type": "keyword" + }, + "metadata": { + "properties": { + "title": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "author": { + "properties": { + "name": { + "type": "keyword" + }, + "date": { + "type": "keyword" + }, + "email": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "compatibility": { + "type": "keyword" + }, + "integration": { + "type": "keyword" + }, + "versions": { + "type": "keyword" + }, + "references": { + "type": "keyword" + } + } + }, + "parents": { + "type": "keyword" + }, + "allow": { + "type": "keyword" + }, + "normalize": { + "type": "keyword" + }, + "outputs": { + "type": "keyword" + }, + "definitions": { + "type": "keyword" + }, + "wazuh": { + "properties": { + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": 1000 + } + }, + "refresh_interval": "2s" + } + } +} diff --git a/scripts/sample-data/dataset/outputs/main.py b/scripts/sample-data/dataset/outputs/main.py new file mode 100644 index 0000000000..24d82658f6 --- /dev/null +++ b/scripts/sample-data/dataset/outputs/main.py @@ -0,0 +1,114 @@ +import random +import sys +import os.path +import json +from opensearchpy import helpers +from pathlib import Path + +index_template_file='template.json' +default_count='10000' +default_index_name='wazuh-outputs' +asset_identifier='output' + +def generate_document(params): + id_int = int(params["id"]) + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + + # https://github.com/wazuh/wazuh/blob/11334-dev-new-wazuh-engine/src/engine/ruleset/schemas/wazuh-asset.json + data = { + "ecs": {"version": "1.7.0"}, + "name": f'{asset_identifier}/{str(id_int)}/0', + "metadata": { + "title": f'Asset title {str(id_int)}', + "description": f'Asset description {str(id_int)}', + "author": { + "name": f'Asset author name {str(id_int)}', + "date": f'2024-07-04', + "email": f'email@sample.com', + "url": f'web.sample.com' + }, + "compatibility": f'compatibiliy_device', + "integration": f'integration {random.choice(["1","2","3"])}', + "versions": ['0.1', '0.2'], + "references": [f'Ref 01', f'Ref 02'] + }, + "wazuh": { + "cluster": { + "name": "wazuh" + } + }, + "parents": [], + # "check": {}, # enhance + # "allow": {}, # enhance + # "normalize": {}, # enhance + # "outputs": [], # enhance + # "definitions": {} # enhance + } + if(bool(random.getrandbits(1))): + top_limit = id_int - 1 if id_int - 1 > 0 else 0 + data["parents"] = [f'{asset_identifier}/{str(random.randint(0, top_limit))}/0'] + + return data + +def generate_documents(params): + for i in range(0, int(params["count"])): + yield generate_document({"id": i}) + +def get_params(ctx): + count = '' + while not count.isdigit(): + count = input_question(f'How many documents do you want to generate? [default={default_count}]', {"default_value": default_count}) + + index_name = input_question(f'Enter the index name [default={default_index_name}]', {"default_value": default_index_name}) + + return { + "count": count, + "index_name": index_name + } + +def input_question(message, options = {}): + response = input(message) + + if(options["default_value"] and response == ''): + response = options["default_value"] + + return response + + +def main(ctx): + client = ctx["client"] + logger = ctx["logger"] + logger.info('Getting configuration') + + config = get_params(ctx) + logger.info(f'Config {config}') + + resolved_index_template_file = os.path.join(Path(__file__).parent, index_template_file) + + logger.info(f'Checking existence of index [{config["index_name"]}]') + if client.indices.exists(config["index_name"]): + logger.info(f'Index found [{config["index_name"]}]') + should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) + if should_delete_index == 'Y': + client.indices.delete(config["index_name"]) + logger.info(f'Index [{config["index_name"]}] deleted') + else: + logger.error(f'Index found [{config["index_name"]}] should be removed before create and insert documents') + sys.exit(1) + + if not os.path.exists(resolved_index_template_file): + logger.error(f'Index template found [{resolved_index_template_file}]') + sys.exit(1) + + with open(resolved_index_template_file) as templateFile: + index_template = json.load(templateFile) + try: + client.indices.create(index=config["index_name"], body=index_template) + logger.info(f'Index [{config["index_name"]}] created') + except Exception as e: + logger.error(f'Error: {e}') + sys.exit(1) + + helpers.bulk(client, generate_documents(config), index=config['index_name']) + logger.info(f'Data was indexed into [{config["index_name"]}]') + diff --git a/scripts/sample-data/dataset/outputs/template.json b/scripts/sample-data/dataset/outputs/template.json new file mode 100644 index 0000000000..c906e9d5cb --- /dev/null +++ b/scripts/sample-data/dataset/outputs/template.json @@ -0,0 +1,97 @@ +{ + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "name": { + "type": "keyword" + }, + "metadata": { + "properties": { + "title": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "author": { + "properties": { + "name": { + "type": "keyword" + }, + "date": { + "type": "keyword" + }, + "email": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "compatibility": { + "type": "keyword" + }, + "integration": { + "type": "keyword" + }, + "versions": { + "type": "keyword" + }, + "references": { + "type": "keyword" + } + } + }, + "parents": { + "type": "keyword" + }, + "allow": { + "type": "keyword" + }, + "normalize": { + "type": "keyword" + }, + "outputs": { + "type": "keyword" + }, + "definitions": { + "type": "keyword" + }, + "wazuh": { + "properties": { + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": 1000 + } + }, + "refresh_interval": "2s" + } + } +} From 64af5973dfbd459a7b35d8188fc42f640abe0ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 Jul 2024 16:28:45 +0200 Subject: [PATCH 17/34] feat(engine): add view of outputs asset --- plugins/main/public/app-router.tsx | 4 + .../pattern/outputs/data-source-repository.ts | 45 ++++ .../pattern/outputs/data-source.ts | 28 +++ .../data-source/pattern/outputs/index.ts | 2 + .../components/outputs/components/detail.tsx | 140 +++++++++++ .../outputs/components/file-editor.tsx | 50 ++++ .../components/outputs/components/form.tsx | 24 ++ .../components/outputs/components/index.ts | 1 + .../components/outputs/components/layout.tsx | 48 ++++ .../public/components/outputs/index.ts | 2 +- .../public/components/outputs/outputs.tsx | 5 - .../components/outputs/pages/create.tsx | 61 +++++ .../public/components/outputs/pages/edit.tsx | 61 +++++ .../public/components/outputs/pages/list.tsx | 223 +++++++++++++++++ .../public/components/outputs/router.tsx | 25 ++ .../public/components/outputs/spec-merge.json | 112 +++++++++ .../public/components/outputs/spec.json | 227 ++++++++++++++++++ .../outputs/utils/transform-asset-spec.ts | 62 +++++ .../components/outputs/visualization.ts | 54 +++++ .../rules/components/file-editor.tsx | 2 +- plugins/wazuh-engine/public/hocs/index.ts | 2 + .../public/hocs/with-data-source-fetch.tsx | 20 ++ .../wazuh-engine/public/hocs/with-guard.tsx | 76 ++++++ 23 files changed, 1267 insertions(+), 7 deletions(-) create mode 100644 plugins/main/public/components/common/data-source/pattern/outputs/data-source-repository.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/outputs/data-source.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/outputs/index.ts create mode 100644 plugins/wazuh-engine/public/components/outputs/components/detail.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/components/form.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/components/index.ts create mode 100644 plugins/wazuh-engine/public/components/outputs/components/layout.tsx delete mode 100644 plugins/wazuh-engine/public/components/outputs/outputs.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/pages/create.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/pages/edit.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/pages/list.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/router.tsx create mode 100644 plugins/wazuh-engine/public/components/outputs/spec-merge.json create mode 100644 plugins/wazuh-engine/public/components/outputs/spec.json create mode 100644 plugins/wazuh-engine/public/components/outputs/utils/transform-asset-spec.ts create mode 100644 plugins/wazuh-engine/public/components/outputs/visualization.ts create mode 100644 plugins/wazuh-engine/public/hocs/index.ts create mode 100644 plugins/wazuh-engine/public/hocs/with-data-source-fetch.tsx create mode 100644 plugins/wazuh-engine/public/hocs/with-guard.tsx diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 3054796091..fe75c2cc77 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -58,6 +58,8 @@ import { } from './controllers/management/components/management/common/actions-buttons'; import WzListEditor from './controllers/management/components/management/cdblists/views/list-editor.tsx'; import { DocumentViewTableAndJson } from './components/common/wazuh-discover/components/document-view-table-and-json'; +import { OutputsDataSource } from './components/common/data-source/pattern/outputs/data-source'; +import { OutputsDataSourceRepository } from './components/common/data-source/pattern/outputs/data-source-repository'; export function Application(props) { const dispatch = useDispatch(); const navigationService = NavigationService.getInstance(); @@ -153,6 +155,8 @@ export function Application(props) { FILTER_OPERATOR={FILTER_OPERATOR} RulesDataSource={RulesDataSource} RulesDataSourceRepository={RulesDataSourceRepository} + OutputsDataSource={OutputsDataSource} + OutputsDataSourceRepository={OutputsDataSourceRepository} useDocViewer={useDocViewer} DocViewer={DocViewer} DocumentViewTableAndJson={DocumentViewTableAndJson} diff --git a/plugins/main/public/components/common/data-source/pattern/outputs/data-source-repository.ts b/plugins/main/public/components/common/data-source/pattern/outputs/data-source-repository.ts new file mode 100644 index 0000000000..afae016175 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/outputs/data-source-repository.ts @@ -0,0 +1,45 @@ +import { PatternDataSourceRepository } from '../pattern-data-source-repository'; +import { tParsedIndexPattern } from '../../index'; + +export class OutputsDataSourceRepository extends PatternDataSourceRepository { + constructor() { + super(); + } + + async get(id: string) { + const dataSource = await super.get(id); + if (this.validate(dataSource)) { + return dataSource; + } else { + throw new Error('Outputs index pattern not found'); + } + } + + async getAll() { + const indexs = await super.getAll(); + return indexs.filter(this.validate); + } + + validate(dataSource): boolean { + // check if the dataSource has the id or the title have the vulnerabilities word + const fieldsToCheck = ['id', 'attributes.title']; + // must check in the object and the attributes + for (const field of fieldsToCheck) { + if ( + dataSource[field] && + dataSource[field].toLowerCase().includes('outputs') + ) { + return true; + } + } + return false; + } + + getDefault() { + return Promise.resolve(null); + } + + setDefault(dataSource: tParsedIndexPattern) { + return; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/outputs/data-source.ts b/plugins/main/public/components/common/data-source/pattern/outputs/data-source.ts new file mode 100644 index 0000000000..a11190bb51 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/outputs/data-source.ts @@ -0,0 +1,28 @@ +import { + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, +} from '../../../../../../common/constants'; +import { tFilter, PatternDataSourceFilterManager } from '../../index'; +import { PatternDataSource } from '../pattern-data-source'; + +export class OutputsDataSource extends PatternDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getFetchFilters(): Filter[] { + return []; + } + + getFixedFilters(): tFilter[] { + return [...this.getClusterManagerFilters()]; + } + + getClusterManagerFilters() { + return PatternDataSourceFilterManager.getClusterManagerFilters( + this.id, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, + ); + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/outputs/index.ts b/plugins/main/public/components/common/data-source/pattern/outputs/index.ts new file mode 100644 index 0000000000..cae336db28 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/outputs/index.ts @@ -0,0 +1,2 @@ +export * from './data-source'; +export * from './data-source-repository'; diff --git a/plugins/wazuh-engine/public/components/outputs/components/detail.tsx b/plugins/wazuh-engine/public/components/outputs/components/detail.tsx new file mode 100644 index 0000000000..dfd50496d1 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/detail.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { getDashboard } from '../visualization'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public/'; +import { getCore } from '../../../plugin-services'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlexGroup, + EuiTitle, +} from '@elastic/eui'; +import { withDataSourceFetch } from '../../../hocs/with-data-source-fetch'; + +export const Detail = withDataSourceFetch( + ({ + data, + indexPattern, + onClose, + DocumentViewTableAndJson, + WazuhFlyoutDiscover, + PatternDataSource, + AppState, + PatternDataSourceFilterManager, + FILTER_OPERATOR, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + DashboardContainerByValueRenderer: DashboardByRenderer, + }) => { + // To be able to display a non-loaded rule, the component should fetch it before + // to display it + return ( + + + +

Details: {data._source.name}

+
+
+ + + [ + { + id: 'relationship', + name: 'Relationship', + content: () => ( + + ), + }, + { + id: 'events', + name: 'Events', + content: () => { + const filterManager = React.useMemo( + () => new FilterManager(getCore().uiSettings), + [], + ); + return ( + + // this.renderDiscoverExpandedRow(...args) + // } + /> + ); + }, + }, + ], + }} + tableProps={{ + onFilter(...rest) { + // TODO: implement using the dataSource + }, + onToggleColumn() { + // TODO: reseach if make sense the ability to toggle columns + }, + }} + /> + + +
+ ); + }, +); diff --git a/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx b/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx new file mode 100644 index 0000000000..f00f072ee3 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { + EuiCodeEditor, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; + +export const RuleFileEditor = ({ + initialContent = '', + isEditable = false, + ...props +}) => { + const { useForm, InputForm } = props; + const { fields } = useForm({ + filename: { type: 'text', initialValue: '' }, + content: { type: 'text', initialValue: initialContent || '' }, + }); + + return ( + <> + + + + + + {isEditable && ( + {}}> + Save + + )} + + + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/components/form.tsx b/plugins/wazuh-engine/public/components/outputs/components/form.tsx new file mode 100644 index 0000000000..44b3776fa3 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/form.tsx @@ -0,0 +1,24 @@ +import React, { useMemo } from 'react'; +import spec from '../spec.json'; +import specMerge from '../spec-merge.json'; +import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; +import { FormGroup } from '../../../common/form'; + +export const RuleForm = props => { + const { useForm, InputForm } = props; + const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); + + return ( + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/components/index.ts b/plugins/wazuh-engine/public/components/outputs/components/index.ts new file mode 100644 index 0000000000..5d15fe1b3c --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/index.ts @@ -0,0 +1 @@ +export * from './layout'; diff --git a/plugins/wazuh-engine/public/components/outputs/components/layout.tsx b/plugins/wazuh-engine/public/components/outputs/components/layout.tsx new file mode 100644 index 0000000000..da302cbb75 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/layout.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, +} from '@elastic/eui'; + +export const Layout = ({ + title, + children, + actions, +}: { + title: React.ReactNode; + children: React.ReactNode; + actions?: any; +}) => { + return ( + <> + + + +

{title}

+
+
+ {actions && ( + + + + )} +
+ +
{children}
+ + ); +}; + +const ViewActions = ({ actions }) => { + return Array.isArray(actions) ? ( + + {actions.map(action => ( + {action} + ))} + + ) : ( + actions() + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/index.ts b/plugins/wazuh-engine/public/components/outputs/index.ts index e41beb42b1..0a6d8e8a96 100644 --- a/plugins/wazuh-engine/public/components/outputs/index.ts +++ b/plugins/wazuh-engine/public/components/outputs/index.ts @@ -1 +1 @@ -export { Outputs } from './outputs'; +export { Outputs } from './router'; diff --git a/plugins/wazuh-engine/public/components/outputs/outputs.tsx b/plugins/wazuh-engine/public/components/outputs/outputs.tsx deleted file mode 100644 index 31bbcf9db9..0000000000 --- a/plugins/wazuh-engine/public/components/outputs/outputs.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const Outputs = () => { - return <>Outputs; -}; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx new file mode 100644 index 0000000000..139adfffea --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { RuleFileEditor } from '../components/file-editor'; + +export const Create = props => { + const [view, setView] = useState('visual-editor'); + + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ...(view === 'visual-editor' + ? [ + { + setView('file-editor'); + }} + iconType='apmTrace' + > + Switch to file editor + , + ] + : [ + { + setView('visual-editor'); + }} + iconType='apmTrace' + > + Switch to visual editor + , + ]), + ]; + + return ( + + {view === 'visual-editor' && } + {view === 'file-editor' && ( + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx new file mode 100644 index 0000000000..0bcdfc1c28 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { RuleFileEditor } from '../components/file-editor'; + +export const Edit = props => { + const [view, setView] = useState('visual-editor'); + + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ...(view === 'visual-editor' + ? [ + { + setView('file-editor'); + }} + iconType='apmTrace' + > + Switch to file editor + , + ] + : [ + { + setView('visual-editor'); + }} + iconType='apmTrace' + > + Switch to visual editor + , + ]), + ]; + + return ( + + {view === 'visual-editor' && } + {view === 'file-editor' && ( + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/list.tsx b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx new file mode 100644 index 0000000000..2d7a8cf2eb --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx @@ -0,0 +1,223 @@ +import React, { useState } from 'react'; +import { + EuiButton, + EuiContextMenu, + EuiPopover, + EuiButtonEmpty, +} from '@elastic/eui'; +import { Layout } from '../components'; +import specification from '../spec.json'; +import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; +import { Detail } from '../components/detail'; + +export const List = props => { + const { + TableIndexer, + OutputsDataSource, + OutputsDataSourceRepository, + title, + } = props; + + const actions = [ + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + { + props.navigationService + .getInstance() + .navigate('/engine/outputs/create'); + }} + > + Create Output + , + ]; + + const [indexPattern, setIndexPattern] = React.useState(null); + const [inspectedHit, setInspectedHit] = React.useState(null); + const [selectedItems, setSelectedItems] = useState([]); + + const defaultColumns = React.useMemo( + () => [ + ...transformAssetSpecToListTableColumn(specification, { + name: { + render: (prop, item) => ( + setInspectedHit(item._document)}> + {prop} + + ), + show: true, + }, + parents: { + render: (prop, item) => + prop.map(parent => ( + { + // TODO: implement + // setInspectedHit(parent); + }} + > + {prop} + + )), + }, + 'metadata.title': { + show: true, + }, + 'metadata.description': { + show: true, + }, + }), + { + name: 'Actions', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: ({ _document }) => { + setInspectedHit(_document); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit', + icon: 'pencil', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-edit', + }, + { + name: 'Export', + isPrimary: true, + description: 'Export file', + icon: 'exportAction', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-export', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete file', + icon: 'trash', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-delete', + }, + ], + }, + ], + [], + ); + + return ( + + TableActions({ ...props, selectedItems }), + tableSortingInitialField: defaultColumns[0].field, + tableSortingInitialDirection: 'asc', + tableProps: { + itemId: 'name', + selection: { + onSelectionChange: item => { + setSelectedItems(item); + }, + }, + isSelectable: true, + }, + saveStateStorage: { + system: 'localStorage', + key: 'wz-engine:outputs-main', + }, + }} + exportCSVPrefixFilename='outputs' + onSetIndexPattern={setIndexPattern} + /> + {inspectedHit && ( + setInspectedHit(null)} + data={inspectedHit} + indexPattern={indexPattern} + /> + )} + + ); +}; + +const TableActions = ({ selectedItems }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(state => !state)} + > + Actions + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + { + /* TODO: implement */ + }, + }, + { isSeparator: true }, + { + name: 'Delete', + disabled: selectedItems.length === 0, + 'data-test-subj': 'deleteAction', + onClick: () => { + /* TODO: implement */ + }, + }, + ], + }, + ]} + /> + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/router.tsx b/plugins/wazuh-engine/public/components/outputs/router.tsx new file mode 100644 index 0000000000..643556a3dd --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/router.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { List } from './pages/list'; +import { Create } from './pages/create'; +import { Edit } from './pages/edit'; + +export const Outputs = props => { + return ( + + + + + { + return ; + }} + > + } + > + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/spec-merge.json b/plugins/wazuh-engine/public/components/outputs/spec-merge.json new file mode 100644 index 0000000000..caa7373785 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/spec-merge.json @@ -0,0 +1,112 @@ +{ + "name": { + "_meta": { + "label": "Name", + "groupForm": "metadata" + } + }, + "metadata.author.name": { + "_meta": { + "label": "Name", + "groupForm": "author" + } + }, + "metadata.author.date": { + "_meta": { + "label": "Date", + "groupForm": "author" + } + }, + "metadata.author.url": { + "_meta": { + "label": "URL", + "groupForm": "author" + } + }, + "metadata.author.email": { + "_meta": { + "label": "Email", + "groupForm": "author" + } + }, + "metadata.references": { + "type": "arrayOf", + "initialValue": [{ "reference": "" }], + "fields": { + "reference": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "References", + "groupForm": "metadata" + } + }, + "metadata.integration": { + "_meta": { + "label": "Integration", + "groupForm": "metadata" + } + }, + "metadata.title": { + "_meta": { + "label": "Title", + "groupForm": "metadata" + } + }, + "metadata.description": { + "_meta": { + "label": "Description", + "groupForm": "metadata" + } + }, + "metadata.compatibility": { + "_meta": { + "label": "Comptability", + "groupForm": "metadata" + } + }, + "metadata.versions": { + "type": "arrayOf", + "initialValue": [{ "version": "" }], + "fields": { + "version": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Versions", + "groupForm": "metadata" + } + }, + "parents": { + "type": "arrayOf", + "initialValue": [{ "parent": "" }], + "fields": { + "parent": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Parents", + "groupForm": "metadata" + } + }, + "check": { + "type": "textarea", + "_meta": { + "label": "Checks", + "groupForm": "steps" + } + }, + "outputs": { + "type": "textarea", + "_meta": { + "label": "Outputs", + "groupForm": "steps" + } + } +} diff --git a/plugins/wazuh-engine/public/components/outputs/spec.json b/plugins/wazuh-engine/public/components/outputs/spec.json new file mode 100644 index 0000000000..b7ff85f54c --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/spec.json @@ -0,0 +1,227 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "wazuh-asset.json", + "name": "schema/wazuh-asset/0", + "title": "Schema for Wazuh assets", + "type": "object", + "description": "Schema for Wazuh assets", + "additionalProperties": false, + "required": ["name", "metadata"], + "anyOf": [ + { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["parse"] + }, + { + "required": ["normalize"] + } + ], + "not": { + "anyOf": [ + { + "required": ["allow"] + }, + { + "required": ["outputs"] + } + ] + } + }, + { + "required": ["outputs"], + "not": { + "anyOf": [ + { + "required": ["normalize"] + }, + { + "required": ["parse"] + } + ] + } + }, + { + "required": ["allow", "parents"], + "not": { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["normalize"] + } + ] + } + } + ], + "patternProperties": { + "parse\\|\\S+": { + "$ref": "#/definitions/_parse" + } + }, + "properties": { + "name": { + "type": "string", + "description": "Name of the asset, short and concise name to identify this asset", + "pattern": "^output/[^/]+/[^/]+$" + }, + "metadata": { + "type": "object", + "description": "Metadata of this asset", + "additionalProperties": false, + "required": [ + "integration", + "title", + "description", + "compatibility", + "versions", + "author", + "references" + ], + "properties": { + "integration": { + "type": "string", + "description": "The integration this asset belongs to" + }, + "title": { + "type": "string", + "description": "Short and concise description of this asset" + }, + "description": { + "type": "string", + "description": "Long description of this asset, explaining what it does and how it works" + }, + "compatibility": { + "type": "string", + "description": "Description of the supported services and versions of the logs processed by this asset" + }, + "versions": { + "type": "array", + "description": "A list of the service versions supported", + "items": { + "type": "string" + } + }, + "author": { + "type": "object", + "description": "Author", + "additionalProperties": false, + "required": ["name", "date"], + "properties": { + "name": { + "type": "string", + "description": "Name/Organization" + }, + "email": { + "type": "string", + "description": "Email" + }, + "url": { + "type": "string", + "description": "URL linking to the author's website" + }, + "date": { + "type": "string", + "description": "Date of the author" + } + } + }, + "references": { + "type": "array", + "description": "References to external resources" + } + } + }, + "parents": { + "type": "array", + "description": "This asset will process events coming only from the specified parents", + "items": { + "type": "string" + } + }, + "allow": { + "$ref": "#/definitions/_check" + }, + "check": { + "type": "array", + "description": "Modify the event. All operations are performed in declaration order and on best effort, this stage is a list composed of blocks, where each block can be a map [map] or a conditional map [check, map].", + "minItems": 1, + "items": { + "$ref": "#/definitions/_normalizeBlock" + } + }, + "outputs": { + "type": "array", + "description": "Outputs of the asset. All outputs are performed in declaration order and on best effort, this stage is a list composed of specific outputs types.", + "minItems": 1 + }, + "definitions": { + "type": "object", + "description": "Variable definitions, used to define variables that can be reused in other parts of the asset", + "minProperties": 1 + } + }, + "definitions": { + "_check": { + "oneOf": [ + { + "type": "array", + "description": "Check list, all conditions must be met in order to further process events with this asset, conditions are expressed as `field`: `condition`, where `field` is the field to check and `condition` can be a value, a reference or a conditional helper function.", + "items": { + "allOf": [ + { + "$ref": "fields.json#" + }, + { + "maxProperties": 1 + } + ] + }, + "minItems": 1 + }, + { + "type": "string", + "description": "Check conditional expression, the expression must be valuated to true in order to further process events with this asset" + } + ] + }, + "_parse": { + "type": "array", + "description": "Parse the event using the specified parser engine. Suports `logpar` parser.", + "minItems": 1, + "items": { + "type": "string" + } + }, + "_normalizeBlock": { + "type": "object", + "description": "Never shown", + "minItems": 1, + "additionalProperties": true, + "properties": { + "map": { + "description": "Modify fields on the event, an array composed of tuples with syntax `- field`: `value`, where `field` is the field to modify and `value` is the new value. If `value` is a function helper, it will be executed and the result will be used as new value if executed correctly. If `value` is a reference it will be used as new value only if the reference exists.", + "type": "array", + "minItems": 1, + "items": { + "allOf": [ + { + "$ref": "fields.json#" + }, + { + "maxProperties": 1 + } + ] + } + }, + "check": { + "$ref": "#/definitions/_check" + } + } + } + } +} diff --git a/plugins/wazuh-engine/public/components/outputs/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/outputs/utils/transform-asset-spec.ts new file mode 100644 index 0000000000..866dc2048c --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/utils/transform-asset-spec.ts @@ -0,0 +1,62 @@ +const mapSpecTypeToInput = { + string: 'text', +}; + +function createIter(fnItem) { + function iter(spec, parent = '') { + if (!spec.properties) { + return {}; + } + return Object.fromEntries( + Object.entries(spec.properties).reduce((accum, [key, value]) => { + const keyPath = [parent, key].filter(v => v).join('.'); + if (value.type === 'object') { + Object.entries(iter(value, keyPath)).forEach(entry => + accum.push(entry), + ); + } else if (value.type) { + accum.push([keyPath, fnItem({ key, keyPath, spec: value })]); + } + return accum; + }, []), + ); + } + + return iter; +} + +export const transfromAssetSpecToForm = function ( + spec, + mergeProps?: { [key: string]: string } = {}, +) { + return createIter(({ keyPath, spec }) => ({ + type: mapSpecTypeToInput[spec.type] || spec.type, + initialValue: '', + _spec: spec, + ...(spec.pattern + ? { + validate: value => + new RegExp(spec.pattern).test(value) + ? undefined + : `Value does not match the pattern: ${spec.pattern}`, + } + : {}), + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), + }))(spec); +}; + +export const transformAssetSpecToListTableColumn = function ( + spec, + mergeProps?: { [key: string]: string } = {}, +) { + const t = createIter(({ keyPath }) => ({ + field: keyPath, + name: keyPath, + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), + })); + + return Object.entries(t(spec)).map(([key, value]) => ({ + field: key, + ...value, + })); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/visualization.ts b/plugins/wazuh-engine/public/components/outputs/visualization.ts new file mode 100644 index 0000000000..bd58c70ec6 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/visualization.ts @@ -0,0 +1,54 @@ +const getVisualization = (indexPatternId: string, ruleID: string) => { + return { + id: 'Wazuh-rules-vega', + title: `Child outputs of ${ruleID}`, + type: 'vega', + params: { + spec: `{\n $schema: https://vega.github.io/schema/vega/v5.json\n description: An example of Cartesian layouts for a node-link diagram of hierarchical data.\n padding: 5\n signals: [\n {\n name: labels\n value: true\n bind: {\n input: checkbox\n }\n }\n {\n name: layout\n value: tidy\n bind: {\n input: radio\n options: [\n tidy\n cluster\n ]\n }\n }\n {\n name: links\n value: diagonal\n bind: {\n input: select\n options: [\n line\n curve\n diagonal\n orthogonal\n ]\n }\n }\n {\n name: separation\n value: false\n bind: {\n input: checkbox\n }\n }\n ]\n data: [\n {\n name: tree\n url: {\n /*\n An object instead of a string for the "url" param is treated as an OpenSearch query. Anything inside this object is not part of the Vega language, but only understood by OpenSearch Dashboards and OpenSearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\n\n OpenSearch Dashboards has a special handling for the fields surrounded by "%". They are processed before the the query is sent to OpenSearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\n */\n\n // Apply dashboard context filters when set\n // %context%: true\n // Filter the time picker (upper right corner) with this field\n // %timefield%: @timestamp\n\n /*\n See .search() documentation for : https://opensearch.org/docs/latest/clients/javascript/\n */\n\n // Which index to search\n index: wazuh-rules\n\n\n // If "data_source.enabled: true", optionally set the data source name to query from (omit field if querying from local cluster)\n // data_source_name: Example US Cluster\n\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\n body: {\n query: {\n bool: {\n should: [\n {\n match_phrase: {\n name: ${ruleID}\n }\n }\n {\n match_phrase: {\n parents: ${ruleID}\n }\n }\n ]\n minimum_should_match: 1\n }\n }\n /* query: {\n match_all: {\n }\n } */\n size: 1000\n }\n }\n /*\n OpenSearch will return results in this format:\n\n aggregations: {\n time_buckets: {\n buckets: [\n {\n key_as_string: 2015-11-30T22:00:00.000Z\n key: 1448920800000\n doc_count: 0\n },\n {\n key_as_string: 2015-11-30T23:00:00.000Z\n key: 1448924400000\n doc_count: 0\n }\n ...\n ]\n }\n }\n\n For our graph, we only need the list of bucket values. Use the format.property to discard everything else.\n */\n format: {\n property: hits.hits\n }\n transform: [\n {\n type: stratify\n key: _source.id\n parentKey: _source.parents\n }\n {\n type: tree\n method: {\n signal: layout\n }\n size: [\n {\n signal: height\n }\n {\n signal: width - 100\n }\n ]\n separation: {\n signal: separation\n }\n as: [\n y\n x\n depth\n children\n ]\n }\n ]\n }\n {\n name: links\n source: tree\n transform: [\n {\n type: treelinks\n }\n {\n type: linkpath\n orient: horizontal\n shape: {\n signal: links\n }\n }\n ]\n }\n ]\n scales: [\n {\n name: color\n type: linear\n range: {\n scheme: magma\n }\n domain: {\n data: tree\n field: depth\n }\n zero: true\n }\n ]\n marks: [\n {\n type: path\n from: {\n data: links\n }\n encode: {\n update: {\n path: {\n field: path\n }\n stroke: {\n value: "#ccc"\n }\n }\n }\n }\n {\n type: symbol\n from: {\n data: tree\n }\n encode: {\n enter: {\n size: {\n value: 100\n }\n stroke: {\n value: "#fff"\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n fill: {\n scale: color\n field: depth\n }\n }\n }\n }\n {\n type: text\n from: {\n data: tree\n }\n encode: {\n enter: {\n text: {\n field: _source.id\n }\n fontSize: {\n value: 15\n }\n baseline: {\n value: middle\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n dx: {\n signal: datum.children ? -7 : 7\n }\n align: {\n signal: datum.children ? \'right\' : \'left\'\n }\n opacity: {\n signal: labels ? 1 : 0\n }\n }\n }\n }\n ]\n}`, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [], + }, + }; +}; + +export const getDashboard = ( + indexPatternId: string, + ruleID: string, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + ruleVis: { + gridData: { + w: 42, + h: 12, + x: 0, + y: 0, + i: 'ruleVis', + }, + type: 'visualization', + explicitInput: { + id: 'ruleVis', + savedVis: getVisualization(indexPatternId, ruleID), + }, + }, + }; +}; diff --git a/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx b/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx index 29b1aa6149..99dc60cc8e 100644 --- a/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx @@ -38,7 +38,7 @@ export const RuleFileEditor = ({ width='100%' height={`calc(100vh - 270px)`} {...fields.content} - mode='xml' + mode='yml' isReadOnly={!isEditable} wrapEnabled // setOptions={this.codeEditorOptions} diff --git a/plugins/wazuh-engine/public/hocs/index.ts b/plugins/wazuh-engine/public/hocs/index.ts new file mode 100644 index 0000000000..4ad6a3532d --- /dev/null +++ b/plugins/wazuh-engine/public/hocs/index.ts @@ -0,0 +1,2 @@ +export * from './with-data-source-fetch'; +export * from './with-guard'; diff --git a/plugins/wazuh-engine/public/hocs/with-data-source-fetch.tsx b/plugins/wazuh-engine/public/hocs/with-data-source-fetch.tsx new file mode 100644 index 0000000000..2af0ac8453 --- /dev/null +++ b/plugins/wazuh-engine/public/hocs/with-data-source-fetch.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { withGuardAsync } from './with-guard'; + +export const withDataSourceFetch = withGuardAsync( + ({ data }) => { + if (typeof data === 'string') { + return { + // TODO: fetch data and return + ok: true, + data: {}, + }; + } + return { + ok: false, + data: { data }, + }; + }, + () => <>, + () => <>, +); diff --git a/plugins/wazuh-engine/public/hocs/with-guard.tsx b/plugins/wazuh-engine/public/hocs/with-guard.tsx new file mode 100644 index 0000000000..e29d42ba7a --- /dev/null +++ b/plugins/wazuh-engine/public/hocs/with-guard.tsx @@ -0,0 +1,76 @@ +/* + * Wazuh app - React HOC to render a component depending of if it fulfills a condition or the wrapped component instead + * 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 } from 'react'; + +export const withGuard = + (condition: (props: any) => boolean, ComponentFulfillsCondition) => + WrappedComponent => + props => { + return condition(props) ? ( + + ) : ( + + ); + }; + +export const withGuardAsync = + ( + condition: (props: any) => { ok: boolean; data: any }, + ComponentFulfillsCondition: React.JSX.Element, + ComponentLoadingResolution: null | React.JSX.Element = null, + ) => + WrappedComponent => + props => { + const [loading, setLoading] = useState(true); + const [fulfillsCondition, setFulfillsCondition] = useState({ + ok: false, + data: {}, + }); + + const execCondition = async () => { + try { + setLoading(true); + setFulfillsCondition({ ok: false, data: {} }); + setFulfillsCondition( + await condition({ ...props, check: execCondition }), + ); + } catch (error) { + setFulfillsCondition({ ok: false, data: { error } }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + execCondition(); + }, []); + + if (loading) { + return ComponentLoadingResolution ? ( + + ) : null; + } + + return fulfillsCondition.ok ? ( + + ) : ( + + ); + }; From f4830b322a0f96ea14e00c4b075a44701b55eb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Fri, 12 Jul 2024 16:56:33 +0200 Subject: [PATCH 18/34] feat(engine): enhance the sample data injector script documentation --- scripts/sample-data/README.md | 83 +++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 scripts/sample-data/README.md diff --git a/scripts/sample-data/README.md b/scripts/sample-data/README.md new file mode 100644 index 0000000000..ec42715b2b --- /dev/null +++ b/scripts/sample-data/README.md @@ -0,0 +1,83 @@ +# Sample data injector + +This script generates sample data for different datasets and injects the data into an index on a Wazuh indexer instance. + +## Files + +- `script.py`: main script file +- `connection.json`: persistence of Wazuh indexer connection details +- `datasets`: directory that contains the available datasets + +# Getting started + +1. Install the dependencies: + +```console +pip install -r requirements.txt +``` + +For some operating systems it will fail and suggest a different way to install it (`sudo pacman -S python-xyz`, `sudo apt install python-xyz`, etc.). + +If the package is not found in this way, we can install it running `pip install -r requirements.txt --break-system-packages` (It is recommended to avoid this option if possible) + +2. Run the script selecting the dataset: + +```console +python3 script.py +``` + +where: + +- ``: is the name of the dataset. See the [available datasets](#datasets). + +3. Follow the instructions that it will show on the console. + +# Datasets + +Built-in datasets: + +- decoders +- filters +- outputs +- rules + +## Create dataset + +1. Create a new folder on `datasets` directory. The directory name will be the name of the dataset. + +2. Dataset directory: + +Create a `main.py`. +This script must define a `main` function that is run when the dataset creator is called. + +This receives the following parameters: + +- context: + - client: OpenSearch client to interact with the Wazuh indexer instance + - logger: a logger + +See some built-in dataset to know more. + +# Exploring the data on Wazuh dashboard + +The indexed data needs an index pattern that match with the index of the data to be explorable on +on Wazuh dashboard. So, if this is not created by another source, tt could be necessary to create +the index pattern manually if it was not previously created. + +In the case it does not exist, create it with from Dashboard management > Dashboard Management > Index patterns: + +- title: `wazuh-DATASET_NAME`. + +where: + +- `DATASET_NAME` is the name of the dataset. + +example: `wazuh-DATASET_NAME`. + +- id: `wazuh-rules`. + +where: + +- `DATASET_NAME` is the name of the dataset. + +example: `wazuh-rules`. From f94fdf94da2901a4b11af9fd70cd0a6444bb92e5 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Tue, 16 Jul 2024 11:24:59 +0200 Subject: [PATCH 19/34] Decoders --- plugins/main/public/app-router.tsx | 2 + .../public/common/styles/styles.scss | 128 +++++++++++ .../decoders/components/decoders-columns.tsx | 93 ++++++++ .../decoders/components/decoders-overview.tsx | 94 ++++++++ .../details/decoders-details-columns.tsx | 34 +++ .../components/details/decoders-details.tsx | 216 ++++++++++++++++++ .../decoders/components/forms/addDatabase.tsx | 104 +++++++++ .../public/components/decoders/decoders.tsx | 5 - .../public/components/decoders/index.ts | 2 +- .../public/components/decoders/router.tsx | 18 ++ .../components/decoders/spec-merge.json | 17 ++ .../public/components/decoders/spec.json | 42 ++++ .../public/controllers/resources-handler.ts | 8 +- 13 files changed, 756 insertions(+), 7 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/styles/styles.scss create mode 100644 plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx delete mode 100644 plugins/wazuh-engine/public/components/decoders/decoders.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/router.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/spec-merge.json create mode 100644 plugins/wazuh-engine/public/components/decoders/spec.json diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index fe75c2cc77..8b9c8656df 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -60,6 +60,7 @@ import WzListEditor from './controllers/management/components/management/cdblist import { DocumentViewTableAndJson } from './components/common/wazuh-discover/components/document-view-table-and-json'; import { OutputsDataSource } from './components/common/data-source/pattern/outputs/data-source'; import { OutputsDataSourceRepository } from './components/common/data-source/pattern/outputs/data-source-repository'; +import WzDecoderInfo from './controllers/management/components/management/decoders/views/decoder-info.tsx'; export function Application(props) { const dispatch = useDispatch(); const navigationService = NavigationService.getInstance(); @@ -166,6 +167,7 @@ export function Application(props) { TableWzAPI={TableWzAPI} WzRequest={WzRequest} WzListEditor={WzListEditor} + WzDecoderInfo={WzDecoderInfo} {...props} /> ); diff --git a/plugins/wazuh-engine/public/common/styles/styles.scss b/plugins/wazuh-engine/public/common/styles/styles.scss new file mode 100644 index 0000000000..958f223062 --- /dev/null +++ b/plugins/wazuh-engine/public/common/styles/styles.scss @@ -0,0 +1,128 @@ +.subdued-color { + color: #808184; +} + +.wz-decoders-flyout { + .flyout-header { + padding-right: 42px; + } + + .euiAccordion__button { + margin-top: 8px; + } + + .euiFlyoutBody__overflow { + padding-top: 3px; + } + .euiAccordion__children-isLoading { + line-height: inherit; + } +} + +.wz-inventory { + .flyout-header { + padding: 12px 16px; + } + + .flyout-body > .euiFlyoutBody__overflow { + padding: 8px 16px 16px 16px; + mask-image: unset; + } + + .flyout-row { + padding: 16px; + border-top: 1px solid #d3dae6; + } + + .details-row { + background: #fcfdfe; + } + + .detail-icon { + position: absolute; + margin-top: 6px; + } + + .detail-title { + margin-left: 36px; + font-size: 14px; + font-weight: 500; + } + + .detail-value { + font-size: 14px; + font-weight: 300; + cursor: default; + margin-left: 36px; + float: left; + white-space: break-spaces; + position: relative; + } + + .buttonAddFilter { + min-height: 0; + } + + .detail-value-checksum { + font-size: 13px !important; + display: inline-flex; + } + + .detail-value-perm { + word-break: break-word; + display: inline-block; + overflow: hidden; + } + + .detail-tooltip { + position: absolute; + right: 36px; + background-color: #fcfdfe; + } + + .application .euiAccordion, + .flyout-body .euiAccordion { + margin: 0px -16px; + border-top: none; + border-bottom: 1px solid #d3dae6; + } + + .application .euiAccordion__triggerWrapper, + .flyout-body .euiAccordion__triggerWrapper { + padding: 0px 16px; + } + + .application .euiAccordion__triggerWrapper .euiTitle, + .flyout-body .euiAccordion__triggerWrapper .euiTitle { + font-size: 16px; + } + + .application .euiAccordion__button, + .flyout-body .euiAccordion__button { + padding: 0px 0px 8px 0; + } + + .events-accordion, + .euiStat .euiTitle .detail-value .euiAccordion { + border-bottom: none !important; + } + + .view-in-events-btn { + height: 30px; + } + + .module-discover-table .euiTableRow-isExpandedRow .euiTableCellContent { + animation: none !important; + } + + /* This resets to the original value of "display" style for EuiTooltip used in ".euiAccordion__childWrapper .euiToolTipAnchor" definition, + which is defined above in this same file as block!important. +*/ + .rule_reset_display_anchor .euiToolTipAnchor { + display: inline-block !important; + } +} + +.wzApp .euiFlyoutBody .euiFlyoutBody__overflowContent { + padding: 0 !important; +} diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx new file mode 100644 index 0000000000..5670442ee4 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; + +export const columns = (setIsFlyoutVisible, setDetailsRequest) => { + return [ + { + field: 'filename', + name: 'Name', + align: 'left', + sortable: true, + }, + { + field: 'status', + name: 'Status', + align: 'left', + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + field: 'position', + name: 'Position', + align: 'left', + sortable: true, + }, + { + field: 'details.order', + name: 'Order', + align: 'left', + sortable: true, + }, + { + name: 'Actions', + align: 'left', + render: item => { + return ( + <> + { + const file = { + name: item.filename, + path: item.relative_dirname, + details: item.details, + }; + setDetailsRequest(file); + setIsFlyoutVisible(true); + }} + /> + + ); + }, + }, + ]; +}; + +export const colors = [ + '#004A65', + '#00665F', + '#BF4B45', + '#BF9037', + '#1D8C2E', + 'BB3ABF', + '#00B1F1', + '#00F2E2', + '#7F322E', + '#7F6025', + '#104C19', + '7C267F', + '#0079A5', + '#00A69B', + '#FF645C', + '#FFC04A', + '#2ACC43', + 'F94DFF', + '#0082B2', + '#00B3A7', + '#401917', + '#403012', + '#2DD947', + '3E1340', + '#00668B', + '#008C83', + '#E55A53', + '#E5AD43', + '#25B23B', + 'E045E5', +]; diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx new file mode 100644 index 0000000000..b4402f8e10 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; +import { columns } from './decoders-columns'; +import { EuiFlyout, EuiButtonEmpty } from '@elastic/eui'; +import { getServices } from '../../../services'; +import { DecodersDetails } from './details/decoders-details'; + +export const DecodersTable = () => { + const TableWzAPI = getServices().TableWzAPI; + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [getDecodersRequest, setDecodersRequest] = useState(false); + const WzRequest = getServices().WzRequest; + const navigationService = getServices().navigationService; + const closeFlyout = () => setIsFlyoutVisible(false); + const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], + }; + + const actionButtons = [ + { + navigationService.getInstance().navigate('/engine/decoders/new'); + }} + > + Add new decoders file + , + ]; + + return ( +
+ { + try { + const response = await WzRequest.apiReq('GET', '/decoders', { + params: { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + searchTable + endpoint={'/decoders'} + isExpandable={true} + downloadCsv + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + /> + {isFlyoutVisible && ( + + + + )} +
+ ); +}; diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx new file mode 100644 index 0000000000..3334ac9c01 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx @@ -0,0 +1,34 @@ +export const columns = () => { + return [ + { + field: 'filename', + name: 'Name', + align: 'left', + sortable: true, + }, + { + field: 'status', + name: 'Status', + align: 'left', + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + field: 'position', + name: 'Position', + align: 'left', + sortable: true, + }, + { + field: 'details.order', + name: 'Order', + align: 'left', + sortable: true, + }, + ]; +}; diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx new file mode 100644 index 0000000000..9301d1d298 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx @@ -0,0 +1,216 @@ +import React from 'react'; +import _ from 'lodash'; +// Eui components +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlexGrid, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiToolTip, + EuiLink, + EuiSpacer, + EuiAccordion, +} from '@elastic/eui'; +import { getServices } from '../../../../services'; +import { columns } from './decoders-details-columns'; +import { colors } from '../decoders-columns'; + +export const DecodersDetails = ({ item }) => { + const TableWzAPI = getServices().TableWzAPI; + /** + * Render the basic information in a list + * @param {Number} position + * @param {String} file + * @param {String} path + */ + const renderInfo = (position, file, path) => { + return ( + + + Position + {position} + + + File + + + + this.setNewFiltersAndBack({ q: `filename=${file}` }) + } + > +  {file} + + + + + + Path + + + + this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) + } + > +  {path} + + + + + + + ); + }; + + /** + * Render a list with the details + * @param {Array} details + */ + const renderDetails = details => { + const detailsToRender = []; + const capitalize = str => str[0].toUpperCase() + str.slice(1); + + Object.keys(details).forEach(key => { + let content = details[key]; + if (key === 'order') { + content = colorOrder(content); + } else if (typeof details[key] === 'object') { + content = ( +
    + {Object.keys(details[key]).map(k => ( +
  • + {k}:  + {details[key][k]} +
    +
  • + ))} +
+ ); + } else { + content = {details[key]}; + } + detailsToRender.push( + + {capitalize(key)} +
{content}
+
, + ); + }); + + return {detailsToRender}; + }; + + /** + * This set a color to a given order + * @param {String} order + */ + const colorOrder = order => { + order = order.toString(); + let valuesArray = order.split(','); + const result = []; + for (let i = 0, len = valuesArray.length; i < len; i++) { + const coloredString = ( + + {valuesArray[i].startsWith(' ') + ? valuesArray[i] + : ` ${valuesArray[i]}`} + + ); + result.push(coloredString); + } + return result; + }; + + return ( + <> + + {/* Decoder description name */} + + + + {name} + + + + + + {/* Cards */} + + {/* General info */} + + +

Information

+ + } + paddingSize='l' + initialIsOpen={true} + > + {renderInfo(item.position, item.name, item.path)} +
+
+
+ + + +

Details

+ + } + paddingSize='l' + initialIsOpen={true} + > + {renderDetails(item.details)} +
+
+
+ {/* Table */} + + + +

Related decoders

+ + } + paddingSize='none' + initialIsOpen={true} + > +
+ + + + + +
+
+
+
+
+ + ); +}; diff --git a/plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx b/plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx new file mode 100644 index 0000000000..13b7089668 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx @@ -0,0 +1,104 @@ +import React, { useMemo, useState } from 'react'; +import spec from '../../spec.json'; +import specMerge from '../../spec-merge.json'; + +import { transfromAssetSpecToForm } from '../../../rules/utils/transform-asset-spec'; +import { getServices } from '../../../../services'; +import { + EuiButton, + EuiButtonIcon, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiConfirmModal, +} from '@elastic/eui'; + +export const AddDatabase = () => { + const [isGoBackModalVisible, setIsGoBackModalVisible] = useState(false); + const InputForm = getServices().InputForm; + const useForm = getServices().useForm; + const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); + const { fields } = useForm(specForm); + const navigationService = getServices().navigationService; + + let modal; + + if (isGoBackModalVisible) { + modal = ( + { + setIsGoBackModalVisible(false); + }} + onConfirm={async () => { + setIsGoBackModalVisible(false); + navigationService.getInstance().navigate('/engine/kvdbs'); + }} + cancelButtonText="No, don't do it" + confirmButtonText='Yes, do it' + defaultFocusedButton='confirm' + > +

Are you sure you'll come back? All changes will be lost.

+
+ ); + } + + return ( + <> + + + setIsGoBackModalVisible(true)} + /> + + + +

Create new database

+
+
+ + + Documentation + + + + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + + + + { + /*TODO=> Add funcionallity*/ + }} + > + Save + + +
+ + {Object.entries(fields).map(([name, formField]) => ( + + ))} + {modal} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/decoders/decoders.tsx b/plugins/wazuh-engine/public/components/decoders/decoders.tsx deleted file mode 100644 index e734d6b98b..0000000000 --- a/plugins/wazuh-engine/public/components/decoders/decoders.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const Decoders = () => { - return <>Decoders; -}; diff --git a/plugins/wazuh-engine/public/components/decoders/index.ts b/plugins/wazuh-engine/public/components/decoders/index.ts index 3ba7021444..a8a26eb236 100644 --- a/plugins/wazuh-engine/public/components/decoders/index.ts +++ b/plugins/wazuh-engine/public/components/decoders/index.ts @@ -1 +1 @@ -export { Decoders } from './decoders'; +export { Decoders } from './router'; diff --git a/plugins/wazuh-engine/public/components/decoders/router.tsx b/plugins/wazuh-engine/public/components/decoders/router.tsx new file mode 100644 index 0000000000..d73c5c2633 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/router.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { DecodersTable } from './components/decoders-overview'; +import { AddDatabase } from './components/forms/addDatabase'; + +export const Decoders = props => { + return ( + + + + + } + > + + ); +}; diff --git a/plugins/wazuh-engine/public/components/decoders/spec-merge.json b/plugins/wazuh-engine/public/components/decoders/spec-merge.json new file mode 100644 index 0000000000..d321c39402 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/spec-merge.json @@ -0,0 +1,17 @@ +{ + "filename": { + "_meta": { + "label": "Name" + } + }, + "description": { + "_meta": { + "label": "Description" + } + }, + "relative_dirname": { + "_meta": { + "label": "Path" + } + } +} diff --git a/plugins/wazuh-engine/public/components/decoders/spec.json b/plugins/wazuh-engine/public/components/decoders/spec.json new file mode 100644 index 0000000000..c7a7f1825c --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/spec.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "wazuh-asset.json", + "name": "schema/wazuh-asset/0", + "title": "Schema for Wazuh assets", + "type": "object", + "description": "Schema for Wazuh assets", + "additionalProperties": false, + "required": ["filename"], + "properties": { + "filename": { + "type": "string", + "description": "Name of the asset, short and concise name to identify this asset", + "pattern": "^[^/]+$" + }, + "relative_dirname": { + "type": "string", + "description": "Description of the asset", + "pattern": "^.*$" + }, + "status": { + "type": "string", + "description": "Relative directory name where the asset is located", + "pattern": "^[^/]+(/[^/]+)*$" + }, + "name": { + "type": "string", + "description": "Description of the asset", + "pattern": "^.*$" + }, + "position": { + "type": "string", + "description": "Description of the asset", + "pattern": "^.*$" + }, + "details": { + "type": "string", + "description": "Description of the asset", + "pattern": "^.*$" + } + } +} diff --git a/plugins/wazuh-engine/public/controllers/resources-handler.ts b/plugins/wazuh-engine/public/controllers/resources-handler.ts index 4cb322c95f..2f9458b28b 100644 --- a/plugins/wazuh-engine/public/controllers/resources-handler.ts +++ b/plugins/wazuh-engine/public/controllers/resources-handler.ts @@ -1,9 +1,11 @@ import { getServices } from '../services'; type LISTS = 'lists'; -export type Resource = LISTS; +type DECODERS = 'decoders'; +export type Resource = DECODERS | LISTS; export const ResourcesConstants = { LISTS: 'lists', + DECODERS: 'decoders', }; export const resourceDictionary = { @@ -11,6 +13,10 @@ export const resourceDictionary = { resourcePath: '/lists', permissionResource: value => `list:file:${value}`, }, + [ResourcesConstants.DECODERS]: { + resourcePath: '/decoders', + permissionResource: value => `decoders:file:${value}`, + }, }; export class ResourcesHandler { From 6e0efe76d700a5bb0653ccdced3dec07148ce688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Jul 2024 17:53:26 +0200 Subject: [PATCH 20/34] feat: add ability to remove field form arrayOf form field --- .../main/public/components/common/form/hooks.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index 4c9176c429..92e5b07409 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -134,6 +134,21 @@ export function enhanceFormFields( return cloneDeep(newstate); }); }, + removeItem: index => { + setState(state => { + const _state = get(state, [...pathField, 'fields']); + + _state.splice(index, 1); + + const newState = set( + state, + [...pathField, 'fields'], + _state, + ); + + return cloneDeep(newState); + }); + }, } : { ...field, From fce7b77ec0b6a1dff80350e470f6f78cc547cbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 24 Jul 2024 17:56:31 +0200 Subject: [PATCH 21/34] feat(engine): enhance components - Create component to select the configuration method - Replace the creator with configuration method switch - Create paths for editors or rules and outputs - Enhance spec of rule and outputs - Enhance render of arrayOf form fields on form editor --- .../public/common/create-asset-selector.tsx | 147 ++++++++++++++++++ .../public/common/form/group-form.tsx | 27 +++- .../components/outputs/pages/create.tsx | 60 +++---- .../public/components/outputs/pages/list.tsx | 37 ++++- .../public/components/outputs/router.tsx | 9 +- .../public/components/outputs/spec-merge.json | 11 +- .../public/components/outputs/spec.json | 7 +- .../public/components/rules/pages/create.tsx | 62 ++++---- .../public/components/rules/pages/list.tsx | 39 ++++- .../public/components/rules/router.tsx | 9 +- .../public/components/rules/spec-merge.json | 46 ++++++ .../public/components/rules/spec.json | 14 +- 12 files changed, 376 insertions(+), 92 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/create-asset-selector.tsx diff --git a/plugins/wazuh-engine/public/common/create-asset-selector.tsx b/plugins/wazuh-engine/public/common/create-asset-selector.tsx new file mode 100644 index 0000000000..e2bff2be5c --- /dev/null +++ b/plugins/wazuh-engine/public/common/create-asset-selector.tsx @@ -0,0 +1,147 @@ +import React, { useState } from 'react'; +import { + EuiButton, + EuiModal, + EuiModalHeader, + EuiModalBody, + EuiOverlayMask, + EuiModalHeaderTitle, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPanel, + EuiFormRow, + EuiRadio, + EuiModalFooter, + EuiButtonEmpty, +} from '@elastic/eui'; + +interface CreateAssetModal { + onClose: () => void; + onClickContinue: (selectedOption: string) => void; + options: { id: string; label: string; help: string }[]; + defaultOption?: string; +} + +type CreateAssetModalButton = CreateAssetModal & { + buttonLabel: string; +}; + +const CreateAssetSelectorModal: React.SFC = ({ + onClose, + onClickContinue, + options, + defaultOption, +}: CreateAssetModal) => { + const [selectedOption, setSelectedOption] = useState( + defaultOption || options[0].id, + ); + return ( + + {/* + // @ts-ignore */} + + + Configuration method + + + + + + + Choose how you would like to create the asset, either using a + visual editor or file editor. + + + + + {options.map(option => { + const checked = option.id === selectedOption; + + return ( + + + + setSelectedOption(option.id)} + data-test-subj='createAssetModalVisualRadio' + /> + + + + ); + })} + + + + + + + + + + Cancel + + + + { + onClose(); + onClickContinue(selectedOption); + }} + fill + data-test-subj='createAssetModalContinueButton' + > + Continue + + + + + + + ); +}; + +export const CreateAssetSelectorButton: React.SFC = ({ + buttonLabel, + options, + defaultOption, + onClickContinue, +}: CreateAssetModalButton) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const onCloseModal = () => setIsModalOpen(false); + + return ( + <> + { + setIsModalOpen(state => !state); + }} + > + {buttonLabel} + + {isModalOpen && ( + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/common/form/group-form.tsx b/plugins/wazuh-engine/public/common/form/group-form.tsx index d62be0bc28..5947d1dd15 100644 --- a/plugins/wazuh-engine/public/common/form/group-form.tsx +++ b/plugins/wazuh-engine/public/common/form/group-form.tsx @@ -7,6 +7,7 @@ import { EuiTitle, EuiSpacer, EuiButton, + EuiButtonIcon, EuiIcon, EuiToolTip, } from '@elastic/eui'; @@ -66,13 +67,27 @@ export const FormGroup = ({
- {rest.fields.map(item => ( - <> - {Object.entries(item).map(([name, field]) => - renderInput({ ...field, name }), - )} - + {rest.fields.map((item, index) => ( + + + {Object.entries(item).map(([name, field]) => + renderInput({ + ...field, + name, + }), + )} + + + rest.removeItem(index)} + > + + ))} + Add ); diff --git a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx index 139adfffea..2a2eab581f 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx @@ -4,9 +4,35 @@ import { RuleForm } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { RuleFileEditor } from '../components/file-editor'; -export const Create = props => { - const [view, setView] = useState('visual-editor'); +export const CreateVisual = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; +export const CreateFile = props => { const actions = [ { > Import file , - ...(view === 'visual-editor' - ? [ - { - setView('file-editor'); - }} - iconType='apmTrace' - > - Switch to file editor - , - ] - : [ - { - setView('visual-editor'); - }} - iconType='apmTrace' - > - Switch to visual editor - , - ]), ]; return ( - - {view === 'visual-editor' && } - {view === 'file-editor' && ( - - )} + + ); }; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/list.tsx b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx index 2d7a8cf2eb..fdac62395f 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx @@ -9,6 +9,26 @@ import { Layout } from '../components'; import specification from '../spec.json'; import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; import { Detail } from '../components/detail'; +import { CreateAssetSelectorButton } from '../../../common/create-asset-selector'; + +const modalOptions = isEdit => [ + { + id: 'create-asset-visual', + label: 'Visual', + help: `Use the visual editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'visual', + }, + { + id: 'create-asset-file-editor', + label: 'File editor', + help: `Use the file editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'file', + }, +]; export const List = props => { const { @@ -27,16 +47,19 @@ export const List = props => { > Import file , - { + { props.navigationService .getInstance() - .navigate('/engine/outputs/create'); + .navigate( + `/engine/outputs/create/${ + modalOptions(false).find(({ id }) => id === editor)?.routePath + }`, + ); }} - > - Create Output - , + >, ]; const [indexPattern, setIndexPattern] = React.useState(null); diff --git a/plugins/wazuh-engine/public/components/outputs/router.tsx b/plugins/wazuh-engine/public/components/outputs/router.tsx index 643556a3dd..b572c42af5 100644 --- a/plugins/wazuh-engine/public/components/outputs/router.tsx +++ b/plugins/wazuh-engine/public/components/outputs/router.tsx @@ -1,14 +1,17 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { List } from './pages/list'; -import { Create } from './pages/create'; +import { CreateFile, CreateVisual } from './pages/create'; import { Edit } from './pages/edit'; export const Outputs = props => { return ( - - + + + + + { - const [view, setView] = useState('visual-editor'); +export const CreateRuleVisual = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; +export const CreateRuleFile = props => { const actions = [ { > Import file , - ...(view === 'visual-editor' - ? [ - { - setView('file-editor'); - }} - iconType='apmTrace' - > - Switch to file editor - , - ] - : [ - { - setView('visual-editor'); - }} - iconType='apmTrace' - > - Switch to visual editor - , - ]), ]; return ( - {view === 'visual-editor' && } - {view === 'file-editor' && ( - - )} + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx index acb577087e..ecc7725f35 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -9,6 +9,26 @@ import { Layout } from '../components'; import specification from '../spec.json'; import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; import { Detail } from '../components/detail'; +import { CreateAssetSelectorButton } from '../../../common/create-asset-selector'; + +const modalOptions = isEdit => [ + { + id: 'create-asset-visual', + label: 'Visual', + help: `Use the visual editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'visual', + }, + { + id: 'create-asset-file-editor', + label: 'File editor', + help: `Use the file editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'file', + }, +]; export const RulesList = props => { const { TableIndexer, RulesDataSource, RulesDataSourceRepository, title } = @@ -23,14 +43,19 @@ export const RulesList = props => { > Import file , - { - props.navigationService.getInstance().navigate('/engine/rules/create'); + { + props.navigationService + .getInstance() + .navigate( + `/engine/rules/create/${ + modalOptions(false).find(({ id }) => id === editor)?.routePath + }`, + ); }} - > - Create Rule - , + >, ]; const [indexPattern, setIndexPattern] = React.useState(null); diff --git a/plugins/wazuh-engine/public/components/rules/router.tsx b/plugins/wazuh-engine/public/components/rules/router.tsx index 74d90709a4..cb58d72710 100644 --- a/plugins/wazuh-engine/public/components/rules/router.tsx +++ b/plugins/wazuh-engine/public/components/rules/router.tsx @@ -1,14 +1,17 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { RulesList } from './pages/list'; -import { CreateRule } from './pages/create'; +import { CreateRuleFile, CreateRuleVisual } from './pages/create'; import { EditRule } from './pages/edit'; export const Rules = props => { return ( - - + + + + + Date: Thu, 25 Jul 2024 14:25:48 +0200 Subject: [PATCH 22/34] feat(sample-data): add dataset of integrations --- .../sample-data/dataset/integrations/main.py | 119 ++++++++++++++++++ .../dataset/integrations/template.json | 109 ++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 scripts/sample-data/dataset/integrations/main.py create mode 100644 scripts/sample-data/dataset/integrations/template.json diff --git a/scripts/sample-data/dataset/integrations/main.py b/scripts/sample-data/dataset/integrations/main.py new file mode 100644 index 0000000000..fa5e5fe1d7 --- /dev/null +++ b/scripts/sample-data/dataset/integrations/main.py @@ -0,0 +1,119 @@ +import random +import sys +import os.path +import json +from opensearchpy import helpers +from pathlib import Path + +index_template_file='template.json' +default_count='10000' +default_index_name='wazuh-integrations' +asset_identifier='integration' + +def generate_document(params): + id_int = int(params["id"]) + top_limit = id_int - 1 if id_int - 1 > -1 else 0 + + # https://github.com/wazuh/wazuh/blob/11334-dev-new-wazuh-engine/src/engine/ruleset/schemas/wazuh-asset.json + data = { + "ecs": {"version": "1.7.0"}, + "name": f'{asset_identifier}/{str(id_int)}/0', + "metadata": { + "title": f'Asset title {str(id_int)}', + "description": f'Asset description {str(id_int)}', + "author": { + "name": f'Asset author name {str(id_int)}', + "date": f'2024-07-04', + "email": f'email@sample.com', + "url": f'web.sample.com' + }, + "compatibility": f'compatibiliy_device', + "integration": f'integration {random.choice(["1","2","3"])}', + "versions": ['0.1', '0.2'], + "references": [f'Ref 01', f'Ref 02'] + }, + "wazuh": { + "cluster": { + "name": "wazuh" + } + }, + "parents": [], + "decoders": [], + "rules": [], + "outputs": [], + "filters": [], + "decoders": [] + # "check": {}, # enhance + # "allow": {}, # enhance + # "normalize": {}, # enhance + # "outputs": [], # enhance + # "definitions": {} # enhance + } + if(bool(random.getrandbits(1))): + top_limit = id_int - 1 if id_int - 1 > 0 else 0 + data["parents"] = [f'{asset_identifier}/{str(random.randint(0, top_limit))}/0'] + + return data + +def generate_documents(params): + for i in range(0, int(params["count"])): + yield generate_document({"id": i}) + +def get_params(ctx): + count = '' + while not count.isdigit(): + count = input_question(f'How many documents do you want to generate? [default={default_count}]', {"default_value": default_count}) + + index_name = input_question(f'Enter the index name [default={default_index_name}]', {"default_value": default_index_name}) + + return { + "count": count, + "index_name": index_name + } + +def input_question(message, options = {}): + response = input(message) + + if(options["default_value"] and response == ''): + response = options["default_value"] + + return response + + +def main(ctx): + client = ctx["client"] + logger = ctx["logger"] + logger.info('Getting configuration') + + config = get_params(ctx) + logger.info(f'Config {config}') + + resolved_index_template_file = os.path.join(Path(__file__).parent, index_template_file) + + logger.info(f'Checking existence of index [{config["index_name"]}]') + if client.indices.exists(config["index_name"]): + logger.info(f'Index found [{config["index_name"]}]') + should_delete_index = input_question(f'Remove the [{config["index_name"]}] index? [Y/n]', {"default_value": 'Y'}) + if should_delete_index == 'Y': + client.indices.delete(config["index_name"]) + logger.info(f'Index [{config["index_name"]}] deleted') + else: + logger.error(f'Index found [{config["index_name"]}] should be removed before create and insert documents') + sys.exit(1) + + if not os.path.exists(resolved_index_template_file): + logger.error(f'Index template found [{resolved_index_template_file}]') + sys.exit(1) + + with open(resolved_index_template_file) as templateFile: + index_template = json.load(templateFile) + try: + client.indices.create(index=config["index_name"], body=index_template) + logger.info(f'Index [{config["index_name"]}] created') + except Exception as e: + logger.error(f'Error: {e}') + sys.exit(1) + + helpers.bulk(client, generate_documents(config), index=config['index_name']) + logger.info(f'Data was indexed into [{config["index_name"]}]') + diff --git a/scripts/sample-data/dataset/integrations/template.json b/scripts/sample-data/dataset/integrations/template.json new file mode 100644 index 0000000000..c52e89b195 --- /dev/null +++ b/scripts/sample-data/dataset/integrations/template.json @@ -0,0 +1,109 @@ +{ + "mappings": { + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "name": { + "type": "keyword" + }, + "metadata": { + "properties": { + "title": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "author": { + "properties": { + "name": { + "type": "keyword" + }, + "date": { + "type": "keyword" + }, + "email": { + "type": "keyword" + }, + "url": { + "type": "keyword" + } + } + }, + "compatibility": { + "type": "keyword" + }, + "integration": { + "type": "keyword" + }, + "versions": { + "type": "keyword" + }, + "references": { + "type": "keyword" + } + } + }, + "parents": { + "type": "keyword" + }, + "allow": { + "type": "keyword" + }, + "normalize": { + "type": "keyword" + }, + "outputs": { + "type": "keyword" + }, + "definitions": { + "type": "keyword" + }, + "decoders": { + "type": "keyword" + }, + "rules": { + "type": "keyword" + }, + "filters": { + "type": "keyword" + }, + "kvdbs": { + "type": "keyword" + }, + "wazuh": { + "properties": { + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": 1000 + } + }, + "refresh_interval": "2s" + } + } +} From ca4033f7d85b0e577b8f46f38851d15ea2980467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 25 Jul 2024 14:28:37 +0200 Subject: [PATCH 23/34] feat(engine): add integrations section --- plugins/main/public/app-router.tsx | 8 + .../integrations/data-source-repository.ts | 45 ++++ .../pattern/integrations/data-source.ts | 28 +++ .../data-source/pattern/integrations/index.ts | 2 + .../{ => assets}/create-asset-selector.tsx | 0 .../assets}/file-editor.tsx | 7 +- .../public/common/assets/file-viewer.tsx | 41 ++++ .../public/common/assets/index.ts | 2 + .../integrations/components/detail.tsx | 140 +++++++++++ .../integrations/components/form.tsx | 24 ++ .../integrations/components/index.ts | 1 + .../integrations/components/layout.tsx | 48 ++++ .../public/components/integrations/index.ts | 2 +- .../components/integrations/integrations.tsx | 5 - .../components/integrations/pages/create.tsx | 32 +++ .../components/integrations/pages/edit.tsx | 59 +++++ .../components/integrations/pages/list.tsx | 227 ++++++++++++++++++ .../public/components/integrations/router.tsx | 25 ++ .../components/integrations/spec-merge.json | 85 +++++++ .../public/components/integrations/spec.json | 112 +++++++++ .../utils/transform-asset-spec.ts | 62 +++++ .../components/integrations/visualization.ts | 54 +++++ .../components/outputs/components/detail.tsx | 6 + .../outputs/components/file-editor.tsx | 50 ---- .../components/outputs/pages/create.tsx | 8 +- .../public/components/outputs/pages/edit.tsx | 6 +- .../public/components/outputs/pages/list.tsx | 8 +- .../components/rules/components/detail.tsx | 7 + .../public/components/rules/pages/create.tsx | 4 +- .../public/components/rules/pages/edit.tsx | 6 +- .../public/components/rules/pages/list.tsx | 8 +- 31 files changed, 1036 insertions(+), 76 deletions(-) create mode 100644 plugins/main/public/components/common/data-source/pattern/integrations/data-source-repository.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/integrations/data-source.ts create mode 100644 plugins/main/public/components/common/data-source/pattern/integrations/index.ts rename plugins/wazuh-engine/public/common/{ => assets}/create-asset-selector.tsx (100%) rename plugins/wazuh-engine/public/{components/rules/components => common/assets}/file-editor.tsx (86%) create mode 100644 plugins/wazuh-engine/public/common/assets/file-viewer.tsx create mode 100644 plugins/wazuh-engine/public/common/assets/index.ts create mode 100644 plugins/wazuh-engine/public/components/integrations/components/detail.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/components/form.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/components/index.ts create mode 100644 plugins/wazuh-engine/public/components/integrations/components/layout.tsx delete mode 100644 plugins/wazuh-engine/public/components/integrations/integrations.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/pages/create.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/pages/edit.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/pages/list.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/router.tsx create mode 100644 plugins/wazuh-engine/public/components/integrations/spec-merge.json create mode 100644 plugins/wazuh-engine/public/components/integrations/spec.json create mode 100644 plugins/wazuh-engine/public/components/integrations/utils/transform-asset-spec.ts create mode 100644 plugins/wazuh-engine/public/components/integrations/visualization.ts delete mode 100644 plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 8b9c8656df..9db1bd47dc 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -61,6 +61,10 @@ import { DocumentViewTableAndJson } from './components/common/wazuh-discover/com import { OutputsDataSource } from './components/common/data-source/pattern/outputs/data-source'; import { OutputsDataSourceRepository } from './components/common/data-source/pattern/outputs/data-source-repository'; import WzDecoderInfo from './controllers/management/components/management/decoders/views/decoder-info.tsx'; +import { + IntegrationsDataSource, + IntegrationsDataSourceRepository, +} from './components/common/data-source/pattern/integrations'; export function Application(props) { const dispatch = useDispatch(); const navigationService = NavigationService.getInstance(); @@ -158,6 +162,10 @@ export function Application(props) { RulesDataSourceRepository={RulesDataSourceRepository} OutputsDataSource={OutputsDataSource} OutputsDataSourceRepository={OutputsDataSourceRepository} + IntegrationsDataSource={IntegrationsDataSource} + IntegrationsDataSourceRepository={ + IntegrationsDataSourceRepository + } useDocViewer={useDocViewer} DocViewer={DocViewer} DocumentViewTableAndJson={DocumentViewTableAndJson} diff --git a/plugins/main/public/components/common/data-source/pattern/integrations/data-source-repository.ts b/plugins/main/public/components/common/data-source/pattern/integrations/data-source-repository.ts new file mode 100644 index 0000000000..7e1f9e88e3 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/integrations/data-source-repository.ts @@ -0,0 +1,45 @@ +import { PatternDataSourceRepository } from '../pattern-data-source-repository'; +import { tParsedIndexPattern } from '../../index'; + +export class IntegrationsDataSourceRepository extends PatternDataSourceRepository { + constructor() { + super(); + } + + async get(id: string) { + const dataSource = await super.get(id); + if (this.validate(dataSource)) { + return dataSource; + } else { + throw new Error('Integrations index pattern not found'); + } + } + + async getAll() { + const indexs = await super.getAll(); + return indexs.filter(this.validate); + } + + validate(dataSource): boolean { + // check if the dataSource has the id or the title have the vulnerabilities word + const fieldsToCheck = ['id', 'attributes.title']; + // must check in the object and the attributes + for (const field of fieldsToCheck) { + if ( + dataSource[field] && + dataSource[field].toLowerCase().includes('integrations') + ) { + return true; + } + } + return false; + } + + getDefault() { + return Promise.resolve(null); + } + + setDefault(dataSource: tParsedIndexPattern) { + return; + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/integrations/data-source.ts b/plugins/main/public/components/common/data-source/pattern/integrations/data-source.ts new file mode 100644 index 0000000000..ec9d2e2772 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/integrations/data-source.ts @@ -0,0 +1,28 @@ +import { + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, +} from '../../../../../../common/constants'; +import { tFilter, PatternDataSourceFilterManager } from '../../index'; +import { PatternDataSource } from '../pattern-data-source'; + +export class IntegrationsDataSource extends PatternDataSource { + constructor(id: string, title: string) { + super(id, title); + } + + getFetchFilters(): Filter[] { + return []; + } + + getFixedFilters(): tFilter[] { + return [...this.getClusterManagerFilters()]; + } + + getClusterManagerFilters() { + return PatternDataSourceFilterManager.getClusterManagerFilters( + this.id, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + VULNERABILITY_IMPLICIT_CLUSTER_MODE_FILTER, + ); + } +} diff --git a/plugins/main/public/components/common/data-source/pattern/integrations/index.ts b/plugins/main/public/components/common/data-source/pattern/integrations/index.ts new file mode 100644 index 0000000000..cae336db28 --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/integrations/index.ts @@ -0,0 +1,2 @@ +export * from './data-source'; +export * from './data-source-repository'; diff --git a/plugins/wazuh-engine/public/common/create-asset-selector.tsx b/plugins/wazuh-engine/public/common/assets/create-asset-selector.tsx similarity index 100% rename from plugins/wazuh-engine/public/common/create-asset-selector.tsx rename to plugins/wazuh-engine/public/common/assets/create-asset-selector.tsx diff --git a/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx b/plugins/wazuh-engine/public/common/assets/file-editor.tsx similarity index 86% rename from plugins/wazuh-engine/public/components/rules/components/file-editor.tsx rename to plugins/wazuh-engine/public/common/assets/file-editor.tsx index 99dc60cc8e..898ffa302f 100644 --- a/plugins/wazuh-engine/public/components/rules/components/file-editor.tsx +++ b/plugins/wazuh-engine/public/common/assets/file-editor.tsx @@ -7,7 +7,7 @@ import { EuiButton, } from '@elastic/eui'; -export const RuleFileEditor = ({ +export const FileEditor = ({ initialContent = '', isEditable = false, ...props @@ -36,13 +36,12 @@ export const RuleFileEditor = ({ ); diff --git a/plugins/wazuh-engine/public/common/assets/file-viewer.tsx b/plugins/wazuh-engine/public/common/assets/file-viewer.tsx new file mode 100644 index 0000000000..d183efa04d --- /dev/null +++ b/plugins/wazuh-engine/public/common/assets/file-viewer.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; +import { withGuardAsync } from '../../hocs'; + +export const FileViewer = ({ content }: { content: string }) => { + return ( + + ); +}; + +export const FileViewerFetchContent = withGuardAsync( + props => { + try { + // TODO: fetch asset content + // const data = await props.fetch(); + return { + ok: false, + data: { + content: '', + }, + }; + } catch (error) { + return { + ok: true, + data: { + error, + }, + }; + } + }, + () => null, +)(FileViewer); diff --git a/plugins/wazuh-engine/public/common/assets/index.ts b/plugins/wazuh-engine/public/common/assets/index.ts new file mode 100644 index 0000000000..1b325bd425 --- /dev/null +++ b/plugins/wazuh-engine/public/common/assets/index.ts @@ -0,0 +1,2 @@ +export * from './file-editor'; +export * from './create-asset-selector'; diff --git a/plugins/wazuh-engine/public/components/integrations/components/detail.tsx b/plugins/wazuh-engine/public/components/integrations/components/detail.tsx new file mode 100644 index 0000000000..dfd50496d1 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/components/detail.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { getDashboard } from '../visualization'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public/'; +import { getCore } from '../../../plugin-services'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlexGroup, + EuiTitle, +} from '@elastic/eui'; +import { withDataSourceFetch } from '../../../hocs/with-data-source-fetch'; + +export const Detail = withDataSourceFetch( + ({ + data, + indexPattern, + onClose, + DocumentViewTableAndJson, + WazuhFlyoutDiscover, + PatternDataSource, + AppState, + PatternDataSourceFilterManager, + FILTER_OPERATOR, + DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER, + DashboardContainerByValueRenderer: DashboardByRenderer, + }) => { + // To be able to display a non-loaded rule, the component should fetch it before + // to display it + return ( + + + +

Details: {data._source.name}

+
+
+ + + [ + { + id: 'relationship', + name: 'Relationship', + content: () => ( + + ), + }, + { + id: 'events', + name: 'Events', + content: () => { + const filterManager = React.useMemo( + () => new FilterManager(getCore().uiSettings), + [], + ); + return ( + + // this.renderDiscoverExpandedRow(...args) + // } + /> + ); + }, + }, + ], + }} + tableProps={{ + onFilter(...rest) { + // TODO: implement using the dataSource + }, + onToggleColumn() { + // TODO: reseach if make sense the ability to toggle columns + }, + }} + /> + + +
+ ); + }, +); diff --git a/plugins/wazuh-engine/public/components/integrations/components/form.tsx b/plugins/wazuh-engine/public/components/integrations/components/form.tsx new file mode 100644 index 0000000000..9634ba29d7 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/components/form.tsx @@ -0,0 +1,24 @@ +import React, { useMemo } from 'react'; +import spec from '../spec.json'; +import specMerge from '../spec-merge.json'; +import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; +import { FormGroup } from '../../../common/form'; + +export const RuleForm = props => { + const { useForm, InputForm } = props; + const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); + + return ( + + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/components/index.ts b/plugins/wazuh-engine/public/components/integrations/components/index.ts new file mode 100644 index 0000000000..5d15fe1b3c --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/components/index.ts @@ -0,0 +1 @@ +export * from './layout'; diff --git a/plugins/wazuh-engine/public/components/integrations/components/layout.tsx b/plugins/wazuh-engine/public/components/integrations/components/layout.tsx new file mode 100644 index 0000000000..da302cbb75 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/components/layout.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, +} from '@elastic/eui'; + +export const Layout = ({ + title, + children, + actions, +}: { + title: React.ReactNode; + children: React.ReactNode; + actions?: any; +}) => { + return ( + <> + + + +

{title}

+
+
+ {actions && ( + + + + )} +
+ +
{children}
+ + ); +}; + +const ViewActions = ({ actions }) => { + return Array.isArray(actions) ? ( + + {actions.map(action => ( + {action} + ))} + + ) : ( + actions() + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/index.ts b/plugins/wazuh-engine/public/components/integrations/index.ts index 1e9cfbc4f1..cd1681fa9b 100644 --- a/plugins/wazuh-engine/public/components/integrations/index.ts +++ b/plugins/wazuh-engine/public/components/integrations/index.ts @@ -1 +1 @@ -export { Integrations } from './integrations'; +export { Integrations } from './router'; diff --git a/plugins/wazuh-engine/public/components/integrations/integrations.tsx b/plugins/wazuh-engine/public/components/integrations/integrations.tsx deleted file mode 100644 index 21ad97b621..0000000000 --- a/plugins/wazuh-engine/public/components/integrations/integrations.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const Integrations = () => { - return <>Integrations; -}; diff --git a/plugins/wazuh-engine/public/components/integrations/pages/create.tsx b/plugins/wazuh-engine/public/components/integrations/pages/create.tsx new file mode 100644 index 0000000000..1a3b71f92e --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/pages/create.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiLink } from '@elastic/eui'; + +export const Create = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx b/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx new file mode 100644 index 0000000000..7afe1bf02e --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { RuleForm } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { FileEditor } from '../../../common/assets'; + +export const Edit = props => { + const [view, setView] = useState('visual-editor'); + + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ...(view === 'visual-editor' + ? [ + { + setView('file-editor'); + }} + iconType='apmTrace' + > + Switch to file editor + , + ] + : [ + { + setView('visual-editor'); + }} + iconType='apmTrace' + > + Switch to visual editor + , + ]), + ]; + + return ( + + {view === 'visual-editor' && } + {view === 'file-editor' && } + + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/pages/list.tsx b/plugins/wazuh-engine/public/components/integrations/pages/list.tsx new file mode 100644 index 0000000000..9bcc884f45 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/pages/list.tsx @@ -0,0 +1,227 @@ +import React, { useState } from 'react'; +import { + EuiButton, + EuiContextMenu, + EuiPopover, + EuiButtonEmpty, +} from '@elastic/eui'; +import { Layout } from '../components'; +import specification from '../spec.json'; +import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; +import { Detail } from '../components/detail'; +import { CreateAssetSelectorButton } from '../../../common/assets'; + +const modalOptions = isEdit => [ + { + id: 'create-asset-visual', + label: 'Visual', + help: `Use the visual editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'visual', + }, + { + id: 'create-asset-file-editor', + label: 'File editor', + help: `Use the file editor to ${isEdit ? 'update' : 'create'} your asset${ + isEdit ? '' : ' using pre-defined options.' + }`, + routePath: 'file', + }, +]; + +export const List = props => { + const { + TableIndexer, + IntegrationsDataSource, + IntegrationsDataSourceRepository, + title, + } = props; + + const actions = [ + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + { + props.navigationService + .getInstance() + .navigate('/engine/integrations/create'); + }} + iconType='importAction' + > + Create Integration + , + ]; + + const [indexPattern, setIndexPattern] = React.useState(null); + const [inspectedHit, setInspectedHit] = React.useState(null); + const [selectedItems, setSelectedItems] = useState([]); + + const defaultColumns = React.useMemo( + () => [ + ...transformAssetSpecToListTableColumn(specification, { + title: { + render: (prop, item) => ( + setInspectedHit(item._document)}> + {prop} + + ), + show: true, + }, + }), + { + // The field property does not exist on the data, but it used to display the column with + // show + field: 'actions', + name: 'Actions', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: ({ _document }) => { + setInspectedHit(_document); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit', + icon: 'pencil', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-edit', + }, + { + name: 'Export', + isPrimary: true, + description: 'Export file', + icon: 'exportAction', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-export', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete file', + icon: 'trash', + type: 'icon', + onClick: (...rest) => { + console.log({ rest }); + }, + 'data-test-subj': 'action-delete', + }, + ], + }, + ], + [], + ); + + return ( + + TableActions({ ...props, selectedItems }), + tableSortingInitialField: defaultColumns[0].field, + tableSortingInitialDirection: 'asc', + tableProps: { + itemId: 'name', + selection: { + onSelectionChange: item => { + setSelectedItems(item); + }, + }, + isSelectable: true, + }, + saveStateStorage: { + system: 'localStorage', + key: 'wz-engine:integrations-main', + }, + }} + exportCSVPrefixFilename='integrations' + onSetIndexPattern={setIndexPattern} + /> + {inspectedHit && ( + setInspectedHit(null)} + data={inspectedHit} + indexPattern={indexPattern} + /> + )} + + ); +}; + +const TableActions = ({ selectedItems }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(state => !state)} + > + Actions + + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + > + { + /* TODO: implement */ + }, + }, + { isSeparator: true }, + { + name: 'Delete', + disabled: selectedItems.length === 0, + 'data-test-subj': 'deleteAction', + onClick: () => { + /* TODO: implement */ + }, + }, + ], + }, + ]} + /> + + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/router.tsx b/plugins/wazuh-engine/public/components/integrations/router.tsx new file mode 100644 index 0000000000..6a14f22c2e --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/router.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { List } from './pages/list'; +import { Create } from './pages/create'; +import { Edit } from './pages/edit'; + +export const Integrations = props => { + return ( + + + + + { + return ; + }} + > + } + > + + ); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/spec-merge.json b/plugins/wazuh-engine/public/components/integrations/spec-merge.json new file mode 100644 index 0000000000..68476bc0e9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/spec-merge.json @@ -0,0 +1,85 @@ +{ + "title": { + "_meta": { + "label": "Name", + "groupForm": "documentation" + } + }, + "labels": { + "type": "arrayOf", + "initialValue": [{ "label": "" }], + "fields": { + "label": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Labels", + "groupForm": "documentation" + } + }, + "overview": { + "type": "textarea", + "_meta": { + "label": "Overview", + "groupForm": "documentation" + } + }, + "compatibility": { + "type": "textarea", + "_meta": { + "label": "Compatibility", + "groupForm": "documentation" + } + }, + "configuration": { + "type": "textarea", + "_meta": { + "label": "Configuration", + "groupForm": "documentation" + } + }, + "decoders": { + "type": "text", + "_meta": { + "label": "Decoders", + "groupForm": "policy" + } + }, + "rules": { + "type": "text", + "_meta": { + "label": "Rules", + "groupForm": "policy" + } + }, + "filters": { + "type": "text", + "_meta": { + "label": "Filters", + "groupForm": "policy" + } + }, + "outputs": { + "type": "text", + "_meta": { + "label": "Outputs", + "groupForm": "policy" + } + }, + "kvdbs": { + "type": "text", + "_meta": { + "label": "KVDBs", + "groupForm": "policy" + } + }, + "changelog": { + "type": "textarea", + "_meta": { + "label": "Changelog", + "groupForm": "changelog" + } + } +} diff --git a/plugins/wazuh-engine/public/components/integrations/spec.json b/plugins/wazuh-engine/public/components/integrations/spec.json new file mode 100644 index 0000000000..2a40e90ef6 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/spec.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "wazuh-asset.json", + "name": "schema/wazuh-asset/0", + "title": "Schema for Wazuh assets", + "type": "object", + "description": "Schema for Wazuh assets", + "additionalProperties": false, + "required": ["name", "metadata"], + "anyOf": [ + { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["parse"] + }, + { + "required": ["normalize"] + } + ], + "not": { + "anyOf": [ + { + "required": ["allow"] + }, + { + "required": ["outputs"] + } + ] + } + }, + { + "required": ["outputs"], + "not": { + "anyOf": [ + { + "required": ["normalize"] + }, + { + "required": ["parse"] + } + ] + } + }, + { + "required": ["allow", "parents"], + "not": { + "anyOf": [ + { + "required": ["check"] + }, + { + "required": ["normalize"] + } + ] + } + } + ], + "patternProperties": { + "parse\\|\\S+": { + "$ref": "#/definitions/_parse" + } + }, + "properties": { + "title": { + "type": "string", + "description": "Name of the integration" + }, + "labels": { + "type": "string", + "description": "Fields to identify the integration" + }, + "overview": { + "type": "string", + "description": "Overview of the integration purpose" + }, + "compatibility": { + "type": "string", + "description": "Description of the general compatibility of the integration, including supported devices or services, and their versions and formats" + }, + "configuration": { + "type": "string", + "description": "Description of how to configure Wazuh for the integration to work, including any necessary steps or settings and the expected configuration of the service or device" + }, + "decoders": { + "type": "string", + "description": "Description of how to configure Wazuh for the integration to work, including any necessary steps or settings and the expected configuration of the service or device" + }, + "rules": { + "type": "string", + "description": "Description of how to configure Wazuh for the integration to work, including any necessary steps or settings and the expected configuration of the service or device" + }, + "filters": { + "type": "string", + "description": "Description of how to configure Wazuh for the integration to work, including any necessary steps or settings and the expected configuration of the service or device" + }, + "outputs": { + "type": "string", + "description": "Description of how to configure Wazuh for the integration to work, including any necessary steps or settings and the expected configuration of the service or device" + }, + "kvdbs": { + "type": "string", + "description": "Contains input JSON files to create kvdbs with the same name." + }, + "changelog": { + "type": "string", + "description": "For each version, the changelog should include the following information: version tag, short description and link to the full changelog" + } + } +} diff --git a/plugins/wazuh-engine/public/components/integrations/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/integrations/utils/transform-asset-spec.ts new file mode 100644 index 0000000000..866dc2048c --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/utils/transform-asset-spec.ts @@ -0,0 +1,62 @@ +const mapSpecTypeToInput = { + string: 'text', +}; + +function createIter(fnItem) { + function iter(spec, parent = '') { + if (!spec.properties) { + return {}; + } + return Object.fromEntries( + Object.entries(spec.properties).reduce((accum, [key, value]) => { + const keyPath = [parent, key].filter(v => v).join('.'); + if (value.type === 'object') { + Object.entries(iter(value, keyPath)).forEach(entry => + accum.push(entry), + ); + } else if (value.type) { + accum.push([keyPath, fnItem({ key, keyPath, spec: value })]); + } + return accum; + }, []), + ); + } + + return iter; +} + +export const transfromAssetSpecToForm = function ( + spec, + mergeProps?: { [key: string]: string } = {}, +) { + return createIter(({ keyPath, spec }) => ({ + type: mapSpecTypeToInput[spec.type] || spec.type, + initialValue: '', + _spec: spec, + ...(spec.pattern + ? { + validate: value => + new RegExp(spec.pattern).test(value) + ? undefined + : `Value does not match the pattern: ${spec.pattern}`, + } + : {}), + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), + }))(spec); +}; + +export const transformAssetSpecToListTableColumn = function ( + spec, + mergeProps?: { [key: string]: string } = {}, +) { + const t = createIter(({ keyPath }) => ({ + field: keyPath, + name: keyPath, + ...(mergeProps?.[keyPath] ? mergeProps?.[keyPath] : {}), + })); + + return Object.entries(t(spec)).map(([key, value]) => ({ + field: key, + ...value, + })); +}; diff --git a/plugins/wazuh-engine/public/components/integrations/visualization.ts b/plugins/wazuh-engine/public/components/integrations/visualization.ts new file mode 100644 index 0000000000..bd58c70ec6 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/visualization.ts @@ -0,0 +1,54 @@ +const getVisualization = (indexPatternId: string, ruleID: string) => { + return { + id: 'Wazuh-rules-vega', + title: `Child outputs of ${ruleID}`, + type: 'vega', + params: { + spec: `{\n $schema: https://vega.github.io/schema/vega/v5.json\n description: An example of Cartesian layouts for a node-link diagram of hierarchical data.\n padding: 5\n signals: [\n {\n name: labels\n value: true\n bind: {\n input: checkbox\n }\n }\n {\n name: layout\n value: tidy\n bind: {\n input: radio\n options: [\n tidy\n cluster\n ]\n }\n }\n {\n name: links\n value: diagonal\n bind: {\n input: select\n options: [\n line\n curve\n diagonal\n orthogonal\n ]\n }\n }\n {\n name: separation\n value: false\n bind: {\n input: checkbox\n }\n }\n ]\n data: [\n {\n name: tree\n url: {\n /*\n An object instead of a string for the "url" param is treated as an OpenSearch query. Anything inside this object is not part of the Vega language, but only understood by OpenSearch Dashboards and OpenSearch server. This query counts the number of documents per time interval, assuming you have a @timestamp field in your data.\n\n OpenSearch Dashboards has a special handling for the fields surrounded by "%". They are processed before the the query is sent to OpenSearch. This way the query becomes context aware, and can use the time range and the dashboard filters.\n */\n\n // Apply dashboard context filters when set\n // %context%: true\n // Filter the time picker (upper right corner) with this field\n // %timefield%: @timestamp\n\n /*\n See .search() documentation for : https://opensearch.org/docs/latest/clients/javascript/\n */\n\n // Which index to search\n index: wazuh-rules\n\n\n // If "data_source.enabled: true", optionally set the data source name to query from (omit field if querying from local cluster)\n // data_source_name: Example US Cluster\n\n // Aggregate data by the time field into time buckets, counting the number of documents in each bucket.\n body: {\n query: {\n bool: {\n should: [\n {\n match_phrase: {\n name: ${ruleID}\n }\n }\n {\n match_phrase: {\n parents: ${ruleID}\n }\n }\n ]\n minimum_should_match: 1\n }\n }\n /* query: {\n match_all: {\n }\n } */\n size: 1000\n }\n }\n /*\n OpenSearch will return results in this format:\n\n aggregations: {\n time_buckets: {\n buckets: [\n {\n key_as_string: 2015-11-30T22:00:00.000Z\n key: 1448920800000\n doc_count: 0\n },\n {\n key_as_string: 2015-11-30T23:00:00.000Z\n key: 1448924400000\n doc_count: 0\n }\n ...\n ]\n }\n }\n\n For our graph, we only need the list of bucket values. Use the format.property to discard everything else.\n */\n format: {\n property: hits.hits\n }\n transform: [\n {\n type: stratify\n key: _source.id\n parentKey: _source.parents\n }\n {\n type: tree\n method: {\n signal: layout\n }\n size: [\n {\n signal: height\n }\n {\n signal: width - 100\n }\n ]\n separation: {\n signal: separation\n }\n as: [\n y\n x\n depth\n children\n ]\n }\n ]\n }\n {\n name: links\n source: tree\n transform: [\n {\n type: treelinks\n }\n {\n type: linkpath\n orient: horizontal\n shape: {\n signal: links\n }\n }\n ]\n }\n ]\n scales: [\n {\n name: color\n type: linear\n range: {\n scheme: magma\n }\n domain: {\n data: tree\n field: depth\n }\n zero: true\n }\n ]\n marks: [\n {\n type: path\n from: {\n data: links\n }\n encode: {\n update: {\n path: {\n field: path\n }\n stroke: {\n value: "#ccc"\n }\n }\n }\n }\n {\n type: symbol\n from: {\n data: tree\n }\n encode: {\n enter: {\n size: {\n value: 100\n }\n stroke: {\n value: "#fff"\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n fill: {\n scale: color\n field: depth\n }\n }\n }\n }\n {\n type: text\n from: {\n data: tree\n }\n encode: {\n enter: {\n text: {\n field: _source.id\n }\n fontSize: {\n value: 15\n }\n baseline: {\n value: middle\n }\n }\n update: {\n x: {\n field: x\n }\n y: {\n field: y\n }\n dx: {\n signal: datum.children ? -7 : 7\n }\n align: {\n signal: datum.children ? \'right\' : \'left\'\n }\n opacity: {\n signal: labels ? 1 : 0\n }\n }\n }\n }\n ]\n}`, + }, + data: { + searchSource: { + query: { + language: 'kuery', + query: '', + }, + filter: [], + index: indexPatternId, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: indexPatternId, + }, + ], + aggs: [], + }, + }; +}; + +export const getDashboard = ( + indexPatternId: string, + ruleID: string, +): { + [panelId: string]: DashboardPanelState< + EmbeddableInput & { [k: string]: unknown } + >; +} => { + return { + ruleVis: { + gridData: { + w: 42, + h: 12, + x: 0, + y: 0, + i: 'ruleVis', + }, + type: 'visualization', + explicitInput: { + id: 'ruleVis', + savedVis: getVisualization(indexPatternId, ruleID), + }, + }, + }; +}; diff --git a/plugins/wazuh-engine/public/components/outputs/components/detail.tsx b/plugins/wazuh-engine/public/components/outputs/components/detail.tsx index dfd50496d1..4baa49a40f 100644 --- a/plugins/wazuh-engine/public/components/outputs/components/detail.tsx +++ b/plugins/wazuh-engine/public/components/outputs/components/detail.tsx @@ -11,6 +11,7 @@ import { EuiTitle, } from '@elastic/eui'; import { withDataSourceFetch } from '../../../hocs/with-data-source-fetch'; +import { FileViewerFetchContent } from '../../../common/assets/file-viewer'; export const Detail = withDataSourceFetch( ({ @@ -42,6 +43,11 @@ export const Detail = withDataSourceFetch( indexPattern={indexPattern} extraTabs={{ post: params => [ + { + id: 'file', + name: 'File', + content: () => ''} />, + }, { id: 'relationship', name: 'Relationship', diff --git a/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx b/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx deleted file mode 100644 index f00f072ee3..0000000000 --- a/plugins/wazuh-engine/public/components/outputs/components/file-editor.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { - EuiCodeEditor, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiButton, -} from '@elastic/eui'; - -export const RuleFileEditor = ({ - initialContent = '', - isEditable = false, - ...props -}) => { - const { useForm, InputForm } = props; - const { fields } = useForm({ - filename: { type: 'text', initialValue: '' }, - content: { type: 'text', initialValue: initialContent || '' }, - }); - - return ( - <> - - - - - - {isEditable && ( - {}}> - Save - - )} - - - - - - ); -}; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx index 2a2eab581f..378114e517 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Layout } from '../components'; import { RuleForm } from '../components/form'; -import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -import { RuleFileEditor } from '../components/file-editor'; +import { EuiButton, EuiLink } from '@elastic/eui'; +import { FileEditor } from '../../../common/assets'; export const CreateVisual = props => { const actions = [ @@ -55,7 +55,7 @@ export const CreateFile = props => { return ( - + ); }; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx index 0bcdfc1c28..7afe1bf02e 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Layout } from '../components'; import { RuleForm } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -import { RuleFileEditor } from '../components/file-editor'; +import { FileEditor } from '../../../common/assets'; export const Edit = props => { const [view, setView] = useState('visual-editor'); @@ -53,9 +53,7 @@ export const Edit = props => { return ( {view === 'visual-editor' && } - {view === 'file-editor' && ( - - )} + {view === 'file-editor' && } ); }; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/list.tsx b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx index fdac62395f..a261f7668e 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx @@ -9,7 +9,7 @@ import { Layout } from '../components'; import specification from '../spec.json'; import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; import { Detail } from '../components/detail'; -import { CreateAssetSelectorButton } from '../../../common/create-asset-selector'; +import { CreateAssetSelectorButton } from '../../../common/assets'; const modalOptions = isEdit => [ { @@ -97,8 +97,14 @@ export const List = props => { 'metadata.description': { show: true, }, + 'metadata.integration': { + show: true, + }, }), { + // The field property does not exist on the data, but it used to display the column with + // show + field: 'actions', name: 'Actions', show: true, actions: [ diff --git a/plugins/wazuh-engine/public/components/rules/components/detail.tsx b/plugins/wazuh-engine/public/components/rules/components/detail.tsx index 5c8507ab3b..1112b378c4 100644 --- a/plugins/wazuh-engine/public/components/rules/components/detail.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/detail.tsx @@ -11,6 +11,7 @@ import { EuiTitle, } from '@elastic/eui'; import { withDataSourceFetch } from '../hocs/with-data-source-fetch'; +import { FileViewerFetchContent } from '../../../common/assets/file-viewer'; export const Detail = withDataSourceFetch( ({ @@ -28,6 +29,7 @@ export const Detail = withDataSourceFetch( }) => { // To be able to display a non-loaded rule, the component should fetch it before // to display it + return ( @@ -42,6 +44,11 @@ export const Detail = withDataSourceFetch( indexPattern={indexPattern} extraTabs={{ post: params => [ + { + id: 'file', + name: 'File', + content: () => ''} />, + }, { id: 'relationship', name: 'Relationship', diff --git a/plugins/wazuh-engine/public/components/rules/pages/create.tsx b/plugins/wazuh-engine/public/components/rules/pages/create.tsx index d1cc83ed9a..502959e259 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/create.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Layout } from '../components'; import { RuleForm } from '../components/form'; import { EuiButton, EuiLink } from '@elastic/eui'; -import { RuleFileEditor } from '../components/file-editor'; +import { FileEditor } from '../../../common/assets'; export const CreateRuleVisual = props => { const actions = [ @@ -55,7 +55,7 @@ export const CreateRuleFile = props => { return ( - + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/pages/edit.tsx b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx index 0643d3ec96..c3063bf275 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/edit.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Layout } from '../components'; import { RuleForm } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -import { RuleFileEditor } from '../components/file-editor'; +import { FileEditor } from '../../../common/assets'; export const EditRule = props => { const [view, setView] = useState('visual-editor'); @@ -53,9 +53,7 @@ export const EditRule = props => { return ( {view === 'visual-editor' && } - {view === 'file-editor' && ( - - )} + {view === 'file-editor' && } ); }; diff --git a/plugins/wazuh-engine/public/components/rules/pages/list.tsx b/plugins/wazuh-engine/public/components/rules/pages/list.tsx index ecc7725f35..f75fe6ca28 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/list.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -9,7 +9,7 @@ import { Layout } from '../components'; import specification from '../spec.json'; import { transformAssetSpecToListTableColumn } from '../utils/transform-asset-spec'; import { Detail } from '../components/detail'; -import { CreateAssetSelectorButton } from '../../../common/create-asset-selector'; +import { CreateAssetSelectorButton } from '../../../common/assets'; const modalOptions = isEdit => [ { @@ -93,8 +93,14 @@ export const RulesList = props => { 'metadata.description': { show: true, }, + 'metadata.integration': { + show: true, + }, }), { + // The field property does not exist on the data, but it used to display the column with + // show + field: 'actions', name: 'Actions', show: true, actions: [ From 4a22a6ff729ade6d57e1bf0017a042cdccc9f967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Thu, 25 Jul 2024 17:08:35 +0200 Subject: [PATCH 24/34] feat(engine): rename form component and minor fixes --- .../components/integrations/components/form.tsx | 2 +- .../components/integrations/pages/create.tsx | 14 +++----------- .../public/components/integrations/pages/edit.tsx | 4 ++-- .../public/components/integrations/spec-merge.json | 2 +- .../public/components/outputs/components/form.tsx | 2 +- .../public/components/outputs/pages/create.tsx | 4 ++-- .../public/components/outputs/pages/edit.tsx | 4 ++-- .../public/components/rules/components/form.tsx | 2 +- .../public/components/rules/pages/create.tsx | 4 ++-- .../public/components/rules/pages/edit.tsx | 4 ++-- 10 files changed, 17 insertions(+), 25 deletions(-) diff --git a/plugins/wazuh-engine/public/components/integrations/components/form.tsx b/plugins/wazuh-engine/public/components/integrations/components/form.tsx index 9634ba29d7..0073096525 100644 --- a/plugins/wazuh-engine/public/components/integrations/components/form.tsx +++ b/plugins/wazuh-engine/public/components/integrations/components/form.tsx @@ -4,7 +4,7 @@ import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; import { FormGroup } from '../../../common/form'; -export const RuleForm = props => { +export const Form = props => { const { useForm, InputForm } = props; const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); diff --git a/plugins/wazuh-engine/public/components/integrations/pages/create.tsx b/plugins/wazuh-engine/public/components/integrations/pages/create.tsx index 1a3b71f92e..9e349a0488 100644 --- a/plugins/wazuh-engine/public/components/integrations/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/integrations/pages/create.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; -import { EuiButton, EuiLink } from '@elastic/eui'; +import { Form } from '../components/form'; +import { EuiLink } from '@elastic/eui'; export const Create = props => { const actions = [ @@ -14,19 +14,11 @@ export const Create = props => { > Documentation
, - { - // TODO: Implement - }} - iconType='importAction' - > - Import file - , ]; return ( - +
); }; diff --git a/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx b/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx index 7afe1bf02e..5ae07f6634 100644 --- a/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx +++ b/plugins/wazuh-engine/public/components/integrations/pages/edit.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; +import { Form } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { FileEditor } from '../../../common/assets'; @@ -52,7 +52,7 @@ export const Edit = props => { return ( - {view === 'visual-editor' && } + {view === 'visual-editor' && } {view === 'file-editor' && } ); diff --git a/plugins/wazuh-engine/public/components/integrations/spec-merge.json b/plugins/wazuh-engine/public/components/integrations/spec-merge.json index 68476bc0e9..f111c755f9 100644 --- a/plugins/wazuh-engine/public/components/integrations/spec-merge.json +++ b/plugins/wazuh-engine/public/components/integrations/spec-merge.json @@ -1,7 +1,7 @@ { "title": { "_meta": { - "label": "Name", + "label": "Title", "groupForm": "documentation" } }, diff --git a/plugins/wazuh-engine/public/components/outputs/components/form.tsx b/plugins/wazuh-engine/public/components/outputs/components/form.tsx index 44b3776fa3..b3cbfa77da 100644 --- a/plugins/wazuh-engine/public/components/outputs/components/form.tsx +++ b/plugins/wazuh-engine/public/components/outputs/components/form.tsx @@ -4,7 +4,7 @@ import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; import { FormGroup } from '../../../common/form'; -export const RuleForm = props => { +export const Form = props => { const { useForm, InputForm } = props; const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); diff --git a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx index 378114e517..824ba4d070 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; +import { Form } from '../components/form'; import { EuiButton, EuiLink } from '@elastic/eui'; import { FileEditor } from '../../../common/assets'; @@ -27,7 +27,7 @@ export const CreateVisual = props => { return ( - + ); }; diff --git a/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx index 7afe1bf02e..5ae07f6634 100644 --- a/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx +++ b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; +import { Form } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { FileEditor } from '../../../common/assets'; @@ -52,7 +52,7 @@ export const Edit = props => { return ( - {view === 'visual-editor' && } + {view === 'visual-editor' && } {view === 'file-editor' && } ); diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index 44b3776fa3..b3cbfa77da 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -4,7 +4,7 @@ import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; import { FormGroup } from '../../../common/form'; -export const RuleForm = props => { +export const Form = props => { const { useForm, InputForm } = props; const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); diff --git a/plugins/wazuh-engine/public/components/rules/pages/create.tsx b/plugins/wazuh-engine/public/components/rules/pages/create.tsx index 502959e259..dbdfd2da15 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/create.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/create.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; +import { Form } from '../components/form'; import { EuiButton, EuiLink } from '@elastic/eui'; import { FileEditor } from '../../../common/assets'; @@ -27,7 +27,7 @@ export const CreateRuleVisual = props => { return ( - + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/pages/edit.tsx b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx index c3063bf275..1acf524d5f 100644 --- a/plugins/wazuh-engine/public/components/rules/pages/edit.tsx +++ b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Layout } from '../components'; -import { RuleForm } from '../components/form'; +import { Form } from '../components/form'; import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; import { FileEditor } from '../../../common/assets'; @@ -52,7 +52,7 @@ export const EditRule = props => { return ( - {view === 'visual-editor' && } + {view === 'visual-editor' && } {view === 'file-editor' && } ); From 4fb0b623b6f63675ea850c54aa4dc024d9452493 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Tue, 30 Jul 2024 08:42:08 +0200 Subject: [PATCH 25/34] Some routes changed, added interactive path to columns --- .../decoders/components/decoders-columns.tsx | 26 +++++++++++++------ .../decoders/components/decoders-overview.tsx | 5 +++- .../components/details/decoders-details.tsx | 14 +++------- .../public/components/kvdbs/index.ts | 2 +- .../kvdbs/{kvdbs.tsx => router.tsx} | 0 5 files changed, 26 insertions(+), 21 deletions(-) rename plugins/wazuh-engine/public/components/kvdbs/{kvdbs.tsx => router.tsx} (100%) diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx index 5670442ee4..e4d96d7cc0 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx @@ -1,26 +1,21 @@ import React from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; +import { EuiButtonIcon, EuiLink } from '@elastic/eui'; export const columns = (setIsFlyoutVisible, setDetailsRequest) => { return [ { - field: 'filename', + field: 'name', name: 'Name', align: 'left', sortable: true, }, + { field: 'status', name: 'Status', align: 'left', sortable: true, }, - { - field: 'relative_dirname', - name: 'Path', - align: 'left', - sortable: true, - }, { field: 'position', name: 'Position', @@ -33,6 +28,20 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { align: 'left', sortable: true, }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + name: 'File', + align: 'left', + sortable: true, + render: item => { + return  {item.filename}; + }, + }, { name: 'Actions', align: 'left', @@ -47,6 +56,7 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { name: item.filename, path: item.relative_dirname, details: item.details, + position: item.position, }; setDetailsRequest(file); setIsFlyoutVisible(true); diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx index b4402f8e10..d364c1e252 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx @@ -86,7 +86,10 @@ export const DecodersTable = () => { onClose={closeFlyout} className='wz-inventory wzApp wz-decoders-flyout' > - + )}
diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx index 9301d1d298..4b6874624e 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx @@ -17,7 +17,7 @@ import { getServices } from '../../../../services'; import { columns } from './decoders-details-columns'; import { colors } from '../decoders-columns'; -export const DecodersDetails = ({ item }) => { +export const DecodersDetails = ({ item, setIsFlyoutVisible }) => { const TableWzAPI = getServices().TableWzAPI; /** * Render the basic information in a list @@ -36,11 +36,7 @@ export const DecodersDetails = ({ item }) => { File - - this.setNewFiltersAndBack({ q: `filename=${file}` }) - } - > + setIsFlyoutVisible(false)}>  {file} @@ -50,11 +46,7 @@ export const DecodersDetails = ({ item }) => { Path - - this.setNewFiltersAndBack({ q: `relative_dirname=${path}` }) - } - > + setIsFlyoutVisible(false)}>  {path} diff --git a/plugins/wazuh-engine/public/components/kvdbs/index.ts b/plugins/wazuh-engine/public/components/kvdbs/index.ts index 58adcc2291..42e7143e8f 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/index.ts +++ b/plugins/wazuh-engine/public/components/kvdbs/index.ts @@ -1 +1 @@ -export { KVDBs } from './kvdbs'; +export { KVDBs } from './router'; diff --git a/plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx b/plugins/wazuh-engine/public/components/kvdbs/router.tsx similarity index 100% rename from plugins/wazuh-engine/public/components/kvdbs/kvdbs.tsx rename to plugins/wazuh-engine/public/components/kvdbs/router.tsx From 9ef32ae384398322f508bfe583f55ba8c6af8d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 6 Aug 2024 15:01:44 +0200 Subject: [PATCH 26/34] fix(form): fix form when using arrayOf --- .../public/components/common/form/hooks.tsx | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index 92e5b07409..a202d80aae 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -115,20 +115,33 @@ export function enhanceFormFields( })(), addNewItem: () => { setState(state => { - const _state = get(state, [...pathField, 'fields']); + const _state = get(state, [...pathFormField, 'fields']); const newstate = set( state, - [...pathField, 'fields', _state.length], - Object.entries(field.fields).reduce( - (accum, [key, { defaultValue, initialValue }]) => ({ - ...accum, - [key]: { - currentValue: cloneDeep(initialValue), - initialValue: cloneDeep(initialValue), - defaultValue: cloneDeep(defaultValue), + [...pathFormField, 'fields', _state.length], + Object.fromEntries( + Object.entries(field.fields).map( + ([key, { defaultValue, initialValue, ...rest }]) => { + console.log({ + key, + defaultValue, + initialValue, + rest, + }); + return [ + key, + rest.type === 'arrayOf' + ? { + fields: [], + } + : { + currentValue: cloneDeep(initialValue), + initialValue: cloneDeep(initialValue), + defaultValue: cloneDeep(defaultValue), + }, + ]; }, - }), - {}, + ), ), ); return cloneDeep(newstate); @@ -136,13 +149,13 @@ export function enhanceFormFields( }, removeItem: index => { setState(state => { - const _state = get(state, [...pathField, 'fields']); + const _state = get(state, [...pathFormField, 'fields']); _state.splice(index, 1); const newState = set( state, - [...pathField, 'fields'], + [...pathFormField, 'fields'], _state, ); From 23edfec63f6f99853bef58a5e52019036e01478b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 6 Aug 2024 15:05:06 +0200 Subject: [PATCH 27/34] feat(engine): enhance rule form --- .../public/common/form/group-form.tsx | 75 ++++-- .../wazuh-engine/public/common/form/index.ts | 2 + .../public/common/form/input-asset-check.tsx | 93 ++++++++ .../public/common/form/input-asset-map.tsx | 93 ++++++++ .../public/components/outputs/spec-merge.json | 2 +- .../components/rules/components/form.tsx | 218 ++++++++++++++++-- .../public/components/rules/spec-merge.json | 79 +++++-- 7 files changed, 510 insertions(+), 52 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/form/input-asset-check.tsx create mode 100644 plugins/wazuh-engine/public/common/form/input-asset-map.tsx diff --git a/plugins/wazuh-engine/public/common/form/group-form.tsx b/plugins/wazuh-engine/public/common/form/group-form.tsx index 5947d1dd15..0e15e034d7 100644 --- a/plugins/wazuh-engine/public/common/form/group-form.tsx +++ b/plugins/wazuh-engine/public/common/form/group-form.tsx @@ -9,10 +9,11 @@ import { EuiButton, EuiButtonIcon, EuiIcon, + EuiText, EuiToolTip, } from '@elastic/eui'; -const addSpaceBetween = (accum, item) => ( +export const addSpaceBetween = (accum, item) => ( <> {accum} @@ -20,7 +21,7 @@ const addSpaceBetween = (accum, item) => ( ); -const groupFieldsFormByGroup = (fields, groupKey) => +export const groupFieldsFormByGroup = (fields, groupKey) => Object.entries(fields) .map(([name, item]) => ({ ...item, name })) .reduce((accum, item) => { @@ -59,20 +60,14 @@ export const FormGroup = ({ } return ( - - - - -

{name}

-
-
-
+ {rest.fields.map((item, index) => ( {Object.entries(item).map(([name, field]) => renderInput({ ...field, + _label: name, name, }), )} @@ -89,7 +84,7 @@ export const FormGroup = ({ ))} Add -
+ ); }; @@ -97,7 +92,7 @@ export const FormGroup = ({ <> {renderGroups .map(({ title, groupKey }) => ( - ( - - - - -

{title}

-
-
-
+export const FormInputGroup = ({ + description, + title, + fields, + renderInput, +}: { + title: React.ReactNode; + description?: React.ReactNode; + fields: any[]; + renderInput: () => React.ReactNode; +}) => ( + {fields .map(formField => renderInput({ ...formField, + _label: formField?._meta?.label || formField?.name, name: ( - ), }), ) - .reduce(addSpaceBetween)} -
+ .reduce(addSpaceBetween, <>)} + ); -const InputLabel = ({ +export const FormInputLabel = ({ label, description, }: { @@ -163,3 +162,29 @@ const InputLabel = ({ )} ); + +export const FormInputGroupPanel = ({ + title, + description, + actions, + children, +}) => ( + + + + +

{title}

+
+
+ {actions && {actions}} +
+ {description && ( + + + {description} + + + )} + {children} +
+); diff --git a/plugins/wazuh-engine/public/common/form/index.ts b/plugins/wazuh-engine/public/common/form/index.ts index 90b4e2df9d..0e0c9f6f6e 100644 --- a/plugins/wazuh-engine/public/common/form/index.ts +++ b/plugins/wazuh-engine/public/common/form/index.ts @@ -1 +1,3 @@ export * from './group-form'; +export * from './input-asset-check'; +export * from './input-asset-map'; diff --git a/plugins/wazuh-engine/public/common/form/input-asset-check.tsx b/plugins/wazuh-engine/public/common/form/input-asset-check.tsx new file mode 100644 index 0000000000..0be5669541 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/input-asset-check.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +export const InputAssetCheck = props => { + const { useForm, InputForm } = props; + + const { fields } = useForm({ + mode: { + type: 'select', + initialValue: 'text', + options: { + select: [ + { + text: 'text', + value: 'text', + }, + { + text: 'list', + value: 'list', + }, + ], + }, + }, + mode_text_content: { + type: 'textarea', + initialValue: '', + placeholder: 'Test', + }, + // property:value + mode_list_content: { + type: 'arrayOf', + initialValue: [ + { + field: '', + value: '', + }, + ], + fields: { + field: { + type: 'text', + initialValue: '', + }, + value: { + type: 'text', + initialValue: '', + }, + }, + }, + }); + return ( + <> + + + {fields.mode.value === 'text' ? ( + + ) : ( + <> + {fields.mode_list_content.fields.map((field, index) => ( + <> + + + + + + + + + fields.mode_list_content.removeItem(index)} + > + + + + + ))} + + Add + + + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/common/form/input-asset-map.tsx b/plugins/wazuh-engine/public/common/form/input-asset-map.tsx new file mode 100644 index 0000000000..d7cb927236 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/input-asset-map.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +export const InputAssetMap = props => { + const { useForm, InputForm } = props; + + const { fields } = useForm({ + mode: { + type: 'select', + initialValue: 'text', + options: { + select: [ + { + text: 'text', + value: 'text', + }, + { + text: 'list', + value: 'list', + }, + ], + }, + }, + mode_text_content: { + type: 'textarea', + initialValue: '', + placeholder: 'Test', + }, + // property:value + mode_list_content: { + type: 'arrayOf', + initialValue: [ + { + field: '', + value: '', + }, + ], + fields: { + field: { + type: 'text', + initialValue: '', + }, + value: { + type: 'text', + initialValue: '', + }, + }, + }, + }); + return ( + <> + + + {fields.mode.value === 'text' ? ( + + ) : ( + <> + {fields.mode_list_content.fields.map((field, index) => ( + <> + + + + + + + + + fields.mode_list_content.removeItem(index)} + > + + + + + ))} + + Add + + + + )} + + ); +}; diff --git a/plugins/wazuh-engine/public/components/outputs/spec-merge.json b/plugins/wazuh-engine/public/components/outputs/spec-merge.json index a319334f1e..78e27dfe00 100644 --- a/plugins/wazuh-engine/public/components/outputs/spec-merge.json +++ b/plugins/wazuh-engine/public/components/outputs/spec-merge.json @@ -63,7 +63,7 @@ }, "metadata.compatibility": { "_meta": { - "label": "Comptability", + "label": "Compatibility", "groupForm": "metadata" } }, diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index b3cbfa77da..bb49bad4a8 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -2,23 +2,215 @@ import React, { useMemo } from 'react'; import spec from '../spec.json'; import specMerge from '../spec-merge.json'; import { transfromAssetSpecToForm } from '../utils/transform-asset-spec'; -import { FormGroup } from '../../../common/form'; +import { + FormGroup, + FormInputGroupPanel, + FormInputLabel, + InputAssetCheck, + InputAssetMap, + addSpaceBetween, +} from '../../../common/form'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, +} from '@elastic/eui'; export const Form = props => { const { useForm, InputForm } = props; - const specForm = useMemo(() => transfromAssetSpecToForm(spec, specMerge), []); + const specForm = useMemo(() => { + const result = transfromAssetSpecToForm(spec, specMerge); + return result; + }, []); + + const { fields } = useForm(specForm); return ( - + <> + + <> + + {['title', 'description', 'compatibility', 'integration'].map(key => { + const keyProp = `metadata.${key}`; + return ( + + } + /> + ); + })} + + + <> + {fields['metadata.versions'].fields.map( + ({ version: field }, indexField) => ( + ( + + + fields['metadata.versions'].removeItem(indexField) + } + > + + )} + /> + ), + )} + + + Add + + + + + + <> + {fields['metadata.references'].fields.map( + ({ reference: field }, indexField) => ( + ( + + + fields['metadata.references'].removeItem(indexField) + } + > + + )} + /> + ), + )} + + + Add + + + + + + + + <> + {['name', 'url', 'email', 'date'].map(key => { + const keyProp = `metadata.author.${key}`; + return ( + + ); + })} + + + + + <> + {fields['definitions'].fields.map((field, indexField) => ( + + {['key', 'value'].map(fieldName => ( + + + + ))} + + fields['definitions'].removeItem(indexField)} + > + + + ))} + + Add + + + + + + + + + + + + + <> + {fields.normalize.fields + .map((field, indexNormalize) => ( + <> + + fields.normalize.removeItem(indexNormalize) + } + > + } + > + + + + + + + + + + + + + + )) + .reduce(addSpaceBetween, <>)} + + Add + + + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/spec-merge.json b/plugins/wazuh-engine/public/components/rules/spec-merge.json index e3783a3711..a2e4f5c355 100644 --- a/plugins/wazuh-engine/public/components/rules/spec-merge.json +++ b/plugins/wazuh-engine/public/components/rules/spec-merge.json @@ -31,7 +31,7 @@ }, "metadata.references": { "type": "arrayOf", - "initialValue": [{ "reference": "" }], + "initialValue": [], "fields": { "reference": { "type": "text", @@ -56,20 +56,22 @@ } }, "metadata.description": { + "type": "textarea", "_meta": { "label": "Description", "groupForm": "metadata" } }, "metadata.compatibility": { + "type": "textarea", "_meta": { - "label": "Comptability", + "label": "Compatibility", "groupForm": "metadata" } }, "metadata.versions": { "type": "arrayOf", - "initialValue": [{ "version": "" }], + "initialValue": [], "fields": { "version": { "type": "text", @@ -83,7 +85,7 @@ }, "parents": { "type": "arrayOf", - "initialValue": [{ "parent": "" }], + "initialValue": [], "fields": { "parent": { "type": "text", @@ -97,7 +99,7 @@ }, "definitions": { "type": "arrayOf", - "initialValue": [{ "key": "", "value": "" }], + "initialValue": [], "fields": { "key": { "type": "text", @@ -114,23 +116,27 @@ } }, "check": { - "type": "arrayOf", - "initialValue": [{ "value": "" }], + "type": "custom", + "initialValue": [], "fields": { - "value": { + "check": { "type": "text", "initialValue": "" } }, "_meta": { "label": "Check", - "groupForm": "steps" + "groupForm": "check" } }, "map": { "type": "arrayOf", - "initialValue": [{ "value": "" }], + "initialValue": [], "fields": { + "field": { + "type": "text", + "initialValue": "" + }, "value": { "type": "text", "initialValue": "" @@ -138,14 +144,61 @@ }, "_meta": { "label": "Map", - "groupForm": "steps" + "groupForm": "map" } }, "normalize": { - "type": "textarea", + "type": "arrayOf", + "initialValue": [], + "fields": { + "check": { + "type": "custom", + "initialValue": [], + "_meta": { + "label": "Check", + "groupForm": "check" + } + }, + "map": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "field": { + "type": "text", + "initialValue": "" + }, + "value": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Map", + "groupForm": "map" + } + }, + "parse": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "field": { + "type": "text", + "initialValue": "" + }, + "value": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Map", + "groupForm": "map" + } + } + }, "_meta": { "label": "Normalize", - "groupForm": "steps" + "groupForm": "normalize" } } } From d770227ca266b80a1f74c1948b318706e8cc7639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Tue, 6 Aug 2024 15:51:07 +0200 Subject: [PATCH 28/34] fix(form): remove console.log on hook --- .../public/components/common/form/hooks.tsx | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/plugins/main/public/components/common/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index a202d80aae..3b3787e265 100644 --- a/plugins/main/public/components/common/form/hooks.tsx +++ b/plugins/main/public/components/common/form/hooks.tsx @@ -121,26 +121,18 @@ export function enhanceFormFields( [...pathFormField, 'fields', _state.length], Object.fromEntries( Object.entries(field.fields).map( - ([key, { defaultValue, initialValue, ...rest }]) => { - console.log({ - key, - defaultValue, - initialValue, - rest, - }); - return [ - key, - rest.type === 'arrayOf' - ? { - fields: [], - } - : { - currentValue: cloneDeep(initialValue), - initialValue: cloneDeep(initialValue), - defaultValue: cloneDeep(defaultValue), - }, - ]; - }, + ([key, { defaultValue, initialValue, ...rest }]) => [ + key, + rest.type === 'arrayOf' + ? { + fields: [], + } + : { + currentValue: cloneDeep(initialValue), + initialValue: cloneDeep(initialValue), + defaultValue: cloneDeep(defaultValue), + }, + ], ), ), ); From b122df8e77f41e0b617bb9ec5c9c3a7573353803 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Wed, 7 Aug 2024 08:57:15 +0200 Subject: [PATCH 29/34] Changes on decoders --- ...et_decorer_raw.txt => get_decoder_raw.txt} | 0 docker/imposter/decoders/get_decoders.json | 61 +++++++ docker/imposter/wazuh-config.yml | 6 +- plugins/main/public/app-router.tsx | 6 - .../decoders/components/decoders-columns.tsx | 172 ++++++++++++++---- .../components/decoders-files/files-info.tsx | 96 ++++++++++ .../decoders/components/decoders-overview.tsx | 13 +- .../details/decoders-details-columns.tsx | 26 +-- .../components/details/decoders-details.tsx | 58 +++--- .../forms/{addDatabase.tsx => addDecoder.tsx} | 8 +- .../public/components/decoders/router.tsx | 20 +- .../public/controllers/resources-handler.ts | 21 +++ 12 files changed, 381 insertions(+), 106 deletions(-) rename docker/imposter/decoders/{get_decorer_raw.txt => get_decoder_raw.txt} (100%) create mode 100644 docker/imposter/decoders/get_decoders.json create mode 100644 plugins/wazuh-engine/public/components/decoders/components/decoders-files/files-info.tsx rename plugins/wazuh-engine/public/components/decoders/components/forms/{addDatabase.tsx => addDecoder.tsx} (94%) diff --git a/docker/imposter/decoders/get_decorer_raw.txt b/docker/imposter/decoders/get_decoder_raw.txt similarity index 100% rename from docker/imposter/decoders/get_decorer_raw.txt rename to docker/imposter/decoders/get_decoder_raw.txt diff --git a/docker/imposter/decoders/get_decoders.json b/docker/imposter/decoders/get_decoders.json new file mode 100644 index 0000000000..4023254dcc --- /dev/null +++ b/docker/imposter/decoders/get_decoders.json @@ -0,0 +1,61 @@ +{ + "data": { + "affected_items": [ + { + "name": "decoder/journald/3", + "parents": [], + "module": "journald-http", + "title": "Journald HTTP Agents error logs decoder", + "description": "Decoder for Journald HTTP Agents error logs.", + "versions": ["2.2.31"], + "compatibility": "The Apache datasets were tested with Apache 2.4.12 and 2.4.46 and are expected to work with all versions >= 2.2.31 and >= 2.4.16 (independent from operating system).", + "author": { + "name": "", + "date": "", + "email": "" + }, + "references": [] + }, + { + "name": "decoder/systemctl/1", + "parents": ["journald"], + "module": "systemctl-http", + "title": "Systemctl Manager error logs decoder", + "description": "Decoder for Systemctl Manager error logs.", + "versions": [], + "compatibility": "The Apache datasets were tested with Apache 2.4.12 and 2.4.46 and are expected to work with all versions >= 2.2.31 and >= 2.4.16 (independent from operating system).", + "author": { + "name": "Wazuh Inc.", + "date": "2021-12-31", + "email": "wazuh@wazuh.com" + }, + "references": [ + "https://documentation.wazuh.com/current/user-manual/ruleset/ruleset-xml-syntax/decoders.html" + ] + }, + { + "name": "decoder/apache-access/0", + "parents": ["decoder/apache-common/0", "decoder/journald-apache/0"], + "module": "apache-http", + "title": "Apache HTTP Server error logs decoder", + "description": "Decoder for Apache HTTP Server error logs.", + "versions": ["2.2.31", "2.4.16"], + "compatibility": "The Apache datasets were tested with Apache 2.4.12 and 2.4.46 and are expected to work with all versions >= 2.2.31 and >= 2.4.16 (independent from operating system).", + "author": { + "name": "Wazuh Inc.", + "date": "2023-11-29", + "email": "" + }, + "references": [ + "https://httpd.apache.org/docs/2.4/logs.html", + "https://httpd.apache.org/docs/2.4/custom-error.html" + ] + } + ], + "total_affected_items": 3, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All selected decoders were returned", + "error": 0 +} diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index a29d132aae..333baa6c5c 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -279,7 +279,9 @@ resources: # List decoders - method: GET path: /decoders - + response: + statusCode: 200 + staticFile: decoders/get_decoders.json # Get files - method: GET path: /decoders/files @@ -291,7 +293,7 @@ resources: raw: true response: statusCode: 200 - staticFile: rules/get_rule_raw.txt + staticFile: decoders/get_decoder_raw.txt # Update decoders file - method: PUT diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 9db1bd47dc..dde67ab42b 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -50,12 +50,6 @@ import { useForm } from './components/common/form/hooks'; import { InputForm } from './components/common/form'; import { TableWzAPI } from './components/common/tables'; -import { - AddNewCdbListButton, - AddNewFileButton, - ManageFiles, - UploadFilesButton, -} from './controllers/management/components/management/common/actions-buttons'; import WzListEditor from './controllers/management/components/management/cdblists/views/list-editor.tsx'; import { DocumentViewTableAndJson } from './components/common/wazuh-discover/components/document-view-table-and-json'; import { OutputsDataSource } from './components/common/data-source/pattern/outputs/data-source'; diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx index e4d96d7cc0..c8dcf98b07 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx @@ -1,68 +1,178 @@ -import React from 'react'; -import { EuiButtonIcon, EuiLink } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiLink, + EuiPopover, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { getServices } from '../../../services'; export const columns = (setIsFlyoutVisible, setDetailsRequest) => { + const navigationService = getServices().navigationService; + const [openPopoverId, setOpenPopoverId] = useState(null); + + const togglePopover = id => { + setOpenPopoverId(currentOpenId => (currentOpenId === id ? null : id)); + }; + + const closePopover = () => { + setOpenPopoverId(null); + }; return [ { - field: 'name', name: 'Name', + field: 'name', + align: 'left', + show: true, + render: name => { + return ( + <> + { + navigationService + .getInstance() + .navigate(`/engine/decoders/file/${name}`); + }} + > +  {name} + + + ); + }, + }, + { + field: 'title', + name: 'Title', align: 'left', sortable: true, + show: true, }, - { - field: 'status', - name: 'Status', + field: 'description', + name: 'Description', align: 'left', sortable: true, + show: true, }, { - field: 'position', - name: 'Position', + field: 'compatibility', + name: 'Compatibility', align: 'left', sortable: true, }, { - field: 'details.order', - name: 'Order', + field: 'parents', + name: 'Parents', align: 'left', sortable: true, + show: true, }, { - field: 'relative_dirname', - name: 'Path', + field: 'module', + name: 'Module', align: 'left', sortable: true, + show: true, }, { - name: 'File', + field: 'versions', + name: 'Versions', align: 'left', sortable: true, - render: item => { - return  {item.filename}; + show: true, + }, + { + field: 'author', + name: 'Author', + align: 'left', + sortable: true, + render: author => { + return ( + <> + {author?.name} {author?.date} {author?.email} + + ); }, + show: true, }, { name: 'Actions', align: 'left', + show: true, render: item => { + const isOpen = openPopoverId === item.name; return ( - <> - { - const file = { - name: item.filename, - path: item.relative_dirname, - details: item.details, - position: item.position, - }; - setDetailsRequest(file); - setIsFlyoutVisible(true); - }} - /> - + togglePopover(item.name)} + /> + } + isOpen={isOpen} + closePopover={closePopover} + > + + + { + const file = { + name: item.name, + module: item.module, + details: { + parents: item.parents, + author: item.author, + references: item.references, + }, + }; + setDetailsRequest(file); + setIsFlyoutVisible(true); + closePopover(); + }} + > + View + + + + { + closePopover(); + }} + > + Edit + + { + closePopover(); + }} + color='danger' + > + Delete + + + + { + ev.stopPropagation(); + closePopover(); + }} + > + Export + + + + ); }, }, diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-files/files-info.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-files/files-info.tsx new file mode 100644 index 0000000000..17b8a5605c --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-files/files-info.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react'; +import _ from 'lodash'; +import { + EuiPage, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiButtonIcon, + EuiFieldText, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FileViewer } from '../../../../common/assets/file-viewer'; +import { ResourcesHandler } from '../../../../controllers/resources-handler'; +import { getServices } from '../../../../services'; + +export const DecodersFile = ({ type, name, version }) => { + const fileName = `${type}${name}${version}`; + const navigationService = getServices().navigationService; + const resourcesHandler = new ResourcesHandler('decoders'); + const [content, setContent] = useState(false); + + //Get file content + useEffect(() => { + const fetchData = async () => { + setContent(await resourcesHandler.getFileContent(fileName, '')); + }; + fetchData(); + }, []); + + return ( + <> + + + + + {(!content && ( + + + + {}} + /> + + + + + + + )) || ( + + + + { + navigationService + .getInstance() + .navigate(`/engine/decoders`); + }} + /> + + {`${type}/${name}/${version}`} + + + )} + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx index d364c1e252..d097328eae 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx @@ -32,6 +32,13 @@ export const DecodersTable = () => { > Add new decoders file , + {}} + > + Exports files + , ]; return ( @@ -78,6 +85,7 @@ export const DecodersTable = () => { endpoint={'/decoders'} isExpandable={true} downloadCsv + showFieldSelector showReload tablePageSizeOptions={[10, 25, 50, 100]} /> @@ -86,10 +94,7 @@ export const DecodersTable = () => { onClose={closeFlyout} className='wz-inventory wzApp wz-decoders-flyout' > - + )} diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx index 3334ac9c01..71ab7a7cc4 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx @@ -1,34 +1,10 @@ export const columns = () => { return [ { - field: 'filename', + field: 'name', name: 'Name', align: 'left', sortable: true, }, - { - field: 'status', - name: 'Status', - align: 'left', - sortable: true, - }, - { - field: 'relative_dirname', - name: 'Path', - align: 'left', - sortable: true, - }, - { - field: 'position', - name: 'Position', - align: 'left', - sortable: true, - }, - { - field: 'details.order', - name: 'Order', - align: 'left', - sortable: true, - }, ]; }; diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx index 4b6874624e..9143ddb2e7 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx @@ -17,37 +17,25 @@ import { getServices } from '../../../../services'; import { columns } from './decoders-details-columns'; import { colors } from '../decoders-columns'; -export const DecodersDetails = ({ item, setIsFlyoutVisible }) => { +export const DecodersDetails = ({ item }) => { + const navigationService = getServices().navigationService; const TableWzAPI = getServices().TableWzAPI; - /** - * Render the basic information in a list - * @param {Number} position - * @param {String} file - * @param {String} path - */ - const renderInfo = (position, file, path) => { + + const renderInfo = (path, params, type) => { return ( - - Position - {position} - - - File - - - setIsFlyoutVisible(false)}> -  {file} - - - - - - Path + + {type} - - setIsFlyoutVisible(false)}> -  {path} + + { + navigationService + .getInstance() + .navigate(`/engine/${path}/${params}`); + }} + > +  {params} @@ -78,8 +66,11 @@ export const DecodersDetails = ({ item, setIsFlyoutVisible }) => { style={{ marginBottom: '4px', wordBreak: 'break-word' }} className='subdued-color' > - {k}:  - {details[key][k]} + {key == 'references' ? ( + {details[key][k]} + ) : ( + details[key][k] + )}
))} @@ -154,7 +145,14 @@ export const DecodersDetails = ({ item, setIsFlyoutVisible }) => { paddingSize='l' initialIsOpen={true} > - {renderInfo(item.position, item.name, item.path)} + + + {renderInfo('decoders/file', item.name, 'File')} + + + {renderInfo('integrations', item.module, 'Integration')} + +
diff --git a/plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx b/plugins/wazuh-engine/public/components/decoders/components/forms/addDecoder.tsx similarity index 94% rename from plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx rename to plugins/wazuh-engine/public/components/decoders/components/forms/addDecoder.tsx index 13b7089668..d4b3d64a32 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/forms/addDatabase.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/forms/addDecoder.tsx @@ -15,7 +15,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; -export const AddDatabase = () => { +export const AddDecoder = () => { const [isGoBackModalVisible, setIsGoBackModalVisible] = useState(false); const InputForm = getServices().InputForm; const useForm = getServices().useForm; @@ -28,13 +28,13 @@ export const AddDatabase = () => { if (isGoBackModalVisible) { modal = ( { setIsGoBackModalVisible(false); }} onConfirm={async () => { setIsGoBackModalVisible(false); - navigationService.getInstance().navigate('/engine/kvdbs'); + navigationService.getInstance().navigate('/engine/decoders'); }} cancelButtonText="No, don't do it" confirmButtonText='Yes, do it' @@ -59,7 +59,7 @@ export const AddDatabase = () => {
-

Create new database

+

Create new decoder

diff --git a/plugins/wazuh-engine/public/components/decoders/router.tsx b/plugins/wazuh-engine/public/components/decoders/router.tsx index d73c5c2633..7019f77bc8 100644 --- a/plugins/wazuh-engine/public/components/decoders/router.tsx +++ b/plugins/wazuh-engine/public/components/decoders/router.tsx @@ -1,18 +1,30 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; import { DecodersTable } from './components/decoders-overview'; -import { AddDatabase } from './components/forms/addDatabase'; +import { AddDecoder } from './components/forms/addDecoder'; +import { DecodersFile } from './components/decoders-files/files-info'; export const Decoders = props => { return ( - + } + path={`${props.basePath}/file/:type/:name/:version`} + render={fetchParams => { + return ( + + ); + }} > + + + ); }; diff --git a/plugins/wazuh-engine/public/controllers/resources-handler.ts b/plugins/wazuh-engine/public/controllers/resources-handler.ts index 2f9458b28b..389a1c6e9c 100644 --- a/plugins/wazuh-engine/public/controllers/resources-handler.ts +++ b/plugins/wazuh-engine/public/controllers/resources-handler.ts @@ -74,6 +74,27 @@ export class ResourcesHandler { } } + /** + * Get the content of any type of file KVDB lists... + * @param {String} fileName + */ + async getDecodersContent(name) { + try { + const result: any = await this.WzRequest.apiReq( + 'GET', + this.getResourceFilesPath(name), + { + params: { + raw: true, + }, + }, + ); + return (result || {}).data || ''; + } catch (error) { + throw error; + } + } + /** * Update the content of any type of file KVDB lists... * @param {String} fileName From 2f0782988e888df052c05b80517dcda329c105a3 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Wed, 7 Aug 2024 13:05:38 +0200 Subject: [PATCH 30/34] Added somes styles to flyouts --- .../public/common/flyout/engine-flyout.tsx | 14 ++ .../public/common/flyout/index.ts | 1 + .../public/common/flyout/styles.scss | 124 +++++++++++++++++ .../public/common/styles/styles.scss | 19 +-- .../decoders/components/decoders-columns.tsx | 127 ++++++++---------- .../decoders/components/decoders-overview.tsx | 13 +- .../kvdbs/components/kvdb-overview.tsx | 18 ++- 7 files changed, 211 insertions(+), 105 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/flyout/engine-flyout.tsx create mode 100644 plugins/wazuh-engine/public/common/flyout/index.ts create mode 100644 plugins/wazuh-engine/public/common/flyout/styles.scss diff --git a/plugins/wazuh-engine/public/common/flyout/engine-flyout.tsx b/plugins/wazuh-engine/public/common/flyout/engine-flyout.tsx new file mode 100644 index 0000000000..2e758da602 --- /dev/null +++ b/plugins/wazuh-engine/public/common/flyout/engine-flyout.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import './styles.scss'; +import { EuiFlyout } from '@elastic/eui'; +export const EngineFlyout = ({ onClose, children }) => { + return ( + + {children} + + ); +}; diff --git a/plugins/wazuh-engine/public/common/flyout/index.ts b/plugins/wazuh-engine/public/common/flyout/index.ts new file mode 100644 index 0000000000..42354c069a --- /dev/null +++ b/plugins/wazuh-engine/public/common/flyout/index.ts @@ -0,0 +1 @@ +export * from './engine-flyout'; diff --git a/plugins/wazuh-engine/public/common/flyout/styles.scss b/plugins/wazuh-engine/public/common/flyout/styles.scss new file mode 100644 index 0000000000..7dcd1f981d --- /dev/null +++ b/plugins/wazuh-engine/public/common/flyout/styles.scss @@ -0,0 +1,124 @@ +.wzApp { + padding: 0 !important; +} + +.wz-inventory { + .flyout-header { + padding: 12px 16px; + } + + .flyout-body > .euiFlyoutBody__overflow { + padding: 8px 16px 16px 16px; + mask-image: unset; + } + + .flyout-row { + padding: 16px; + border-top: 1px solid #d3dae6; + } + + .details-row { + background: #fcfdfe; + } + + .detail-icon { + position: absolute; + margin-top: 6px; + } + + .detail-title { + margin-left: 36px; + font-size: 14px; + font-weight: 500; + } + + .detail-value { + font-size: 14px; + font-weight: 300; + cursor: default; + margin-left: 36px; + float: left; + white-space: break-spaces; + position: relative; + } + + .buttonAddFilter { + min-height: 0; + } + + .detail-value-checksum { + font-size: 13px !important; + display: inline-flex; + } + + .detail-value-perm { + word-break: break-word; + display: inline-block; + overflow: hidden; + } + + .detail-tooltip { + position: absolute; + right: 36px; + background-color: #fcfdfe; + } + + .application .euiAccordion, + .flyout-body .euiAccordion { + margin: 0px -16px; + border-top: none; + border-bottom: 1px solid #d3dae6; + } + + .application .euiAccordion__triggerWrapper, + .flyout-body .euiAccordion__triggerWrapper { + padding: 0px 16px; + } + + .application .euiAccordion__triggerWrapper .euiTitle, + .flyout-body .euiAccordion__triggerWrapper .euiTitle { + font-size: 16px; + } + + .application .euiAccordion__button, + .flyout-body .euiAccordion__button { + padding: 0px 0px 8px 0; + } + + .events-accordion, + .euiStat .euiTitle .detail-value .euiAccordion { + border-bottom: none !important; + } + + .view-in-events-btn { + height: 30px; + } + + .module-discover-table .euiTableRow-isExpandedRow .euiTableCellContent { + animation: none !important; + } + + /* This resets to the original value of "display" style for EuiTooltip used in ".euiAccordion__childWrapper .euiToolTipAnchor" definition, + which is defined above in this same file as block!important. +*/ + .rule_reset_display_anchor .euiToolTipAnchor { + display: inline-block !important; + } +} + +.wz-decoders-flyout { + .flyout-header { + padding-right: 42px; + } + + .euiAccordion__button { + margin-top: 8px; + } + + .euiFlyoutBody__overflow { + padding-top: 3px; + } + .euiAccordion__children-isLoading { + line-height: inherit; + } +} diff --git a/plugins/wazuh-engine/public/common/styles/styles.scss b/plugins/wazuh-engine/public/common/styles/styles.scss index 958f223062..94d25088b2 100644 --- a/plugins/wazuh-engine/public/common/styles/styles.scss +++ b/plugins/wazuh-engine/public/common/styles/styles.scss @@ -2,23 +2,6 @@ color: #808184; } -.wz-decoders-flyout { - .flyout-header { - padding-right: 42px; - } - - .euiAccordion__button { - margin-top: 8px; - } - - .euiFlyoutBody__overflow { - padding-top: 3px; - } - .euiAccordion__children-isLoading { - line-height: inherit; - } -} - .wz-inventory { .flyout-header { padding: 12px 16px; @@ -123,6 +106,6 @@ } } -.wzApp .euiFlyoutBody .euiFlyoutBody__overflowContent { +.euiFlyoutBody .euiFlyoutBody__overflowContent { padding: 0 !important; } diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx index c8dcf98b07..b7744e354e 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx @@ -101,80 +101,59 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { name: 'Actions', align: 'left', show: true, - render: item => { - const isOpen = openPopoverId === item.name; - return ( - togglePopover(item.name)} - /> - } - isOpen={isOpen} - closePopover={closePopover} - > - - - { - const file = { - name: item.name, - module: item.module, - details: { - parents: item.parents, - author: item.author, - references: item.references, - }, - }; - setDetailsRequest(file); - setIsFlyoutVisible(true); - closePopover(); - }} - > - View - - - - { - closePopover(); - }} - > - Edit - - { - closePopover(); - }} - color='danger' - > - Delete - - - - { - ev.stopPropagation(); - closePopover(); - }} - > - Export - - - - - ); - }, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: async item => { + const file = { + name: item.name, + module: item.module, + details: { + parents: item.parents, + author: item.author, + references: item.references, + }, + }; + setDetailsRequest(file); + setIsFlyoutVisible(true); + closePopover(); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit decoder', + icon: 'pencil', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-edit', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete decoder', + icon: 'trash', + type: 'icon', + onClick: async item => { + const file = {}; + }, + 'data-test-subj': 'action-delete', + }, + { + name: 'Import', + isPrimary: true, + description: 'Import decoder', + icon: 'eye', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-import', + }, + ], }, ]; }; diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx index d097328eae..fae6789d6c 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx @@ -1,8 +1,9 @@ import React, { useState } from 'react'; import { columns } from './decoders-columns'; -import { EuiFlyout, EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { getServices } from '../../../services'; import { DecodersDetails } from './details/decoders-details'; +import { EngineFlyout } from '../../../common/flyout'; export const DecodersTable = () => { const TableWzAPI = getServices().TableWzAPI; @@ -90,12 +91,12 @@ export const DecodersTable = () => { tablePageSizeOptions={[10, 25, 50, 100]} /> {isFlyoutVisible && ( - - - + children={ + + } + > )} ); diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx index 84c1649ebe..d8e9800c6b 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx @@ -1,8 +1,9 @@ import React, { useState } from 'react'; import { columns } from './kvdb-columns'; -import { EuiFlyout, EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { KeyInfo } from './keys/key-info'; import { getServices } from '../../../services'; +import { EngineFlyout } from '../../../common/flyout'; export const KVDBTable = ({ TableWzAPI }) => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); @@ -81,12 +82,15 @@ export const KVDBTable = ({ TableWzAPI }) => { tablePageSizeOptions={[10, 25, 50, 100]} /> {isFlyoutVisible && ( - - - + + } + > )} ); From 279330f9d6211a6165b099964203da0210f676c9 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Wed, 7 Aug 2024 13:47:56 +0200 Subject: [PATCH 31/34] Directories changes and policies first steps --- .../components/details/decoders-details.tsx | 2 +- .../overview/decoders-overview-columns.tsx | 176 ++++++++++++++++++ .../{ => overview}/decoders-overview.tsx | 8 +- .../public/components/decoders/router.tsx | 2 +- .../kvdbs/components/kvdb-columns.tsx | 109 ----------- .../overview/kvdb-overview-columns.tsx | 124 ++++++++++++ .../{ => overview}/kvdb-overview.tsx | 8 +- .../public/components/kvdbs/router.tsx | 2 +- .../components/policies-overview/index.ts | 0 .../policies-overview-columns.tsx} | 20 +- .../policies-overview/policies-overview.tsx | 101 ++++++++++ .../public/components/policies/index.ts | 2 +- .../public/components/policies/policies.tsx | 5 - .../public/components/policies/router.tsx | 15 ++ 14 files changed, 430 insertions(+), 144 deletions(-) create mode 100644 plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx rename plugins/wazuh-engine/public/components/decoders/components/{ => overview}/decoders-overview.tsx (92%) delete mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx rename plugins/wazuh-engine/public/components/kvdbs/components/{ => overview}/kvdb-overview.tsx (93%) create mode 100644 plugins/wazuh-engine/public/components/policies/components/policies-overview/index.ts rename plugins/wazuh-engine/public/components/{decoders/components/decoders-columns.tsx => policies/components/policies-overview/policies-overview-columns.tsx} (88%) create mode 100644 plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx delete mode 100644 plugins/wazuh-engine/public/components/policies/policies.tsx create mode 100644 plugins/wazuh-engine/public/components/policies/router.tsx diff --git a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx index 9143ddb2e7..683cf59b51 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { getServices } from '../../../../services'; import { columns } from './decoders-details-columns'; -import { colors } from '../decoders-columns'; +import { colors } from '../overview/decoders-overview-columns'; export const DecodersDetails = ({ item }) => { const navigationService = getServices().navigationService; diff --git a/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx new file mode 100644 index 0000000000..c878e35006 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { getServices } from '../../../../services'; + +export const columns = (setIsFlyoutVisible, setDetailsRequest) => { + const navigationService = getServices().navigationService; + + return [ + { + name: 'Name', + field: 'name', + align: 'left', + show: true, + render: name => { + return ( + <> + { + navigationService + .getInstance() + .navigate(`/engine/decoders/file/${name}`); + }} + > +  {name} + + + ); + }, + }, + { + field: 'title', + name: 'Title', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'compatibility', + name: 'Compatibility', + align: 'left', + sortable: true, + }, + { + field: 'parents', + name: 'Parents', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'module', + name: 'Module', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'versions', + name: 'Versions', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'author', + name: 'Author', + align: 'left', + sortable: true, + render: author => { + return ( + <> + {author?.name} {author?.date} {author?.email} + + ); + }, + show: true, + }, + { + name: 'Actions', + align: 'left', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: async item => { + const file = { + name: item.name, + module: item.module, + details: { + parents: item.parents, + author: item.author, + references: item.references, + }, + }; + setDetailsRequest(file); + setIsFlyoutVisible(true); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit decoder', + icon: 'pencil', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-edit', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete decoder', + icon: 'trash', + type: 'icon', + onClick: async item => { + const file = {}; + }, + 'data-test-subj': 'action-delete', + }, + { + name: 'Import', + isPrimary: true, + description: 'Import decoder', + icon: 'eye', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-import', + }, + ], + }, + ]; +}; + +export const colors = [ + '#004A65', + '#00665F', + '#BF4B45', + '#BF9037', + '#1D8C2E', + 'BB3ABF', + '#00B1F1', + '#00F2E2', + '#7F322E', + '#7F6025', + '#104C19', + '7C267F', + '#0079A5', + '#00A69B', + '#FF645C', + '#FFC04A', + '#2ACC43', + 'F94DFF', + '#0082B2', + '#00B3A7', + '#401917', + '#403012', + '#2DD947', + '3E1340', + '#00668B', + '#008C83', + '#E55A53', + '#E5AD43', + '#25B23B', + 'E045E5', +]; diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx similarity index 92% rename from plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx rename to plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx index fae6789d6c..25c755f9bf 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-overview.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; -import { columns } from './decoders-columns'; +import { columns } from './decoders-overview-columns'; import { EuiButtonEmpty } from '@elastic/eui'; -import { getServices } from '../../../services'; -import { DecodersDetails } from './details/decoders-details'; -import { EngineFlyout } from '../../../common/flyout'; +import { getServices } from '../../../../services'; +import { DecodersDetails } from '../details/decoders-details'; +import { EngineFlyout } from '../../../../common/flyout'; export const DecodersTable = () => { const TableWzAPI = getServices().TableWzAPI; diff --git a/plugins/wazuh-engine/public/components/decoders/router.tsx b/plugins/wazuh-engine/public/components/decoders/router.tsx index 7019f77bc8..ab7a092090 100644 --- a/plugins/wazuh-engine/public/components/decoders/router.tsx +++ b/plugins/wazuh-engine/public/components/decoders/router.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DecodersTable } from './components/decoders-overview'; +import { DecodersTable } from './components/overview/decoders-overview'; import { AddDecoder } from './components/forms/addDecoder'; import { DecodersFile } from './components/decoders-files/files-info'; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx deleted file mode 100644 index 1d8f522303..0000000000 --- a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-columns.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from 'react'; -import { EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { ResourcesHandler } from '../../../controllers/resources-handler'; - -export const columns = (setIsFlyoutVisible, setKeysRequest) => { - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const [getActualDB, setActualDB] = useState(null); - let modal; - - if (isDeleteModalVisible) { - modal = ( - { - setIsDeleteModalVisible(false); - }} - onConfirm={async () => { - await resourcesHandler.deleteFile( - getActualDB.filename || getActualDB.name, - ); - setIsDeleteModalVisible(false); - }} - cancelButtonText="No, don't do it" - confirmButtonText='Yes, do it' - defaultFocusedButton='confirm' - > -

Are you sure?

-
- ); - } - const resourcesHandler = new ResourcesHandler('lists'); - - return [ - { - field: 'date', - name: 'Date', - align: 'left', - sortable: true, - }, - { - field: 'filename', - name: 'Name', - align: 'left', - sortable: true, - }, - { - field: 'description', - name: 'Description', - align: 'left', - sortable: true, - }, - { - field: 'relative_dirname', - name: 'Path', - align: 'left', - sortable: true, - }, - { - field: 'elements', - name: 'Elements', - align: 'left', - sortable: true, - }, - { - name: 'Actions', - align: 'left', - render: item => { - return ( - <> - { - const result = await resourcesHandler.getFileContent( - item.filename, - item.relative_dirname, - ); - const file = { - name: item.filename, - content: result, - path: item.relative_dirname, - }; - setKeysRequest(file); - setIsFlyoutVisible(true); - }} - /> - { - setActualDB(item); - setIsDeleteModalVisible(true); - }} - color='danger' - /> - { - ev.stopPropagation(); - }} - /> - {modal} - - ); - }, - }, - ]; -}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx new file mode 100644 index 0000000000..b21a086f91 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { EuiConfirmModal } from '@elastic/eui'; +import { ResourcesHandler } from '../../../../controllers/resources-handler'; + +export const columns = (setIsFlyoutVisible, setKeysRequest) => { + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const [getActualDB, setActualDB] = useState(null); + let modal; + + if (isDeleteModalVisible) { + modal = ( + { + setIsDeleteModalVisible(false); + }} + onConfirm={async () => { + await resourcesHandler.deleteFile( + getActualDB.filename || getActualDB.name, + ); + setIsDeleteModalVisible(false); + }} + cancelButtonText="No, don't do it" + confirmButtonText='Yes, do it' + defaultFocusedButton='confirm' + > +

Are you sure?

+
+ ); + } + const resourcesHandler = new ResourcesHandler('lists'); + + return [ + { + field: 'date', + name: 'Date', + align: 'left', + sortable: true, + }, + { + field: 'filename', + name: 'Name', + align: 'left', + sortable: true, + }, + { + field: 'description', + name: 'Description', + align: 'left', + sortable: true, + }, + { + field: 'relative_dirname', + name: 'Path', + align: 'left', + sortable: true, + }, + { + field: 'elements', + name: 'Elements', + align: 'left', + sortable: true, + }, + { + field: '', + name: 'Actions', + align: 'left', + sortable: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: async item => { + const result = await resourcesHandler.getFileContent( + item.filename, + item.relative_dirname, + ); + const file = { + name: item.filename, + content: result, + path: item.relative_dirname, + }; + setKeysRequest(file); + setIsFlyoutVisible(true); + }, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit database', + icon: 'pencil', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-edit', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete database', + icon: 'trash', + type: 'icon', + onClick: async item => { + setActualDB(item); + setIsDeleteModalVisible(true); + }, + 'data-test-subj': 'action-delete', + }, + { + name: 'Import', + isPrimary: true, + description: 'Import database', + icon: 'eye', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-import', + }, + ], + }, + ]; +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx similarity index 93% rename from plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx rename to plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx index d8e9800c6b..ea7a88d98a 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/kvdb-overview.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; -import { columns } from './kvdb-columns'; +import { columns } from './kvdb-overview-columns'; import { EuiButtonEmpty } from '@elastic/eui'; -import { KeyInfo } from './keys/key-info'; -import { getServices } from '../../../services'; -import { EngineFlyout } from '../../../common/flyout'; +import { KeyInfo } from '../keys/key-info'; +import { getServices } from '../../../../services'; +import { EngineFlyout } from '../../../../common/flyout'; export const KVDBTable = ({ TableWzAPI }) => { const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); diff --git a/plugins/wazuh-engine/public/components/kvdbs/router.tsx b/plugins/wazuh-engine/public/components/kvdbs/router.tsx index f243d3a1f6..4fa2da84c5 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/router.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/router.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { KVDBTable } from './components/kvdb-overview'; +import { KVDBTable } from './components/overview/kvdb-overview'; import { getServices } from '../../services'; import { AddDatabase } from './components/forms/addDatabase'; diff --git a/plugins/wazuh-engine/public/components/policies/components/policies-overview/index.ts b/plugins/wazuh-engine/public/components/policies/components/policies-overview/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx similarity index 88% rename from plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx rename to plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx index b7744e354e..824781b694 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/decoders-columns.tsx +++ b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx @@ -1,25 +1,10 @@ import React, { useState } from 'react'; -import { - EuiButtonEmpty, - EuiButtonIcon, - EuiLink, - EuiPopover, - EuiFlexItem, - EuiFlexGroup, -} from '@elastic/eui'; -import { getServices } from '../../../services'; +import { EuiLink } from '@elastic/eui'; +import { getServices } from '../../../../services'; export const columns = (setIsFlyoutVisible, setDetailsRequest) => { const navigationService = getServices().navigationService; - const [openPopoverId, setOpenPopoverId] = useState(null); - const togglePopover = id => { - setOpenPopoverId(currentOpenId => (currentOpenId === id ? null : id)); - }; - - const closePopover = () => { - setOpenPopoverId(null); - }; return [ { name: 'Name', @@ -120,7 +105,6 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { }; setDetailsRequest(file); setIsFlyoutVisible(true); - closePopover(); }, 'data-test-subj': 'action-view', }, diff --git a/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx new file mode 100644 index 0000000000..77a8b4e550 --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { columns } from './policies-overview-columns'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { getServices } from '../../../../services'; +import { EngineFlyout } from '../../../../common/flyout'; + +export const PoliciesTable = () => { + const TableWzAPI = getServices().TableWzAPI; + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [policiesRequest, setPoliciesRequest] = useState(false); + const WzRequest = getServices().WzRequest; + const navigationService = getServices().navigationService; + const closeFlyout = () => setIsFlyoutVisible(false); + + const searchBarWQLOptions = { + searchTermFields: ['filename', 'relative_dirname'], + filterButtons: [ + { + id: 'relative-dirname', + input: 'relative_dirname=etc/lists', + label: 'Custom lists', + }, + ], + }; + + const actionButtons = [ + { + navigationService.getInstance().navigate('/engine/policies/new'); + }} + > + Add new policy + , + {}} + > + Exports files + , + ]; + + return ( +
+ { + try { + const response = await WzRequest.apiReq('GET', '/decoders', { + params: { + distinct: true, + limit: 30, + select: field, + sort: `+${field}`, + ...(currentValue ? { q: `${field}~${currentValue}` } : {}), + }, + }); + return response?.data?.data.affected_items.map(item => ({ + label: item[field], + })); + } catch (error) { + return []; + } + }, + }, + }} + searchTable + endpoint={'/policies'} + isExpandable={true} + downloadCsv + showFieldSelector + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + /> + {isFlyoutVisible && ( + } + > + )} +
+ ); +}; diff --git a/plugins/wazuh-engine/public/components/policies/index.ts b/plugins/wazuh-engine/public/components/policies/index.ts index 76a4276901..e52c4de3fa 100644 --- a/plugins/wazuh-engine/public/components/policies/index.ts +++ b/plugins/wazuh-engine/public/components/policies/index.ts @@ -1 +1 @@ -export { Policies } from './policies'; +export { Policies } from './router'; diff --git a/plugins/wazuh-engine/public/components/policies/policies.tsx b/plugins/wazuh-engine/public/components/policies/policies.tsx deleted file mode 100644 index 0e5ff09310..0000000000 --- a/plugins/wazuh-engine/public/components/policies/policies.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export const Policies = () => { - return <>Policies; -}; diff --git a/plugins/wazuh-engine/public/components/policies/router.tsx b/plugins/wazuh-engine/public/components/policies/router.tsx new file mode 100644 index 0000000000..073eb0e6a6 --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/router.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { PoliciesTable } from './components/policies-overview/policies-overview'; + +export const Policies = props => { + return ( + + + } + > + + ); +}; From 6584357174665706eaab5418e5bb1fc61f67da29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20David=20Guti=C3=A9rrez?= Date: Wed, 7 Aug 2024 14:30:38 +0200 Subject: [PATCH 32/34] feat(engine): enhance form --- .../wazuh-engine/public/common/form/index.ts | 1 + .../public/common/form/input-asset-check.tsx | 14 +- .../public/common/form/input-asset-map.tsx | 97 +++----------- .../public/common/form/input-asset-parse.tsx | 34 +++++ .../components/rules/components/form.tsx | 125 ++++++++++++++---- .../public/components/rules/spec-merge.json | 2 +- 6 files changed, 161 insertions(+), 112 deletions(-) create mode 100644 plugins/wazuh-engine/public/common/form/input-asset-parse.tsx diff --git a/plugins/wazuh-engine/public/common/form/index.ts b/plugins/wazuh-engine/public/common/form/index.ts index 0e0c9f6f6e..51f450b1b2 100644 --- a/plugins/wazuh-engine/public/common/form/index.ts +++ b/plugins/wazuh-engine/public/common/form/index.ts @@ -1,3 +1,4 @@ export * from './group-form'; export * from './input-asset-check'; export * from './input-asset-map'; +export * from './input-asset-parse'; diff --git a/plugins/wazuh-engine/public/common/form/input-asset-check.tsx b/plugins/wazuh-engine/public/common/form/input-asset-check.tsx index 0be5669541..f96a9ec144 100644 --- a/plugins/wazuh-engine/public/common/form/input-asset-check.tsx +++ b/plugins/wazuh-engine/public/common/form/input-asset-check.tsx @@ -64,12 +64,14 @@ export const InputAssetCheck = props => { {fields.mode_list_content.fields.map((field, index) => ( <> - - - - - - + {[ + { key: 'field', label: 'Field' }, + { key: 'field', label: 'Value' }, + ].map(({ key, label }) => ( + + + + ))} { - const { useForm, InputForm } = props; - - const { fields } = useForm({ - mode: { - type: 'select', - initialValue: 'text', - options: { - select: [ - { - text: 'text', - value: 'text', - }, - { - text: 'list', - value: 'list', - }, - ], - }, - }, - mode_text_content: { - type: 'textarea', - initialValue: '', - placeholder: 'Test', - }, - // property:value - mode_list_content: { - type: 'arrayOf', - initialValue: [ - { - field: '', - value: '', - }, - ], - fields: { - field: { - type: 'text', - initialValue: '', - }, - value: { - type: 'text', - initialValue: '', - }, - }, - }, - }); +export const InputAssetMap = ({ field, InputForm }) => { return ( <> - - - {fields.mode.value === 'text' ? ( - - ) : ( - <> - {fields.mode_list_content.fields.map((field, index) => ( - <> - - - - - - - - - fields.mode_list_content.removeItem(index)} - > - - - - + {field.fields.map((fieldNested, indexField) => ( + + {['field', 'value'].map(fieldName => ( + + + ))} - - Add - - - - )} + + field.removeItem(indexField)} + > + + + ))} + + Add ); }; diff --git a/plugins/wazuh-engine/public/common/form/input-asset-parse.tsx b/plugins/wazuh-engine/public/common/form/input-asset-parse.tsx new file mode 100644 index 0000000000..d65fda37a2 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/input-asset-parse.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +export const InputAssetParse = ({ field, InputForm }) => { + return ( + <> + {field.fields.map((fieldNested, indexField) => ( + + {['field', 'value'].map(fieldName => ( + + + + ))} + + field.removeItem(indexField)} + > + + + ))} + + Add + + ); +}; diff --git a/plugins/wazuh-engine/public/components/rules/components/form.tsx b/plugins/wazuh-engine/public/components/rules/components/form.tsx index bb49bad4a8..57f45a261b 100644 --- a/plugins/wazuh-engine/public/components/rules/components/form.tsx +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -8,6 +8,7 @@ import { FormInputLabel, InputAssetCheck, InputAssetMap, + InputAssetParse, addSpaceBetween, } from '../../../common/form'; import { @@ -19,13 +20,13 @@ import { } from '@elastic/eui'; export const Form = props => { - const { useForm, InputForm } = props; + const { useForm, InputForm, onSave } = props; const specForm = useMemo(() => { const result = transfromAssetSpecToForm(spec, specMerge); return result; }, []); - const { fields } = useForm(specForm); + const { fields, errors, changed } = useForm(specForm); return ( <> @@ -37,18 +38,24 @@ export const Form = props => { return ( } /> ); })} - + + } + > <> {fields['metadata.versions'].fields.map( ({ version: field }, indexField) => ( @@ -77,7 +84,14 @@ export const Form = props => { - + + } + > <> {fields['metadata.references'].fields.map( ({ reference: field }, indexField) => ( @@ -115,14 +129,26 @@ export const Form = props => { return ( + } /> ); })} - + + } + > <> {fields['definitions'].fields.map((field, indexField) => ( @@ -146,7 +172,14 @@ export const Form = props => { - + + } + > { /> - - + + } + > + - + + } + > <> {fields.normalize.fields .map((field, indexNormalize) => ( @@ -180,7 +223,14 @@ export const Form = props => { > } > - + + } + > { /> - - + + } + > + - - + } + > + @@ -211,6 +270,18 @@ export const Form = props => { Add + + + + onSave({ fields, errors, changed })} + > + Save + + + ); }; diff --git a/plugins/wazuh-engine/public/components/rules/spec-merge.json b/plugins/wazuh-engine/public/components/rules/spec-merge.json index a2e4f5c355..cab4dc1ff1 100644 --- a/plugins/wazuh-engine/public/components/rules/spec-merge.json +++ b/plugins/wazuh-engine/public/components/rules/spec-merge.json @@ -186,7 +186,7 @@ "initialValue": "" }, "value": { - "type": "text", + "type": "textarea", "initialValue": "" } }, From 32231aa1bf5960848f6f193071467c30721e5eba Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Wed, 7 Aug 2024 14:53:25 +0200 Subject: [PATCH 33/34] Added colums to policies --- .../security/policies/get-policies.json | 1060 ++++++++++------- .../overview/decoders-overview-columns.tsx | 2 +- .../overview/kvdb-overview-columns.tsx | 2 +- .../policies-overview-columns.tsx | 83 +- .../policies-overview/policies-overview.tsx | 2 +- 5 files changed, 656 insertions(+), 493 deletions(-) diff --git a/docker/imposter/security/policies/get-policies.json b/docker/imposter/security/policies/get-policies.json index 928b625433..41738ea8c4 100644 --- a/docker/imposter/security/policies/get-policies.json +++ b/docker/imposter/security/policies/get-policies.json @@ -2,429 +2,647 @@ "data": { "affected_items": [ { - "id": 1, - "name": "agents_all_resourceless", - "policy": { - "actions": ["agent:create", "group:create"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1, 5] - }, - { - "id": 2, - "name": "agents_all_agents", - "policy": { - "actions": [ - "agent:read", - "agent:delete", - "agent:modify_group", - "agent:reconnect", - "agent:restart", - "agent:upgrade" - ], - "resources": ["agent:id:*", "agent:group:*"], - "effect": "allow" - }, - "roles": [1, 5] - }, - { - "id": 3, - "name": "agents_all_groups", - "policy": { - "actions": [ - "group:read", - "group:delete", - "group:update_config", - "group:modify_assignments" - ], - "resources": ["group:id:*"], - "effect": "allow" - }, - "roles": [1, 5] - }, - { - "id": 4, - "name": "agents_read_agents", - "policy": { - "actions": ["agent:read"], - "resources": ["agent:id:*", "agent:group:*"], - "effect": "allow" - }, - "roles": [2, 4, 100] - }, - { - "id": 5, - "name": "agents_read_groups", - "policy": { - "actions": ["group:read"], - "resources": ["group:id:*"], - "effect": "allow" - }, - "roles": [2, 4, 100] - }, - { - "id": 6, - "name": "agents_commands_agents", - "policy": { - "actions": ["active-response:command"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 7, - "name": "security_all_resourceless", - "policy": { - "actions": [ - "security:create", - "security:create_user", - "security:read_config", - "security:update_config", - "security:revoke", - "security:edit_run_as" - ], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 8, - "name": "security_all_security", - "policy": { - "actions": ["security:read", "security:update", "security:delete"], - "resources": ["role:id:*", "policy:id:*", "user:id:*", "rule:id:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 9, - "name": "users_all_resourceless", - "policy": { - "actions": [ - "security:create_user", - "security:revoke", - "security:edit_run_as" - ], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [3] - }, - { - "id": 10, - "name": "users_all_users", - "policy": { - "actions": ["security:read", "security:update", "security:delete"], - "resources": ["user:id:*"], - "effect": "allow" - }, - "roles": [3] - }, - { - "id": 11, - "name": "users_modify_run_as_flag", - "policy": { - "actions": ["security:edit_run_as"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [] - }, - { - "id": 12, - "name": "ciscat_read_ciscat", - "policy": { - "actions": ["ciscat:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1, 2, 100] - }, - { - "id": 13, - "name": "decoders_read_decoders", - "policy": { - "actions": ["decoders:read"], - "resources": ["decoder:file:*"], - "effect": "allow" - }, - "roles": [2, 100] - }, - { - "id": 14, - "name": "decoders_all_files", - "policy": { - "actions": ["decoders:read", "decoders:delete"], - "resources": ["decoder:file:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 15, - "name": "decoders_all_resourceless", - "policy": { - "actions": ["decoders:update"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 16, - "name": "mitre_read_mitre", - "policy": { - "actions": ["mitre:read"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1, 2, 100] - }, - { - "id": 17, - "name": "lists_read_rules", - "policy": { - "actions": ["lists:read"], - "resources": ["list:file:*"], - "effect": "allow" - }, - "roles": [2, 100] - }, - { - "id": 18, - "name": "lists_all_rules", - "policy": { - "actions": ["lists:read", "lists:delete"], - "resources": ["list:file:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 19, - "name": "lists_all_resourceless", - "policy": { - "actions": ["lists:update"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 20, - "name": "rootcheck_read_rootcheck", - "policy": { - "actions": ["rootcheck:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [2, 100] - }, - { - "id": 21, - "name": "rootcheck_all_rootcheck", - "policy": { - "actions": ["rootcheck:clear", "rootcheck:read", "rootcheck:run"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 22, - "name": "rules_read_rules", - "policy": { - "actions": ["rules:read"], - "resources": ["rule:file:*"], - "effect": "allow" - }, - "roles": [2, 100] - }, - { - "id": 23, - "name": "rules_all_files", - "policy": { - "actions": ["rules:read", "rules:delete"], - "resources": ["rule:file:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 24, - "name": "rules_all_resourceless", - "policy": { - "actions": ["rules:update"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 25, - "name": "sca_read_sca", - "policy": { - "actions": ["sca:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1, 2, 100] - }, - { - "id": 26, - "name": "syscheck_read_syscheck", - "policy": { - "actions": ["syscheck:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [2, 100] - }, - { - "id": 27, - "name": "syscheck_all_syscheck", - "policy": { - "actions": ["syscheck:clear", "syscheck:read", "syscheck:run"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 28, - "name": "syscollector_read_syscollector", - "policy": { - "actions": ["syscollector:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1, 2, 100] - }, - { - "id": 29, - "name": "cluster_all_resourceless", - "policy": { - "actions": [ - "cluster:status", - "manager:read", - "manager:read_api_config", - "manager:update_config", - "manager:restart" - ], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1, 7] - }, - { - "id": 30, - "name": "cluster_all_nodes", - "policy": { - "actions": [ - "cluster:read_api_config", - "cluster:read", - "cluster:restart", - "cluster:update_config" - ], - "resources": ["node:id:*"], - "effect": "allow" - }, - "roles": [1, 7] - }, - { - "id": 31, - "name": "cluster_read_resourceless", - "policy": { - "actions": [ - "cluster:status", - "manager:read", - "manager:read_api_config" - ], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [2, 6, 100] - }, - { - "id": 32, - "name": "cluster_read_nodes", - "policy": { - "actions": [ - "cluster:read_api_config", - "cluster:read", - "cluster:read_api_config" - ], - "resources": ["node:id:*"], - "effect": "allow" - }, - "roles": [2, 6, 100] - }, - { - "id": 33, - "name": "logtest_all_logtest", - "policy": { - "actions": ["logtest:run"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1, 100] - }, - { - "id": 34, - "name": "task_status_task", - "policy": { - "actions": ["task:status"], - "resources": ["*:*:*"], - "effect": "allow" - }, - "roles": [1] - }, - { - "id": 35, - "name": "vulnerability_read_vulnerability", - "policy": { - "actions": ["vulnerability:read"], - "resources": ["agent:id:*"], - "effect": "allow" - }, - "roles": [1, 2, 100] - }, - { - "id": 100, - "name": "manager_deny_read", - "policy": { - "actions": ["manager:read", "cluster:read"], - "resources": ["*:*:*", "node:id:*"], - "effect": "deny" - }, - "roles": [100] - }, - { - "id": 101, - "name": "custom_manager_deny_read", - "policy": { - "actions": ["manager:read", "cluster:read"], - "resources": ["*:*:*", "node:id:*"], - "effect": "deny" - }, - "roles": [100] - }, - { - "id": 102, - "name": "custom_manager_deny_read2", - "policy": { - "actions": ["manager:read", "cluster:read"], - "resources": ["*:*:*", "node:id:*"], - "effect": "deny" - }, - "roles": [100] + "policy": "policy/wazuh/0", + "hash": "13009429687400424171", + "assets": [ + "integration/wazuh-core/0", + "integration/syslog/0", + "integration/system/0", + "integration/windows/0", + "integration/apache-http/0", + "integration/suricata/0" + ], + "default_parents": { + "user": "decoder/integrations/0", + "wazuh": ["rule/enrichment/0", "decoder/integrations/0"] + } + }, + { + "policy": "policy/siem/1", + "hash": "42893465918273619834", + "assets": [ + "integration/elk/1", + "integration/sysmon/1", + "integration/zeek/1", + "integration/linux/1", + "integration/nginx/1", + "integration/cisco/1" + ], + "default_parents": { + "user": "decoder/integrations/1", + "wazuh": ["rule/enrichment/1", "decoder/integrations/1"] + } + }, + { + "policy": "policy/security/2", + "hash": "12938402938402984012", + "assets": [ + "integration/firewall/2", + "integration/idp/2", + "integration/ossec/2", + "integration/windows/2", + "integration/apache-http/2", + "integration/suricata/2" + ], + "default_parents": { + "user": "decoder/integrations/2", + "wazuh": ["rule/enrichment/2", "decoder/integrations/2"] + } + }, + { + "policy": "policy/incident-response/3", + "hash": "98723498723987239847", + "assets": [ + "integration/splunk/3", + "integration/syslog/3", + "integration/linux/3", + "integration/windows/3", + "integration/palo-alto/3", + "integration/fortigate/3" + ], + "default_parents": { + "user": "decoder/integrations/3", + "wazuh": ["rule/enrichment/3", "decoder/integrations/3"] + } + }, + { + "policy": "policy/network-monitoring/4", + "hash": "43509283409823984092", + "assets": [ + "integration/nmap/4", + "integration/sysmon/4", + "integration/zeek/4", + "integration/linux/4", + "integration/nginx/4", + "integration/cisco/4" + ], + "default_parents": { + "user": "decoder/integrations/4", + "wazuh": ["rule/enrichment/4", "decoder/integrations/4"] + } + }, + { + "policy": "policy/log-management/5", + "hash": "12094823409283402984", + "assets": [ + "integration/elk/5", + "integration/syslog/5", + "integration/linux/5", + "integration/windows/5", + "integration/apache-http/5", + "integration/suricata/5" + ], + "default_parents": { + "user": "decoder/integrations/5", + "wazuh": ["rule/enrichment/5", "decoder/integrations/5"] + } + }, + { + "policy": "policy/threat-hunting/6", + "hash": "23908409238409283490", + "assets": [ + "integration/wazuh-core/6", + "integration/sysmon/6", + "integration/zeek/6", + "integration/linux/6", + "integration/nginx/6", + "integration/cisco/6" + ], + "default_parents": { + "user": "decoder/integrations/6", + "wazuh": ["rule/enrichment/6", "decoder/integrations/6"] + } + }, + { + "policy": "policy/vulnerability-management/7", + "hash": "32094823094823094823", + "assets": [ + "integration/openvas/7", + "integration/syslog/7", + "integration/linux/7", + "integration/windows/7", + "integration/apache-http/7", + "integration/suricata/7" + ], + "default_parents": { + "user": "decoder/integrations/7", + "wazuh": ["rule/enrichment/7", "decoder/integrations/7"] + } + }, + { + "policy": "policy/compliance/8", + "hash": "94023840238402384238", + "assets": [ + "integration/nist/8", + "integration/sysmon/8", + "integration/zeek/8", + "integration/linux/8", + "integration/nginx/8", + "integration/cisco/8" + ], + "default_parents": { + "user": "decoder/integrations/8", + "wazuh": ["rule/enrichment/8", "decoder/integrations/8"] + } + }, + { + "policy": "policy/encryption/9", + "hash": "94823094823094823094", + "assets": [ + "integration/ssl/9", + "integration/syslog/9", + "integration/linux/9", + "integration/windows/9", + "integration/apache-http/9", + "integration/suricata/9" + ], + "default_parents": { + "user": "decoder/integrations/9", + "wazuh": ["rule/enrichment/9", "decoder/integrations/9"] + } + }, + { + "policy": "policy/malware-detection/10", + "hash": "94823094823402938409", + "assets": [ + "integration/clamav/10", + "integration/sysmon/10", + "integration/zeek/10", + "integration/linux/10", + "integration/nginx/10", + "integration/cisco/10" + ], + "default_parents": { + "user": "decoder/integrations/10", + "wazuh": ["rule/enrichment/10", "decoder/integrations/10"] + } + }, + { + "policy": "policy/cloud-security/11", + "hash": "23840923840239408234", + "assets": [ + "integration/aws/11", + "integration/syslog/11", + "integration/linux/11", + "integration/windows/11", + "integration/apache-http/11", + "integration/suricata/11" + ], + "default_parents": { + "user": "decoder/integrations/11", + "wazuh": ["rule/enrichment/11", "decoder/integrations/11"] + } + }, + { + "policy": "policy/container-security/12", + "hash": "32094832094823094823", + "assets": [ + "integration/docker/12", + "integration/sysmon/12", + "integration/zeek/12", + "integration/linux/12", + "integration/nginx/12", + "integration/cisco/12" + ], + "default_parents": { + "user": "decoder/integrations/12", + "wazuh": ["rule/enrichment/12", "decoder/integrations/12"] + } + }, + { + "policy": "policy/endpoint-security/13", + "hash": "94823094823402938402", + "assets": [ + "integration/bitdefender/13", + "integration/syslog/13", + "integration/linux/13", + "integration/windows/13", + "integration/apache-http/13", + "integration/suricata/13" + ], + "default_parents": { + "user": "decoder/integrations/13", + "wazuh": ["rule/enrichment/13", "decoder/integrations/13"] + } + }, + { + "policy": "policy/data-protection/14", + "hash": "23409823409823409823", + "assets": [ + "integration/gdpr/14", + "integration/sysmon/14", + "integration/zeek/14", + "integration/linux/14", + "integration/nginx/14", + "integration/cisco/14" + ], + "default_parents": { + "user": "decoder/integrations/14", + "wazuh": ["rule/enrichment/14", "decoder/integrations/14"] + } + }, + { + "policy": "policy/risk-management/15", + "hash": "93482934829348239482", + "assets": [ + "integration/iso27001/15", + "integration/syslog/15", + "integration/linux/15", + "integration/windows/15", + "integration/apache-http/15", + "integration/suricata/15" + ], + "default_parents": { + "user": "decoder/integrations/15", + "wazuh": ["rule/enrichment/15", "decoder/integrations/15"] + } + }, + { + "policy": "policy/zero-trust/16", + "hash": "49823094820394820384", + "assets": [ + "integration/okta/16", + "integration/sysmon/16", + "integration/zeek/16", + "integration/linux/16", + "integration/nginx/16", + "integration/cisco/16" + ], + "default_parents": { + "user": "decoder/integrations/16", + "wazuh": ["rule/enrichment/16", "decoder/integrations/16"] + } + }, + { + "policy": "policy/business-continuity/17", + "hash": "23908402398402398409", + "assets": [ + "integration/backup/17", + "integration/syslog/17", + "integration/linux/17", + "integration/windows/17", + "integration/apache-http/17", + "integration/suricata/17" + ], + "default_parents": { + "user": "decoder/integrations/17", + "wazuh": ["rule/enrichment/17", "decoder/integrations/17"] + } + }, + { + "policy": "policy/identity-management/18", + "hash": "23094823094823094823", + "assets": [ + "integration/ad/18", + "integration/sysmon/18", + "integration/zeek/18", + "integration/linux/18", + "integration/nginx/18", + "integration/cisco/18" + ], + "default_parents": { + "user": "decoder/integrations/18", + "wazuh": ["rule/enrichment/18", "decoder/integrations/18"] + } + }, + { + "policy": "policy/supply-chain-security/19", + "hash": "43820394820394820394", + "assets": [ + "integration/scm/19", + "integration/syslog/19", + "integration/linux/19", + "integration/windows/19", + "integration/apache-http/19", + "integration/suricata/19" + ], + "default_parents": { + "user": "decoder/integrations/19", + "wazuh": ["rule/enrichment/19", "decoder/integrations/19"] + } + }, + { + "policy": "policy/privacy/20", + "hash": "93482039482039482039", + "assets": [ + "integration/ccpa/20", + "integration/sysmon/20", + "integration/zeek/20", + "integration/linux/20", + "integration/nginx/20", + "integration/cisco/20" + ], + "default_parents": { + "user": "decoder/integrations/20", + "wazuh": ["rule/enrichment/20", "decoder/integrations/20"] + } + }, + { + "policy": "policy/mobile-security/21", + "hash": "23498239482394823984", + "assets": [ + "integration/mdm/21", + "integration/syslog/21", + "integration/linux/21", + "integration/windows/21", + "integration/apache-http/21", + "integration/suricata/21" + ], + "default_parents": { + "user": "decoder/integrations/21", + "wazuh": ["rule/enrichment/21", "decoder/integrations/21"] + } + }, + { + "policy": "policy/data-leak-prevention/22", + "hash": "92834928349823498234", + "assets": [ + "integration/dlp/22", + "integration/sysmon/22", + "integration/zeek/22", + "integration/linux/22", + "integration/nginx/22", + "integration/cisco/22" + ], + "default_parents": { + "user": "decoder/integrations/22", + "wazuh": ["rule/enrichment/22", "decoder/integrations/22"] + } + }, + { + "policy": "policy/incident-response/23", + "hash": "09823098230982309823", + "assets": [ + "integration/splunk/23", + "integration/syslog/23", + "integration/linux/23", + "integration/windows/23", + "integration/palo-alto/23", + "integration/fortigate/23" + ], + "default_parents": { + "user": "decoder/integrations/23", + "wazuh": ["rule/enrichment/23", "decoder/integrations/23"] + } + }, + { + "policy": "policy/remote-access/24", + "hash": "94820394820394820384", + "assets": [ + "integration/vpn/24", + "integration/syslog/24", + "integration/linux/24", + "integration/windows/24", + "integration/apache-http/24", + "integration/suricata/24" + ], + "default_parents": { + "user": "decoder/integrations/24", + "wazuh": ["rule/enrichment/24", "decoder/integrations/24"] + } + }, + { + "policy": "policy/social-engineering/25", + "hash": "29308429304820394823", + "assets": [ + "integration/phishing/25", + "integration/sysmon/25", + "integration/zeek/25", + "integration/linux/25", + "integration/nginx/25", + "integration/cisco/25" + ], + "default_parents": { + "user": "decoder/integrations/25", + "wazuh": ["rule/enrichment/25", "decoder/integrations/25"] + } + }, + { + "policy": "policy/data-recovery/26", + "hash": "92834092834098234098", + "assets": [ + "integration/backup/26", + "integration/syslog/26", + "integration/linux/26", + "integration/windows/26", + "integration/apache-http/26", + "integration/suricata/26" + ], + "default_parents": { + "user": "decoder/integrations/26", + "wazuh": ["rule/enrichment/26", "decoder/integrations/26"] + } + }, + { + "policy": "policy/encryption/27", + "hash": "23908402384092384029", + "assets": [ + "integration/ssl/27", + "integration/sysmon/27", + "integration/zeek/27", + "integration/linux/27", + "integration/nginx/27", + "integration/cisco/27" + ], + "default_parents": { + "user": "decoder/integrations/27", + "wazuh": ["rule/enrichment/27", "decoder/integrations/27"] + } + }, + { + "policy": "policy/patch-management/28", + "hash": "23908402384923840923", + "assets": [ + "integration/wsus/28", + "integration/syslog/28", + "integration/linux/28", + "integration/windows/28", + "integration/apache-http/28", + "integration/suricata/28" + ], + "default_parents": { + "user": "decoder/integrations/28", + "wazuh": ["rule/enrichment/28", "decoder/integrations/28"] + } + }, + { + "policy": "policy/intrusion-detection/29", + "hash": "93840293840923840923", + "assets": [ + "integration/ossec/29", + "integration/sysmon/29", + "integration/zeek/29", + "integration/linux/29", + "integration/nginx/29", + "integration/cisco/29" + ], + "default_parents": { + "user": "decoder/integrations/29", + "wazuh": ["rule/enrichment/29", "decoder/integrations/29"] + } + }, + { + "policy": "policy/fraud-detection/30", + "hash": "29308402384023840239", + "assets": [ + "integration/aml/30", + "integration/syslog/30", + "integration/linux/30", + "integration/windows/30", + "integration/apache-http/30", + "integration/suricata/30" + ], + "default_parents": { + "user": "decoder/integrations/30", + "wazuh": ["rule/enrichment/30", "decoder/integrations/30"] + } + }, + { + "policy": "policy/privacy/31", + "hash": "23984023984023984029", + "assets": [ + "integration/gdpr/31", + "integration/sysmon/31", + "integration/zeek/31", + "integration/linux/31", + "integration/nginx/31", + "integration/cisco/31" + ], + "default_parents": { + "user": "decoder/integrations/31", + "wazuh": ["rule/enrichment/31", "decoder/integrations/31"] + } + }, + { + "policy": "policy/digital-rights/32", + "hash": "23908402394823984092", + "assets": [ + "integration/drm/32", + "integration/syslog/32", + "integration/linux/32", + "integration/windows/32", + "integration/apache-http/32", + "integration/suricata/32" + ], + "default_parents": { + "user": "decoder/integrations/32", + "wazuh": ["rule/enrichment/32", "decoder/integrations/32"] + } + }, + { + "policy": "policy/audit/33", + "hash": "28309823098423908420", + "assets": [ + "integration/siem/33", + "integration/sysmon/33", + "integration/zeek/33", + "integration/linux/33", + "integration/nginx/33", + "integration/cisco/33" + ], + "default_parents": { + "user": "decoder/integrations/33", + "wazuh": ["rule/enrichment/33", "decoder/integrations/33"] + } + }, + { + "policy": "policy/access-control/34", + "hash": "39482039482039482398", + "assets": [ + "integration/rbac/34", + "integration/syslog/34", + "integration/linux/34", + "integration/windows/34", + "integration/apache-http/34", + "integration/suricata/34" + ], + "default_parents": { + "user": "decoder/integrations/34", + "wazuh": ["rule/enrichment/34", "decoder/integrations/34"] + } + }, + { + "policy": "policy/cloud-compliance/35", + "hash": "23984023984023984023", + "assets": [ + "integration/azure/35", + "integration/sysmon/35", + "integration/zeek/35", + "integration/linux/35", + "integration/nginx/35", + "integration/cisco/35" + ], + "default_parents": { + "user": "decoder/integrations/35", + "wazuh": ["rule/enrichment/35", "decoder/integrations/35"] + } + }, + { + "policy": "policy/endpoint-management/36", + "hash": "23094823094823094823", + "assets": [ + "integration/mem/36", + "integration/syslog/36", + "integration/linux/36", + "integration/windows/36", + "integration/apache-http/36", + "integration/suricata/36" + ], + "default_parents": { + "user": "decoder/integrations/36", + "wazuh": ["rule/enrichment/36", "decoder/integrations/36"] + } + }, + { + "policy": "policy/network-defense/37", + "hash": "23984023984023984029", + "assets": [ + "integration/firewall/37", + "integration/sysmon/37", + "integration/zeek/37", + "integration/linux/37", + "integration/nginx/37", + "integration/cisco/37" + ], + "default_parents": { + "user": "decoder/integrations/37", + "wazuh": ["rule/enrichment/37", "decoder/integrations/37"] + } + }, + { + "policy": "policy/network-monitoring/38", + "hash": "23908409238409283409", + "assets": [ + "integration/snmp/38", + "integration/syslog/38", + "integration/linux/38", + "integration/windows/38", + "integration/apache-http/38", + "integration/suricata/38" + ], + "default_parents": { + "user": "decoder/integrations/38", + "wazuh": ["rule/enrichment/38", "decoder/integrations/38"] + } + }, + { + "policy": "policy/data-loss-prevention/39", + "hash": "94823094823094823094", + "assets": [ + "integration/dlp/39", + "integration/sysmon/39", + "integration/zeek/39", + "integration/linux/39", + "integration/nginx/39", + "integration/cisco/39" + ], + "default_parents": { + "user": "decoder/integrations/39", + "wazuh": ["rule/enrichment/39", "decoder/integrations/39"] + } } ], - "total_affected_items": 36, + "total_affected_items": 39, "total_failed_items": 0, "failed_items": [] }, diff --git a/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx index c878e35006..b07f14b766 100644 --- a/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx +++ b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx @@ -132,7 +132,7 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { name: 'Import', isPrimary: true, description: 'Import decoder', - icon: 'eye', + icon: 'importAction', type: 'icon', onClick: async item => {}, 'data-test-subj': 'action-import', diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx index b21a086f91..5b3e730398 100644 --- a/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx +++ b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx @@ -113,7 +113,7 @@ export const columns = (setIsFlyoutVisible, setKeysRequest) => { name: 'Import', isPrimary: true, description: 'Import database', - icon: 'eye', + icon: 'importAction', type: 'icon', onClick: async item => {}, 'data-test-subj': 'action-import', diff --git a/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx index 824781b694..cd27bfc9b9 100644 --- a/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx +++ b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx @@ -7,81 +7,38 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { return [ { - name: 'Name', - field: 'name', + name: 'Policy', + field: 'policy', align: 'left', show: true, render: name => { return ( <> - { - navigationService - .getInstance() - .navigate(`/engine/decoders/file/${name}`); - }} - > -  {name} - + {}}> {name} ); }, }, { - field: 'title', - name: 'Title', + field: 'hash', + name: 'Hash', align: 'left', sortable: true, show: true, }, { - field: 'description', - name: 'Description', + field: 'assets', + name: 'Assets', align: 'left', sortable: true, show: true, }, { - field: 'compatibility', - name: 'Compatibility', + field: 'default_parents', + name: 'Default parents', align: 'left', sortable: true, }, - { - field: 'parents', - name: 'Parents', - align: 'left', - sortable: true, - show: true, - }, - { - field: 'module', - name: 'Module', - align: 'left', - sortable: true, - show: true, - }, - { - field: 'versions', - name: 'Versions', - align: 'left', - sortable: true, - show: true, - }, - { - field: 'author', - name: 'Author', - align: 'left', - sortable: true, - render: author => { - return ( - <> - {author?.name} {author?.date} {author?.email} - - ); - }, - show: true, - }, { name: 'Actions', align: 'left', @@ -93,25 +50,13 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { description: 'View details', icon: 'eye', type: 'icon', - onClick: async item => { - const file = { - name: item.name, - module: item.module, - details: { - parents: item.parents, - author: item.author, - references: item.references, - }, - }; - setDetailsRequest(file); - setIsFlyoutVisible(true); - }, + onClick: async item => {}, 'data-test-subj': 'action-view', }, { name: 'Edit', isPrimary: true, - description: 'Edit decoder', + description: 'Edit policy', icon: 'pencil', type: 'icon', onClick: async item => {}, @@ -120,7 +65,7 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { { name: 'Delete', isPrimary: true, - description: 'Delete decoder', + description: 'Delete policy', icon: 'trash', type: 'icon', onClick: async item => { @@ -131,8 +76,8 @@ export const columns = (setIsFlyoutVisible, setDetailsRequest) => { { name: 'Import', isPrimary: true, - description: 'Import decoder', - icon: 'eye', + description: 'Import policy', + icon: 'importAction', type: 'icon', onClick: async item => {}, 'data-test-subj': 'action-import', diff --git a/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx index 77a8b4e550..7b74f00e6e 100644 --- a/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx +++ b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx @@ -83,7 +83,7 @@ export const PoliciesTable = () => { }, }} searchTable - endpoint={'/policies'} + endpoint={'/security/policies'} isExpandable={true} downloadCsv showFieldSelector From 419757fcf78a295cc2af4a6ffd2e8f0f61ebe84c Mon Sep 17 00:00:00 2001 From: Federico Rodriguez Date: Fri, 23 Aug 2024 12:43:45 +0200 Subject: [PATCH 34/34] Prettier --- docker/imposter/api-info/api_info.json | 2 +- plugins/wazuh-engine/scripts/jest.js | 5 ++++- plugins/wazuh-engine/scripts/manifest.js | 5 ++--- .../server/services/saved-object/types/available-updates.ts | 5 ++++- plugins/wazuh-engine/tsconfig.json | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docker/imposter/api-info/api_info.json b/docker/imposter/api-info/api_info.json index 28a2ba4e14..fbd8634f9e 100644 --- a/docker/imposter/api-info/api_info.json +++ b/docker/imposter/api-info/api_info.json @@ -9,4 +9,4 @@ "timestamp": "2022-06-13T17:20:03Z" }, "error": 0 -} \ No newline at end of file +} diff --git a/plugins/wazuh-engine/scripts/jest.js b/plugins/wazuh-engine/scripts/jest.js index cb58c54ec0..9d6bc3ae8d 100644 --- a/plugins/wazuh-engine/scripts/jest.js +++ b/plugins/wazuh-engine/scripts/jest.js @@ -11,7 +11,10 @@ // See all cli options in https://facebook.github.io/jest/docs/cli.html const path = require('path'); -process.argv.push('--config', path.resolve(__dirname, '../test/jest/config.js')); +process.argv.push( + '--config', + path.resolve(__dirname, '../test/jest/config.js'), +); require('../../../src/setup_node_env'); const jest = require('../../../node_modules/jest'); diff --git a/plugins/wazuh-engine/scripts/manifest.js b/plugins/wazuh-engine/scripts/manifest.js index 711059d3ac..d98460c0fc 100644 --- a/plugins/wazuh-engine/scripts/manifest.js +++ b/plugins/wazuh-engine/scripts/manifest.js @@ -1,4 +1,3 @@ - /* eslint-disable @typescript-eslint/no-var-requires */ const fs = require('fs'); @@ -13,5 +12,5 @@ function loadPackageJson() { } module.exports = { - loadPackageJson -}; \ No newline at end of file + loadPackageJson, +}; diff --git a/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts b/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts index 40820e4f87..425bd878b6 100644 --- a/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts +++ b/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts @@ -1,4 +1,7 @@ -import { SavedObjectsFieldMapping, SavedObjectsType } from 'opensearch-dashboards/server'; +import { + SavedObjectsFieldMapping, + SavedObjectsType, +} from 'opensearch-dashboards/server'; import { SAVED_OBJECT_UPDATES } from '../../../../common/constants'; const updateObjectType: SavedObjectsFieldMapping = { diff --git a/plugins/wazuh-engine/tsconfig.json b/plugins/wazuh-engine/tsconfig.json index d3b63f9aee..cc7e3e157f 100644 --- a/plugins/wazuh-engine/tsconfig.json +++ b/plugins/wazuh-engine/tsconfig.json @@ -14,4 +14,4 @@ "public/hooks" ], "exclude": [] -} \ No newline at end of file +}