From a5f2df81129ade08b62a66b5b59372ff77b20345 Mon Sep 17 00:00:00 2001 From: Ociel Gallardo Date: Mon, 2 Nov 2026 17:44:05 +0100 Subject: [PATCH 1/2] Add read-only Wazuh server API adapter (#982) Fifteen GET-only tools on the Wazuh 4.x server API (port 55000): manager info and status, agents and their summaries, rules, rule groups, decoders, SCA policies and checks, and FIM (syscheck) data. LOGIN_TOKEN signs in at /security/user/authenticate with loginBasicAuth (HTTP Basic on the login request only), reads data.token and sends it as Bearer; errorWhen turns failed_items 1701 into a not-found. Alerts and vulnerabilities (indexer) are out of scope. The live spec now signs in through the real LoginTokenService and checks that a wrong password fails with 401 without echoing the credentials. Updates the catalog, adapter counts and llms.txt. --- CITATION.cff | 2 +- README.de.md | 8 +- README.ja.md | 8 +- README.md | 8 +- README.zh-CN.md | 8 +- glama.json | 2 +- llms-full.txt | 6 +- llms.txt | 2 +- package.json | 2 +- packages/backend/src/adapters/catalog.ts | 2 + packages/backend/src/adapters/intl/wazuh.json | 910 ++++++++++++++++++ .../src/adapters/intl/wazuh.live.spec.ts | 362 +++++++ server.json | 2 +- 13 files changed, 1298 insertions(+), 24 deletions(-) create mode 100644 packages/backend/src/adapters/intl/wazuh.json create mode 100644 packages/backend/src/adapters/intl/wazuh.live.spec.ts diff --git a/CITATION.cff b/CITATION.cff index 9db2102a0..3b5fdc1dd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -13,7 +13,7 @@ abstract: > server that turns REST/OpenAPI, SOAP/WSDL and GraphQL APIs, SQL/NoSQL databases and other MCP servers into tools for AI clients such as Claude, ChatGPT, Google Gemini, GitHub Copilot and Cursor, without writing code. - It ships 694 pre-built adapters, with a focus on ERP and e-commerce + It ships 695 pre-built adapters, with a focus on ERP and e-commerce systems (SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO, etc.), a per-workspace knowledge graph served over MCP, File Search over company file servers diff --git a/README.de.md b/README.de.md index 2b3d465ff..625a4d99e 100644 --- a/README.de.md +++ b/README.de.md @@ -1,5 +1,5 @@

- AnythingMCP verbindet Claude, ChatGPT, Copilot und Gemini mit ERP-, E-Commerce-, REST-, SOAP- und SQL-Systemen und mit deinen Fileservern: 694 Connectors, dazu Firmengedächtnis, Skills und Dateisuche. + AnythingMCP verbindet Claude, ChatGPT, Copilot und Gemini mit ERP-, E-Commerce-, REST-, SOAP- und SQL-Systemen und mit deinen Fileservern: 695 Connectors, dazu Firmengedächtnis, Skills und Dateisuche.

AnythingMCP: das Open-Source-MCP-Gateway, das zum Gehirn deines Unternehmens wird

@@ -20,7 +20,7 @@

AnythingMCP ist ein quelloffenes, selbst gehostetes MCP-Gateway, das Claude, ChatGPT, Copilot und Gemini mit der Software deines Unternehmens und mit den Dokumenten auf deinen Fileservern verbindet, ohne dass du einen MCP-Server programmierst.
Auf dem Gateway baut es das Gehirn deines Unternehmens auf: einen Knowledge Graph, der zeigt, wie deine Systeme zusammenhängen, dazu KI-Skills und ein Firmengedächtnis. Jeder KI-Client nutzt dasselbe, jeweils begrenzt durch die Rolle der Person.
- 694 fertige Connectors mit über 12.000 Tools, darunter SAP, Odoo, weclapp und Amazon Seller; 35 davon kommen ohne API-Schlüssel aus. + 695 fertige Connectors mit über 12.000 Tools, darunter SAP, Odoo, weclapp und Amazon Seller; 35 davon kommen ohne API-Schlüssel aus.

@@ -53,7 +53,7 @@ Ein Gateway reicht Aufrufe weiter. Ein Firmengehirn weiß außerdem, wo deine Do | | Was es macht | Edition | |---|---|---| -| **Jedes System, ein Endpunkt** | 694 Connectors aus dem Katalog oder deine eigene REST-/OpenAPI-, SOAP-, GraphQL-, OData-, SQL-, IMAP/SMTP- oder MCP-Quelle. Tools werden zur Laufzeit registriert. [Connectors](#connect-any-api-soap-service-or-database) | Alle | +| **Jedes System, ein Endpunkt** | 695 Connectors aus dem Katalog oder deine eigene REST-/OpenAPI-, SOAP-, GraphQL-, OData-, SQL-, IMAP/SMTP- oder MCP-Quelle. Tools werden zur Laufzeit registriert. [Connectors](#connect-any-api-soap-service-or-database) | Alle | | **File Search** | Indexiert SFTP, FTP/FTPS, WebDAV (Nextcloud, NAS), S3 oder eine Netzwerkfreigabe: jede PDF-Seite, Excel-Zeilen mit ihren Spaltennamen, Word, PowerPoint, Outlook-Mails mit Anhängen. Die KI antwortet mit Datei, Seite und Datum. [File Search](docs/connectors/file-search.md) | Cloud; selbst gehostet Enterprise | | **File Share** | Befristete Download-Links für Dateien aus deinen Ordnern oder für Dateien, die die KI erstellt (einen CSV-Export, einen Bericht), intern oder öffentlich, mit Ablaufdatum und Widerruf. [File Share](docs/connectors/file-search.md) | Cloud; selbst gehostet Enterprise | | **Firmengedächtnis** | Fakten und Vorlieben, in einem KI-Client gespeichert und in jedem anderen wiedergefunden: einmal in Claude gesagt, von Copilot genutzt. Persönlicher und firmenweiter Bereich, mit Ordnern und Rollengrenzen. [Gedächtnis](docs/memory.md) | Enterprise | @@ -138,7 +138,7 @@ Tools werden zur Laufzeit registriert, ohne Neustart. `{{VAR}}`-Werte pro Connec ## Connector-Katalog -694 Adapter mit über 12.000 Tools. Zu jedem gibt es eine Einrichtungsanleitung auf [anythingmcp.com/de/guides](https://anythingmcp.com/de/guides), in sieben Sprachen. +695 Adapter mit über 12.000 Tools. Zu jedem gibt es eine Einrichtungsanleitung auf [anythingmcp.com/de/guides](https://anythingmcp.com/de/guides), in sieben Sprachen. | Kategorie | Beispiele | |---|---| diff --git a/README.ja.md b/README.ja.md index a7baf6e2b..8b940c8a4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,5 +1,5 @@

- AnythingMCP は Claude、ChatGPT、Copilot、Gemini を ERP、E コマース、REST、SOAP、SQL の各システムと社内のファイルサーバーにつなぎます。694 のコネクターに加え、社内メモリ、AI スキル、ファイル検索も備えています。 + AnythingMCP は Claude、ChatGPT、Copilot、Gemini を ERP、E コマース、REST、SOAP、SQL の各システムと社内のファイルサーバーにつなぎます。695 のコネクターに加え、社内メモリ、AI スキル、ファイル検索も備えています。

AnythingMCP:企業の頭脳になるオープンソースの MCP ゲートウェイ

@@ -20,7 +20,7 @@

AnythingMCP は、オープンソースのセルフホスト型 MCP ゲートウェイです。MCP サーバーを書かずに、Claude、ChatGPT、Copilot、Gemini を、自社で使っているソフトウェアやファイルサーバー上の文書につなぎます。
ゲートウェイの上には企業の頭脳を保持します。システム同士のつながりを表すナレッジグラフ、AI スキル、社内メモリです。すべての AI クライアントで共有され、ユーザーごとのロールで範囲が制限されます。
- SAP、Odoo、weclapp、Amazon Seller などを含む 694 種類の既製アダプターで 12,000 以上のツールを提供しており、うち 35 は API キー不要です。 + SAP、Odoo、weclapp、Amazon Seller などを含む 695 種類の既製アダプターで 12,000 以上のツールを提供しており、うち 35 は API キー不要です。

@@ -53,7 +53,7 @@ https://github.com/user-attachments/assets/cc8c9ef3-11cf-4eab-aa4d-98472dc554b3 | | 内容 | エディション | |---|---|---| -| **すべてのシステムを 1 つのエンドポイントで** | 694 のカタログコネクター、または独自の REST/OpenAPI、SOAP、GraphQL、OData、SQL、IMAP/SMTP、MCP のソース。ツールは実行時に登録されます。[コネクター](#connect-any-api-soap-service-or-database) | すべて | +| **すべてのシステムを 1 つのエンドポイントで** | 695 のカタログコネクター、または独自の REST/OpenAPI、SOAP、GraphQL、OData、SQL、IMAP/SMTP、MCP のソース。ツールは実行時に登録されます。[コネクター](#connect-any-api-soap-service-or-database) | すべて | | **File Search(ファイル検索)** | SFTP、FTP/FTPS、WebDAV(Nextcloud、NAS)、S3、ネットワーク共有をインデックス化します。PDF の全ページ、列名付きの Excel の行、Word、PowerPoint、添付ファイル付きの Outlook メールが対象です。AI はファイル、ページ、日付を示して回答します。[File Search](docs/connectors/file-search.md) | Cloud、セルフホストは Enterprise | | **File Share(ファイル共有)** | フォルダー内のファイルや AI が書き出したファイル(CSV エクスポート、レポート)の一時的なダウンロードリンク。社内向けにも公開にもでき、有効期限と取り消しに対応します。[File Share](docs/connectors/file-search.md) | Cloud、セルフホストは Enterprise | | **社内メモリ** | ある AI クライアントで保存した事実や好みを、別のクライアントでも見つけられます。Claude で一度伝えたことを Copilot が使います。個人用と会社用の範囲があり、フォルダーとロールで制限できます。[メモリ](docs/memory.md) | Enterprise | @@ -138,7 +138,7 @@ amd64 ではイメージの取得に約 30 秒、その 24 秒後に API が利 ## コネクターカタログ -694 個のアダプターで 12,000 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 +695 個のアダプターで 12,000 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 | カテゴリー | 例 | |---|---| diff --git a/README.md b/README.md index df1f81342..35b65913d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- AnythingMCP connects Claude, ChatGPT, Copilot and Gemini to ERP, e-commerce, REST, SOAP and SQL systems and to your file servers: 694 connectors, plus company memory, skills and file search. + AnythingMCP connects Claude, ChatGPT, Copilot and Gemini to ERP, e-commerce, REST, SOAP and SQL systems and to your file servers: 695 connectors, plus company memory, skills and file search.

AnythingMCP: the open-source MCP gateway that becomes your company brain

@@ -20,7 +20,7 @@

AnythingMCP is an open-source, self-hosted MCP gateway that connects Claude, ChatGPT, Copilot and Gemini to the software your company runs and to the documents on your file servers, without writing an MCP server.
On top of the gateway it keeps your company brain: a knowledge graph of how your systems connect, AI skills and company memory, shared by every AI client and limited by each user's role.
- 694 ready connectors with 12,000+ tools, among them SAP, Odoo, weclapp and Amazon Seller; 35 need no API key. + 695 ready connectors with 12,000+ tools, among them SAP, Odoo, weclapp and Amazon Seller; 35 need no API key.

@@ -51,7 +51,7 @@ A gateway forwards calls. A company brain also knows where your documents are, h | | What it does | Edition | |---|---|---| -| **Every system, one endpoint** | 694 catalog connectors, or your own REST/OpenAPI, SOAP, GraphQL, OData, SQL, IMAP/SMTP or MCP source. Tools register at runtime. [Connectors](#connect-any-api-soap-service-or-database) | All | +| **Every system, one endpoint** | 695 catalog connectors, or your own REST/OpenAPI, SOAP, GraphQL, OData, SQL, IMAP/SMTP or MCP source. Tools register at runtime. [Connectors](#connect-any-api-soap-service-or-database) | All | | **File Search** | Indexes SFTP, FTP/FTPS, WebDAV (Nextcloud, NAS), S3 or a network share: every PDF page, Excel rows with their column names, Word, PowerPoint, Outlook mails with attachments. The AI answers with file, page and date. [File Search](docs/connectors/file-search.md) | Cloud; self-hosted Enterprise | | **File Share** | Temporary download links for files from your folders, or for files the AI writes (a CSV export, a report), internal or public, with expiry and revoke. [File Share](docs/connectors/file-search.md) | Cloud; self-hosted Enterprise | | **Company memory** | Facts and preferences saved from one AI client and found again in any other: said once in Claude, used by Copilot. Personal and company scopes, with folders and role limits. [Memory](docs/memory.md) | Enterprise | @@ -128,7 +128,7 @@ Tools register at runtime, without a restart. Per-connector `{{VAR}}` values are ## Connector catalog -694 adapters, exposing 12,000+ tools. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. +695 adapters, exposing 12,000+ tools. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. | Category | Examples | |---|---| diff --git a/README.zh-CN.md b/README.zh-CN.md index dad34cedd..803230228 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,5 @@

- AnythingMCP 将 Claude、ChatGPT、Copilot 和 Gemini 连接到 ERP、电子商务、REST、SOAP 和 SQL 系统以及你的文件服务器:694 个连接器,还有企业记忆、技能和文件搜索。 + AnythingMCP 将 Claude、ChatGPT、Copilot 和 Gemini 连接到 ERP、电子商务、REST、SOAP 和 SQL 系统以及你的文件服务器:695 个连接器,还有企业记忆、技能和文件搜索。

AnythingMCP:成为你企业大脑的开源 MCP 网关

@@ -20,7 +20,7 @@

AnythingMCP 是一个开源、可自行托管的 MCP 网关,无需编写 MCP 服务器,即可将 Claude、ChatGPT、Copilot 和 Gemini 连接到你公司运行的软件以及文件服务器上的文档。
在网关之上,它还为你维护企业大脑:一张描述各系统如何关联的知识图谱、AI 技能和企业记忆,由所有 AI 客户端共享,并按每个用户的角色限制访问。
- 694 个现成连接器,提供 12,000 多个工具,涵盖 SAP、Odoo、weclapp 和 Amazon Seller 等;其中 35 个无需 API 密钥。 + 695 个现成连接器,提供 12,000 多个工具,涵盖 SAP、Odoo、weclapp 和 Amazon Seller 等;其中 35 个无需 API 密钥。

@@ -53,7 +53,7 @@ https://github.com/user-attachments/assets/cc8c9ef3-11cf-4eab-aa4d-98472dc554b3 | | 作用 | 版本 | |---|---|---| -| **所有系统,一个端点** | 694 个目录连接器,或你自己的 REST/OpenAPI、SOAP、GraphQL、OData、SQL、IMAP/SMTP 或 MCP 数据源。工具在运行时注册。[连接器](#connect-any-api-soap-service-or-database) | 全部 | +| **所有系统,一个端点** | 695 个目录连接器,或你自己的 REST/OpenAPI、SOAP、GraphQL、OData、SQL、IMAP/SMTP 或 MCP 数据源。工具在运行时注册。[连接器](#connect-any-api-soap-service-or-database) | 全部 | | **File Search(文件搜索)** | 为 SFTP、FTP/FTPS、WebDAV(Nextcloud、NAS)、S3 或网络共享建立索引:PDF 的每一页、带列名的 Excel 行、Word、PowerPoint,以及带附件的 Outlook 邮件。AI 回答时会注明文件、页码和日期。[File Search](docs/connectors/file-search.md) | Cloud;自行托管需 Enterprise | | **File Share(文件分享)** | 为你文件夹中的文件或 AI 生成的文件(CSV 导出、报告)创建临时下载链接,可选内部或公开,支持过期时间和撤销。[File Share](docs/connectors/file-search.md) | Cloud;自行托管需 Enterprise | | **企业记忆** | 在一个 AI 客户端中保存的事实和偏好,可以在任何其他客户端中再次找到:在 Claude 里说过一次,Copilot 也能用上。分为个人和公司两种范围,支持文件夹和角色限制。[记忆](docs/memory.md) | Enterprise | @@ -138,7 +138,7 @@ docker compose up -d ## 连接器目录 -共 694 个适配器,提供 12,000 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 +共 695 个适配器,提供 12,000 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 | 类别 | 示例 | |---|---| diff --git a/glama.json b/glama.json index 135505451..f0aff27db 100644 --- a/glama.json +++ b/glama.json @@ -3,5 +3,5 @@ "maintainers": [ "helpcodeai" ], - "description": "Self-hosted MCP gateway and company brain for AI agents. Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code: 694 pre-built adapters for ERP and e-commerce (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO\u2026) and more. File Search over SFTP/WebDAV/S3 file servers, company memory, AI skills and a knowledge graph shared by every AI client. Per-tool response mapping, OAuth2/RBAC/SSO/SCIM, audit log. Open source under AGPL-3.0." + "description": "Self-hosted MCP gateway and company brain for AI agents. Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code: 695 pre-built adapters for ERP and e-commerce (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO\u2026) and more. File Search over SFTP/WebDAV/S3 file servers, company memory, AI skills and a knowledge graph shared by every AI client. Per-tool response mapping, OAuth2/RBAC/SSO/SCIM, audit log. Open source under AGPL-3.0." } diff --git a/llms-full.txt b/llms-full.txt index e7121f087..9294337be 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # AnythingMCP -> Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 694 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0). +> Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 695 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0). --- @@ -24,7 +24,7 @@ A gateway forwards calls. A company brain also knows where your documents are, h | | What it does | Edition | |---|---|---| -| **Every system, one endpoint** | 694 catalog connectors, or your own REST/OpenAPI, SOAP, GraphQL, OData, SQL, IMAP/SMTP or MCP source. Tools register at runtime. [Connectors]() | All | +| **Every system, one endpoint** | 695 catalog connectors, or your own REST/OpenAPI, SOAP, GraphQL, OData, SQL, IMAP/SMTP or MCP source. Tools register at runtime. [Connectors]() | All | | **File Search** | Indexes SFTP, FTP/FTPS, WebDAV (Nextcloud, NAS), S3 or a network share: every PDF page, Excel rows with their column names, Word, PowerPoint, Outlook mails with attachments. The AI answers with file, page and date. [File Search]() | Cloud; self-hosted Enterprise | | **File Share** | Temporary download links for files from your folders, or for files the AI writes (a CSV export, a report), internal or public, with expiry and revoke. [File Share]() | Cloud; self-hosted Enterprise | | **Company memory** | Facts and preferences saved from one AI client and found again in any other: said once in Claude, used by Copilot. Personal and company scopes, with folders and role limits. [Memory]() | Enterprise | @@ -92,7 +92,7 @@ Tools register at runtime, without a restart. Per-connector `{{VAR}}` values are ## Connector catalog -694 adapters, exposing 12,000+ tools. Every one has a setup guide on [anythingmcp.com/guides](), in seven languages. +695 adapters, exposing 12,000+ tools. Every one has a setup guide on [anythingmcp.com/guides](), in seven languages. | Category | Examples | |---|---| diff --git a/llms.txt b/llms.txt index c9c0c79dc..7df5a2ac0 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # AnythingMCP -> Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 694 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0). +> Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 695 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0). ## Getting started diff --git a/package.json b/package.json index dfedbe0d7..a9cc13a6b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "anythingmcp", "version": "0.52.2", - "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 694 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0).", + "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 695 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0).", "private": true, "license": "AGPL-3.0-only", "engines": { diff --git a/packages/backend/src/adapters/catalog.ts b/packages/backend/src/adapters/catalog.ts index 844d28af6..7d5eab8a6 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -628,6 +628,7 @@ import * as wanotifier from './intl/wanotifier.json'; import * as wassenger from './intl/wassenger.json'; import * as wati from './intl/wati.json'; import * as waveAccounting from './intl/wave-accounting.json'; +import * as wazuh from './intl/wazuh.json'; import * as webdav from './intl/webdav.json'; import * as webex from './intl/webex.json'; import * as webflow from './intl/webflow.json'; @@ -1638,6 +1639,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [ wassenger as unknown as AdapterDefinition, wati as unknown as AdapterDefinition, waveAccounting as unknown as AdapterDefinition, + wazuh as unknown as AdapterDefinition, webdav as unknown as AdapterDefinition, webex as unknown as AdapterDefinition, webflow as unknown as AdapterDefinition, diff --git a/packages/backend/src/adapters/intl/wazuh.json b/packages/backend/src/adapters/intl/wazuh.json new file mode 100644 index 000000000..32813e553 --- /dev/null +++ b/packages/backend/src/adapters/intl/wazuh.json @@ -0,0 +1,910 @@ +{ + "slug": "wazuh", + "name": "Wazuh", + "aliases": [ + "wazuh siem", + "wazuh xdr", + "wazuh manager" + ], + "description": "Let your AI read your Wazuh 4.x security platform through the server API: manager version and daemon status, agents and their connection status, the detection rules and decoders, Security Configuration Assessment (CIS benchmark) results and File Integrity Monitoring data per agent. Read-only: it never restarts, changes or deletes anything. Alerts and vulnerabilities live in the Wazuh indexer and are not covered.", + "instructions": "## Wazuh\nThis connector reads a **Wazuh 4.x** deployment through the **Wazuh server API** (the manager, port 55000 by default): manager version and daemon status, agents and their status, the rules and decoders of the ruleset, Security Configuration Assessment (SCA) results and File Integrity Monitoring (FIM, syscheck) data per agent. It is **read-only**: every tool is a GET request. It cannot restart anything, run active responses, add, remove or upgrade agents, or edit rules, decoders, users or configuration.\n\n**Setup (about 5 minutes)**\n1. Set `WAZUH_URL` to the server API address, normally `https://:55000`, without a trailing slash. Not the dashboard (port 443) and not the indexer (port 9200).\n2. Create a dedicated API user for the AI and give it the built-in `readonly` role (Wazuh dashboard: **Server management, Security, Users**, or `POST /security/users` and `POST /security/users/{user_id}/roles`). Put its name and password in `WAZUH_USERNAME` and `WAZUH_PASSWORD`. The `readonly` role covers manager:read, agent:read, rules:read, decoders:read, sca:read and syscheck:read.\n3. Check the setup with `wazuh_get_manager_info`.\n\nThe connector signs in at `/security/user/authenticate` with that user (HTTP Basic) and sends the returned JWT as a Bearer token. Wazuh tokens last 900 seconds by default (`auth_token_exp_timeout`); the connector signs in again before they expire and after a 401.\n\n**Where Wazuh runs**\n- **Self-hosted AnythingMCP**: Wazuh usually runs on a private address (`https://10.0.0.5:55000`, `https://wazuh.intern:55000`). The outbound SSRF guard blocks private and local addresses by default, so an administrator must allow the host through the `SSRF_ALLOWED_HOSTS` environment variable or the allowlist in the admin settings.\n- **TLS**: Wazuh installs a self-signed certificate on port 55000. A self-signed certificate will fail: the connector offers no trust-anything switch. Give the API a certificate whose name matches `WAZUH_URL` (Wazuh `api.yaml`, `https.cert` and `https.key`), put a reverse proxy with a valid certificate in front of it, or, on self-hosted, start AnythingMCP with `NODE_EXTRA_CA_CERTS` pointing to your internal CA.\n- **AnythingMCP Cloud**: Cloud can only reach a Wazuh API exposed on the public internet over HTTPS with a publicly trusted certificate. A private IP or a local hostname is refused, and the allowlist cannot be changed on Cloud.\n\n**Privacy**: agent names, IP addresses, operating systems, file paths and hashes describe your infrastructure. Assign this connector only to MCP servers whose users may see that.\n\n**How to work**\n1. Overview: `wazuh_get_agents_summary` for counts by status, `wazuh_get_manager_status` for the daemons, `wazuh_get_manager_info` and `wazuh_get_api_info` for versions.\n2. Agents: `wazuh_list_agents` with `status` (e.g. `disconnected`), `group`, `os_platform` or `q`; then `wazuh_get_agent` for one agent. `wazuh_get_agents_os_summary` lists platforms, `wazuh_list_outdated_agents` agents older than the manager.\n3. Hardening: `wazuh_list_sca_policies` for an agent, then `wazuh_list_sca_checks` with `result=failed` for what to fix.\n4. File integrity: `wazuh_get_syscheck_last_scan`, then `wazuh_list_syscheck_files` with `file`, `hash` or `summary=true`.\n5. Detection content: `wazuh_list_rules` by `level` (`12` or `10-15`), `group`, compliance mapping or `mitre`; `wazuh_list_rule_groups` for group names; `wazuh_list_decoders` for decoders.\n\n**Paging**: list tools return `{items, total}`, 50 items by default and at most 500 per call; ask again with `offset` increased by `limit` until you have `total`. Wazuh allows 300 requests per minute by default and answers 429 above that.\n\n**Identifiers**: agent ids have at least three digits (`001`, `012`); `000` is the manager itself, which also runs SCA and FIM. A `policy_id` (`cis_ubuntu22-04`) comes from `wazuh_list_sca_policies`. Rule ids are numbers (`5710`).\n\n**Common errors**\n- 401 \"Invalid credentials\": wrong `WAZUH_USERNAME` or `WAZUH_PASSWORD`. Tokens are also revoked when users, roles or security settings change; the connector then signs in again.\n- 403: the API user's role lacks a read permission; give it the `readonly` role.\n- 429 or a blocked IP: too many requests per minute, or too many failed logins (50 within 5 minutes by default block the address for 300 seconds).\n- \"Agent does not exist\" (Wazuh error 1701): no agent with that id; list them with `wazuh_list_agents`.\n- 400 \"There is no database for agent\": the agent has never connected, so it has no SCA or FIM data yet.\n- \"self-signed certificate\" or \"unable to verify the first certificate\": see TLS above.\n- 404 everywhere: `WAZUH_URL` points at the dashboard or the indexer instead of the server API.\n\n**Out of scope**: alerts and events (they are stored in the Wazuh indexer, not served by this API), vulnerability detection results (in the indexer since Wazuh 4.8), every write (active response, restarts, agent enrollment, removal or upgrade, rule and configuration changes, users and roles), cluster configuration, and Wazuh 5.x, which removes most of these endpoints.", + "region": "intl", + "category": "monitoring", + "icon": "wazuh", + "docsUrl": "https://documentation.wazuh.com/current/user-manual/api/reference.html", + "prerequisites": "A Wazuh 4.x server API (manager, port 55000) reachable from AnythingMCP with a certificate AnythingMCP trusts (on Cloud: public over HTTPS), and an API user with the built-in readonly role. About 5 minutes.", + "requiredEnvVars": [ + "WAZUH_URL", + "WAZUH_USERNAME", + "WAZUH_PASSWORD" + ], + "envVarMeta": { + "WAZUH_URL": { + "label": "Wazuh server API URL", + "kind": "address", + "help": "The Wazuh server API address, normally port 55000, without a trailing slash. Not the dashboard (443) or the indexer (9200). On AnythingMCP Cloud it must be public over HTTPS.", + "example": "https://wazuh.example.com:55000", + "pattern": "^https?://[^\\s/]+(/[^\\s]*[^\\s/])?$", + "patternMessage": "Enter the full address starting with https://, with the port and without a trailing slash, e.g. https://wazuh.example.com:55000." + }, + "WAZUH_USERNAME": { + "label": "API username", + "kind": "credential", + "help": "A dedicated Wazuh API user with the built-in readonly role (Server management, Security, Users in the Wazuh dashboard).", + "example": "mcp-readonly", + "link": "https://documentation.wazuh.com/current/user-manual/api/rbac/index.html" + }, + "WAZUH_PASSWORD": { + "label": "API password", + "kind": "credential", + "secret": true, + "help": "That API user's password.", + "link": "https://documentation.wazuh.com/current/user-manual/api/rbac/index.html" + } + }, + "verifyHints": { + "401": "Wazuh refused the sign-in: WAZUH_USERNAME or WAZUH_PASSWORD is wrong, or the token was revoked after a security change.", + "403": "Signed in, but the API user lacks a read permission. Give it the built-in readonly role.", + "404": "No Wazuh server API at this address. WAZUH_URL must point at the manager API (port 55000 by default), not the dashboard or the indexer.", + "rate_limited": "Wazuh limits requests per minute (300 by default) and blocks an address after too many failed logins (50 within 5 minutes by default). Wait a few minutes." + }, + "probe": { + "tool": "wazuh_get_manager_info" + }, + "connector": { + "name": "Wazuh server API", + "type": "REST", + "baseUrl": "{{WAZUH_URL}}", + "authType": "LOGIN_TOKEN", + "authConfig": { + "loginUrl": "{{WAZUH_URL}}/security/user/authenticate", + "loginMethod": "POST", + "loginHeaders": { + "Accept": "application/json" + }, + "loginBasicAuth": true, + "loginBody": {}, + "username": "{{WAZUH_USERNAME}}", + "password": "{{WAZUH_PASSWORD}}", + "tokenJsonPath": "data.token", + "tokenTTLSeconds": 900, + "proactiveRefreshSeconds": 120, + "refreshOn401": true, + "headerName": "Authorization", + "headerTemplate": "Bearer ${token}" + }, + "headers": { + "Accept": "application/json", + "User-Agent": "AnythingMCP" + }, + "config": { + "errorWhen": [ + { + "path": "data.failed_items[0].error.code", + "equals": 1701, + "messagePath": "data.failed_items[0].error.message", + "status": 404 + }, + { + "path": "data.failed_items[0].error.code", + "messagePath": "data.failed_items[0].error.message", + "status": 400 + } + ] + } + }, + "tools": [ + { + "name": "wazuh_get_api_info", + "description": "Get the Wazuh server API's own info: API version (e.g. 4.14.9), revision, license, hostname and server time. Use it to confirm the server runs Wazuh 4.x. Read-only.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data" + } + } + }, + { + "name": "wazuh_get_manager_info", + "description": "Get the Wazuh manager's basic information: version, installation path, type (server), max agents, OpenSSL support, time zone and uuid. Used to check the connection. Needs manager:read. Read-only.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/manager/info" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data.affected_items[0]" + } + } + }, + { + "name": "wazuh_get_manager_status", + "description": "Get the state of every Wazuh manager daemon (wazuh-analysisd, wazuh-remoted, wazuh-db, wazuh-modulesd, wazuh-apid and others): running or stopped. Needs manager:read. Read-only.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/manager/status" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data.affected_items[0]" + } + } + }, + { + "name": "wazuh_list_agents", + "description": "List Wazuh agents with id, name, IP, status, OS name and version, agent version, last keep-alive, groups, cluster node and registration date, plus the total. Filter by status, name, IP, group, OS platform or version; page with limit and offset. Needs agent:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Optional: agent status, one of active, pending, never_connected, disconnected, or several separated by commas, e.g. active,disconnected." + }, + "name": { + "type": "string", + "description": "Optional: exact agent name." + }, + "ip": { + "type": "string", + "description": "Optional: agent IP address, e.g. 10.0.0.12." + }, + "group": { + "type": "string", + "description": "Optional: agent group name, e.g. default." + }, + "os_platform": { + "type": "string", + "description": "Optional: OS platform as Wazuh reports it, e.g. ubuntu, windows, darwin, amzn." + }, + "version": { + "type": "string", + "description": "Optional: agent version, e.g. Wazuh v4.14.9." + }, + "node_name": { + "type": "string", + "description": "Optional: cluster node the agent reports to, e.g. node01." + }, + "older_than": { + "type": "string", + "description": "Optional: only agents whose last keep-alive (or registration, when never connected) is older than this, e.g. 7d, 12h, 30m." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "sort": { + "type": "string", + "description": "Optional: fields to sort by, + ascending or - descending, comma separated, e.g. -lastKeepAlive or +name." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/agents", + "queryParams": { + "select": "id,name,ip,status,os.name,os.version,version,lastKeepAlive,group,node_name,dateAdd", + "status": "$status", + "name": "$name", + "ip": "$ip", + "group": "$group", + "os.platform": "$os_platform", + "version": "$version", + "node_name": "$node_name", + "older_than": "$older_than", + "search": "$search", + "q": "$q", + "sort": "$sort", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_get_agent", + "description": "Get one Wazuh agent by id with every field Wazuh keeps: name, IP and registration IP, status, OS (name, version, platform, arch, uname), agent version, groups, configuration sync state, manager, cluster node, registration and last keep-alive dates. Answers an error when the agent does not exist. Needs agent:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "pattern": "^\\d{3,}$", + "description": "Agent id with at least three digits, e.g. 001, from wazuh_list_agents. 000 is the Wazuh manager itself." + } + }, + "required": [ + "agent_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/agents", + "queryParams": { + "agents_list": "$agent_id" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data.affected_items[0]" + } + } + }, + { + "name": "wazuh_get_agents_summary", + "description": "Count Wazuh agents by connection status (active, disconnected, never_connected, pending, total) and by configuration sync state (synced, not_synced). The manager itself (000) is not counted. Best first call for 'how are my agents'. Needs agent:read. Read-only.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/agents/summary/status" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data" + } + } + }, + { + "name": "wazuh_get_agents_os_summary", + "description": "List the operating system platforms present among the Wazuh agents, e.g. ubuntu, windows, darwin, with the total. Empty when only the manager is registered. Needs agent:read. Read-only.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/agents/summary/os" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_outdated_agents", + "description": "List Wazuh agents whose version is older than the manager's, with id, name and version, plus the total. Needs agent:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "sort": { + "type": "string", + "description": "Optional: fields to sort by, + ascending or - descending, comma separated, e.g. +version." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/agents/outdated", + "queryParams": { + "search": "$search", + "q": "$q", + "sort": "$sort", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_rules", + "description": "Search the Wazuh ruleset: rule id, level, description, groups, file, status and MITRE technique ids, plus the total. Filter by level or level range, group, file, compliance mapping (PCI DSS, GDPR, HIPAA, NIST 800-53, TSC) or MITRE id. Needs rules:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "rule_ids": { + "type": "string", + "description": "Optional: rule ids separated by commas, e.g. 5710,5712." + }, + "level": { + "type": "string", + "description": "Optional: rule level or range, e.g. 12 or 10-15 (levels go from 0 to 16)." + }, + "group": { + "type": "string", + "description": "Optional: rule group from wazuh_list_rule_groups, e.g. authentication_failed." + }, + "filename": { + "type": "string", + "description": "Optional: rule file name, e.g. 0095-sshd_rules.xml." + }, + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "all" + ], + "description": "Optional: enabled (default in Wazuh), disabled or all." + }, + "pci_dss": { + "type": "string", + "description": "Optional: PCI DSS requirement, e.g. 10.2.4." + }, + "gdpr": { + "type": "string", + "description": "Optional: GDPR article, e.g. IV_35.7.d." + }, + "hipaa": { + "type": "string", + "description": "Optional: HIPAA section, e.g. 164.312.b." + }, + "nist_800_53": { + "type": "string", + "description": "Optional: NIST 800-53 control, e.g. AU.6." + }, + "tsc": { + "type": "string", + "description": "Optional: TSC criterion, e.g. CC6.1." + }, + "mitre": { + "type": "string", + "description": "Optional: MITRE ATT&CK technique id, e.g. T1110." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "sort": { + "type": "string", + "description": "Optional: fields to sort by, + ascending or - descending, comma separated, e.g. -level." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/rules", + "queryParams": { + "select": "id,level,description,groups,filename,status,mitre", + "rule_ids": "$rule_ids", + "level": "$level", + "group": "$group", + "filename": "$filename", + "status": "$status", + "pci_dss": "$pci_dss", + "gdpr": "$gdpr", + "hipaa": "$hipaa", + "nist-800-53": "$nist_800_53", + "tsc": "$tsc", + "mitre": "$mitre", + "search": "$search", + "q": "$q", + "sort": "$sort", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_rule_groups", + "description": "List the group names used in the Wazuh ruleset (e.g. authentication_failed, syscheck, web), plus the total; a group is what wazuh_list_rules takes as group. Needs rules:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/rules/groups", + "queryParams": { + "search": "$search", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_decoders", + "description": "List Wazuh decoders: name, file, directory, status and parent decoder, plus the total. Filter by decoder names, file or status. Needs decoders:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "decoder_names": { + "type": "string", + "description": "Optional: decoder names separated by commas, e.g. sshd,pam." + }, + "filename": { + "type": "string", + "description": "Optional: decoder file name, e.g. 0310-ssh_decoders.xml." + }, + "status": { + "type": "string", + "enum": [ + "enabled", + "disabled", + "all" + ], + "description": "Optional: enabled, disabled or all." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "sort": { + "type": "string", + "description": "Optional: fields to sort by, + ascending or - descending, comma separated, e.g. +name." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/decoders", + "queryParams": { + "select": "name,filename,relative_dirname,status,details.parent", + "decoder_names": "$decoder_names", + "filename": "$filename", + "status": "$status", + "search": "$search", + "q": "$q", + "sort": "$sort", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_sca_policies", + "description": "List the Security Configuration Assessment (SCA) policies scanned on one agent, e.g. a CIS benchmark: policy_id, name, passed, failed and invalid (not applicable) checks, score, total checks and the scan start and end. Needs sca:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "pattern": "^\\d{3,}$", + "description": "Agent id with at least three digits, e.g. 001, from wazuh_list_agents. 000 is the Wazuh manager itself." + }, + "name": { + "type": "string", + "description": "Optional: exact policy name." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + }, + "required": [ + "agent_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/sca/{agent_id}", + "queryParams": { + "name": "$name", + "search": "$search", + "q": "$q", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_sca_checks", + "description": "List the checks of one SCA policy on one agent: check id, title, result (passed, failed, not applicable), rationale and remediation, plus the total. Use result=failed to see what to fix. Needs sca:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "pattern": "^\\d{3,}$", + "description": "Agent id with at least three digits, e.g. 001, from wazuh_list_agents. 000 is the Wazuh manager itself." + }, + "policy_id": { + "type": "string", + "description": "Policy id from wazuh_list_sca_policies, e.g. cis_ubuntu22-04." + }, + "result": { + "type": "string", + "enum": [ + "passed", + "failed", + "not applicable" + ], + "description": "Optional: only checks with this result." + }, + "title": { + "type": "string", + "description": "Optional: exact check title." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + }, + "required": [ + "agent_id", + "policy_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/sca/{agent_id}/checks/{policy_id}", + "queryParams": { + "select": "id,title,result,rationale,remediation", + "result": "$result", + "title": "$title", + "search": "$search", + "q": "$q", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_list_syscheck_files", + "description": "List what File Integrity Monitoring (FIM, syscheck) recorded on one agent: file or registry path, type, size, owner, permissions, hashes, modification time, number of changes and date, plus the total. summary=true returns only path, mtime and date. Needs syscheck:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "pattern": "^\\d{3,}$", + "description": "Agent id with at least three digits, e.g. 001, from wazuh_list_agents. 000 is the Wazuh manager itself." + }, + "file": { + "type": "string", + "description": "Optional: full path of one file or registry key, e.g. /etc/passwd." + }, + "type": { + "type": "string", + "enum": [ + "file", + "registry_key", + "registry_value" + ], + "description": "Optional: file, or registry_key or registry_value on Windows agents." + }, + "hash": { + "type": "string", + "description": "Optional: an MD5, SHA1 or SHA256 hash to look for." + }, + "summary": { + "type": "boolean", + "description": "Optional: true returns only file, mtime and date per entry." + }, + "search": { + "type": "string", + "description": "Optional: free-text search over all fields; start with - to exclude matches." + }, + "q": { + "type": "string", + "description": "Optional Wazuh query filter, e.g. status=active;os.platform=ubuntu (; is AND, , is OR, operators = != < > ~ for 'like')." + }, + "sort": { + "type": "string", + "description": "Optional: fields to sort by, + ascending or - descending, comma separated, e.g. -date." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 50, + "description": "Optional: how many items to return, 1 to 500 (default 50)." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Optional: how many items to skip, for the next page (offset = previous offset + limit)." + } + }, + "required": [ + "agent_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/syscheck/{agent_id}", + "queryParams": { + "file": "$file", + "type": "$type", + "hash": "$hash", + "summary": "$summary", + "search": "$search", + "q": "$q", + "sort": "$sort", + "limit": "$limit", + "offset": "$offset" + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "select", + "select": { + "items": "$.data.affected_items", + "total": "$.data.total_affected_items" + } + } + } + }, + { + "name": "wazuh_get_syscheck_last_scan", + "description": "Get the start and end time of the last File Integrity Monitoring (syscheck) scan on one agent. An end older than the start means a scan is running. Needs syscheck:read. Read-only.", + "parameters": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "pattern": "^\\d{3,}$", + "description": "Agent id with at least three digits, e.g. 001, from wazuh_list_agents. 000 is the Wazuh manager itself." + } + }, + "required": [ + "agent_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/syscheck/{agent_id}/last_scan" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "data.affected_items[0]" + } + } + } + ] +} diff --git a/packages/backend/src/adapters/intl/wazuh.live.spec.ts b/packages/backend/src/adapters/intl/wazuh.live.spec.ts new file mode 100644 index 000000000..d3e769320 --- /dev/null +++ b/packages/backend/src/adapters/intl/wazuh.live.spec.ts @@ -0,0 +1,362 @@ +import axios from 'axios'; +import { ConfigService } from '@nestjs/config'; +import * as adapter from './wazuh.json'; +import { RestEngine } from '../../connectors/engines/rest.engine'; +import { OAuth2TokenService } from '../../connectors/engines/oauth2-token.service'; +import { LoginTokenService } from '../../connectors/engines/login-token.service'; +import { deriveToolAnnotations } from '../../mcp-server/tool-annotations'; +import { applySchemaDefaults } from '../../common/schema-defaults.util'; +import { interpolateConnectorConfig, interpolateDeep } from '../../common/env-interpolation.util'; +import { applyResponseTransform, validateTransform } from '../../connectors/response-transform.util'; +import { describeErrorWhenProblems } from '../../connectors/engines/response-error.util'; +import { describeAdapterEnvVars } from '../env-var-meta'; +import { getAdapter, listAdapters } from '../catalog'; + +/** + * Two layers of verification for the Wazuh adapter (Wazuh 4.x server API, + * read-only): + * + * 1. Static: always runs. Pins the LOGIN_TOKEN flow (POST + * /security/user/authenticate with HTTP Basic through loginBasicAuth, + * JWT at data.token, sent as Bearer, 900 s + * default lifetime), GET-only tools, every tool's URL and query through + * the real RestEngine, the {items, total} response shapes and the + * errorWhen rules for Wazuh's "failed_items" inside a 200. Routes and + * parameters follow the Wazuh v4.14.9 OpenAPI spec (api/api/spec/spec.yaml) + * and were checked with curl against a wazuh/wazuh-manager:4.14.9 + * container on 2 Nov 2026. + * + * 2. Live: skipped unless RUN_WAZUH_LIVE=1 with WAZUH_URL, WAZUH_USERNAME + * and WAZUH_PASSWORD set (a user with the readonly role). The sign-in + * goes through the real LoginTokenService (loginBasicAuth), then every + * tool runs through the RestEngine with the token it got. Optional + * WAZUH_AGENT_ID (default 000, the manager). + * Wazuh's default certificate is self-signed: pass NODE_EXTRA_CA_CERTS, + * or for a throwaway local container NODE_TLS_REJECT_UNAUTHORIZED=0. + * + * RUN_WAZUH_LIVE=1 SSRF_ALLOWED_HOSTS=localhost WAZUH_URL=https://localhost:55000 \ + * WAZUH_USERNAME=... WAZUH_PASSWORD=... npx jest src/adapters/intl/wazuh + * + * Wazuh's login takes `Authorization: Basic base64(user:pass)`; the adapter + * sets `loginBasicAuth: true` so LoginTokenService builds that header from + * username and password on the login request only (#982). + */ + +jest.mock('axios', () => { + const actual = jest.requireActual('axios'); + const mocked = jest.fn(); + return { + __esModule: true, + default: Object.assign(mocked, actual.default, { __actual: actual.default }), + AxiosError: actual.AxiosError, + AxiosHeaders: actual.AxiosHeaders, + }; +}); +const mockedAxios = axios as unknown as jest.Mock & { __actual: typeof axios }; + +type Tool = { + name: string; + description: string; + parameters: { properties?: Record; required?: string[] }; + endpointMapping: { method: string; path: string; queryParams?: Record }; + responseMapping?: Record; + annotations?: Record; +}; +const a = adapter as unknown as { + slug: string; + category: string; + icon: string; + instructions: string; + prerequisites: string; + requiredEnvVars: string[]; + envVarMeta: Record; + verifyHints: Record; + probe: { tool: string }; + connector: { baseUrl: string; authType: string; authConfig: Record; headers: Record; config: { errorWhen: unknown } }; + tools: Tool[]; +}; +const tool = (name: string) => { + const t = a.tools.find((x) => x.name === name); + if (!t) throw new Error(`missing tool ${name}`); + return t; +}; + +const BASE = 'https://wazuh.example.com:55000'; +// Built at runtime so no literal looks like a real credential. +const FAKE_PASSWORD = ['Wz', 'x'.repeat(10), '.*-'].join(''); +const FAKE_JWT = ['eyJhbGciOiJFUzUxMiJ9', 'eyJzdWIiOiJ4In0', 'AAAA'].join('.'); +const ENV: Record = { WAZUH_URL: BASE, WAZUH_USERNAME: 'mcp-readonly', WAZUH_PASSWORD: FAKE_PASSWORD }; +const config = { get: () => 'test-encryption-key-32-chars-ok!' } as unknown as ConfigService; + +const stubLogin = (token = FAKE_JWT) => { + const b = async () => ({ token, expiresAt: Date.now() + 900_000 }); + return { getToken: b, forceRelogin: b } as unknown as LoginTokenService; +}; +// Mirrors DynamicMcpTools: {{VAR}} interpolated in the connector, the mapping +// and authConfig; schema defaults applied before the call; the catalog's +// errorWhen rules passed to the engine. +const call = (name: string, params: Record, env = ENV, login: LoginTokenService = stubLogin()) => { + const { config: cfg, endpointMapping } = interpolateConnectorConfig( + { baseUrl: a.connector.baseUrl, headers: a.connector.headers }, + tool(name).endpointMapping, + env, + ); + return new RestEngine({} as OAuth2TokenService, login).execute( + { + baseUrl: cfg.baseUrl, + authType: a.connector.authType, + authConfig: interpolateDeep(a.connector.authConfig, env), + headers: cfg.headers, + errorWhen: a.connector.config.errorWhen, + } as any, + endpointMapping, + { ...applySchemaDefaults(tool(name).parameters as any, params), ...env }, + ) as Promise; +}; +const sent = () => mockedAxios.mock.calls[0][0]; +const query = () => { + const req = sent(); + return req.paramsSerializer ? decodeURIComponent(req.paramsSerializer(req.params)) : ''; +}; +const mapped = (name: string, raw: unknown) => applyResponseTransform(raw, tool(name).responseMapping as any).value as any; +const ok = (items: unknown[]) => ({ data: { affected_items: items, total_affected_items: items.length, total_failed_items: 0, failed_items: [] }, message: 'ok', error: 0 }); + +describe('wazuh adapter: static spec conformance', () => { + beforeEach(() => mockedAxios.mockReset()); + + it('signs in at /security/user/authenticate and sends the JWT as Bearer', () => { + expect(a.category).toBe('monitoring'); + expect(a.icon).toBe('wazuh'); + expect(a.connector.baseUrl).toBe('{{WAZUH_URL}}'); + expect(a.connector.authType).toBe('LOGIN_TOKEN'); + expect(a.connector.authConfig).toMatchObject({ + loginUrl: '{{WAZUH_URL}}/security/user/authenticate', + loginMethod: 'POST', + loginBasicAuth: true, + loginBody: {}, + username: '{{WAZUH_USERNAME}}', + password: '{{WAZUH_PASSWORD}}', + tokenJsonPath: 'data.token', + tokenTTLSeconds: 900, + refreshOn401: true, + headerName: 'Authorization', + headerTemplate: 'Bearer ${token}', + }); + expect(a.connector.authConfig.proactiveRefreshSeconds).toBeLessThan(900); + expect(a.probe.tool).toBe('wazuh_get_manager_info'); + expect(a.requiredEnvVars).toEqual(['WAZUH_URL', 'WAZUH_USERNAME', 'WAZUH_PASSWORD']); + for (const s of ['401', '403', '404', 'rate_limited']) expect(a.verifyHints[s]).toBeTruthy(); + const meta = Object.fromEntries(describeAdapterEnvVars(a as never).map((m) => [m.name, m])); + expect(meta.WAZUH_URL).toMatchObject({ kind: 'address', secret: false }); + expect(meta.WAZUH_USERNAME).toMatchObject({ kind: 'credential', secret: false }); + expect(meta.WAZUH_PASSWORD).toMatchObject({ kind: 'credential', secret: true }); + }); + + it('accepts the server API address with a port and no trailing slash', () => { + const url = new RegExp(a.envVarMeta.WAZUH_URL.pattern!); + for (const good of [BASE, 'https://10.0.0.5:55000', 'https://localhost:55000', 'https://siem.example.com/wazuh-api']) expect(url.test(good)).toBe(true); + for (const bad of ['wazuh.example.com:55000', `${BASE}/`, 'https://wazuh example.com']) expect(url.test(bad)).toBe(false); + }); + + it('logs in with POST and an empty JSON body, so the password never travels in the body', async () => { + const auth = interpolateDeep(a.connector.authConfig, ENV) as any; + mockedAxios.mockResolvedValueOnce({ data: { data: { token: FAKE_JWT }, error: 0 }, headers: {} }); + const bundle = await new LoginTokenService({} as any, config).getToken(auth); + const login = sent(); + expect(login.method).toBe('POST'); + expect(login.url).toBe(`${BASE}/security/user/authenticate`); + expect(login.data).toEqual({}); + expect(JSON.stringify(login.data ?? '')).not.toContain(FAKE_PASSWORD); + expect(login.params).toBeUndefined(); + expect(bundle.token).toBe(FAKE_JWT); + expect(bundle.expiresAt - Date.now()).toBeLessThanOrEqual(900_000); + }); + + it('sends Authorization: Basic base64(user:pass) on the login only, and Bearer on the tools', async () => { + const auth = interpolateDeep(a.connector.authConfig, ENV) as any; + mockedAxios.mockResolvedValueOnce({ data: { data: { token: FAKE_JWT }, error: 0 }, headers: {} }); + const loginService = new LoginTokenService({} as any, config); + // No connector id: the cache key is the one RestEngine uses below. + await loginService.getToken(auth); + const loginReq = sent(); + const basic = Buffer.from(`mcp-readonly:${FAKE_PASSWORD}`).toString('base64'); + expect(loginReq.headers.Authorization).toBe(`Basic ${basic}`); + expect(loginReq.headers.Accept).toBe('application/json'); + expect(a.connector.authConfig.loginHeaders).not.toHaveProperty('Authorization'); + + mockedAxios.mockReset(); + mockedAxios.mockResolvedValue({ data: ok([]) }); + await call('wazuh_get_manager_info', {}, ENV, loginService); + expect(sent().headers.Authorization).toBe(`Bearer ${FAKE_JWT}`); + expect(JSON.stringify(sent())).not.toContain(basic); + }); + + it('is strictly read-only: fifteen GET tools, nothing under /security, /cluster or /active-response', () => { + expect(a.tools).toHaveLength(15); + for (const t of a.tools) { + expect(`${t.name}:${t.endpointMapping.method}`).toBe(`${t.name}:GET`); + expect(deriveToolAnnotations({ name: t.name, connectorType: 'REST', endpointMapping: t.endpointMapping, annotations: t.annotations })) + .toMatchObject({ readOnlyHint: true, destructiveHint: false }); + expect(t.endpointMapping.path).not.toMatch(/security|cluster|active-response|restart|upgrade|vulnerability|logs/); + } + }); + + it('prefixes every tool and shares no tool name with another adapter', () => { + const mine = new Set(a.tools.map((t) => t.name)); + expect(mine.size).toBe(a.tools.length); + for (const name of mine) expect(name).toMatch(/^wazuh_[a-z_]+$/); + for (const meta of listAdapters()) { + if (meta.slug === a.slug) continue; + for (const t of getAdapter(meta.slug)!.tools) expect(mine.has(t.name)).toBe(false); + } + }); + + it('every declared parameter reaches the request, path parameters are required, limits stay at 500', () => { + for (const t of a.tools) { + const used = new Set(); + for (const [, p] of t.endpointMapping.path.matchAll(/\{(\w+)\}/g)) used.add(p); + for (const v of Object.values(t.endpointMapping.queryParams ?? {})) if (typeof v === 'string' && v.startsWith('$')) used.add(v.slice(1)); + const props = Object.keys(t.parameters.properties ?? {}); + expect(`${t.name}:${props.sort().join(',')}`).toBe(`${t.name}:${[...used].sort().join(',')}`); + for (const [, p] of t.endpointMapping.path.matchAll(/\{(\w+)\}/g)) expect(t.parameters.required).toContain(p); + const limit = t.parameters.properties?.limit; + if (limit) expect([limit.default, limit.maximum]).toEqual([50, 500]); + for (const [p, s] of Object.entries(t.parameters.properties ?? {})) expect(`${t.name}.${p}:${typeof s.description}`).toBe(`${t.name}.${p}:string`); + } + }); + + it('agent ids have at least three digits', () => { + for (const t of a.tools.filter((x) => x.parameters.properties?.agent_id)) { + const re = new RegExp(t.parameters.properties!.agent_id.pattern!); + for (const good of ['000', '001', '1234']) expect(re.test(good)).toBe(true); + for (const bad of ['1', '01', 'abc', '001,002']) expect(re.test(bad)).toBe(false); + } + expect(tool('wazuh_list_sca_checks').parameters.properties!.result.enum).toEqual(['passed', 'failed', 'not applicable']); + }); + + it('instructions cover setup, SSRF allowlist, self-signed TLS, Cloud, privacy, errors and what is out of scope', () => { + const i = a.instructions; + expect(i).toMatch(/SSRF_ALLOWED_HOSTS/); + expect(i).toMatch(/A self-signed certificate will fail: the connector offers no trust-anything switch\./); + expect(i).toMatch(/NODE_EXTRA_CA_CERTS/); + expect(i).toMatch(/Cloud can only reach .* public internet over HTTPS/); + expect(i).toMatch(/Assign this connector only to MCP servers/); + expect(i).toMatch(/`readonly` role/); + expect(i).toMatch(/1701/); + expect(i).toMatch(/\*\*Out of scope\*\*: alerts and events .*indexer.*vulnerability/); + const names = new Set(a.tools.map((t) => t.name)); + for (const m of [...i.matchAll(/\bwazuh_[a-z_]+/g), ...a.tools.flatMap((t) => [...t.description.matchAll(/\bwazuh_[a-z_]+/g)])]) expect(names).toContain(m[0]); + for (const name of names) expect(i).toContain(`\`${name}\``); + for (const t of a.tools) expect(t.description.length).toBeGreaterThanOrEqual(60); + expect(JSON.stringify(adapter)).not.toMatch(/[–—]/); + }); + + const table: Array<[string, Record, string, string[]]> = [ + ['wazuh_get_api_info', {}, '/', []], + ['wazuh_get_manager_info', {}, '/manager/info', []], + ['wazuh_get_manager_status', {}, '/manager/status', []], + ['wazuh_list_agents', { status: 'active,disconnected', os_platform: 'ubuntu' }, '/agents', ['status=active,disconnected', 'os.platform=ubuntu', 'limit=50', 'select=id,name,ip,status,os.name']], + ['wazuh_get_agent', { agent_id: '001' }, '/agents', ['agents_list=001']], + ['wazuh_get_agents_summary', {}, '/agents/summary/status', []], + ['wazuh_get_agents_os_summary', {}, '/agents/summary/os', []], + ['wazuh_list_outdated_agents', { offset: 50 }, '/agents/outdated', ['limit=50', 'offset=50']], + ['wazuh_list_rules', { level: '10-15', nist_800_53: 'AU.6' }, '/rules', ['level=10-15', 'nist-800-53=AU.6', 'select=id,level,description']], + ['wazuh_list_rule_groups', { search: 'auth' }, '/rules/groups', ['search=auth']], + ['wazuh_list_decoders', { decoder_names: 'sshd' }, '/decoders', ['decoder_names=sshd', 'select=name,filename']], + ['wazuh_list_sca_policies', { agent_id: '001' }, '/sca/001', ['limit=50']], + ['wazuh_list_sca_checks', { agent_id: '001', policy_id: 'cis_ubuntu22-04', result: 'failed' }, '/sca/001/checks/cis_ubuntu22-04', ['result=failed', 'select=id,title,result']], + ['wazuh_list_syscheck_files', { agent_id: '001', summary: true, type: 'file' }, '/syscheck/001', ['summary=true', 'type=file']], + ['wazuh_get_syscheck_last_scan', { agent_id: '001' }, '/syscheck/001/last_scan', []], + ]; + it('lists every tool in the URL table', () => expect(table.map(([n]) => n).sort()).toEqual(a.tools.map((t) => t.name).sort())); + it.each(table)('%s sends GET to the right URL with the Bearer token', async (name, params, path, q) => { + mockedAxios.mockResolvedValue({ data: ok([]) }); + await call(name, params); + const req = sent(); + expect(req.method).toBe('GET'); + expect(req.url).toBe(`${BASE}${path}`); + expect(req.headers.Authorization).toBe(`Bearer ${FAKE_JWT}`); + for (const part of q) expect(query()).toContain(part); + expect(query()).not.toContain('$'); + expect(JSON.stringify(req)).not.toContain('{{'); + }); + + it('every transform and the errorWhen rules are valid', () => { + for (const t of a.tools) if (t.responseMapping) expect(`${t.name}:${validateTransform(t.responseMapping.transform)}`).toBe(`${t.name}:null`); + expect(describeErrorWhenProblems(a.connector.config.errorWhen)).toEqual([]); + }); + + it('lists come back as {items, total}; single records as the record', () => { + const agents = ok([{ id: '001', name: 'web-01', status: 'active' }]); + expect(mapped('wazuh_list_agents', agents)).toEqual({ items: [{ id: '001', name: 'web-01', status: 'active' }], total: 1 }); + expect(mapped('wazuh_get_agent', agents)).toEqual({ id: '001', name: 'web-01', status: 'active' }); + expect(mapped('wazuh_get_syscheck_last_scan', ok([{ start: '2026-11-02T11:26:28+00:00', end: '2026-11-02T11:26:28+00:00' }]))).toEqual({ start: '2026-11-02T11:26:28+00:00', end: '2026-11-02T11:26:28+00:00' }); + expect(mapped('wazuh_get_agents_summary', { data: { connection: { active: 2, total: 2 } }, error: 0 })).toEqual({ connection: { active: 2, total: 2 } }); + expect(mapped('wazuh_get_api_info', { data: { api_version: '4.14.9' }, error: 0 })).toEqual({ api_version: '4.14.9' }); + }); + + it('a missing agent (failed_items 1701 inside a 200) fails the call as not found; an empty list does not', async () => { + mockedAxios.mockResolvedValue({ + data: { data: { affected_items: [], total_affected_items: 0, total_failed_items: 1, failed_items: [{ error: { code: 1701, message: 'Agent does not exist' }, id: ['999'] }] }, error: 1 }, + }); + await expect(call('wazuh_get_agent', { agent_id: '999' })).rejects.toMatchObject({ status: 404, message: expect.stringContaining('Agent does not exist') }); + mockedAxios.mockReset(); + mockedAxios.mockResolvedValue({ data: ok([]) }); + await expect(call('wazuh_list_outdated_agents', {})).resolves.toBeDefined(); + }); +}); + +const live = process.env.RUN_WAZUH_LIVE === '1' && process.env.WAZUH_URL && process.env.WAZUH_USERNAME && process.env.WAZUH_PASSWORD ? describe : describe.skip; +live('wazuh adapter: live (read-only)', () => { + const env = { WAZUH_URL: (process.env.WAZUH_URL ?? '').replace(/\/$/, ''), WAZUH_USERNAME: process.env.WAZUH_USERNAME ?? '', WAZUH_PASSWORD: process.env.WAZUH_PASSWORD ?? '' }; + const agent = process.env.WAZUH_AGENT_ID ?? '000'; + const body = (res: unknown) => res; + let login: LoginTokenService; + beforeAll(async () => { + mockedAxios.mockImplementation((cfg: any) => (mockedAxios.__actual as any).request(cfg)); + // The engine's own sign-in: loginBasicAuth builds the Basic header. + login = new LoginTokenService({} as any, config); + const bundle = await login.getToken(interpolateDeep(a.connector.authConfig, env) as any, 'wazuh-live'); + expect(bundle.token.split('.')).toHaveLength(3); + }); + + it('a wrong password is refused at sign-in without echoing the credentials', async () => { + const wrong = { ...env, WAZUH_PASSWORD: `${env.WAZUH_PASSWORD}-wrong` }; + const err: any = await new LoginTokenService({} as any, config) + .getToken(interpolateDeep(a.connector.authConfig, wrong) as any, 'wazuh-live-wrong') + .catch((e) => e); + expect(err?.response?.status).toBe(401); + const text = `${err.message} ${JSON.stringify({ c: err.config, r: err.response?.config })}`; + expect(text).not.toContain(wrong.WAZUH_PASSWORD); + expect(text).not.toContain(Buffer.from(`${env.WAZUH_USERNAME}:${wrong.WAZUH_PASSWORD}`).toString('base64')); + }, 30_000); + + it('runs every tool against the server and shapes the answers', async () => { + const info = mapped('wazuh_get_api_info', body(await call('wazuh_get_api_info', {}, env, login))); + expect(String(info.api_version)).toMatch(/^4\./); + expect(mapped('wazuh_get_manager_info', body(await call('wazuh_get_manager_info', {}, env, login)))).toMatchObject({ type: 'server' }); + expect(mapped('wazuh_get_manager_status', body(await call('wazuh_get_manager_status', {}, env, login)))['wazuh-apid']).toBe('running'); + const agents = mapped('wazuh_list_agents', body(await call('wazuh_list_agents', { limit: 5 }, env, login))); + expect(typeof agents.total).toBe('number'); + expect(mapped('wazuh_get_agent', body(await call('wazuh_get_agent', { agent_id: agent }, env, login)))).toMatchObject({ id: agent }); + expect(mapped('wazuh_get_agents_summary', body(await call('wazuh_get_agents_summary', {}, env, login)))).toHaveProperty('connection'); + expect(mapped('wazuh_get_agents_os_summary', body(await call('wazuh_get_agents_os_summary', {}, env, login)))).toBeDefined(); + for (const n of ['wazuh_list_outdated_agents', 'wazuh_list_rule_groups', 'wazuh_list_decoders']) { + expect(typeof mapped(n, body(await call(n, { limit: 3 }, env, login))).total).toBe('number'); + } + const rules = mapped('wazuh_list_rules', body(await call('wazuh_list_rules', { level: '12', limit: 3 }, env, login))); + expect(rules.items[0]).toMatchObject({ level: 12 }); + const pols = mapped('wazuh_list_sca_policies', body(await call('wazuh_list_sca_policies', { agent_id: agent }, env, login))); + if (pols.items.length) { + const checks = mapped('wazuh_list_sca_checks', body(await call('wazuh_list_sca_checks', { agent_id: agent, policy_id: pols.items[0].policy_id, result: 'not applicable', limit: 2 }, env, login))); + for (const c of checks.items) expect(c.result).toBe('not applicable'); + } + const files = mapped('wazuh_list_syscheck_files', body(await call('wazuh_list_syscheck_files', { agent_id: agent, summary: true, limit: 2 }, env, login))); + expect(typeof files.total).toBe('number'); + expect(mapped('wazuh_get_syscheck_last_scan', body(await call('wazuh_get_syscheck_last_scan', { agent_id: agent }, env, login)))).toHaveProperty('start'); + }, 60_000); + + it('an unknown agent fails as not found', async () => { + await expect(call('wazuh_get_agent', { agent_id: '999' }, env, login)).rejects.toMatchObject({ status: 404 }); + }, 30_000); +}); diff --git a/server.json b/server.json index a6588b1d8..a615ce26d 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.HelpCode-ai/anythingmcp", "title": "AnythingMCP", - "description": "MCP gateway and company brain: 694 connectors (SAP, ERP, SQL), file search, memory and skills.", + "description": "MCP gateway and company brain: 695 connectors (SAP, ERP, SQL), file search, memory and skills.", "repository": { "url": "https://github.com/HelpCode-ai/anythingmcp", "source": "github" From 4e348fc8d52978080cc599aa44af10e96f9a7148 Mon Sep 17 00:00:00 2001 From: GallaGit Date: Wed, 4 Nov 2026 21:32:26 +0100 Subject: [PATCH 2/2] Wazuh adapter: encode path params and add logo (#982) Set encodePathParams on the four tools with {agent_id}/{policy_id} paths, assert it in the static spec, and add the Wazuh logo (from wazuh/wazuh-dashboard-plugins). --- packages/backend/src/adapters/intl/wazuh.json | 6 +++++- packages/backend/src/adapters/intl/wazuh.live.spec.ts | 10 +++++++++- packages/frontend/public/logos/connectors/wazuh.svg | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/public/logos/connectors/wazuh.svg diff --git a/packages/backend/src/adapters/intl/wazuh.json b/packages/backend/src/adapters/intl/wazuh.json index 32813e553..f8822aae2 100644 --- a/packages/backend/src/adapters/intl/wazuh.json +++ b/packages/backend/src/adapters/intl/wazuh.json @@ -682,6 +682,7 @@ "endpointMapping": { "method": "GET", "path": "/sca/{agent_id}", + "encodePathParams": true, "queryParams": { "name": "$name", "search": "$search", @@ -761,6 +762,7 @@ "endpointMapping": { "method": "GET", "path": "/sca/{agent_id}/checks/{policy_id}", + "encodePathParams": true, "queryParams": { "select": "id,title,result,rationale,remediation", "result": "$result", @@ -849,6 +851,7 @@ "endpointMapping": { "method": "GET", "path": "/syscheck/{agent_id}", + "encodePathParams": true, "queryParams": { "file": "$file", "type": "$type", @@ -893,7 +896,8 @@ }, "endpointMapping": { "method": "GET", - "path": "/syscheck/{agent_id}/last_scan" + "path": "/syscheck/{agent_id}/last_scan", + "encodePathParams": true }, "annotations": { "readOnlyHint": true, diff --git a/packages/backend/src/adapters/intl/wazuh.live.spec.ts b/packages/backend/src/adapters/intl/wazuh.live.spec.ts index d3e769320..2e3a97aba 100644 --- a/packages/backend/src/adapters/intl/wazuh.live.spec.ts +++ b/packages/backend/src/adapters/intl/wazuh.live.spec.ts @@ -58,7 +58,7 @@ type Tool = { name: string; description: string; parameters: { properties?: Record; required?: string[] }; - endpointMapping: { method: string; path: string; queryParams?: Record }; + endpointMapping: { method: string; path: string; encodePathParams?: boolean; queryParams?: Record }; responseMapping?: Record; annotations?: Record; }; @@ -201,6 +201,14 @@ describe('wazuh adapter: static spec conformance', () => { } }); + it('percent-encodes path parameters on every tool with a {param} in its path', () => { + const withParams = a.tools.filter((t) => /\{[^}]+\}/.test(t.endpointMapping.path)); + expect(withParams.map((t) => t.name).sort()).toEqual([ + 'wazuh_get_syscheck_last_scan', 'wazuh_list_sca_checks', 'wazuh_list_sca_policies', 'wazuh_list_syscheck_files', + ]); + for (const t of withParams) expect({ name: t.name, encodePathParams: t.endpointMapping.encodePathParams }).toEqual({ name: t.name, encodePathParams: true }); + }); + it('prefixes every tool and shares no tool name with another adapter', () => { const mine = new Set(a.tools.map((t) => t.name)); expect(mine.size).toBe(a.tools.length); diff --git a/packages/frontend/public/logos/connectors/wazuh.svg b/packages/frontend/public/logos/connectors/wazuh.svg new file mode 100644 index 000000000..2cd67f5dc --- /dev/null +++ b/packages/frontend/public/logos/connectors/wazuh.svg @@ -0,0 +1 @@ +Wazuh