From 0cf78ba14f025de4aab80f6b61c8e8710b638e02 Mon Sep 17 00:00:00 2001 From: JuanGarriuz Date: Fri, 23 Aug 2024 12:52:18 +0200 Subject: [PATCH] Add POC engine UX (#6803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added engine plugin with a first nav menu and a simple react router system * Added simple styles * Mocked imposter * added kvdb * 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 * feat(script): sample data generator for rules dataset * feat(engine): switch order menu and replace placeholders on views * feat(engine): use wazuh-asset specification to manage the creation or list of rules * fix: useForm for arrayOf type * Add routing, db creation, actions buttons are replaced and imposter response from pathname change * 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 * Create database feat updated * feat(engine): refactor form based on group of inputs * remove(engine): unused script files related to rules sample data * feat(engine): enhance sample data rule dataset * feat(engine): add filters and outputs sample data datasets * feat(engine): add view of outputs asset * feat(engine): enhance the sample data injector script documentation * Decoders * feat: add ability to remove field form arrayOf form field * 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 * feat(sample-data): add dataset of integrations * feat(engine): add integrations section * feat(engine): rename form component and minor fixes * Some routes changed, added interactive path to columns * fix(form): fix form when using arrayOf * feat(engine): enhance rule form * fix(form): remove console.log on hook * Changes on decoders * Added somes styles to flyouts * Directories changes and policies first steps * feat(engine): enhance form * Added colums to policies * Prettier --------- Co-authored-by: Antonio David GutiƩrrez Co-authored-by: Federico Rodriguez --- docker/imposter/api-info/api_info.json | 2 +- ...t_decoders_raw.txt => get_decoder_raw.txt} | 0 docker/imposter/decoders/get_decoders.json | 61 + docker/imposter/lists/get_lists.json | 25 + .../security/policies/get-policies.json | 1060 ++++++++++------- docker/imposter/wazuh-config.yml | 9 +- docker/osd-dev/dev.sh | 3 +- docker/osd-dev/dev.yml | 1 + plugins/main/opensearch_dashboards.json | 1 + plugins/main/public/app-router.tsx | 77 +- .../common/data-grid/data-grid-service.ts | 6 +- .../integrations/data-source-repository.ts | 45 + .../pattern/integrations/data-source.ts | 28 + .../data-source/pattern/integrations/index.ts | 2 + .../pattern/outputs/data-source-repository.ts | 45 + .../pattern/outputs/data-source.ts | 28 + .../data-source/pattern/outputs/index.ts | 2 + .../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 | 42 +- .../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 ++ .../public/components/common/form/hooks.tsx | 45 +- .../public/components/common/form/index.tsx | 10 +- .../common/search-bar/search-bar-service.ts | 1 + .../public/components/common/tables/index.ts | 1 + .../common/tables/table-data-basic.tsx | 236 ++++ .../components/common/tables/table-data.tsx | 292 +++++ .../common/tables/table-indexer-engine.tsx | 175 +++ .../document-view-table-and-json.tsx | 32 +- .../management/cdblists/views/list-editor.tsx | 178 +-- plugins/main/public/kibana-services.ts | 3 + plugins/main/public/plugin.ts | 2 + 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 | 14 + plugins/wazuh-engine/package.json | 25 + .../common/assets/create-asset-selector.tsx | 147 +++ .../public/common/assets/file-editor.tsx | 48 + .../public/common/assets/file-viewer.tsx | 41 + .../public/common/assets/index.ts | 2 + .../public/common/flyout/engine-flyout.tsx | 14 + .../public/common/flyout/index.ts | 1 + .../public/common/flyout/styles.scss | 124 ++ .../public/common/form/group-form.tsx | 190 +++ .../wazuh-engine/public/common/form/index.ts | 4 + .../public/common/form/input-asset-check.tsx | 95 ++ .../public/common/form/input-asset-map.tsx | 34 + .../public/common/form/input-asset-parse.tsx | 34 + .../public/common/styles/styles.scss | 111 ++ .../components/decoders-files/files-info.tsx | 96 ++ .../details/decoders-details-columns.tsx | 10 + .../components/details/decoders-details.tsx | 206 ++++ .../decoders/components/forms/addDecoder.tsx | 104 ++ .../overview/decoders-overview-columns.tsx | 176 +++ .../components/overview/decoders-overview.tsx | 103 ++ .../public/components/decoders/index.ts | 1 + .../public/components/decoders/router.tsx | 30 + .../components/decoders/spec-merge.json | 17 + .../public/components/decoders/spec.json | 42 + .../public/components/engine-layout.tsx | 26 + .../wazuh-engine/public/components/engine.tsx | 123 ++ .../public/components/filters/filters.tsx | 5 + .../public/components/filters/index.ts | 1 + .../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 | 1 + .../components/integrations/pages/create.tsx | 24 + .../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 + .../kvdbs/components/forms/addDatabase.tsx | 104 ++ .../kvdbs/components/keys/key-info.tsx | 30 + .../kvdbs/components/keys/keys-columns.tsx | 14 + .../overview/kvdb-overview-columns.tsx | 124 ++ .../components/overview/kvdb-overview.tsx | 97 ++ .../public/components/kvdbs/index.ts | 1 + .../public/components/kvdbs/router.tsx | 19 + .../public/components/kvdbs/spec-merge.json | 17 + .../public/components/kvdbs/spec.json | 27 + .../components/outputs/components/detail.tsx | 146 +++ .../components/outputs/components/form.tsx | 24 + .../components/outputs/components/index.ts | 1 + .../components/outputs/components/layout.tsx | 48 + .../public/components/outputs/index.ts | 1 + .../components/outputs/pages/create.tsx | 61 + .../public/components/outputs/pages/edit.tsx | 59 + .../public/components/outputs/pages/list.tsx | 252 ++++ .../public/components/outputs/router.tsx | 28 + .../public/components/outputs/spec-merge.json | 119 ++ .../public/components/outputs/spec.json | 232 ++++ .../outputs/utils/transform-asset-spec.ts | 62 + .../components/outputs/visualization.ts | 54 + .../components/policies-overview/index.ts | 0 .../policies-overview-columns.tsx | 121 ++ .../policies-overview/policies-overview.tsx | 101 ++ .../public/components/policies/index.ts | 1 + .../public/components/policies/router.tsx | 15 + .../components/rules/components/detail.tsx | 147 +++ .../components/rules/components/form.tsx | 287 +++++ .../components/rules/components/index.ts | 1 + .../components/rules/components/layout.tsx | 48 + .../public/components/rules/hocs/index.ts | 2 + .../rules/hocs/with-data-source-fetch.tsx | 20 + .../components/rules/hocs/with-guard.tsx | 76 ++ .../public/components/rules/index.ts | 1 + .../public/components/rules/pages/create.tsx | 61 + .../public/components/rules/pages/edit.tsx | 59 + .../public/components/rules/pages/list.tsx | 248 ++++ .../public/components/rules/router.tsx | 28 + .../public/components/rules/spec-merge.json | 204 ++++ .../public/components/rules/spec.json | 240 ++++ .../rules/utils/transform-asset-spec.ts | 62 + .../public/components/rules/visualization.ts | 54 + .../public/controllers/resources-handler.ts | 152 +++ plugins/wazuh-engine/public/hocs/index.ts | 2 + .../public/hocs/with-data-source-fetch.tsx | 20 + .../wazuh-engine/public/hocs/with-guard.tsx | 76 ++ 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 | 3 + plugins/wazuh-engine/public/types.ts | 9 + plugins/wazuh-engine/public/utils/index.ts | 0 plugins/wazuh-engine/scripts/jest.js | 22 + plugins/wazuh-engine/scripts/manifest.js | 16 + 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 | 84 ++ .../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 + scripts/sample-data/README.md | 83 ++ scripts/sample-data/dataset/filters/main.py | 114 ++ .../sample-data/dataset/filters/template.json | 97 ++ .../sample-data/dataset/integrations/main.py | 119 ++ .../dataset/integrations/template.json | 109 ++ scripts/sample-data/dataset/outputs/main.py | 114 ++ .../sample-data/dataset/outputs/template.json | 97 ++ scripts/sample-data/dataset/rules/main.py | 114 ++ .../sample-data/dataset/rules/template.json | 97 ++ scripts/sample-data/requirements.txt | 1 + scripts/sample-data/script.py | 75 ++ 178 files changed, 11005 insertions(+), 522 deletions(-) rename docker/imposter/decoders/{get_decoders_raw.txt => get_decoder_raw.txt} (100%) create mode 100644 docker/imposter/decoders/get_decoders.json create mode 100644 docker/imposter/lists/get_lists.json 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 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/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-engine.tsx 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/common/assets/create-asset-selector.tsx create mode 100644 plugins/wazuh-engine/public/common/assets/file-editor.tsx 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/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 create mode 100644 plugins/wazuh-engine/public/common/form/group-form.tsx create mode 100644 plugins/wazuh-engine/public/common/form/index.ts 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 create mode 100644 plugins/wazuh-engine/public/common/form/input-asset-parse.tsx create mode 100644 plugins/wazuh-engine/public/common/styles/styles.scss create mode 100644 plugins/wazuh-engine/public/components/decoders/components/decoders-files/files-info.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/addDecoder.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx create mode 100644 plugins/wazuh-engine/public/components/decoders/index.ts 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 create mode 100644 plugins/wazuh-engine/public/components/engine-layout.tsx 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/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 create mode 100644 plugins/wazuh-engine/public/components/integrations/index.ts 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 create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx 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/overview/kvdb-overview-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/index.ts create mode 100644 plugins/wazuh-engine/public/components/kvdbs/router.tsx create mode 100644 plugins/wazuh-engine/public/components/kvdbs/spec-merge.json create mode 100644 plugins/wazuh-engine/public/components/kvdbs/spec.json create mode 100644 plugins/wazuh-engine/public/components/outputs/components/detail.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 create mode 100644 plugins/wazuh-engine/public/components/outputs/index.ts 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/components/policies/components/policies-overview/index.ts create mode 100644 plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx create mode 100644 plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview.tsx create mode 100644 plugins/wazuh-engine/public/components/policies/index.ts create mode 100644 plugins/wazuh-engine/public/components/policies/router.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/components/detail.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/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 create mode 100644 plugins/wazuh-engine/public/components/rules/index.ts create mode 100644 plugins/wazuh-engine/public/components/rules/pages/create.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/pages/edit.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/pages/list.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/router.tsx create mode 100644 plugins/wazuh-engine/public/components/rules/spec-merge.json create mode 100644 plugins/wazuh-engine/public/components/rules/spec.json create mode 100644 plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts create mode 100644 plugins/wazuh-engine/public/components/rules/visualization.ts create mode 100644 plugins/wazuh-engine/public/controllers/resources-handler.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 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 create mode 100644 scripts/sample-data/README.md 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/integrations/main.py create mode 100644 scripts/sample-data/dataset/integrations/template.json create mode 100644 scripts/sample-data/dataset/outputs/main.py create mode 100644 scripts/sample-data/dataset/outputs/template.json 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/script.py diff --git a/docker/imposter/api-info/api_info.json b/docker/imposter/api-info/api_info.json index a6e9719fc2..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.10.0", + "api_version": "5.0.0", "revision": 1, "license_name": "GPL 2.0", "license_url": "https://github.com/wazuh/wazuh/blob/4.5/LICENSE", diff --git a/docker/imposter/decoders/get_decoders_raw.txt b/docker/imposter/decoders/get_decoder_raw.txt similarity index 100% rename from docker/imposter/decoders/get_decoders_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/lists/get_lists.json b/docker/imposter/lists/get_lists.json new file mode 100644 index 0000000000..5ce5829763 --- /dev/null +++ b/docker/imposter/lists/get_lists.json @@ -0,0 +1,25 @@ +{ + "data": { + "affected_items": [ + { + "filename": "test1", + "relative_dirname": "mockPath/test/1", + "elements": "8", + "date": "2024-06-03", + "description": "test mock" + }, + { + "filename": "test2", + "relative_dirname": "mockPath/test/version2", + "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/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/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index 8f99c6897e..d23418d081 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: decoders/get_decoders_raw.txt + staticFile: decoders/get_decoder_raw.txt # Update decoders file - method: PUT @@ -406,6 +408,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 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 c52f908135..e7f4349eee 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' - '${SRC}/wazuh-fleet:/home/node/kbn/plugins/wazuh-fleet' - wd_certs:/home/node/kbn/certs/ - ${WAZUH_DASHBOARD_CONF}:/home/node/kbn/config/opensearch_dashboards.yml diff --git a/plugins/main/opensearch_dashboards.json b/plugins/main/opensearch_dashboards.json index 457a87cd60..be6ca61d93 100644 --- a/plugins/main/opensearch_dashboards.json +++ b/plugins/main/opensearch_dashboards.json @@ -19,6 +19,7 @@ "opensearchDashboardsLegacy", "wazuhCheckUpdates", "wazuhCore", + "wazuhEngine", "wazuhFleet" ], "optionalPlugins": [ diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 3bbf4e224a..2e8f3c9357 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -2,14 +2,22 @@ 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, getWazuhFleetPlugin, getWzMainParams, } from './kibana-services'; import { updateCurrentPlatform } from './redux/actions/appStateActions'; import { useDispatch } from 'react-redux'; import { checkPluginVersion } from './utils'; -import { WzAuthentication, loadAppConfig } from './react-services'; +import { + WzAuthentication, + AppState, + GenericRequest, + 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'; @@ -26,6 +34,16 @@ import { WzSecurity } from './components/security'; import $ from 'jquery'; import NavigationService from './react-services/navigation-service'; 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, FleetDataSource, FleetDataSourceRepository, useDataSource, @@ -36,13 +54,26 @@ import { AlertsDataSource, AlertsDataSourceRepository, } 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'; import useSearchBar from './components/common/search-bar/use-search-bar'; import { WzSearchBar } from './components/common/search-bar'; -import { TableIndexer } from './components/common/tables'; +import { TableIndexer, TableIndexerEngine } from './components/common/tables'; import DocDetails from './components/common/wazuh-discover/components/doc-details'; import { useTimeFilter } from './components/common/hooks'; import { LoadingSpinner } from './components/common/loading-spinner/loading-spinner'; +import { TableWzAPI } from './components/common/tables'; +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'; +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(); @@ -144,6 +175,48 @@ export function Application(props) { exact render={props => } > + { + const { Engine } = getWazuhEnginePlugin(); + 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 426f346e4f..8e2378d3a6 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 @@ -84,6 +84,7 @@ export const exportSearchToCSV = async ( 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 @@ -165,7 +166,10 @@ export const exportSearchToCSV = async ( if (blobData) { // @ts-ignore - FileSaver?.saveAs(blobData, `events-${new Date().toISOString()}.csv`); + FileSaver?.saveAs( + blobData, + `${filePrefix}-${new Date().toISOString()}.csv`, + ); } }; 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/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/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 f0249a1eaf..60d707e107 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; @@ -82,8 +87,17 @@ const DocViewer = (props: tDocViewerProps) => { const [fieldRowOpen, setFieldRowOpen] = useState( {} as Record, ); - const { flattened, formatted, mapping, indexPattern, renderFields, docJSON } = - props; + const { + flattened, + formatted, + mapping, + indexPattern, + onFilter, + onToggleColumn, + isColumnActive, + renderFields, + docJSON, + } = props; return ( <> @@ -93,6 +107,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; @@ -123,6 +138,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/form/hooks.tsx b/plugins/main/public/components/common/form/hooks.tsx index e270978295..3b3787e265 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) => @@ -114,25 +115,45 @@ 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 }]) => ({ - ...accum, - [key]: { - currentValue: cloneDeep(defaultValue), - initialValue: cloneDeep(defaultValue), - defaultValue: cloneDeep(defaultValue), - }, - }), - {}, + [...pathFormField, 'fields', _state.length], + Object.fromEntries( + Object.entries(field.fields).map( + ([key, { defaultValue, initialValue, ...rest }]) => [ + key, + rest.type === 'arrayOf' + ? { + fields: [], + } + : { + currentValue: cloneDeep(initialValue), + initialValue: cloneDeep(initialValue), + defaultValue: cloneDeep(defaultValue), + }, + ], + ), ), ); return cloneDeep(newstate); }); }, + removeItem: index => { + setState(state => { + const _state = get(state, [...pathFormField, 'fields']); + + _state.splice(index, 1); + + const newState = set( + state, + [...pathFormField, 'fields'], + _state, + ); + + return cloneDeep(newState); + }); + }, } : { ...field, 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/main/public/components/common/search-bar/search-bar-service.ts b/plugins/main/public/components/common/search-bar/search-bar-service.ts index a8676c6762..228748b3bb 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 @@ -10,6 +10,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/index.ts b/plugins/main/public/components/common/tables/index.ts index 9451ae680d..71fb18f8c0 100644 --- a/plugins/main/public/components/common/tables/index.ts +++ b/plugins/main/public/components/common/tables/index.ts @@ -14,3 +14,4 @@ export * from './table-with-search-bar'; export * from './table-wz-api'; export * from './table-default'; export * from './table-indexer'; +export * from './table-indexer-engine'; 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-engine.tsx b/plugins/main/public/components/common/tables/table-indexer-engine.tsx new file mode 100644 index 0000000000..ebb7d479e9 --- /dev/null +++ b/plugins/main/public/components/common/tables/table-indexer-engine.tsx @@ -0,0 +1,175 @@ +import React from 'react'; +import { IntlProvider } from 'react-intl'; +import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; +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 { WzSearchBar } from '../../common/search-bar'; +import { TableData } from './table-data'; + +export const TableIndexerEngine = ({ + DataSource, + DataSourceRepository, + exportCSVPrefixFilename = '', + tableProps = {}, + onSetIndexPattern, +}: { + DataSource: any; + DataSourceRepository; + onSetIndexPattern: () => void; + 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 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 { + await exportSearchToCSV(params); + } catch (error) { + const searchError = ErrorFactory.create(HttpError, { + error, + message: 'Error downloading csv report', + }); + ErrorHandler.handleError(searchError); + } + }; + + React.useEffect(() => { + if (dataSource?.indexPattern) { + onSetIndexPattern && onSetIndexPattern(dataSource?.indexPattern); + } + }, [dataSource?.indexPattern]); + + const { postActionButtons, ...restTableProps } = tableProps; + + const enhancedPostActionButtons = function (props) { + return ( + <> + {postActionButtons && ( + {postActionButtons(props)} + )} + + + onClickExportResults({ + ...props, + dataSource, + filePrefix: exportCSVPrefixFilename, + }) + } + > + Export formatted + + + + ); + }; + + return ( + + <> + {isDataSourceLoading ? ( + + ) : ( + + } + showFieldSelector + fetchData={({ pagination, sorting, searchParams: { query } }) => { + return fetchData({ + query, + pagination, + sorting: { + columns: [ + { + id: sorting.sort.field, + direction: sorting.sort.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 }} + /> + )} + + + ); +}; 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 2592e340b9..63d27c13a7 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 @@ -8,21 +8,50 @@ export const DocumentViewTableAndJson = ({ document, indexPattern, renderFields, + 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 ( + ), }, { @@ -39,6 +68,7 @@ export const DocumentViewTableAndJson = ({ ), }, + ...renderExtraTabs(extraTabs.post), ]} /> 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/main/public/kibana-services.ts b/plugins/main/public/kibana-services.ts index e70a5859a6..013da25396 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'; import { WazuhFleetPluginStart } from '../../wazuh-fleet/public'; let angularModule: any = null; @@ -48,6 +49,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 316b92fbbd..1e58ec444b 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -24,6 +24,7 @@ import { setWazuhCheckUpdatesPlugin, setHeaderActionMenuMounter, setWazuhCorePlugin, + setWazuhEnginePlugin, setWazuhFleetPlugin, } from './kibana-services'; import { validate as validateNodeCronInterval } from 'node-cron'; @@ -211,6 +212,7 @@ export class WazuhPlugin setErrorOrchestrator(ErrorOrchestratorService); setWazuhCheckUpdatesPlugin(plugins.wazuhCheckUpdates); setWazuhCorePlugin(plugins.wazuhCore); + setWazuhEnginePlugin(plugins.wazuhEngine); setWazuhFleetPlugin(plugins.wazuhFleet); return {}; } diff --git a/plugins/main/public/types.ts b/plugins/main/public/types.ts index a12d598b93..47bcb0a857 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'; import { WazuhFleetPluginStart } from '../../wazuh-fleet/public'; @@ -33,6 +34,7 @@ export interface AppPluginStartDependencies { telemetry: TelemetryPluginStart; wazuhCheckUpdates: WazuhCheckUpdatesPluginStart; wazuhCore: WazuhCorePluginStart; + wazuhEngine: WazuhEnginePluginStart; dashboard: DashboardStart; wazuhFleet: WazuhFleetPluginStart; } diff --git a/plugins/main/public/utils/applications.ts b/plugins/main/public/utils/applications.ts index 7fce7ee21d..033839cccc 100644 --- a/plugins/main/public/utils/applications.ts +++ b/plugins/main/public/utils/applications.ts @@ -852,6 +852,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: 'Security Policies', + }), + breadcrumbLabel: i18n.translate('wz-app-engine-breadcrumbLabel', { + defaultMessage: 'Security Policies', + }), + description: i18n.translate('wz-app-engine-description', { + defaultMessage: 'Change', + }), + euiIconType: 'lensApp', + order: 602, + showInOverviewApp: false, + showInAgentMenu: false, + redirectTo: () => `/engine`, +}; + export const Applications = [ fileIntegrityMonitoring, overview, @@ -877,6 +896,7 @@ export const Applications = [ // endpointSummary, fleetManagement, 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..3ad2b9256c --- /dev/null +++ b/plugins/wazuh-engine/opensearch_dashboards.json @@ -0,0 +1,14 @@ +{ + "id": "wazuhEngine", + "version": "5.0.0-00", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [ + "opensearchDashboardsUtils", + "wazuhCore", + "dashboard", + "embeddable", + "data" + ] +} 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/common/assets/create-asset-selector.tsx b/plugins/wazuh-engine/public/common/assets/create-asset-selector.tsx new file mode 100644 index 0000000000..e2bff2be5c --- /dev/null +++ b/plugins/wazuh-engine/public/common/assets/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/assets/file-editor.tsx b/plugins/wazuh-engine/public/common/assets/file-editor.tsx new file mode 100644 index 0000000000..898ffa302f --- /dev/null +++ b/plugins/wazuh-engine/public/common/assets/file-editor.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + EuiCodeEditor, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, +} from '@elastic/eui'; + +export const FileEditor = ({ + 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/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/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/form/group-form.tsx b/plugins/wazuh-engine/public/common/form/group-form.tsx new file mode 100644 index 0000000000..0e15e034d7 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/group-form.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { get } from 'lodash'; +import { + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiSpacer, + EuiButton, + EuiButtonIcon, + EuiIcon, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +export const addSpaceBetween = (accum, item) => ( + <> + {accum} + + {item} + +); + +export 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 ( + + {rest.fields.map((item, index) => ( + + + {Object.entries(item).map(([name, field]) => + renderInput({ + ...field, + _label: name, + name, + }), + )} + + + rest.removeItem(index)} + > + + + ))} + + Add + + ); + }; + + return ( + <> + {renderGroups + .map(({ title, groupKey }) => ( + + )) + .reduce(addSpaceBetween)} + + + + onSave({ fields, errors, changed })} + > + Save + + + + + ); +}; + +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, <>)} + +); + +export const FormInputLabel = ({ + label, + description, +}: { + label: string; + description: string; +}) => ( + <> + {label} + {description && ( + + + + + + )} + +); + +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 new file mode 100644 index 0000000000..51f450b1b2 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/index.ts @@ -0,0 +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 new file mode 100644 index 0000000000..f96a9ec144 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/input-asset-check.tsx @@ -0,0 +1,95 @@ +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) => ( + <> + + {[ + { key: 'field', label: 'Field' }, + { key: 'field', label: 'Value' }, + ].map(({ key, label }) => ( + + + + ))} + + 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..589bf84394 --- /dev/null +++ b/plugins/wazuh-engine/public/common/form/input-asset-map.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +export const InputAssetMap = ({ field, InputForm }) => { + return ( + <> + {field.fields.map((fieldNested, indexField) => ( + + {['field', 'value'].map(fieldName => ( + + + + ))} + + 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/common/styles/styles.scss b/plugins/wazuh-engine/public/common/styles/styles.scss new file mode 100644 index 0000000000..94d25088b2 --- /dev/null +++ b/plugins/wazuh-engine/public/common/styles/styles.scss @@ -0,0 +1,111 @@ +.subdued-color { + color: #808184; +} + +.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; + } +} + +.euiFlyoutBody .euiFlyoutBody__overflowContent { + padding: 0 !important; +} 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/details/decoders-details-columns.tsx b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx new file mode 100644 index 0000000000..71ab7a7cc4 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details-columns.tsx @@ -0,0 +1,10 @@ +export const columns = () => { + return [ + { + field: 'name', + name: 'Name', + 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..683cf59b51 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/details/decoders-details.tsx @@ -0,0 +1,206 @@ +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 '../overview/decoders-overview-columns'; + +export const DecodersDetails = ({ item }) => { + const navigationService = getServices().navigationService; + const TableWzAPI = getServices().TableWzAPI; + + const renderInfo = (path, params, type) => { + return ( + + + {type} + + + { + navigationService + .getInstance() + .navigate(`/engine/${path}/${params}`); + }} + > +  {params} + + + + + + + ); + }; + + /** + * 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 => ( +
  • + {key == 'references' ? ( + {details[key][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('decoders/file', item.name, 'File')} + + + {renderInfo('integrations', item.module, 'Integration')} + + +
+
+
+ + + +

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/addDecoder.tsx b/plugins/wazuh-engine/public/components/decoders/components/forms/addDecoder.tsx new file mode 100644 index 0000000000..d4b3d64a32 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/forms/addDecoder.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 AddDecoder = () => { + 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/decoders'); + }} + 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 decoder

+
+
+ + + 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/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..b07f14b766 --- /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: 'importAction', + 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/overview/decoders-overview.tsx b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx new file mode 100644 index 0000000000..25c755f9bf --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/components/overview/decoders-overview.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react'; +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'; + +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 + , + {}} + > + 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={'/decoders'} + isExpandable={true} + downloadCsv + showFieldSelector + showReload + tablePageSizeOptions={[10, 25, 50, 100]} + /> + {isFlyoutVisible && ( + + } + > + )} +
+ ); +}; 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..a8a26eb236 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/index.ts @@ -0,0 +1 @@ +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..ab7a092090 --- /dev/null +++ b/plugins/wazuh-engine/public/components/decoders/router.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { DecodersTable } from './components/overview/decoders-overview'; +import { AddDecoder } from './components/forms/addDecoder'; +import { DecodersFile } from './components/decoders-files/files-info'; + +export const Decoders = props => { + return ( + + + + + { + 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/components/engine-layout.tsx b/plugins/wazuh-engine/public/components/engine-layout.tsx new file mode 100644 index 0000000000..91d4a9a656 --- /dev/null +++ b/plugins/wazuh-engine/public/components/engine-layout.tsx @@ -0,0 +1,26 @@ +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: 'Security policies', + }, + { + 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 new file mode 100644 index 0000000000..ee49adaaa4 --- /dev/null +++ b/plugins/wazuh-engine/public/components/engine.tsx @@ -0,0 +1,123 @@ +import React, { useState } from 'react'; +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'; +import { Outputs } from './outputs'; +import { Rules } from './rules'; +import { Integrations } from './integrations'; +import { KVDBs } from './kvdbs'; +import { Policies } from './policies'; +import { EngineLayout } from './engine-layout'; +import { getServices, setServices } from '../services'; + +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: 'Policies', + id: 'policies', + render: Policies, + }, + { + name: 'Integrations', + id: 'integrations', + render: Integrations, + }, + { + name: 'KVDBs', + id: 'kvdbs', + render: KVDBs, + }, +]; + +export const Engine = props => { + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + try { + !getServices(); + } catch (error) { + setServices(props); + } + + const sideNav = [ + { + name: 'Security policies', + id: 'engine', + items: views.map(({ render, ...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} + /> + + + + + {views.map(item => ( + { + return ( + + {item.render({ + ...props, + title: item.name, + basePath: `/engine/${item.id}`, + })} + + ); + }} + /> + ))} + + + + + + ); +}; 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..a8b14115f3 --- /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/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..0073096525 --- /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 Form = 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 new file mode 100644 index 0000000000..cd1681fa9b --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/index.ts @@ -0,0 +1 @@ +export { Integrations } from './router'; 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..9e349a0488 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/pages/create.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Layout } from '../components'; +import { Form } from '../components/form'; +import { EuiLink } from '@elastic/eui'; + +export const Create = props => { + const actions = [ + + Documentation + , + ]; + + 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..5ae07f6634 --- /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 { Form } 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..f111c755f9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/integrations/spec-merge.json @@ -0,0 +1,85 @@ +{ + "title": { + "_meta": { + "label": "Title", + "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/kvdbs/components/forms/addDatabase.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/forms/addDatabase.tsx new file mode 100644 index 0000000000..13b7089668 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/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/kvdbs/components/keys/key-info.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx new file mode 100644 index 0000000000..3e69dda61e --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/keys/key-info.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiFlyoutBody, +} from '@elastic/eui'; +import { getServices } from '../../../../services'; + +export const KeyInfo = ({ keys, setKeysRequest }) => { + const WzListEditor = getServices().WzListEditor; + + return ( + <> + + + { + 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/overview/kvdb-overview-columns.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview-columns.tsx new file mode 100644 index 0000000000..5b3e730398 --- /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: 'importAction', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-import', + }, + ], + }, + ]; +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx new file mode 100644 index 0000000000..ea7a88d98a --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/components/overview/kvdb-overview.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +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'; + +export const KVDBTable = ({ TableWzAPI }) => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [getKeysRequest, setKeysRequest] = 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/kvdbs/new'); + }} + > + Add new database + , + ]; + + 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 + tablePageSizeOptions={[10, 25, 50, 100]} + /> + {isFlyoutVisible && ( + + } + > + )} + + ); +}; 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..42e7143e8f --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/index.ts @@ -0,0 +1 @@ +export { KVDBs } from './router'; diff --git a/plugins/wazuh-engine/public/components/kvdbs/router.tsx b/plugins/wazuh-engine/public/components/kvdbs/router.tsx new file mode 100644 index 0000000000..4fa2da84c5 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/router.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { KVDBTable } from './components/overview/kvdb-overview'; +import { getServices } from '../../services'; +import { AddDatabase } from './components/forms/addDatabase'; + +export const KVDBs = props => { + return ( + + + + + } + > + + ); +}; diff --git a/plugins/wazuh-engine/public/components/kvdbs/spec-merge.json b/plugins/wazuh-engine/public/components/kvdbs/spec-merge.json new file mode 100644 index 0000000000..d321c39402 --- /dev/null +++ b/plugins/wazuh-engine/public/components/kvdbs/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/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": "^[^/]+(/[^/]+)*$" + } + } +} 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..4baa49a40f --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/components/detail.tsx @@ -0,0 +1,146 @@ +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'; +import { FileViewerFetchContent } from '../../../common/assets/file-viewer'; + +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: 'file', + name: 'File', + content: () => ''} />, + }, + { + 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/form.tsx b/plugins/wazuh-engine/public/components/outputs/components/form.tsx new file mode 100644 index 0000000000..b3cbfa77da --- /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 Form = 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 new file mode 100644 index 0000000000..0a6d8e8a96 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/index.ts @@ -0,0 +1 @@ +export { Outputs } from './router'; 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..824ba4d070 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/create.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Layout } from '../components'; +import { Form } from '../components/form'; +import { EuiButton, EuiLink } from '@elastic/eui'; +import { FileEditor } from '../../../common/assets'; + +export const CreateVisual = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; + +export const CreateFile = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; 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..5ae07f6634 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/edit.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { Form } 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/outputs/pages/list.tsx b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx new file mode 100644 index 0000000000..a261f7668e --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/pages/list.tsx @@ -0,0 +1,252 @@ +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, + OutputsDataSource, + OutputsDataSourceRepository, + title, + } = props; + + const actions = [ + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + { + props.navigationService + .getInstance() + .navigate( + `/engine/outputs/create/${ + modalOptions(false).find(({ id }) => id === editor)?.routePath + }`, + ); + }} + >, + ]; + + 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, + }, + '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: [ + { + 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..b572c42af5 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/router.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { List } from './pages/list'; +import { CreateFile, CreateVisual } 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..78e27dfe00 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/spec-merge.json @@ -0,0 +1,119 @@ +{ + "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": "Compatibility", + "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": "arrayOf", + "initialValue": [{ "value": "" }], + "fields": { + "value": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Check", + "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..47344ff616 --- /dev/null +++ b/plugins/wazuh-engine/public/components/outputs/spec.json @@ -0,0 +1,232 @@ +{ + "$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" + }, + "check2": { + "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" + } + }, + "check": { + "type": "array", + "description": "Variable definitions, used to define variables that can be reused in other parts of the asset", + "minProperties": 1 + }, + "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/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/policies/components/policies-overview/policies-overview-columns.tsx b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx new file mode 100644 index 0000000000..cd27bfc9b9 --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/components/policies-overview/policies-overview-columns.tsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { getServices } from '../../../../services'; + +export const columns = (setIsFlyoutVisible, setDetailsRequest) => { + const navigationService = getServices().navigationService; + + return [ + { + name: 'Policy', + field: 'policy', + align: 'left', + show: true, + render: name => { + return ( + <> + {}}> {name} + + ); + }, + }, + { + field: 'hash', + name: 'Hash', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'assets', + name: 'Assets', + align: 'left', + sortable: true, + show: true, + }, + { + field: 'default_parents', + name: 'Default parents', + align: 'left', + sortable: true, + }, + { + name: 'Actions', + align: 'left', + show: true, + actions: [ + { + name: 'View', + isPrimary: true, + description: 'View details', + icon: 'eye', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-view', + }, + { + name: 'Edit', + isPrimary: true, + description: 'Edit policy', + icon: 'pencil', + type: 'icon', + onClick: async item => {}, + 'data-test-subj': 'action-edit', + }, + { + name: 'Delete', + isPrimary: true, + description: 'Delete policy', + icon: 'trash', + type: 'icon', + onClick: async item => { + const file = {}; + }, + 'data-test-subj': 'action-delete', + }, + { + name: 'Import', + isPrimary: true, + description: 'Import policy', + icon: 'importAction', + 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/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..7b74f00e6e --- /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={'/security/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 new file mode 100644 index 0000000000..e52c4de3fa --- /dev/null +++ b/plugins/wazuh-engine/public/components/policies/index.ts @@ -0,0 +1 @@ +export { Policies } from './router'; 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 ( + + + } + > + + ); +}; 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..1112b378c4 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/detail.tsx @@ -0,0 +1,147 @@ +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'; +import { FileViewerFetchContent } from '../../../common/assets/file-viewer'; + +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: 'file', + name: 'File', + content: () => ''} />, + }, + { + 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 new file mode 100644 index 0000000000..57f45a261b --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/components/form.tsx @@ -0,0 +1,287 @@ +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, + FormInputGroupPanel, + FormInputLabel, + InputAssetCheck, + InputAssetMap, + InputAssetParse, + addSpaceBetween, +} from '../../../common/form'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, +} from '@elastic/eui'; + +export const Form = props => { + const { useForm, InputForm, onSave } = props; + const specForm = useMemo(() => { + const result = transfromAssetSpecToForm(spec, specMerge); + return result; + }, []); + + const { fields, errors, changed } = 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 + + + + + + onSave({ fields, errors, changed })} + > + Save + + + + + ); +}; 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/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/index.ts b/plugins/wazuh-engine/public/components/rules/index.ts new file mode 100644 index 0000000000..7280e01ba5 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/index.ts @@ -0,0 +1 @@ +export { Rules } from './router'; diff --git a/plugins/wazuh-engine/public/components/rules/pages/create.tsx b/plugins/wazuh-engine/public/components/rules/pages/create.tsx new file mode 100644 index 0000000000..dbdfd2da15 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/create.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Layout } from '../components'; +import { Form } from '../components/form'; +import { EuiButton, EuiLink } from '@elastic/eui'; +import { FileEditor } from '../../../common/assets'; + +export const CreateRuleVisual = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; + +export const CreateRuleFile = props => { + const actions = [ + + Documentation + , + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + ]; + + return ( + + + + ); +}; 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..1acf524d5f --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/edit.tsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import { Layout } from '../components'; +import { Form } from '../components/form'; +import { EuiButton, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { FileEditor } from '../../../common/assets'; + +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 new file mode 100644 index 0000000000..f75fe6ca28 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/pages/list.tsx @@ -0,0 +1,248 @@ +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 RulesList = props => { + const { TableIndexer, RulesDataSource, RulesDataSourceRepository, title } = + props; + + const actions = [ + { + // TODO: Implement + }} + iconType='importAction' + > + Import file + , + { + props.navigationService + .getInstance() + .navigate( + `/engine/rules/create/${ + modalOptions(false).find(({ id }) => id === editor)?.routePath + }`, + ); + }} + >, + ]; + + 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, + }, + '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: [ + { + 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:rules-main', + }, + }} + exportCSVPrefixFilename='rules' + 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/rules/router.tsx b/plugins/wazuh-engine/public/components/rules/router.tsx new file mode 100644 index 0000000000..cb58d72710 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/router.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { RulesList } from './pages/list'; +import { CreateRuleFile, CreateRuleVisual } 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..cab4dc1ff1 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/spec-merge.json @@ -0,0 +1,204 @@ +{ + "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": [], + "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": { + "type": "textarea", + "_meta": { + "label": "Description", + "groupForm": "metadata" + } + }, + "metadata.compatibility": { + "type": "textarea", + "_meta": { + "label": "Compatibility", + "groupForm": "metadata" + } + }, + "metadata.versions": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "version": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Versions", + "groupForm": "metadata" + } + }, + "parents": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "parent": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Parents", + "groupForm": "metadata" + } + }, + "definitions": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "key": { + "type": "text", + "initialValue": "" + }, + "value": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Definitions", + "groupForm": "metadata" + } + }, + "check": { + "type": "custom", + "initialValue": [], + "fields": { + "check": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Check", + "groupForm": "check" + } + }, + "map": { + "type": "arrayOf", + "initialValue": [], + "fields": { + "field": { + "type": "text", + "initialValue": "" + }, + "value": { + "type": "text", + "initialValue": "" + } + }, + "_meta": { + "label": "Map", + "groupForm": "map" + } + }, + "normalize": { + "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": "textarea", + "initialValue": "" + } + }, + "_meta": { + "label": "Map", + "groupForm": "map" + } + } + }, + "_meta": { + "label": "Normalize", + "groupForm": "normalize" + } + } +} 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..1d48456d56 --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/spec.json @@ -0,0 +1,240 @@ +{ + "$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": "^[^/]+/[^/]+/[^/]+$" + }, + "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" + } + }, + "check2": { + "$ref": "#/definitions/_check" + }, + "allow": { + "$ref": "#/definitions/_check" + }, + "check": { + "type": "array", + "description": "Variable definitions, used to define variables that can be reused in other parts of the asset", + "minProperties": 1 + }, + "map": { + "type": "array", + "description": "Variable definitions, used to define variables that can be reused in other parts of the asset", + "minProperties": 1 + }, + "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": "array", + "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/rules/utils/transform-asset-spec.ts b/plugins/wazuh-engine/public/components/rules/utils/transform-asset-spec.ts new file mode 100644 index 0000000000..866dc2048c --- /dev/null +++ b/plugins/wazuh-engine/public/components/rules/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/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), + }, + }, + }; +}; 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..389a1c6e9c --- /dev/null +++ b/plugins/wazuh-engine/public/controllers/resources-handler.ts @@ -0,0 +1,152 @@ +import { getServices } from '../services'; + +type LISTS = 'lists'; +type DECODERS = 'decoders'; +export type Resource = DECODERS | LISTS; +export const ResourcesConstants = { + LISTS: 'lists', + DECODERS: 'decoders', +}; + +export const resourceDictionary = { + [ResourcesConstants.LISTS]: { + resourcePath: '/lists', + permissionResource: value => `list:file:${value}`, + }, + [ResourcesConstants.DECODERS]: { + resourcePath: '/decoders', + permissionResource: value => `decoders: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; + } + } + + /** + * 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 + * @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/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 ? ( + + ) : ( + + ); + }; 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..5cd86c2c2b --- /dev/null +++ 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(); 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..9d6bc3ae8d --- /dev/null +++ b/plugins/wazuh-engine/scripts/jest.js @@ -0,0 +1,22 @@ +// # 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..d98460c0fc --- /dev/null +++ b/plugins/wazuh-engine/scripts/manifest.js @@ -0,0 +1,16 @@ +/* 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, +}; 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..425bd878b6 --- /dev/null +++ b/plugins/wazuh-engine/server/services/saved-object/types/available-updates.ts @@ -0,0 +1,84 @@ +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..cc7e3e157f --- /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": [] +} 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" 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`. 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/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" + } + } +} 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" + } + } +} diff --git a/scripts/sample-data/dataset/rules/main.py b/scripts/sample-data/dataset/rules/main.py new file mode 100644 index 0000000000..3d7c6b5ddd --- /dev/null +++ b/scripts/sample-data/dataset/rules/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-rules' +asset_identifier='rule' + +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/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/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()